├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ ├── build_deploy.yml │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── docker-compose.yml ├── grafana ├── grafana.ini └── provisioning │ ├── dashboards │ ├── dashboard.yml │ └── rel_metrics.json │ └── datasources │ └── datasource.yml ├── lib ├── rel │ ├── allocation_handler.ex │ ├── attributes │ │ ├── additional-address-family.ex │ │ ├── channel_number.ex │ │ ├── data.ex │ │ ├── even_port.ex │ │ ├── lifetime.ex │ │ ├── requested_address_family.ex │ │ ├── requested_transport.ex │ │ ├── reservation_token.ex │ │ ├── xor_peer_address.ex │ │ └── xor_relayed_address.ex │ ├── auth.ex │ ├── auth_provider.ex │ ├── listener.ex │ ├── listener_supervisor.ex │ ├── monitor.ex │ └── utils.ex └── rel_app.ex ├── mix.exs ├── mix.lock ├── prometheus.yml ├── sample.env └── test └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | # You can also customize the exit_status of each check. 88 | # If you don't want TODO comments to cause `mix credo` to fail, just 89 | # set this value to 0 (zero). 90 | # 91 | {Credo.Check.Design.TagTODO, [exit_status: 0]}, 92 | {Credo.Check.Design.TagFIXME, []}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FunctionArity, []}, 126 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 127 | {Credo.Check.Refactor.MatchInCondition, []}, 128 | {Credo.Check.Refactor.MapJoin, []}, 129 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 130 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 131 | {Credo.Check.Refactor.Nesting, []}, 132 | {Credo.Check.Refactor.UnlessWithElse, []}, 133 | {Credo.Check.Refactor.WithClauses, []}, 134 | {Credo.Check.Refactor.FilterCount, []}, 135 | {Credo.Check.Refactor.FilterFilter, []}, 136 | {Credo.Check.Refactor.RejectReject, []}, 137 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 138 | 139 | # 140 | ## Warnings 141 | # 142 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 143 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 144 | {Credo.Check.Warning.Dbg, []}, 145 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 146 | {Credo.Check.Warning.IExPry, []}, 147 | {Credo.Check.Warning.IoInspect, []}, 148 | {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, 149 | {Credo.Check.Warning.OperationOnSameValues, []}, 150 | {Credo.Check.Warning.OperationWithConstantResult, []}, 151 | {Credo.Check.Warning.RaiseInsideRescue, []}, 152 | {Credo.Check.Warning.SpecWithStruct, []}, 153 | {Credo.Check.Warning.WrongTestFileExtension, []}, 154 | {Credo.Check.Warning.UnusedEnumOperation, []}, 155 | {Credo.Check.Warning.UnusedFileOperation, []}, 156 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 157 | {Credo.Check.Warning.UnusedListOperation, []}, 158 | {Credo.Check.Warning.UnusedPathOperation, []}, 159 | {Credo.Check.Warning.UnusedRegexOperation, []}, 160 | {Credo.Check.Warning.UnusedStringOperation, []}, 161 | {Credo.Check.Warning.UnusedTupleOperation, []}, 162 | {Credo.Check.Warning.UnsafeExec, []} 163 | ], 164 | disabled: [ 165 | # 166 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 167 | 168 | # 169 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 170 | # and be sure to use `mix credo --strict` to see low priority checks) 171 | # 172 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 173 | {Credo.Check.Consistency.UnusedVariableNames, []}, 174 | {Credo.Check.Design.DuplicatedCode, []}, 175 | {Credo.Check.Design.SkipTestWithoutComment, []}, 176 | {Credo.Check.Readability.AliasAs, []}, 177 | {Credo.Check.Readability.BlockPipe, []}, 178 | {Credo.Check.Readability.ImplTrue, []}, 179 | {Credo.Check.Readability.MultiAlias, []}, 180 | {Credo.Check.Readability.NestedFunctionCalls, []}, 181 | {Credo.Check.Readability.OneArityFunctionInPipe, []}, 182 | {Credo.Check.Readability.SeparateAliasRequire, []}, 183 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 184 | {Credo.Check.Readability.SinglePipe, []}, 185 | {Credo.Check.Readability.Specs, []}, 186 | {Credo.Check.Readability.StrictModuleLayout, []}, 187 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 188 | {Credo.Check.Readability.OnePipePerLine, []}, 189 | {Credo.Check.Refactor.ABCSize, []}, 190 | {Credo.Check.Refactor.AppendSingleItem, []}, 191 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 192 | {Credo.Check.Refactor.FilterReject, []}, 193 | {Credo.Check.Refactor.IoPuts, []}, 194 | {Credo.Check.Refactor.MapMap, []}, 195 | {Credo.Check.Refactor.ModuleDependencies, []}, 196 | {Credo.Check.Refactor.NegatedIsNil, []}, 197 | {Credo.Check.Refactor.PassAsyncInTestCases, []}, 198 | {Credo.Check.Refactor.PipeChainStart, []}, 199 | {Credo.Check.Refactor.RejectFilter, []}, 200 | {Credo.Check.Refactor.VariableRebinding, []}, 201 | {Credo.Check.Warning.LazyLogging, []}, 202 | {Credo.Check.Warning.LeakyEnvironment, []}, 203 | {Credo.Check.Warning.MapGetUnsafePass, []}, 204 | {Credo.Check.Warning.MixEnv, []}, 205 | {Credo.Check.Warning.UnsafeToAtom, []} 206 | 207 | # {Credo.Check.Refactor.MapInto, []}, 208 | 209 | # 210 | # Custom checks can be created using `mix credo.gen.check`. 211 | # 212 | ] 213 | } 214 | } 215 | ] 216 | } 217 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/build_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Docker image, deploy to the server 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup QEMU 20 | uses: docker/setup-qemu-action@v2 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | - name: Login to Container Registry 24 | uses: docker/login-action@v2 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@v4 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ github.repository }} 34 | tags: | 35 | type=semver,pattern={{version}} 36 | type=edge,branch=master 37 | 38 | - name: Build and push Docker image 39 | uses: docker/build-push-action@v4 40 | with: 41 | context: . 42 | platforms: linux/amd64 43 | push: true 44 | tags: ${{ steps.meta.outputs.tags }} 45 | cache-from: type=gha 46 | cache-to: type=gha,mode=max 47 | deploy: 48 | runs-on: ubuntu-latest 49 | needs: build-and-push-image 50 | steps: 51 | - uses: actions/checkout@v3 52 | - name: Deploy the new version 53 | uses: JimCronqvist/action-ssh@master 54 | env: 55 | GF_SECURITY_ADMIN_PASSWORD: ${{ secrets.GF_SECURITY_ADMIN_PASSWORD }} 56 | GF_SECURITY_ADMIN_USER: ${{ secrets.GF_SECURITY_ADMIN_USER }} 57 | REALM: ${{ secrets.REALM }} 58 | DIR_NAME: ${{ secrets.DIR_NAME }} 59 | TAG: ${{ github.ref_name }} 60 | with: 61 | hosts: ${{ secrets.SSH_HOST }} 62 | privateKey: ${{ secrets.PRIV_KEY }} 63 | command: | 64 | docker rm -f turn grafana prometheus 65 | rm -rf $DIR_NAME; mkdir $DIR_NAME 66 | cd $DIR_NAME 67 | git clone -b $TAG --depth 1 https://github.com/${{ github.repository }} . 68 | echo "REALM=$REALM 69 | GF_SECURITY_ADMIN_PASSWORD=$GF_SECURITY_ADMIN_PASSWORD 70 | GF_SECURITY_ADMIN_USER=$GF_SECURITY_ADMIN_USER 71 | TAG=${TAG#v}" > .env 72 | docker-compose -p turn up -d --remove-orphans 73 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | 3 | on: push 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | name: lint OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 9 | strategy: 10 | matrix: 11 | otp: ['26'] 12 | elixir: ['1.15.4'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: erlef/setup-beam@v1 16 | with: 17 | otp-version: ${{matrix.otp}} 18 | elixir-version: ${{matrix.elixir}} 19 | - run: mix deps.get 20 | - run: mix credo 21 | - run: mix format --check-formatted 22 | 23 | test: 24 | runs-on: ubuntu-latest 25 | name: test OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 26 | strategy: 27 | matrix: 28 | otp: ['26'] 29 | elixir: ['1.15.4'] 30 | env: 31 | MIX_ENV: test 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: erlef/setup-beam@v1 36 | with: 37 | otp-version: ${{matrix.otp}} 38 | elixir-version: ${{matrix.elixir}} 39 | - run: mix deps.get 40 | - run: mix coveralls.json 41 | - uses: codecov/codecov-action@v3 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ex_turn-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | .elixir_ls/ 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hexpm/elixir:1.15.4-erlang-26.0.2-alpine-3.18.2 as build 2 | 3 | RUN apk add --no-cache --update git 4 | 5 | WORKDIR /app 6 | 7 | RUN mix local.hex --force && \ 8 | mix local.rebar --force 9 | 10 | ENV MIX_ENV=prod 11 | 12 | COPY mix.exs mix.lock ./ 13 | RUN mix deps.get --only $MIX_ENV 14 | 15 | COPY config/config.exs config/${MIX_ENV}.exs config/ 16 | RUN mix deps.compile 17 | 18 | COPY lib lib 19 | RUN mix compile 20 | 21 | COPY config/runtime.exs config/ 22 | 23 | RUN mix release 24 | 25 | FROM alpine:3.18.2 as app 26 | 27 | RUN apk add --no-cache --update libncursesw openssl libstdc++ 28 | 29 | WORKDIR /app 30 | 31 | COPY --from=build /app/_build/prod/rel/rel ./ 32 | 33 | CMD ["bin/rel", "start"] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Elixir WebRTC Developers 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rel 2 | 3 | [![CI](https://img.shields.io/github/actions/workflow/status/elixir-webrtc/rel/ci.yml?logo=github&label=CI)](https://github.com/elixir-webrtc/rel/actions/workflows/ci.yml) 4 | [![Deployment](https://img.shields.io/github/actions/workflow/status/elixir-webrtc/rel/build_deploy.yml?logo=github&label=Deployment)](https://github.com/elixir-webrtc/rel/actions/workflows/build_deploy.yml) 5 | [![Package](https://ghcr-badge.egpl.dev/elixir-webrtc/rel/latest_tag?trim=major&label=latest)](https://github.com/elixir-webrtc/rel/pkgs/container/rel) 6 | 7 | TURN server in pure Elixir. 8 | 9 | Aims to implement: 10 | 11 | - RFC 5389: [Session Traversal Utilities for NAT (STUN)](https://datatracker.ietf.org/doc/html/rfc5389) 12 | - RFC 5766: [Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN)](https://datatracker.ietf.org/doc/html/rfc5766) 13 | - RFC 6156: [Traversal Using Relays around NAT (TURN) Extension for IPv6](https://datatracker.ietf.org/doc/html/rfc6156#autoid-7) 14 | 15 | This project is in early stage of development and some of the features described in the RFCs might be missing. 16 | Expect breaking changes. 17 | 18 | Supports authentication described in [A REST API For Access To TURN Services](https://datatracker.ietf.org/doc/html/draft-uberti-rtcweb-turn-rest-00#section-2.2). 19 | 20 | ## Public deployment 21 | 22 | If you're in need of TURN server for testing purposes, feel free to use this Rel public deployment at `turn.elixir-webrtc.org`. 23 | 24 | In case of any irregularities or bugs, please open an issue with description of the problem. 25 | DO NOT use this deployment in production, as it's intended to be an aid in developement only. 26 | 27 | To obtain a set of credentials, use the built-in credentials mechanism. It does not require any authentication, but the credentials must be refreshed after 3 hours if not used. 28 | 29 | ```console 30 | $ curl -X POST "https://turn.elixir-webrtc.org/?service=turn&username=johnsmith" 31 | {"password":"l6hs9SzUgudFeb5XjrfCfOWKeOQ=","ttl":1728,"uris":["turn:167.235.241.140:3478?transport=udp"],"username":"1691574817:johnsmith"}⏎ 32 | ``` 33 | 34 | Use the obtained credentials in e.g. WebRTC's `RTCPeerConnection`: 35 | 36 | ```js 37 | pc = new RTCPeerConnection({ 38 | iceServers: [ 39 | { 40 | credential: "l6hs9SzUgudFeb5XjrfCfOWKeOQ=", 41 | urls: "turn:167.235.241.140:3478?transport=udp", 42 | username: "1691574817:johnsmith" 43 | } 44 | ] 45 | }); 46 | ``` 47 | 48 | ## Installation and running 49 | 50 | 1. From source 51 | 52 | ```console 53 | git clone https://github.com/elixir-webrtc/rel.git 54 | cd rel 55 | mix deps.get 56 | mix run --no-halt 57 | ``` 58 | 59 | 2. In Docker 60 | 61 | ```console 62 | docker run --network=host ghcr.io/elixir-webrtc/rel:latest 63 | ``` 64 | 65 | ## Features and configuration 66 | 67 | Rel exposes Prometheus metrics endpoint (by default `http://127.0.0.1:9568/metrics`). 68 | 69 | Rel supports authentication described in [A REST API For Access To TURN Services](https://datatracker.ietf.org/doc/html/draft-uberti-rtcweb-turn-rest-00#section-2.2). 70 | By default available under `http://127.0.0.1:4000/`. Example request would be `POST http://127.0.0.1:40000/?service=turn&username=johnsmith`. 71 | Key query parameter currently is not supported. 72 | 73 | Rel is configured via environment variables. All of the possible options are described in [sample env file](./sample.env). 74 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :rel, 4 | # 1 day in seconds, see https://datatracker.ietf.org/doc/html/draft-uberti-rtcweb-turn-rest-00#section-2.2 5 | credentials_lifetime: 24 * 60 * 60, 6 | # 10 minutes in seconds 7 | default_allocation_lifetime: 10 * 60, 8 | # 1 hour in seconds 9 | max_allocation_lifetime: 60 * 60, 10 | # 5 minutes in seconds 11 | permission_lifetime: 60 * 5, 12 | # 10 minutes in seconds 13 | channel_lifetime: 60 * 10, 14 | # 1 hour in nanoseconds, see https://datatracker.ietf.org/doc/html/rfc5766#section-4 15 | nonce_lifetime: 60 * 60 * 1_000_000_000 16 | 17 | config :logger, :console, 18 | format: "$time $metadata[$level] $message\n", 19 | metadata: [:listener, :client, :alloc] 20 | 21 | import_config "#{config_env()}.exs" 22 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-webrtc/rel/d4d9e2213f4aecf13824cf8a18017fa3bc2a9984/config/dev.exs -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # FIXME: temporary, as `:credentials_lifetime` is a compile time variable atm 4 | config :rel, :credentials_lifetime, 3 * 24 * 24 5 | -------------------------------------------------------------------------------- /config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | require Logger 4 | 5 | defmodule ConfigUtils do 6 | @truthy_values ["true", "t", "1"] 7 | 8 | def parse_ip_address(ip) do 9 | case ip |> to_charlist() |> :inet.parse_address() do 10 | {:ok, parsed_ip} -> 11 | parsed_ip 12 | 13 | _other -> 14 | raise(""" 15 | Bad IP format. Expected IP address, got: \ 16 | #{inspect(ip)} 17 | """) 18 | end 19 | end 20 | 21 | def parse_port(port) do 22 | case Integer.parse(port, 10) do 23 | {val, _rem} when val in 0..65_535 -> 24 | val 25 | 26 | _other -> 27 | raise(""" 28 | Bad PORT format. Expected port number, got: \ 29 | #{inspect(port)} 30 | """) 31 | end 32 | end 33 | 34 | def is_truthy?(env_var) do 35 | String.downcase(env_var) in @truthy_values 36 | end 37 | 38 | def guess_external_ip(listen_ip) 39 | when listen_ip not in [{0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}], 40 | do: listen_ip 41 | 42 | def guess_external_ip(_listen_ip) do 43 | with {:ok, opts} <- :inet.getifaddrs(), 44 | addrs <- Enum.map(opts, fn {_intf, opt} -> opt end), 45 | addrs <- Enum.filter(addrs, &is_valid?(&1)), 46 | addrs <- Enum.flat_map(addrs, &Keyword.get_values(&1, :addr)), 47 | addrs <- Enum.filter(addrs, &(not link_local?(&1) and not any?(&1))), 48 | false <- Enum.empty?(addrs) do 49 | case Enum.find(addrs, &(not private?(&1))) do 50 | nil -> hd(addrs) 51 | other -> other 52 | end 53 | else 54 | _other -> 55 | raise "Cannot find an external IP address, pass one explicitely via EXTERNAL_IP env variable" 56 | end 57 | end 58 | 59 | defp is_valid?(inter) do 60 | flags = Keyword.get(inter, :flags) 61 | :up in flags and :loopback not in flags 62 | end 63 | 64 | defp link_local?({169, 254, _, _}), do: true 65 | defp link_local?({0xFE80, _, _, _, _, _, _, _}), do: true 66 | defp link_local?(_other), do: false 67 | 68 | defp any?({0, 0, 0, 0}), do: true 69 | defp any?({0, 0, 0, 0, 0, 0, 0, 0}), do: true 70 | defp any?(_other), do: false 71 | 72 | defp private?({10, _, _, _}), do: true 73 | defp private?({192, 168, _, _}), do: true 74 | defp private?({172, b, _, _}) when b in 16..31, do: true 75 | defp private?({0xFC00, 0, 0, 0, 0, 0, 0, 0}), do: true 76 | defp private?(_other), do: false 77 | end 78 | 79 | # HTTPS for AuthProvider 80 | auth_use_tls? = System.get_env("AUTH_USE_TLS", "false") |> ConfigUtils.is_truthy?() 81 | auth_keyfile = System.get_env("AUTH_KEYFILE") 82 | auth_certfile = System.get_env("AUTH_CERTFILE") 83 | 84 | if auth_use_tls? and (is_nil(auth_keyfile) or is_nil(auth_certfile)) do 85 | raise "Both KEY_FILE_PATH and CERT_FILE_PATH must be set is TLS is used" 86 | end 87 | 88 | # IP addresses for TURN 89 | listen_ip = System.get_env("LISTEN_IP", "0.0.0.0") |> ConfigUtils.parse_ip_address() 90 | 91 | external_listen_ip = 92 | case System.fetch_env("EXTERNAL_LISTEN_IP") do 93 | {:ok, addr} -> ConfitUtils.parse_ip_address(addr) 94 | :error -> ConfigUtils.guess_external_ip(listen_ip) 95 | end 96 | 97 | relay_ip = 98 | case System.fetch_env("RELAY_IP") do 99 | {:ok, addr} -> ConfigUtils.parse_ip_address(addr) 100 | :error -> listen_ip 101 | end 102 | 103 | external_relay_ip = 104 | case System.fetch_env("EXTERNAL_RELAY_IP") do 105 | {:ok, addr} -> ConfigUtils.parse_ip_address(addr) 106 | :error -> external_listen_ip 107 | end 108 | 109 | relay_port_start = System.get_env("RELAY_PORT_START", "49152") |> ConfigUtils.parse_port() 110 | relay_port_end = System.get_env("RELAY_PORT_END", "65535") |> ConfigUtils.parse_port() 111 | 112 | if relay_port_start > relay_port_end, 113 | do: raise("RELAY_PORT_END must be greater or equal to RELAY_PORT_END") 114 | 115 | listener_count = 116 | case System.fetch_env("LISTENER_COUNT") do 117 | {:ok, count} -> 118 | count = String.to_integer(count) 119 | if count <= 0, do: raise("LISTENER_COUNT must be greater than 0") 120 | count 121 | 122 | :error -> 123 | System.schedulers_online() 124 | end 125 | 126 | # AuthProvider/credentials configuration 127 | config :rel, 128 | auth_ip: System.get_env("AUTH_IP", "127.0.0.1") |> ConfigUtils.parse_ip_address(), 129 | auth_port: System.get_env("AUTH_PORT", "4000") |> ConfigUtils.parse_port(), 130 | auth_allow_cors?: System.get_env("AUTH_ALLOW_CORS", "false") |> ConfigUtils.is_truthy?(), 131 | auth_use_tls?: auth_use_tls?, 132 | auth_keyfile: auth_keyfile, 133 | auth_certfile: auth_certfile 134 | 135 | # TURN server configuration 136 | config :rel, 137 | listen_ip: listen_ip, 138 | external_listen_ip: external_listen_ip, 139 | relay_ip: relay_ip, 140 | external_relay_ip: external_relay_ip, 141 | listen_port: System.get_env("LISTEN_PORT", "3478") |> ConfigUtils.parse_port(), 142 | realm: System.get_env("REALM", "example.com"), 143 | relay_port_start: relay_port_start, 144 | relay_port_end: relay_port_end 145 | 146 | # Metrics endpoint configuration 147 | config :rel, 148 | metrics_ip: System.get_env("METRICS_IP", "127.0.0.1") |> ConfigUtils.parse_ip_address(), 149 | metrics_port: System.get_env("METRICS_PORT", "9568") |> ConfigUtils.parse_port() 150 | 151 | # Automatically generated secrets 152 | config :rel, 153 | auth_secret: :crypto.strong_rand_bytes(64), 154 | nonce_secret: :crypto.strong_rand_bytes(64) 155 | 156 | # Other 157 | config :rel, 158 | listener_count: listener_count 159 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-webrtc/rel/d4d9e2213f4aecf13824cf8a18017fa3bc2a9984/config/test.exs -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | rel: 4 | image: ghcr.io/elixir-webrtc/rel:${TAG} 5 | container_name: rel 6 | restart: on-failure 7 | network_mode: host 8 | environment: 9 | REALM: "${REALM}" 10 | 11 | node-exporter: 12 | image: prom/node-exporter:v1.6.1 13 | container_name: node_exporter 14 | restart: on-failure 15 | command: 16 | - --path.rootfs=/host 17 | network_mode: host 18 | pid: host 19 | volumes: 20 | - /:/host:ro,rslave 21 | 22 | prometheus: 23 | image: prom/prometheus:v2.46.0 24 | container_name: prometheus 25 | restart: on-failure 26 | network_mode: host 27 | command: 28 | - --config.file=/etc/prometheus/prometheus.yml 29 | - --web.listen-address=127.0.0.1:9090 30 | - --storage.tsdb.path=/prometheus 31 | volumes: 32 | - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro 33 | - prometheus_data:/prometheus 34 | depends_on: 35 | - rel 36 | - node-exporter 37 | 38 | grafana: 39 | image: grafana/grafana:10.0.3 40 | container_name: grafana 41 | restart: on-failure 42 | network_mode: host 43 | volumes: 44 | - grafana_data:/var/lib/grafana 45 | - ./grafana/:/etc/grafana/ 46 | depends_on: 47 | - prometheus 48 | environment: 49 | GF_SECURITY_ADMIN_PASSWORD: "${GF_SECURITY_ADMIN_PASSWORD}" 50 | GF_SECURITY_ADMIN_USER: "${GF_SECURITY_ADMIN_USER}" 51 | 52 | volumes: 53 | grafana_data: {} 54 | prometheus_data: {} 55 | -------------------------------------------------------------------------------- /grafana/grafana.ini: -------------------------------------------------------------------------------- 1 | [server] 2 | http_addr = "127.0.0.1" 3 | domain = "metrics.elixir-webrtc.org" 4 | -------------------------------------------------------------------------------- /grafana/provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'TURN Stats' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | updateIntervalSeconds: 10 9 | options: 10 | path: /etc/grafana/provisioning/dashboards 11 | -------------------------------------------------------------------------------- /grafana/provisioning/dashboards/rel_metrics.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "links": [], 22 | "liveNow": false, 23 | "panels": [ 24 | { 25 | "datasource": { 26 | "type": "prometheus", 27 | "uid": "PBFA97CFB590B2093" 28 | }, 29 | "fieldConfig": { 30 | "defaults": { 31 | "color": { 32 | "mode": "palette-classic" 33 | }, 34 | "custom": { 35 | "axisCenteredZero": false, 36 | "axisColorMode": "series", 37 | "axisLabel": "", 38 | "axisPlacement": "auto", 39 | "barAlignment": 0, 40 | "drawStyle": "line", 41 | "fillOpacity": 0, 42 | "gradientMode": "none", 43 | "hideFrom": { 44 | "legend": false, 45 | "tooltip": false, 46 | "viz": false 47 | }, 48 | "lineInterpolation": "smooth", 49 | "lineWidth": 1, 50 | "pointSize": 5, 51 | "scaleDistribution": { 52 | "type": "linear" 53 | }, 54 | "showPoints": "auto", 55 | "spanNulls": false, 56 | "stacking": { 57 | "group": "A", 58 | "mode": "none" 59 | }, 60 | "thresholdsStyle": { 61 | "mode": "off" 62 | } 63 | }, 64 | "mappings": [], 65 | "thresholds": { 66 | "mode": "absolute", 67 | "steps": [ 68 | { 69 | "color": "green", 70 | "value": null 71 | }, 72 | { 73 | "color": "red", 74 | "value": 80 75 | } 76 | ] 77 | }, 78 | "unit": "bits" 79 | }, 80 | "overrides": [] 81 | }, 82 | "gridPos": { 83 | "h": 11, 84 | "w": 24, 85 | "x": 0, 86 | "y": 0 87 | }, 88 | "id": 2, 89 | "interval": "5s", 90 | "options": { 91 | "legend": { 92 | "calcs": [ 93 | "lastNotNull", 94 | "max", 95 | "mean" 96 | ], 97 | "displayMode": "table", 98 | "placement": "bottom", 99 | "showLegend": true 100 | }, 101 | "tooltip": { 102 | "mode": "single", 103 | "sort": "none" 104 | } 105 | }, 106 | "targets": [ 107 | { 108 | "datasource": { 109 | "type": "prometheus", 110 | "uid": "PBFA97CFB590B2093" 111 | }, 112 | "editorMode": "builder", 113 | "exemplar": false, 114 | "expr": "rate(turn_listener_client_inbound_traffic_total_bytes[$__interval]) * 8", 115 | "instant": false, 116 | "legendFormat": "Clients -> Listener {{ listener_id }}", 117 | "range": true, 118 | "refId": "client_listener" 119 | }, 120 | { 121 | "datasource": { 122 | "type": "prometheus", 123 | "uid": "PBFA97CFB590B2093" 124 | }, 125 | "editorMode": "builder", 126 | "expr": "sum(rate(turn_listener_client_inbound_traffic_total_bytes[$__interval])) * 8", 127 | "hide": false, 128 | "instant": false, 129 | "legendFormat": "Clients -> Listener (Total)", 130 | "range": true, 131 | "refId": "client_listener_total" 132 | }, 133 | { 134 | "datasource": { 135 | "type": "prometheus", 136 | "uid": "PBFA97CFB590B2093" 137 | }, 138 | "editorMode": "builder", 139 | "expr": "rate(turn_allocations_peer_inbound_traffic_total_bytes[$__interval]) * 8", 140 | "hide": false, 141 | "instant": false, 142 | "legendFormat": "Peers -> Allocation_handlers", 143 | "range": true, 144 | "refId": "peer_allocation" 145 | } 146 | ], 147 | "title": "Bitrates", 148 | "transparent": true, 149 | "type": "timeseries" 150 | }, 151 | { 152 | "datasource": { 153 | "type": "prometheus", 154 | "uid": "PBFA97CFB590B2093" 155 | }, 156 | "fieldConfig": { 157 | "defaults": { 158 | "color": { 159 | "mode": "palette-classic" 160 | }, 161 | "custom": { 162 | "axisCenteredZero": false, 163 | "axisColorMode": "series", 164 | "axisLabel": "", 165 | "axisPlacement": "auto", 166 | "barAlignment": 0, 167 | "drawStyle": "line", 168 | "fillOpacity": 0, 169 | "gradientMode": "none", 170 | "hideFrom": { 171 | "legend": false, 172 | "tooltip": false, 173 | "viz": false 174 | }, 175 | "lineInterpolation": "smooth", 176 | "lineWidth": 1, 177 | "pointSize": 5, 178 | "scaleDistribution": { 179 | "type": "linear" 180 | }, 181 | "showPoints": "auto", 182 | "spanNulls": false, 183 | "stacking": { 184 | "group": "A", 185 | "mode": "none" 186 | }, 187 | "thresholdsStyle": { 188 | "mode": "off" 189 | } 190 | }, 191 | "mappings": [], 192 | "thresholds": { 193 | "mode": "absolute", 194 | "steps": [ 195 | { 196 | "color": "green", 197 | "value": null 198 | }, 199 | { 200 | "color": "red", 201 | "value": 80 202 | } 203 | ] 204 | } 205 | }, 206 | "overrides": [] 207 | }, 208 | "gridPos": { 209 | "h": 10, 210 | "w": 24, 211 | "x": 0, 212 | "y": 11 213 | }, 214 | "id": 7, 215 | "options": { 216 | "legend": { 217 | "calcs": [ 218 | "lastNotNull", 219 | "max", 220 | "mean" 221 | ], 222 | "displayMode": "table", 223 | "placement": "bottom", 224 | "showLegend": true 225 | }, 226 | "tooltip": { 227 | "mode": "single", 228 | "sort": "none" 229 | } 230 | }, 231 | "targets": [ 232 | { 233 | "datasource": { 234 | "type": "prometheus", 235 | "uid": "PBFA97CFB590B2093" 236 | }, 237 | "editorMode": "builder", 238 | "expr": "rate(turn_listener_client_inbound_traffic_packets_total[$__interval])", 239 | "instant": false, 240 | "legendFormat": "Clients -> Listener {{listener_id}}", 241 | "range": true, 242 | "refId": "client_listener" 243 | }, 244 | { 245 | "datasource": { 246 | "type": "prometheus", 247 | "uid": "PBFA97CFB590B2093" 248 | }, 249 | "editorMode": "builder", 250 | "expr": "sum(rate(turn_listener_client_inbound_traffic_packets_total[$__interval]))", 251 | "hide": false, 252 | "instant": false, 253 | "legendFormat": "Client -> Listener (Total)", 254 | "range": true, 255 | "refId": "client_listener_total" 256 | }, 257 | { 258 | "datasource": { 259 | "type": "prometheus", 260 | "uid": "PBFA97CFB590B2093" 261 | }, 262 | "editorMode": "builder", 263 | "expr": "rate(turn_allocations_peer_inbound_traffic_packets_total[$__interval])", 264 | "hide": false, 265 | "instant": false, 266 | "legendFormat": "Peers -> Allocation_handlers", 267 | "range": true, 268 | "refId": "peers_allocation" 269 | } 270 | ], 271 | "title": "Packets", 272 | "transparent": true, 273 | "type": "timeseries" 274 | }, 275 | { 276 | "datasource": { 277 | "type": "prometheus", 278 | "uid": "PBFA97CFB590B2093" 279 | }, 280 | "fieldConfig": { 281 | "defaults": { 282 | "color": { 283 | "mode": "palette-classic" 284 | }, 285 | "custom": { 286 | "axisCenteredZero": false, 287 | "axisColorMode": "series", 288 | "axisLabel": "", 289 | "axisPlacement": "auto", 290 | "barAlignment": 0, 291 | "drawStyle": "line", 292 | "fillOpacity": 0, 293 | "gradientMode": "none", 294 | "hideFrom": { 295 | "legend": false, 296 | "tooltip": false, 297 | "viz": false 298 | }, 299 | "lineInterpolation": "stepBefore", 300 | "lineWidth": 1, 301 | "pointSize": 5, 302 | "scaleDistribution": { 303 | "type": "linear" 304 | }, 305 | "showPoints": "auto", 306 | "spanNulls": false, 307 | "stacking": { 308 | "group": "A", 309 | "mode": "none" 310 | }, 311 | "thresholdsStyle": { 312 | "mode": "off" 313 | } 314 | }, 315 | "mappings": [], 316 | "thresholds": { 317 | "mode": "absolute", 318 | "steps": [ 319 | { 320 | "color": "green", 321 | "value": null 322 | }, 323 | { 324 | "color": "red", 325 | "value": 80 326 | } 327 | ] 328 | }, 329 | "unit": "none" 330 | }, 331 | "overrides": [] 332 | }, 333 | "gridPos": { 334 | "h": 9, 335 | "w": 24, 336 | "x": 0, 337 | "y": 21 338 | }, 339 | "id": 3, 340 | "interval": "5", 341 | "options": { 342 | "legend": { 343 | "calcs": [ 344 | "lastNotNull", 345 | "max", 346 | "mean" 347 | ], 348 | "displayMode": "table", 349 | "placement": "bottom", 350 | "showLegend": true 351 | }, 352 | "tooltip": { 353 | "mode": "single", 354 | "sort": "none" 355 | } 356 | }, 357 | "targets": [ 358 | { 359 | "datasource": { 360 | "type": "prometheus", 361 | "uid": "PBFA97CFB590B2093" 362 | }, 363 | "editorMode": "builder", 364 | "expr": "turn_allocations_created_total", 365 | "instant": false, 366 | "legendFormat": "Created Allocations", 367 | "range": true, 368 | "refId": "created" 369 | }, 370 | { 371 | "datasource": { 372 | "type": "prometheus", 373 | "uid": "PBFA97CFB590B2093" 374 | }, 375 | "editorMode": "builder", 376 | "expr": "turn_allocations_expired_total", 377 | "hide": false, 378 | "instant": false, 379 | "legendFormat": "Expired Allocations", 380 | "range": true, 381 | "refId": "expired" 382 | } 383 | ], 384 | "title": "Active allocations", 385 | "transformations": [ 386 | { 387 | "id": "calculateField", 388 | "options": { 389 | "alias": "Active allocations", 390 | "binary": { 391 | "left": "Created Allocations", 392 | "operator": "-", 393 | "reducer": "sum", 394 | "right": "Expired Allocations" 395 | }, 396 | "mode": "binary", 397 | "reduce": { 398 | "include": [], 399 | "reducer": "diff" 400 | }, 401 | "replaceFields": true 402 | } 403 | } 404 | ], 405 | "transparent": true, 406 | "type": "timeseries" 407 | }, 408 | { 409 | "datasource": { 410 | "type": "prometheus", 411 | "uid": "PBFA97CFB590B2093" 412 | }, 413 | "fieldConfig": { 414 | "defaults": { 415 | "color": { 416 | "mode": "palette-classic" 417 | }, 418 | "custom": { 419 | "axisCenteredZero": false, 420 | "axisColorMode": "series", 421 | "axisLabel": "", 422 | "axisPlacement": "auto", 423 | "barAlignment": 0, 424 | "drawStyle": "line", 425 | "fillOpacity": 0, 426 | "gradientMode": "none", 427 | "hideFrom": { 428 | "legend": false, 429 | "tooltip": false, 430 | "viz": false 431 | }, 432 | "lineInterpolation": "smooth", 433 | "lineWidth": 1, 434 | "pointSize": 5, 435 | "scaleDistribution": { 436 | "type": "linear" 437 | }, 438 | "showPoints": "auto", 439 | "spanNulls": false, 440 | "stacking": { 441 | "group": "A", 442 | "mode": "none" 443 | }, 444 | "thresholdsStyle": { 445 | "mode": "off" 446 | } 447 | }, 448 | "mappings": [], 449 | "thresholds": { 450 | "mode": "absolute", 451 | "steps": [ 452 | { 453 | "color": "green", 454 | "value": null 455 | }, 456 | { 457 | "color": "red", 458 | "value": 80 459 | } 460 | ] 461 | }, 462 | "unit": "percent" 463 | }, 464 | "overrides": [] 465 | }, 466 | "gridPos": { 467 | "h": 9, 468 | "w": 24, 469 | "x": 0, 470 | "y": 30 471 | }, 472 | "id": 6, 473 | "options": { 474 | "legend": { 475 | "calcs": [], 476 | "displayMode": "list", 477 | "placement": "bottom", 478 | "showLegend": true 479 | }, 480 | "tooltip": { 481 | "mode": "single", 482 | "sort": "none" 483 | } 484 | }, 485 | "targets": [ 486 | { 487 | "datasource": { 488 | "type": "prometheus", 489 | "uid": "PBFA97CFB590B2093" 490 | }, 491 | "editorMode": "code", 492 | "expr": "100 - (rate(node_cpu_seconds_total{job=\"node\",mode=\"idle\"}[$__interval]) * 100)", 493 | "instant": false, 494 | "legendFormat": "Core {{cpu}}", 495 | "range": true, 496 | "refId": "cpu" 497 | }, 498 | { 499 | "datasource": { 500 | "type": "prometheus", 501 | "uid": "PBFA97CFB590B2093" 502 | }, 503 | "editorMode": "code", 504 | "expr": "sum(100 - (rate(node_cpu_seconds_total{job=\"node\",mode=\"idle\"}[$__interval]) * 100))", 505 | "hide": false, 506 | "instant": false, 507 | "legendFormat": "Total", 508 | "range": true, 509 | "refId": "cpu_total" 510 | } 511 | ], 512 | "title": "CPU usage", 513 | "transformations": [], 514 | "transparent": true, 515 | "type": "timeseries" 516 | }, 517 | { 518 | "datasource": { 519 | "type": "prometheus", 520 | "uid": "PBFA97CFB590B2093" 521 | }, 522 | "fieldConfig": { 523 | "defaults": { 524 | "color": { 525 | "mode": "palette-classic" 526 | }, 527 | "custom": { 528 | "axisCenteredZero": false, 529 | "axisColorMode": "series", 530 | "axisLabel": "", 531 | "axisPlacement": "auto", 532 | "barAlignment": 0, 533 | "drawStyle": "line", 534 | "fillOpacity": 0, 535 | "gradientMode": "none", 536 | "hideFrom": { 537 | "legend": false, 538 | "tooltip": false, 539 | "viz": false 540 | }, 541 | "lineInterpolation": "smooth", 542 | "lineWidth": 1, 543 | "pointSize": 5, 544 | "scaleDistribution": { 545 | "type": "linear" 546 | }, 547 | "showPoints": "auto", 548 | "spanNulls": false, 549 | "stacking": { 550 | "group": "A", 551 | "mode": "none" 552 | }, 553 | "thresholdsStyle": { 554 | "mode": "off" 555 | } 556 | }, 557 | "mappings": [], 558 | "thresholds": { 559 | "mode": "absolute", 560 | "steps": [ 561 | { 562 | "color": "green", 563 | "value": null 564 | }, 565 | { 566 | "color": "red", 567 | "value": 80 568 | } 569 | ] 570 | }, 571 | "unit": "bytes" 572 | }, 573 | "overrides": [] 574 | }, 575 | "gridPos": { 576 | "h": 9, 577 | "w": 24, 578 | "x": 0, 579 | "y": 39 580 | }, 581 | "id": 4, 582 | "options": { 583 | "legend": { 584 | "calcs": [], 585 | "displayMode": "list", 586 | "placement": "bottom", 587 | "showLegend": true 588 | }, 589 | "tooltip": { 590 | "mode": "single", 591 | "sort": "none" 592 | } 593 | }, 594 | "targets": [ 595 | { 596 | "datasource": { 597 | "type": "prometheus", 598 | "uid": "PBFA97CFB590B2093" 599 | }, 600 | "editorMode": "builder", 601 | "expr": "vm_memory_bytes", 602 | "instant": false, 603 | "legendFormat": "Used memory", 604 | "range": true, 605 | "refId": "memory" 606 | } 607 | ], 608 | "title": "VM Memory", 609 | "transparent": true, 610 | "type": "timeseries" 611 | }, 612 | { 613 | "datasource": { 614 | "type": "prometheus", 615 | "uid": "PBFA97CFB590B2093" 616 | }, 617 | "fieldConfig": { 618 | "defaults": { 619 | "color": { 620 | "mode": "palette-classic" 621 | }, 622 | "custom": { 623 | "axisCenteredZero": false, 624 | "axisColorMode": "series", 625 | "axisLabel": "", 626 | "axisPlacement": "auto", 627 | "barAlignment": 0, 628 | "drawStyle": "line", 629 | "fillOpacity": 0, 630 | "gradientMode": "none", 631 | "hideFrom": { 632 | "legend": false, 633 | "tooltip": false, 634 | "viz": false 635 | }, 636 | "lineInterpolation": "stepBefore", 637 | "lineWidth": 1, 638 | "pointSize": 5, 639 | "scaleDistribution": { 640 | "type": "linear" 641 | }, 642 | "showPoints": "auto", 643 | "spanNulls": false, 644 | "stacking": { 645 | "group": "A", 646 | "mode": "none" 647 | }, 648 | "thresholdsStyle": { 649 | "mode": "off" 650 | } 651 | }, 652 | "mappings": [], 653 | "thresholds": { 654 | "mode": "absolute", 655 | "steps": [ 656 | { 657 | "color": "green", 658 | "value": null 659 | }, 660 | { 661 | "color": "red", 662 | "value": 80 663 | } 664 | ] 665 | }, 666 | "unit": "none" 667 | }, 668 | "overrides": [] 669 | }, 670 | "gridPos": { 671 | "h": 8, 672 | "w": 24, 673 | "x": 0, 674 | "y": 48 675 | }, 676 | "id": 5, 677 | "options": { 678 | "legend": { 679 | "calcs": [], 680 | "displayMode": "list", 681 | "placement": "bottom", 682 | "showLegend": true 683 | }, 684 | "tooltip": { 685 | "mode": "single", 686 | "sort": "none" 687 | } 688 | }, 689 | "targets": [ 690 | { 691 | "datasource": { 692 | "type": "prometheus", 693 | "uid": "PBFA97CFB590B2093" 694 | }, 695 | "editorMode": "builder", 696 | "expr": "vm_run_queue_cpu_length", 697 | "instant": false, 698 | "legendFormat": "CPU", 699 | "range": true, 700 | "refId": "cpu" 701 | }, 702 | { 703 | "datasource": { 704 | "type": "prometheus", 705 | "uid": "PBFA97CFB590B2093" 706 | }, 707 | "editorMode": "builder", 708 | "expr": "vm_run_queue_io_length", 709 | "hide": false, 710 | "instant": false, 711 | "legendFormat": "IO", 712 | "range": true, 713 | "refId": "io" 714 | } 715 | ], 716 | "title": "Run queue lengths", 717 | "transparent": true, 718 | "type": "timeseries" 719 | } 720 | ], 721 | "refresh": "5s", 722 | "schemaVersion": 38, 723 | "style": "dark", 724 | "tags": [], 725 | "templating": { 726 | "list": [] 727 | }, 728 | "time": { 729 | "from": "now-1h", 730 | "to": "now" 731 | }, 732 | "timepicker": {}, 733 | "timezone": "", 734 | "title": "Rel metrics", 735 | "uid": "f89914d6-5064-454e-bfb9-128f69816c36", 736 | "version": 1, 737 | "weekStart": "" 738 | } -------------------------------------------------------------------------------- /grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: Prometheus 5 | orgId: 1 6 | 7 | datasources: 8 | - name: Prometheus 9 | type: prometheus 10 | access: proxy 11 | orgId: 1 12 | uid: "PBFA97CFB590B2093" 13 | url: http://127.0.0.1:9090 14 | editable: true 15 | -------------------------------------------------------------------------------- /lib/rel/allocation_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.AllocationHandler do 2 | @moduledoc false 3 | use GenServer, restart: :transient 4 | 5 | require Logger 6 | 7 | alias ExSTUN.Message 8 | alias ExSTUN.Message.Type 9 | 10 | alias Rel.Auth 11 | alias Rel.Attribute.{ChannelNumber, Data, Lifetime, XORPeerAddress} 12 | alias Rel.Utils 13 | 14 | @type five_tuple() :: 15 | {:inet.ip_address(), :inet.port_number(), :inet.ip_address(), :inet.port_number(), :udp} 16 | 17 | @typedoc """ 18 | Allocation handler init args. 19 | 20 | * `t_id` is the origin allocation request transaction id 21 | * `response` is the origin response for the origin alloaction request 22 | """ 23 | @type alloc_args() :: [ 24 | five_tuple: five_tuple(), 25 | alloc_socket: :gen_udp.socket(), 26 | turn_socket: :gen_udp.socket(), 27 | username: binary(), 28 | time_to_expiry: integer(), 29 | t_id: integer(), 30 | response: binary() 31 | ] 32 | 33 | @permission_lifetime Application.compile_env!(:rel, :permission_lifetime) 34 | @channel_lifetime Application.compile_env!(:rel, :channel_lifetime) 35 | 36 | @spec start_link(alloc_args()) :: GenServer.on_start() 37 | def start_link(args) do 38 | alloc_socket = Keyword.fetch!(args, :alloc_socket) 39 | five_tuple = Keyword.fetch!(args, :five_tuple) 40 | t_id = Keyword.fetch!(args, :t_id) 41 | response = Keyword.fetch!(args, :response) 42 | 43 | {:ok, {_alloc_ip, alloc_port}} = :inet.sockname(alloc_socket) 44 | 45 | alloc_origin_state = %{alloc_port: alloc_port, t_id: t_id, response: response} 46 | 47 | GenServer.start_link( 48 | __MODULE__, 49 | args, 50 | name: {:via, Registry, {Registry.Allocations, five_tuple, alloc_origin_state}} 51 | ) 52 | end 53 | 54 | @spec process_stun_message(GenServer.server(), term()) :: :ok 55 | def process_stun_message(allocation, msg) do 56 | GenServer.cast(allocation, {:stun_message, msg}) 57 | end 58 | 59 | @spec process_channel_message(GenServer.server(), term()) :: :ok 60 | def process_channel_message(allocation, msg) when is_binary(msg) do 61 | GenServer.cast(allocation, {:channel_message, msg}) 62 | end 63 | 64 | @impl true 65 | def init(args) do 66 | five_tuple = Keyword.fetch!(args, :five_tuple) 67 | alloc_socket = Keyword.fetch!(args, :alloc_socket) 68 | turn_socket = Keyword.fetch!(args, :turn_socket) 69 | username = Keyword.fetch!(args, :username) 70 | time_to_expiry = Keyword.fetch!(args, :time_to_expiry) 71 | 72 | {c_ip, c_port, s_ip, s_port, _transport} = five_tuple 73 | alloc_id = "(#{:inet.ntoa(c_ip)}:#{c_port}, #{:inet.ntoa(s_ip)}:#{s_port}, UDP)" 74 | Logger.metadata(alloc: alloc_id) 75 | Logger.info("Starting new allocation handler") 76 | 77 | :telemetry.execute([:allocations], %{created: 1, expired: 0}) 78 | 79 | Process.send_after(self(), :check_expiration, time_to_expiry * 1000) 80 | 81 | {:ok, 82 | %{ 83 | alloc_id: alloc_id, 84 | turn_socket: turn_socket, 85 | socket: alloc_socket, 86 | five_tuple: five_tuple, 87 | username: username, 88 | expiry_timestamp: System.os_time(:second) + time_to_expiry, 89 | permissions: %{}, 90 | chann_to_time: %{}, 91 | chann_to_addr: %{}, 92 | addr_to_chann: %{} 93 | }} 94 | end 95 | 96 | @impl true 97 | def handle_cast({:stun_message, msg}, state) do 98 | case handle_message(msg, state) do 99 | {:ok, state} -> {:noreply, state} 100 | {:allocation_expired, state} -> {:stop, {:shutdown, :allocation_expired}, state} 101 | end 102 | end 103 | 104 | @impl true 105 | def handle_cast( 106 | {:channel_message, <>}, 107 | state 108 | ) do 109 | case Map.fetch(state.chann_to_addr, number) do 110 | {:ok, addr} -> 111 | :ok = :gen_udp.send(state.socket, addr, data) 112 | 113 | :error -> 114 | nil 115 | end 116 | 117 | {:noreply, state} 118 | end 119 | 120 | @impl true 121 | def handle_info({:udp, _socket, ip_addr, port, packet}, state) do 122 | len = byte_size(packet) 123 | 124 | :telemetry.execute([:allocations, :peer], %{inbound: len}) 125 | 126 | if Map.has_key?(state.permissions, ip_addr) do 127 | {c_ip, c_port, _, _, _} = state.five_tuple 128 | 129 | case Map.fetch(state.addr_to_chann, {ip_addr, port}) do 130 | {:ok, number} -> 131 | channel_data = <> 132 | 133 | :ok = 134 | :socket.sendto(state.turn_socket, channel_data, %{ 135 | family: :inet, 136 | addr: c_ip, 137 | port: c_port 138 | }) 139 | 140 | :error -> 141 | xor_addr = %XORPeerAddress{port: port, address: ip_addr} 142 | data = %Data{value: packet} 143 | 144 | response = 145 | %Type{class: :indication, method: :data} 146 | |> Message.new([xor_addr, data]) 147 | |> Message.encode() 148 | 149 | :ok = 150 | :socket.sendto(state.turn_socket, response, %{family: :inet, addr: c_ip, port: c_port}) 151 | end 152 | 153 | {:noreply, state} 154 | else 155 | Logger.warning( 156 | "Received UDP datagram from #{:inet.ntoa(ip_addr)}, but no permission was created" 157 | ) 158 | 159 | {:noreply, state} 160 | end 161 | end 162 | 163 | @impl true 164 | def handle_info(:check_expiration, state) do 165 | if System.os_time(:second) >= state.expiry_timestamp do 166 | Logger.info("Allocation expired, shutting down allocation handler") 167 | {:stop, {:shutdown, :allocation_expired}, state} 168 | else 169 | {:noreply, state} 170 | end 171 | end 172 | 173 | @impl true 174 | def handle_info({:check_permission, addr}, state) do 175 | if System.os_time(:second) >= state.permissions[addr] do 176 | Logger.info("Permission for #{:inet.ntoa(addr)} expired") 177 | {_val, state} = pop_in(state.permissions[addr]) 178 | {:noreply, state} 179 | else 180 | {:noreply, state} 181 | end 182 | end 183 | 184 | @impl true 185 | def handle_info({:check_channel, number}, state) do 186 | if System.os_time(:second) >= state.chann_to_time[number] do 187 | {ip_addr, port} = addr = state.chann_to_addr[number] 188 | Logger.info("Channel binding #{number} <-> #{:inet.ntoa(ip_addr)}:#{port} expired") 189 | {_val, state} = pop_in(state.chann_to_addr[number]) 190 | {_val, state} = pop_in(state.addr_to_chann[addr]) 191 | {_val, state} = pop_in(state.chann_to_time[number]) 192 | 193 | {:noreply, state} 194 | else 195 | {:noreply, state} 196 | end 197 | end 198 | 199 | @impl true 200 | def handle_info(msg, state) do 201 | Logger.warning("Got unexpected OTP message: #{inspect(msg)}") 202 | {:noreply, state} 203 | end 204 | 205 | @impl true 206 | def terminate(reason, _state) do 207 | :telemetry.execute([:allocations], %{created: 0, expired: 1}) 208 | Logger.info("Allocation handler stopped with reason: #{inspect(reason)}") 209 | end 210 | 211 | defp handle_message(%Message{type: %Type{class: :request, method: :refresh}} = msg, state) do 212 | Logger.info("Received 'refresh' request") 213 | {c_ip, c_port, _, _, _} = state.five_tuple 214 | 215 | with {:ok, key} <- Auth.authenticate(msg, username: state.username), 216 | {:ok, time_to_expiry} <- Utils.get_lifetime(msg) do 217 | type = %Type{class: :success_response, method: :refresh} 218 | 219 | response = 220 | msg.transaction_id 221 | |> Message.new(type, [%Lifetime{lifetime: time_to_expiry}]) 222 | |> Message.with_integrity(key) 223 | |> Message.encode() 224 | 225 | :ok = 226 | :socket.sendto(state.turn_socket, response, %{family: :inet, addr: c_ip, port: c_port}) 227 | 228 | if time_to_expiry == 0 do 229 | Logger.info("Allocation deleted with LIFETIME=0 refresh request") 230 | {:allocation_expired, state} 231 | else 232 | state = %{state | expiry_timestamp: System.os_time(:second) + time_to_expiry} 233 | Process.send_after(self(), :check_expiration, time_to_expiry * 1000) 234 | 235 | Logger.info("Succesfully refreshed allocation, new 'time-to-expiry': #{time_to_expiry}") 236 | 237 | {:ok, state} 238 | end 239 | else 240 | {:error, reason} -> 241 | {response, log_msg} = Utils.build_error(reason, msg.transaction_id, msg.type.method) 242 | Logger.warning(log_msg) 243 | 244 | :ok = 245 | :socket.sendto(state.turn_socket, response, %{family: :inet, addr: c_ip, port: c_port}) 246 | 247 | {:ok, state} 248 | end 249 | end 250 | 251 | defp handle_message( 252 | %Message{type: %Type{class: :request, method: :create_permission}} = msg, 253 | state 254 | ) do 255 | Logger.info("Received 'create_permission' request") 256 | {c_ip, c_port, _, _, _} = state.five_tuple 257 | 258 | with {:ok, key} <- Auth.authenticate(msg, username: state.username), 259 | {:ok, state} <- install_of_refresh_permission(msg, state) do 260 | type = %Type{class: :success_response, method: msg.type.method} 261 | 262 | response = 263 | msg.transaction_id 264 | |> Message.new(type, []) 265 | |> Message.with_integrity(key) 266 | |> Message.encode() 267 | 268 | :ok = 269 | :socket.sendto(state.turn_socket, response, %{family: :inet, addr: c_ip, port: c_port}) 270 | 271 | {:ok, state} 272 | else 273 | {:error, reason} -> 274 | {response, log_msg} = Utils.build_error(reason, msg.transaction_id, msg.type.method) 275 | Logger.warning(log_msg) 276 | 277 | :ok = 278 | :socket.sendto(state.turn_socket, response, %{family: :inet, addr: c_ip, port: c_port}) 279 | 280 | {:ok, state} 281 | end 282 | end 283 | 284 | defp handle_message(%Message{type: %Type{class: :indication, method: :send}} = msg, state) do 285 | with {:ok, %XORPeerAddress{address: ip_addr, port: port}} <- get_xor_peer_address(msg), 286 | {:ok, %Data{value: data}} <- get_data(msg), 287 | true <- Map.has_key?(state.permissions, ip_addr) do 288 | # TODO: dont fragment attribute 289 | :ok = :gen_udp.send(state.socket, ip_addr, port, data) 290 | {:ok, state} 291 | else 292 | false -> 293 | {:ok, %XORPeerAddress{address: addr}} = get_xor_peer_address(msg) 294 | 295 | Logger.warning( 296 | "Error while processing 'indication' request, no permission for #{:inet.ntoa(addr)}" 297 | ) 298 | 299 | {:ok, state} 300 | 301 | {:error, reason} -> 302 | {_response, log_msg} = Utils.build_error(reason, msg.transaction_id, msg.type.method) 303 | Logger.warning("Error while processing 'indication' request. " <> log_msg) 304 | # no response here, messages are silently discarded 305 | {:ok, state} 306 | end 307 | end 308 | 309 | defp handle_message(%Message{type: %Type{class: :request, method: :channel_bind}} = msg, state) do 310 | Logger.info("Received 'channel_bind' request") 311 | {c_ip, c_port, _, _, _} = state.five_tuple 312 | 313 | with {:ok, key} <- Auth.authenticate(msg, username: state.username), 314 | {:ok, %XORPeerAddress{address: ip_addr, port: port}} <- get_xor_peer_address(msg), 315 | {:ok, %ChannelNumber{number: number}} <- get_channel_number(msg), 316 | {:ok, state} <- assign_channel(ip_addr, port, number, state) do 317 | type = %Type{class: :success_response, method: msg.type.method} 318 | 319 | {:ok, state} = install_of_refresh_permission(msg, state, limit: 1) 320 | 321 | response = 322 | msg.transaction_id 323 | |> Message.new(type, []) 324 | |> Message.with_integrity(key) 325 | |> Message.encode() 326 | 327 | :ok = 328 | :socket.sendto(state.turn_socket, response, %{family: :inet, addr: c_ip, port: c_port}) 329 | 330 | Logger.info("Succesfully bound channel #{number} to address #{:inet.ntoa(ip_addr)}:#{port}") 331 | 332 | {:ok, state} 333 | else 334 | {:error, reason} -> 335 | {response, log_msg} = Utils.build_error(reason, msg.transaction_id, msg.type.method) 336 | Logger.warning(log_msg) 337 | 338 | :ok = 339 | :socket.sendto(state.turn_socket, response, %{family: :inet, addr: c_ip, port: c_port}) 340 | 341 | {:ok, state} 342 | end 343 | end 344 | 345 | defp handle_message(msg, state) do 346 | Logger.warning("Got unexpected TURN message: #{inspect(msg, limit: :infinity)}") 347 | {:ok, state} 348 | end 349 | 350 | defp install_of_refresh_permission(msg, state, opts \\ []) do 351 | case Message.get_all_attributes(msg, XORPeerAddress) do 352 | nil -> 353 | {:error, :no_xor_peer_address_attribute} 354 | 355 | {:error, _reason} -> 356 | {:error, :invalid_xor_peer_address} 357 | 358 | {:ok, addrs} -> 359 | limit = Keyword.get(opts, :limit) 360 | addrs = if(limit != nil, do: Enum.take(addrs, limit), else: addrs) 361 | 362 | permissions = 363 | Map.new(addrs, fn %XORPeerAddress{address: addr} -> 364 | Process.send_after(self(), {:check_permission, addr}, @permission_lifetime * 1000) 365 | Logger.info("Succesfully created or refreshed permission for #{:inet.ntoa(addr)}") 366 | {addr, System.os_time(:second) + @permission_lifetime} 367 | end) 368 | 369 | state = update_in(state.permissions, &Map.merge(&1, permissions)) 370 | {:ok, state} 371 | end 372 | end 373 | 374 | defp get_xor_peer_address(msg) do 375 | case Message.get_attribute(msg, XORPeerAddress) do 376 | {:ok, _attr} = resp -> resp 377 | nil -> {:error, :no_xor_peer_address_attribute} 378 | {:error, _reason} -> {:error, :invalid_xor_peer_address} 379 | end 380 | end 381 | 382 | defp get_data(msg) do 383 | case Message.get_attribute(msg, Data) do 384 | {:ok, _attr} = resp -> resp 385 | nil -> {:error, :no_data_attribute} 386 | {:error, _reason} -> {:error, :invalid_data} 387 | end 388 | end 389 | 390 | defp get_channel_number(msg) do 391 | case Message.get_attribute(msg, ChannelNumber) do 392 | {:ok, _attr} = resp -> resp 393 | nil -> {:error, :no_channel_number_attribute} 394 | {:error, _reason} -> {:error, :invalid_channel_number} 395 | end 396 | end 397 | 398 | defp assign_channel(ip_addr, port, number, state) do 399 | addr = {ip_addr, port} 400 | cur_addr = Map.get(state.chann_to_addr, number) 401 | cur_number = Map.get(state.addr_to_chann, addr) 402 | 403 | with true <- number in 0x4000..0x7FFE, 404 | {:num, true} <- {:num, is_nil(cur_addr) or cur_addr == addr}, 405 | {:addr, true} <- {:addr, is_nil(cur_number) or cur_number == number} do 406 | state = 407 | state 408 | |> put_in([:chann_to_addr, number], addr) 409 | |> put_in([:addr_to_chann, addr], number) 410 | |> put_in([:chann_to_time, number], System.os_time(:second) + @channel_lifetime) 411 | 412 | Process.send_after(self(), {:check_channel, number}, @channel_lifetime) 413 | 414 | {:ok, state} 415 | else 416 | false -> {:error, :channel_number_out_of_range} 417 | {:num, false} -> {:error, :channel_number_bound} 418 | {:addr, false} -> {:error, :addr_bound_to_channel} 419 | end 420 | end 421 | end 422 | -------------------------------------------------------------------------------- /lib/rel/attributes/additional-address-family.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.AdditionalAddressFamily do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x8000 8 | 9 | @type t() :: %__MODULE__{ 10 | family: :ipv4 | :ipv6 11 | } 12 | 13 | @enforce_keys [:family] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def from_raw(%RawAttribute{} = raw_attr, _msg) do 21 | decode(raw_attr.value) 22 | end 23 | 24 | defp decode(<<0x01, 0, 0, 0>>), do: {:ok, %__MODULE__{family: :ipv4}} 25 | defp decode(<<0x02, 0, 0, 0>>), do: {:ok, %__MODULE__{family: :ipv6}} 26 | defp decode(_other), do: {:error, :invalid_additional_address_family} 27 | end 28 | -------------------------------------------------------------------------------- /lib/rel/attributes/channel_number.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.ChannelNumber do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x000C 8 | 9 | @type t() :: %__MODULE__{ 10 | number: integer() 11 | } 12 | 13 | @enforce_keys [:number] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def from_raw(%RawAttribute{value: <>}, _msg) do 21 | {:ok, %__MODULE__{number: number}} 22 | end 23 | 24 | @impl true 25 | def from_raw(%RawAttribute{}, _msg) do 26 | {:error, :invalid_channel_number} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rel/attributes/data.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.Data do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x0013 8 | 9 | @type t() :: %__MODULE__{ 10 | value: binary() 11 | } 12 | 13 | @enforce_keys [:value] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def from_raw(%RawAttribute{} = raw_attr, _message) do 21 | {:ok, %__MODULE__{value: raw_attr.value}} 22 | end 23 | 24 | @impl true 25 | def to_raw(%__MODULE__{value: value}, _message) do 26 | %RawAttribute{type: @attr_type, value: value} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rel/attributes/even_port.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.EvenPort do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x0018 8 | 9 | @type t() :: %__MODULE__{ 10 | r: boolean() 11 | } 12 | 13 | @enforce_keys [:r] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def from_raw(%RawAttribute{} = raw_attr, _message) do 21 | decode(raw_attr.value) 22 | end 23 | 24 | defp decode(<<1::1, 0::7>>), do: {:ok, %__MODULE__{r: true}} 25 | defp decode(<<0::1, 0::7>>), do: {:ok, %__MODULE__{r: false}} 26 | defp decode(_other), do: {:error, :invalid_even_port} 27 | end 28 | -------------------------------------------------------------------------------- /lib/rel/attributes/lifetime.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.Lifetime do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x000D 8 | 9 | @type t() :: %__MODULE__{ 10 | lifetime: integer() 11 | } 12 | 13 | @enforce_keys [:lifetime] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def to_raw(%__MODULE__{} = attr, _msg) do 21 | %RawAttribute{type: @attr_type, value: <>} 22 | end 23 | 24 | @impl true 25 | def from_raw(%RawAttribute{} = raw_attr, _msg) do 26 | decode(raw_attr.value) 27 | end 28 | 29 | defp decode(<>) do 30 | {:ok, %__MODULE__{lifetime: lifetime}} 31 | end 32 | 33 | defp decode(_data), do: {:error, :invalid_lifetime} 34 | end 35 | -------------------------------------------------------------------------------- /lib/rel/attributes/requested_address_family.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.RequestedAddressFamily do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x0017 8 | 9 | @type t() :: %__MODULE__{ 10 | family: :ipv4 | :ipv6 11 | } 12 | 13 | @enforce_keys [:family] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def from_raw(%RawAttribute{} = raw_attr, _message) do 21 | decode(raw_attr.value) 22 | end 23 | 24 | defp decode(<<0x01, 0, 0, 0>>), do: {:ok, %__MODULE__{family: :ipv4}} 25 | defp decode(<<0x02, 0, 0, 0>>), do: {:ok, %__MODULE__{family: :ipv6}} 26 | defp decode(_other), do: {:error, :invalid_requested_address_family} 27 | end 28 | -------------------------------------------------------------------------------- /lib/rel/attributes/requested_transport.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.RequestedTransport do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x0019 8 | 9 | @type t() :: %__MODULE__{ 10 | protocol: :udp | :tcp 11 | } 12 | 13 | @enforce_keys [:protocol] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def from_raw(%RawAttribute{} = raw_attr, _message) do 21 | decode(raw_attr.value) 22 | end 23 | 24 | defp decode(<<17, 0, 0, 0>>), do: {:ok, %__MODULE__{protocol: :udp}} 25 | defp decode(<<6, 0, 0, 0>>), do: {:ok, %__MODULE__{protocol: :tcp}} 26 | defp decode(_other), do: {:error, :invalid_requested_transport} 27 | end 28 | -------------------------------------------------------------------------------- /lib/rel/attributes/reservation_token.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.ReservationToken do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.RawAttribute 6 | 7 | @attr_type 0x0022 8 | 9 | @type t() :: %__MODULE__{ 10 | token: binary() 11 | } 12 | 13 | @enforce_keys [:token] 14 | defstruct @enforce_keys 15 | 16 | @impl true 17 | def type(), do: @attr_type 18 | 19 | @impl true 20 | def from_raw(%RawAttribute{} = raw_attr, _message) do 21 | decode(raw_attr.value) 22 | end 23 | 24 | defp decode(<>), do: {:ok, %__MODULE__{token: token}} 25 | defp decode(_other), do: {:error, :invalid_reservation_token} 26 | end 27 | -------------------------------------------------------------------------------- /lib/rel/attributes/xor_peer_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.XORPeerAddress do 2 | @moduledoc """ 3 | STUN Message Attribute XOR Peer Address 4 | 5 | It is encoded in the same way as XOR Mapped Address. 6 | """ 7 | @behaviour ExSTUN.Message.Attribute 8 | 9 | alias ExSTUN.Message.Attribute.XORMappedAddress 10 | alias ExSTUN.Message.RawAttribute 11 | 12 | @attr_type 0x0012 13 | 14 | @type t() :: %__MODULE__{ 15 | port: 0..65_535, 16 | address: :inet.ip_address() 17 | } 18 | 19 | @enforce_keys [:port, :address] 20 | defstruct @enforce_keys 21 | 22 | @impl true 23 | def type(), do: @attr_type 24 | 25 | @impl true 26 | def from_raw(%RawAttribute{} = raw_attr, message) do 27 | case XORMappedAddress.from_raw(raw_attr, message) do 28 | {:ok, xor_addr} -> 29 | {:ok, %__MODULE__{port: xor_addr.port, address: xor_addr.address}} 30 | 31 | error -> 32 | error 33 | end 34 | end 35 | 36 | @impl true 37 | def to_raw(%__MODULE__{} = attribute, message) do 38 | mapped_address = %XORMappedAddress{ 39 | port: attribute.port, 40 | address: attribute.address 41 | } 42 | 43 | raw = XORMappedAddress.to_raw(mapped_address, message) 44 | 45 | %RawAttribute{raw | type: @attr_type} 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/rel/attributes/xor_relayed_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Attribute.XORRelayedAddress do 2 | @moduledoc false 3 | @behaviour ExSTUN.Message.Attribute 4 | 5 | alias ExSTUN.Message.Attribute.XORMappedAddress 6 | alias ExSTUN.Message.RawAttribute 7 | 8 | @attr_type 0x0016 9 | 10 | @type t() :: %__MODULE__{ 11 | port: 0..65_535, 12 | address: :inet.ip_address() 13 | } 14 | 15 | @enforce_keys [:port, :address] 16 | defstruct @enforce_keys 17 | 18 | @impl true 19 | def type(), do: @attr_type 20 | 21 | @impl true 22 | def to_raw(%__MODULE__{} = attribute, message) do 23 | mapped_address = %XORMappedAddress{ 24 | port: attribute.port, 25 | address: attribute.address 26 | } 27 | 28 | raw = XORMappedAddress.to_raw(mapped_address, message) 29 | 30 | %RawAttribute{raw | type: @attr_type} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/rel/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Auth do 2 | @moduledoc false 3 | require Logger 4 | 5 | alias ExSTUN.Message 6 | alias ExSTUN.Message.Attribute.{MessageIntegrity, Nonce, Realm, Username} 7 | 8 | @nonce_lifetime Application.compile_env!(:rel, :nonce_lifetime) 9 | @credentials_lifetime Application.compile_env!(:rel, :credentials_lifetime) 10 | 11 | @spec authenticate(Message.t(), username: String.t()) :: {:ok, binary()} | {:error, atom()} 12 | def authenticate(%Message{} = msg, opts \\ []) do 13 | auth_secret = Application.fetch_env!(:rel, :auth_secret) 14 | 15 | with :ok <- verify_message_integrity(msg), 16 | {:ok, username, nonce} <- verify_attrs_presence(msg), 17 | :ok <- verify_username(msg.type.method, username, opts), 18 | :ok <- verify_nonce(nonce), 19 | password <- :crypto.mac(:hmac, :sha, auth_secret, username) |> :base64.encode(), 20 | {:ok, key} <- Message.authenticate_lt(msg, password) do 21 | {:ok, key} 22 | else 23 | {:error, _reason} = err -> err 24 | end 25 | end 26 | 27 | defp verify_message_integrity(msg) do 28 | case Message.get_attribute(msg, MessageIntegrity) do 29 | {:ok, %MessageIntegrity{}} -> :ok 30 | nil -> {:error, :no_message_integrity} 31 | end 32 | end 33 | 34 | defp verify_attrs_presence(msg) do 35 | with {:ok, %Username{value: username}} <- Message.get_attribute(msg, Username), 36 | {:ok, %Realm{value: _realm}} <- Message.get_attribute(msg, Realm), 37 | {:ok, %Nonce{value: nonce}} <- Message.get_attribute(msg, Nonce) do 38 | {:ok, username, nonce} 39 | else 40 | nil -> {:error, :auth_attrs_missing} 41 | end 42 | end 43 | 44 | defp verify_username(:allocate, username, _opts) do 45 | # authentication method described in https://datatracker.ietf.org/doc/html/draft-uberti-rtcweb-turn-rest-00 46 | with [expiry_time | _rest] <- String.split(username, ":", parts: 2), 47 | {expiry_time, _rem} <- Integer.parse(expiry_time, 10), 48 | false <- expiry_time - System.os_time(:second) <= 0 do 49 | :ok 50 | else 51 | _other -> {:error, :invalid_username_timestamp} 52 | end 53 | end 54 | 55 | defp verify_username(_method, username, opts) do 56 | valid_username = Keyword.fetch!(opts, :username) 57 | if username != valid_username, do: {:error, :invalid_username}, else: :ok 58 | end 59 | 60 | defp verify_nonce(nonce) do 61 | [timestamp, hash] = 62 | nonce 63 | |> :base64.decode() 64 | |> String.split(" ", parts: 2) 65 | 66 | nonce_secret = Application.fetch_env!(:rel, :nonce_secret) 67 | 68 | is_hash_valid? = hash == :crypto.hash(:sha256, "#{timestamp}:#{nonce_secret}") 69 | 70 | is_stale? = 71 | String.to_integer(timestamp) + @nonce_lifetime < System.monotonic_time(:nanosecond) 72 | 73 | if is_hash_valid? and not is_stale?, do: :ok, else: {:error, :stale_nonce} 74 | end 75 | 76 | @spec generate_credentials(String.t() | nil) :: 77 | {username :: String.t(), password :: String.t(), ttl :: non_neg_integer()} 78 | def generate_credentials(username \\ nil) do 79 | auth_secret = Application.fetch_env!(:rel, :auth_secret) 80 | timestamp = System.os_time(:second) + @credentials_lifetime 81 | 82 | username = if is_nil(username), do: "#{timestamp}", else: "#{timestamp}:#{username}" 83 | password = :crypto.mac(:hmac, :sha, auth_secret, username) |> :base64.encode() 84 | 85 | {username, password, @credentials_lifetime} 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/rel/auth_provider.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.AuthProvider do 2 | @moduledoc false 3 | # REST service described in https://datatracker.ietf.org/doc/html/draft-uberti-rtcweb-turn-rest-00 4 | defmodule ConditionalCORSPlug do 5 | @moduledoc false 6 | import Plug.Conn 7 | 8 | def init(_opts), do: [] 9 | 10 | def call(conn, _opts) do 11 | allow? = Application.fetch_env!(:rel, :auth_allow_cors?) 12 | 13 | if allow? do 14 | CORSPlug.call(conn, CORSPlug.init([])) 15 | else 16 | conn 17 | end 18 | end 19 | end 20 | 21 | use Plug.Router 22 | 23 | require Logger 24 | 25 | alias Rel.Auth 26 | 27 | plug(ConditionalCORSPlug) 28 | plug(:match) 29 | plug(:dispatch) 30 | 31 | post "/" do 32 | Logger.info("Received credential generation request from #{:inet.ntoa(conn.remote_ip)}") 33 | 34 | with conn <- fetch_query_params(conn), 35 | %{query_params: query_params} <- conn, 36 | %{"service" => "turn"} <- query_params do 37 | username = Map.get(query_params, "username") 38 | {username, password, ttl} = Auth.generate_credentials(username) 39 | 40 | ip_addr = Application.fetch_env!(:rel, :external_listen_ip) 41 | port = Application.fetch_env!(:rel, :listen_port) 42 | 43 | response = 44 | Jason.encode!(%{ 45 | "username" => username, 46 | "password" => password, 47 | "ttl" => ttl, 48 | "uris" => ["turn:#{:inet.ntoa(ip_addr)}:#{port}?transport=udp"] 49 | }) 50 | 51 | Logger.info("Generated credentials for #{:inet.ntoa(conn.remote_ip)}") 52 | 53 | conn 54 | |> put_resp_content_type("application/json") 55 | |> send_resp(200, response) 56 | else 57 | _other -> 58 | conn = fetch_query_params(conn) 59 | 60 | Logger.info( 61 | "Invalid credential request from #{:inet.ntoa(conn.remote_ip)}, query params: #{inspect(conn.query_params)}" 62 | ) 63 | 64 | send_resp(conn, 400, "invalid request") 65 | end 66 | end 67 | 68 | match _ do 69 | send_resp(conn, 400, "invalid request") 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/rel/listener.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Listener do 2 | @moduledoc false 3 | use Task, restart: :permanent 4 | 5 | require Logger 6 | 7 | alias Rel.Attribute.{ 8 | EvenPort, 9 | Lifetime, 10 | RequestedAddressFamily, 11 | RequestedTransport, 12 | ReservationToken, 13 | XORRelayedAddress 14 | } 15 | 16 | alias Rel.AllocationHandler 17 | alias Rel.Auth 18 | alias Rel.Utils 19 | 20 | alias ExSTUN.Message 21 | alias ExSTUN.Message.Type 22 | alias ExSTUN.Message.Attribute.{Username, XORMappedAddress} 23 | 24 | @buf_size 2 * 1024 * 1024 25 | 26 | @spec start_link(term()) :: {:ok, pid()} 27 | def start_link(args) do 28 | Task.start_link(__MODULE__, :listen, args) 29 | end 30 | 31 | @spec listen(:inet.ip_address(), :inet.port_number(), integer()) :: :ok 32 | def listen(ip, port, id) do 33 | listener_addr = "#{:inet.ntoa(ip)}:#{port}/UDP" 34 | 35 | Logger.info("Listener #{id} started on: #{listener_addr}") 36 | Logger.metadata(listener: listener_addr) 37 | 38 | {:ok, socket} = 39 | :socket.open(:inet, :dgram, :udp) 40 | 41 | :ok = :socket.setopt(socket, {:socket, :reuseport}, true) 42 | :ok = :socket.setopt(socket, {:socket, :rcvbuf}, @buf_size) 43 | :ok = :socket.setopt(socket, {:socket, :sndbuf}, @buf_size) 44 | :ok = :socket.bind(socket, %{family: :inet, addr: ip, port: port}) 45 | 46 | spawn(Rel.Monitor, :start, [self(), socket]) 47 | 48 | recv_loop(socket, id) 49 | end 50 | 51 | defp recv_loop(socket, id) do 52 | case :socket.recvfrom(socket) do 53 | {:ok, {%{addr: client_addr, port: client_port}, packet}} -> 54 | :telemetry.execute([:listener, :client], %{inbound: byte_size(packet)}, %{listener_id: id}) 55 | 56 | process(socket, client_addr, client_port, packet) 57 | recv_loop(socket, id) 58 | 59 | {:error, reason} -> 60 | Logger.error("Couldn't receive from the socket, reason: #{inspect(reason)}") 61 | end 62 | end 63 | 64 | defp process(socket, client_ip, client_port, <> = packet) do 65 | {server_ip, server_port} = {{0, 0, 0, 0}, 3478} 66 | five_tuple = {client_ip, client_port, server_ip, server_port, :udp} 67 | 68 | Logger.metadata(client: "#{:inet.ntoa(client_ip)}:#{client_port}") 69 | 70 | # TODO: according to RFCs, unknown comprehension-required 71 | # attributes should result in error response 420, but oh well 72 | case two_bits do 73 | 0 -> 74 | case Message.decode(packet) do 75 | {:ok, msg} -> 76 | handle_stun_message(socket, five_tuple, msg) 77 | 78 | {:error, reason} -> 79 | Logger.warning( 80 | "Failed to decode STUN packet, reason: #{inspect(reason)}, packet: #{inspect(packet)}" 81 | ) 82 | end 83 | 84 | 1 -> 85 | handle_channel_message(five_tuple, packet) 86 | 87 | _other -> 88 | Logger.warning( 89 | "Received packet that is neither STUN formatted nor ChannelData, packet: #{inspect(packet)}" 90 | ) 91 | end 92 | 93 | Logger.metadata(client: nil) 94 | end 95 | 96 | defp handle_stun_message( 97 | socket, 98 | five_tuple, 99 | %Message{type: %Type{class: :request, method: :binding}} = msg 100 | ) do 101 | Logger.info("Received 'binding' request") 102 | {c_ip, c_port, _, _, _} = five_tuple 103 | 104 | type = %Type{class: :success_response, method: :binding} 105 | 106 | response = 107 | msg.transaction_id 108 | |> Message.new(type, [ 109 | %XORMappedAddress{port: c_port, address: c_ip} 110 | ]) 111 | |> Message.encode() 112 | 113 | :ok = :socket.sendto(socket, response, %{family: :inet, addr: c_ip, port: c_port}) 114 | end 115 | 116 | defp handle_stun_message( 117 | socket, 118 | five_tuple, 119 | %Message{type: %Type{class: :request, method: :allocate}} = msg 120 | ) do 121 | Logger.info("Received new allocation request") 122 | {c_ip, c_port, _, _, _} = five_tuple 123 | 124 | handle_error = fn reason, socket, c_ip, c_port, msg -> 125 | {response, log_msg} = Utils.build_error(reason, msg.transaction_id, msg.type.method) 126 | Logger.warning(log_msg) 127 | :ok = :socket.sendto(socket, response, %{family: :inet, addr: c_ip, port: c_port}) 128 | end 129 | 130 | with {:ok, key} <- Auth.authenticate(msg), 131 | :ok <- refute_allocation(five_tuple), 132 | :ok <- check_requested_transport(msg), 133 | :ok <- check_dont_fragment(msg), 134 | {:ok, even_port} <- get_even_port(msg), 135 | {:ok, req_family} <- get_requested_address_family(msg), 136 | :ok <- check_reservation_token(msg, even_port, req_family), 137 | :ok <- check_family(req_family), 138 | :ok <- check_even_port(even_port), 139 | {:ok, alloc_port} <- get_available_port(), 140 | {:ok, lifetime} <- Utils.get_lifetime(msg) do 141 | relay_ip = Application.fetch_env!(:rel, :relay_ip) 142 | external_relay_ip = Application.fetch_env!(:rel, :external_relay_ip) 143 | 144 | type = %Type{class: :success_response, method: msg.type.method} 145 | 146 | response = 147 | msg.transaction_id 148 | |> Message.new(type, [ 149 | %XORRelayedAddress{port: alloc_port, address: external_relay_ip}, 150 | %Lifetime{lifetime: lifetime}, 151 | %XORMappedAddress{port: c_port, address: c_ip} 152 | ]) 153 | |> Message.with_integrity(key) 154 | |> Message.encode() 155 | 156 | {:ok, alloc_socket} = 157 | :gen_udp.open( 158 | alloc_port, 159 | [ 160 | {:inet_backend, :socket}, 161 | {:ifaddr, relay_ip}, 162 | {:active, true}, 163 | {:recbuf, 1024 * 1024}, 164 | :binary 165 | ] 166 | ) 167 | 168 | Logger.info("Succesfully created allocation, relay port: #{alloc_port}") 169 | 170 | {:ok, %Username{value: username}} = Message.get_attribute(msg, Username) 171 | 172 | {:ok, alloc_pid} = 173 | DynamicSupervisor.start_child( 174 | Rel.AllocationSupervisor, 175 | {Rel.AllocationHandler, 176 | [ 177 | five_tuple: five_tuple, 178 | alloc_socket: alloc_socket, 179 | turn_socket: socket, 180 | username: username, 181 | time_to_expiry: lifetime, 182 | t_id: msg.transaction_id, 183 | response: response 184 | ]} 185 | ) 186 | 187 | :ok = :gen_udp.controlling_process(alloc_socket, alloc_pid) 188 | 189 | :ok = :socket.sendto(socket, response, %{family: :inet, addr: c_ip, port: c_port}) 190 | else 191 | {:error, :allocation_exists, %{t_id: origin_t_id, response: origin_response}} 192 | when origin_t_id == msg.transaction_id -> 193 | Logger.info("Allocation request retransmission") 194 | # Section 6.2 suggests we should adjust LIFETIME attribute 195 | # but this would require asking allocation process for the 196 | # current time-to-expiry or saving additional fields in the 197 | # origin_alloc_state. In most cases, this shouldn't be a problem as 198 | # client is encouraged to refresh its allocation one minute 199 | # before its deadline 200 | :ok = :socket.sendto(socket, origin_response, %{family: :inet, addr: c_ip, port: c_port}) 201 | 202 | {:error, :allocation_exists, _alloc_origin_state} -> 203 | handle_error.(:allocation_exists, socket, c_ip, c_port, msg) 204 | 205 | {:error, reason} -> 206 | handle_error.(reason, socket, c_ip, c_port, msg) 207 | end 208 | end 209 | 210 | defp handle_stun_message(socket, five_tuple, msg) do 211 | case fetch_allocation(five_tuple) do 212 | {:ok, alloc, _alloc_origin_state} -> 213 | AllocationHandler.process_stun_message(alloc, msg) 214 | 215 | {:error, :allocation_not_found = reason} -> 216 | {c_ip, c_port, _, _, _} = five_tuple 217 | {response, _log_msg} = Utils.build_error(reason, msg.transaction_id, msg.type.method) 218 | 219 | Logger.warning( 220 | "No allocation and this is not an 'allocate'/'binding' request, message: #{inspect(msg)}" 221 | ) 222 | 223 | # TODO: should this be explicit or maybe silent? 224 | :ok = :socket.sendto(socket, response, %{family: :inet, addr: c_ip, port: c_port}) 225 | end 226 | end 227 | 228 | defp handle_channel_message(five_tuple, <>) do 229 | # TODO: are Registry entries removed fast enough? 230 | case fetch_allocation(five_tuple) do 231 | {:ok, alloc, _alloc_origin_state} -> 232 | AllocationHandler.process_channel_message(alloc, msg) 233 | 234 | {:error, :allocation_not_found} -> 235 | # TODO: should this be silent? 236 | Logger.warning("No allocation and is not a STUN message, silently discarded") 237 | end 238 | end 239 | 240 | defp fetch_allocation(five_tuple) do 241 | case Registry.lookup(Registry.Allocations, five_tuple) do 242 | [{alloc, alloc_origin_state}] -> {:ok, alloc, alloc_origin_state} 243 | [] -> {:error, :allocation_not_found} 244 | end 245 | end 246 | 247 | defp refute_allocation(five_tuple) do 248 | case fetch_allocation(five_tuple) do 249 | {:error, :allocation_not_found} -> :ok 250 | {:ok, _alloc, alloc_origin_state} -> {:error, :allocation_exists, alloc_origin_state} 251 | end 252 | end 253 | 254 | defp check_requested_transport(msg) do 255 | case Message.get_attribute(msg, RequestedTransport) do 256 | {:ok, %RequestedTransport{protocol: :udp}} -> :ok 257 | {:ok, %RequestedTransport{protocol: :tcp}} -> {:error, :requested_transport_tcp} 258 | _other -> {:error, :invalid_requested_transport} 259 | end 260 | end 261 | 262 | defp check_dont_fragment(_msg) do 263 | # TODO: not supported at the moment, proabably should return 420 error 264 | :ok 265 | end 266 | 267 | defp get_even_port(msg) do 268 | case Message.get_attribute(msg, EvenPort) do 269 | {:error, _reason} -> {:error, :invalid_even_port} 270 | {:ok, even_port} -> {:ok, even_port} 271 | nil -> {:ok, nil} 272 | end 273 | end 274 | 275 | defp get_requested_address_family(msg) do 276 | case Message.get_attribute(msg, RequestedAddressFamily) do 277 | {:error, _reason} -> {:error, :invalid_requested_address_family} 278 | {:ok, requested_address_family} -> {:ok, requested_address_family} 279 | nil -> {:ok, nil} 280 | end 281 | end 282 | 283 | defp check_reservation_token(msg, even_port, req_family) do 284 | case Message.get_attribute(msg, ReservationToken) do 285 | {:ok, _reservation_token} -> 286 | if Enum.any?([even_port, req_family], &(&1 != nil)) do 287 | {:error, :reservation_token_with_others} 288 | else 289 | # TODO: implement reservation_token 290 | {:error, :reservation_token_unsupported} 291 | end 292 | 293 | {:error, _reason} -> 294 | {:error, :invalid_reservation_token} 295 | 296 | nil -> 297 | :ok 298 | end 299 | end 300 | 301 | defp check_family(req_family) 302 | when req_family in [nil, %RequestedAddressFamily{family: :ipv4}], 303 | do: :ok 304 | 305 | defp check_family(%RequestedAddressFamily{family: :ipv6}) do 306 | # TODO: implement requested address family 307 | {:error, :requested_address_family_unsupported} 308 | end 309 | 310 | defp check_even_port(nil), do: :ok 311 | 312 | defp check_even_port(%EvenPort{}) do 313 | # TODO: implement even port 314 | {:error, :even_port_unsupported} 315 | end 316 | 317 | defp get_available_port() do 318 | used_alloc_ports = 319 | Registry.Allocations 320 | |> Registry.select([{{:_, :_, :"$3"}, [], [:"$3"]}]) 321 | |> Enum.map(fn alloc_origin_state -> Map.fetch!(alloc_origin_state, :alloc_port) end) 322 | |> MapSet.new() 323 | 324 | relay_port_start = Application.fetch_env!(:rel, :relay_port_start) 325 | relay_port_end = Application.fetch_env!(:rel, :relay_port_end) 326 | default_alloc_ports = MapSet.new(relay_port_start..relay_port_end) 327 | available_alloc_ports = MapSet.difference(default_alloc_ports, used_alloc_ports) 328 | 329 | if MapSet.size(available_alloc_ports) == 0 do 330 | {:error, :out_of_ports} 331 | else 332 | {:ok, Enum.random(available_alloc_ports)} 333 | end 334 | end 335 | end 336 | -------------------------------------------------------------------------------- /lib/rel/listener_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.ListenerSupervisor do 2 | @moduledoc false 3 | use Supervisor 4 | 5 | @spec start_link(any()) :: Supervisor.on_start() 6 | def start_link(_arg) do 7 | Supervisor.start_link(__MODULE__, nil, name: __MODULE__) 8 | end 9 | 10 | @impl true 11 | def init(_arg) do 12 | listen_ip = Application.fetch_env!(:rel, :listen_ip) 13 | listen_port = Application.fetch_env!(:rel, :listen_port) 14 | listener_count = Application.fetch_env!(:rel, :listener_count) 15 | 16 | children = 17 | for id <- 1..listener_count do 18 | Supervisor.child_spec({Rel.Listener, [listen_ip, listen_port, id]}, 19 | id: "listener_#{id}" 20 | ) 21 | end 22 | 23 | Supervisor.init(children, strategy: :one_for_one) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rel/monitor.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Monitor do 2 | @moduledoc false 3 | require Logger 4 | 5 | @spec start(pid(), :socket.socket()) :: :ok 6 | def start(pid, socket) do 7 | ref = Process.monitor(pid) 8 | 9 | receive do 10 | {:DOWN, ^ref, ^pid, _object, _reason} -> 11 | Logger.info("Closing socket #{inspect(socket)}") 12 | :ok = :socket.close(socket) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rel/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.Utils do 2 | @moduledoc false 3 | require Logger 4 | 5 | alias ExSTUN.Message 6 | alias ExSTUN.Message.Method 7 | alias ExSTUN.Message.Type 8 | alias ExSTUN.Message.Attribute.{ErrorCode, Nonce, Realm} 9 | 10 | alias Rel.Attribute.Lifetime 11 | 12 | @default_lifetime Application.compile_env!(:rel, :default_allocation_lifetime) 13 | @max_lifetime Application.compile_env!(:rel, :max_allocation_lifetime) 14 | 15 | @spec get_lifetime(Message.t()) :: {:ok, integer()} | {:error, :invalid_lifetime} 16 | def get_lifetime(msg) do 17 | case Message.get_attribute(msg, Lifetime) do 18 | {:ok, %Lifetime{lifetime: lifetime}} -> 19 | desired_lifetime = 20 | if lifetime == 0, do: 0, else: max(@default_lifetime, min(lifetime, @max_lifetime)) 21 | 22 | {:ok, desired_lifetime} 23 | 24 | nil -> 25 | {:ok, @default_lifetime} 26 | 27 | {:error, _reason} -> 28 | {:error, :invalid_lifetime} 29 | end 30 | end 31 | 32 | @spec build_error(atom(), integer(), Method.t()) :: 33 | {response :: binary(), log_msg :: String.t()} 34 | def build_error(reason, t_id, method) do 35 | realm = Application.fetch_env!(:rel, :realm) 36 | {log_msg, code, with_attrs?} = translate_error(reason) 37 | error_type = %Type{class: :error_response, method: method} 38 | 39 | attrs = [%ErrorCode{code: code}] 40 | 41 | attrs = 42 | if with_attrs? do 43 | attrs ++ [%Nonce{value: build_nonce()}, %Realm{value: realm}] 44 | else 45 | attrs 46 | end 47 | 48 | response = 49 | t_id 50 | |> Message.new(error_type, attrs) 51 | |> Message.encode() 52 | 53 | {response, log_msg <> ", rejected"} 54 | end 55 | 56 | defp build_nonce() do 57 | # inspired by https://datatracker.ietf.org/doc/html/rfc7616#section-5.4 58 | nonce_secret = Application.fetch_env!(:rel, :nonce_secret) 59 | timestamp = System.monotonic_time(:nanosecond) 60 | hash = :crypto.hash(:sha256, "#{timestamp}:#{nonce_secret}") 61 | "#{timestamp} #{hash}" |> :base64.encode() 62 | end 63 | 64 | defp translate_error(:allocation_not_found), 65 | do: {"Allocation mismatch: allocation does not exist", 437, false} 66 | 67 | defp translate_error(:allocation_exists), 68 | do: {"Allocation mismatch: allocation already exists", 437, false} 69 | 70 | defp translate_error(:requested_transport_tcp), 71 | do: {"Unsupported REQUESTED-TRANSPORT: TCP", 442, false} 72 | 73 | defp translate_error(:invalid_requested_transport), 74 | do: {"No or malformed REQUESTED-TRANSPORT", 400, false} 75 | 76 | defp translate_error(:invalid_even_port), 77 | do: {"Failed to decode EVEN-PORT", 400, false} 78 | 79 | defp translate_error(:invalid_requested_address_family), 80 | do: {"Failed to decode REQUESTED-ADDRESS-FAMILY", 400, false} 81 | 82 | defp translate_error(:reservation_token_with_others), 83 | do: {"RESERVATION-TOKEN and (EVEN-PORT|REQUESTED-FAMILY) in the message", 400, false} 84 | 85 | defp translate_error(:reservation_token_unsupported), 86 | do: {"RESERVATION-TOKEN unsupported", 400, false} 87 | 88 | defp translate_error(:invalid_reservation_token), 89 | do: {"Failed to decode RESERVATION-TOKEN", 400, false} 90 | 91 | defp translate_error(:requested_address_family_unsupported), 92 | do: {"REQUESTED-ADDRESS-FAMILY with IPv6 unsupported", 440, false} 93 | 94 | defp translate_error(:even_port_unsupported), 95 | do: {"EVEN-PORT unsupported", 400, false} 96 | 97 | defp translate_error(:out_of_ports), 98 | do: {"No available ports left", 508, false} 99 | 100 | defp translate_error(:invalid_lifetime), 101 | do: {"Failed to decode LIFETIME", 400, false} 102 | 103 | defp translate_error(:no_matching_message_integrity), 104 | do: {"Auth failed, invalid MESSAGE-INTEGRITY", 400, false} 105 | 106 | defp translate_error(:no_message_integrity), 107 | do: {"No message integrity attribute", 401, true} 108 | 109 | defp translate_error(:auth_attrs_missing), 110 | do: {"No username, nonce or realm attribute", 400, false} 111 | 112 | defp translate_error(:invalid_username_timestamp), 113 | do: {"Username timestamp expired", 401, true} 114 | 115 | defp translate_error(:invalid_username), 116 | do: {"Username differs from the one used previously", 441, true} 117 | 118 | defp translate_error(:stale_nonce), 119 | do: {"Stale nonce", 438, true} 120 | 121 | defp translate_error(:no_xor_peer_address_attribute), 122 | do: {"No XOR-PEER-ADDRESS attribute", 400, false} 123 | 124 | defp translate_error(:invalid_xor_peer_address), 125 | do: {"Failed to decode XOR-PEER-ADDRESS", 400, false} 126 | 127 | defp translate_error(:no_data_attribute), 128 | do: {"No DATA attribute", 400, false} 129 | 130 | defp translate_error(:invalid_data), 131 | do: {"Failed to decode DATA", 400, false} 132 | 133 | defp translate_error(:no_channel_number_attribute), 134 | do: {"No CHANNEL-NUMBER attribute", 400, false} 135 | 136 | defp translate_error(:invalid_channel_number), 137 | do: {"Failed to decode CHANNEL-NUMBER", 400, false} 138 | 139 | defp translate_error(:channel_number_out_of_range), 140 | do: {"Channel number is out of allowed range", 400, false} 141 | 142 | defp translate_error(:channel_number_bound), 143 | do: {"Channel number is already bound", 400, false} 144 | 145 | defp translate_error(:addr_bound_to_channel), 146 | do: {"Address is already bound to channel", 400, false} 147 | 148 | defp translate_error(other) do 149 | Logger.error("Unsupported error type: #{other}") 150 | {"", 500} 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/rel_app.ex: -------------------------------------------------------------------------------- 1 | defmodule Rel.App do 2 | @moduledoc false 3 | use Application 4 | 5 | require Logger 6 | 7 | @version Mix.Project.config()[:version] 8 | 9 | @impl true 10 | def start(_, _) do 11 | Logger.info("Starting Rel v#{@version}") 12 | 13 | auth_ip = Application.fetch_env!(:rel, :auth_ip) 14 | auth_port = Application.fetch_env!(:rel, :auth_port) 15 | auth_use_tls? = Application.fetch_env!(:rel, :auth_use_tls?) 16 | auth_keyfile = Application.fetch_env!(:rel, :auth_keyfile) 17 | auth_certfile = Application.fetch_env!(:rel, :auth_certfile) 18 | 19 | auth_opts = 20 | if auth_use_tls? do 21 | [ 22 | scheme: :https, 23 | certfile: auth_certfile, 24 | keyfile: auth_keyfile 25 | ] 26 | else 27 | [scheme: :http] 28 | end 29 | 30 | auth_opts = 31 | auth_opts ++ 32 | [plug: Rel.AuthProvider, ip: auth_ip, port: auth_port] 33 | 34 | metrics_ip = Application.fetch_env!(:rel, :metrics_ip) 35 | metrics_port = Application.fetch_env!(:rel, :metrics_port) 36 | metrics_opts = [metrics: metrics(), port: metrics_port, plug_cowboy_opts: [ip: metrics_ip]] 37 | 38 | children = [ 39 | Rel.ListenerSupervisor, 40 | {DynamicSupervisor, strategy: :one_for_one, name: Rel.AllocationSupervisor}, 41 | {Registry, keys: :unique, name: Registry.Allocations}, 42 | {TelemetryMetricsPrometheus, metrics_opts}, 43 | {Bandit, auth_opts} 44 | ] 45 | 46 | metrics_endpoint = "http://#{:inet.ntoa(metrics_ip)}:#{metrics_port}/metrics" 47 | Logger.info("Starting Prometheus metrics endpoint at: #{metrics_endpoint}") 48 | 49 | scheme = if(auth_use_tls?, do: "https", else: "http") 50 | auth_endpoint = "#{scheme}://#{:inet.ntoa(auth_ip)}:#{auth_port}/" 51 | Logger.info("Starting credentials endpoint at: #{auth_endpoint}") 52 | 53 | Supervisor.start_link(children, strategy: :one_for_one) 54 | end 55 | 56 | defp metrics() do 57 | import Telemetry.Metrics 58 | 59 | [ 60 | sum( 61 | "turn.allocations.created.total", 62 | event_name: [:allocations], 63 | measurement: :created 64 | ), 65 | sum( 66 | "turn.allocations.expired.total", 67 | event_name: [:allocations], 68 | measurement: :expired 69 | ), 70 | sum( 71 | "turn.listener.client_inbound_traffic.total.bytes", 72 | event_name: [:listener, :client], 73 | measurement: :inbound, 74 | unit: :byte, 75 | tags: [:listener_id] 76 | ), 77 | counter( 78 | "turn.listener.client_inbound_traffic.packets.total", 79 | event_name: [:listener, :client], 80 | measurement: :inbound, 81 | tags: [:listener_id] 82 | ), 83 | sum( 84 | "turn.allocations.peer_inbound_traffic.total.bytes", 85 | event_name: [:allocations, :peer], 86 | measurement: :inbound, 87 | unit: :byte 88 | ), 89 | counter( 90 | "turn.allocations.peer_inbound_traffic.packets.total", 91 | event_name: [:allocations, :peer], 92 | measurement: :inbound 93 | ), 94 | 95 | # telemetry poller 96 | last_value( 97 | "vm.memory.bytes", 98 | event_name: [:vm, :memory], 99 | measurement: :total, 100 | unit: :byte 101 | ), 102 | last_value( 103 | "vm.run_queue.cpu.length", 104 | event_name: [:vm, :total_run_queue_lengths], 105 | measurement: :cpu 106 | ), 107 | last_value( 108 | "vm.run_queue.io.length", 109 | event_name: [:vm, :total_run_queue_lengths], 110 | measurement: :io 111 | ) 112 | ] 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rel.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :rel, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | 12 | # code coverage 13 | test_coverage: [tool: ExCoveralls], 14 | preferred_cli_env: [ 15 | coveralls: :test, 16 | "coveralls.detail": :test, 17 | "coveralls.post": :test, 18 | "coveralls.html": :test, 19 | "coveralls.json": :test 20 | ] 21 | ] 22 | end 23 | 24 | # Run "mix help compile.app" to learn about applications. 25 | def application do 26 | [ 27 | mod: {Rel.App, []}, 28 | extra_applications: [:logger, :crypto] 29 | ] 30 | end 31 | 32 | # Run "mix help deps" to learn about dependencies. 33 | defp deps do 34 | [ 35 | {:toml, "~> 0.7"}, 36 | {:ex_stun, "~> 0.1"}, 37 | {:bandit, "~> 0.7"}, 38 | {:cors_plug, "~> 3.0"}, 39 | {:jason, "~> 1.4"}, 40 | 41 | # telemetry 42 | {:telemetry, "~> 1.0"}, 43 | {:telemetry_metrics, "~> 0.6.1"}, 44 | {:telemetry_metrics_prometheus, "~> 1.1"}, 45 | {:telemetry_poller, "~> 1.0"}, 46 | 47 | # dev 48 | {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, 49 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 50 | {:excoveralls, "~> 0.14.6", only: :test, runtime: false} 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bandit": {:hex, :bandit, "0.7.7", "48456d09022607a312cf723a91992236aeaffe4af50615e6e2d2e383fb6bef10", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 0.6.7", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "772f0a32632c2ce41026d85e24b13a469151bb8cea1891e597fb38fde103640a"}, 3 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 4 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 5 | "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, 6 | "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, 7 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 8 | "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, 9 | "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, 10 | "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, 11 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 12 | "ex_stun": {:hex, :ex_stun, "0.1.0", "252474bf4c8519fbf4bc0fbfc6a1b846a634b1478c65dbbfb4b6ab4e33c2a95a", [:mix], [], "hexpm", "629fc8be45b624a92522f81d85ba001877b1f0745889a2419bdb678790d7480c"}, 13 | "excoveralls": {:hex, :excoveralls, "0.14.6", "610e921e25b180a8538229ef547957f7e04bd3d3e9a55c7c5b7d24354abbba70", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0eceddaa9785cfcefbf3cd37812705f9d8ad34a758e513bb975b081dce4eb11e"}, 14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 15 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 16 | "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, 17 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 18 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 19 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 20 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 21 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 22 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 23 | "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, 24 | "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, 25 | "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, 26 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 27 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 28 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 29 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, 30 | "telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"}, 31 | "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"}, 32 | "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, 33 | "thousand_island": {:hex, :thousand_island, "0.6.7", "3a91a7e362ca407036c6691e8a4f6e01ac8e901db3598875863a149279ac8571", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "541a5cb26b88adf8d8180b6b96a90f09566b4aad7a6b3608dcac969648cf6765"}, 34 | "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, 35 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 36 | "websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"}, 37 | } 38 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 2s 3 | 4 | external_labels: 5 | monitor: 'codelab-monitor' 6 | 7 | scrape_configs: 8 | - job_name: 'rel' 9 | static_configs: 10 | - targets: ['127.0.0.1:9568'] 11 | 12 | - job_name: 'node' 13 | static_configs: 14 | - targets: ['127.0.0.1:9100'] 15 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | # Rel config env variables 2 | 3 | # Values presented in this example file are used by default 4 | # except where explicitly specified otherwise 5 | 6 | ## TURN 7 | 8 | # Server address and port on which Rel listens for TURN/STUN requests 9 | LISTEN_IP=0.0.0.0 10 | LISTEN_PORT=3478 11 | 12 | # Server address as seen from the client 13 | # By default it is equal to LISTEN_PORT or (if LISTEN_PORT == 0.0.0.0) Rel 14 | # will try to guess the address based on host's network interfaces 15 | # It must be explicitly set when e.g. running in Docker without `--network=host` 16 | # EXTERNAL_LISTEN_IP=167.235.241.140 17 | 18 | # Address and port range where relay address will be allocated 19 | RELAY_IP=0.0.0.0 20 | RELAY_PORT_START=49152 21 | RELAY_PORT_END=65535 22 | 23 | # Relay address as seen from peers 24 | # Behave the same way as EXTERNAL_LISTEN_IP 25 | # EXTERNAL_RELAY_IP=167.235.241.140 26 | 27 | # Values used in REALM STUN attribute, see https://datatracker.ietf.org/doc/html/rfc5389#section-15.7 28 | REALM=example.com 29 | 30 | # Number of running listener processes. By default equal to number of running Erlang VM schedulers 31 | # LISTENER_COUNT=8 32 | 33 | ## AUTH PROVIDER 34 | 35 | # Auth provider is available under http(s)://$AUTH_IP:$AUTH_PORT/ 36 | AUTH_IP=127.0.0.1 37 | AUTH_PORT=4000 38 | 39 | # whether to use HTTP or HTTPS 40 | # If true, AUTH_KEYFILE and AUTH_CERFILE must be explicitly set 41 | AUTH_USE_TLS=false 42 | # AUTH_KEYFILE=./rel.key 43 | # AUTH_CERTFILE=./rel.cert 44 | 45 | # Whether to allos Cross-Origin Resource Sharing 46 | # May be useful when requesting credentials via JavaScript in the browser 47 | AUTH_ALLOW_CORS=false 48 | 49 | ## METRICS 50 | 51 | # Prometheus metrics are served on http://$METRICS_IP:$METRICS_PORT/metrics 52 | METRICS_IP=127.0.0.1 53 | METRICS_PORT=9568 54 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------