├── .credo.exs ├── .dockerignore ├── .gitignore ├── .travis.yml ├── .travis ├── docker.sh └── script.sh ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── docker ├── Dockerfile.build ├── Dockerfile.release └── start.sh ├── lib ├── mongooseice.ex └── mongooseice │ ├── application.ex │ ├── auth.ex │ ├── evaluator.ex │ ├── evaluator │ ├── allocate │ │ └── request.ex │ ├── binding │ │ └── request.ex │ ├── channel_bind │ │ └── request.ex │ ├── channel_data.ex │ ├── create_permission │ │ └── request.ex │ ├── helper.ex │ ├── indication.ex │ ├── refresh │ │ └── request.ex │ ├── request.ex │ └── send │ │ └── indication.ex │ ├── helper.ex │ ├── reservation_log.ex │ ├── stun.ex │ ├── time.ex │ ├── turn.ex │ ├── turn │ ├── allocation.ex │ ├── channel.ex │ ├── reservation.ex │ └── reservation │ │ └── instance.ex │ ├── udp.ex │ └── udp │ ├── dispatcher.ex │ ├── receiver.ex │ ├── supervisor.ex │ ├── worker.ex │ └── worker_supervisor.ex ├── mix.exs ├── mix.lock ├── priv └── .keep ├── rel ├── config.exs ├── plugins │ └── .gitignore └── vm.args ├── static └── mongooseim_logo.png └── test ├── helper.ex ├── helper ├── allocation.ex ├── macros.ex ├── port_master.ex └── udp.ex ├── mongooseice └── udp │ ├── allocate_test.exs │ ├── auth_template.ex │ ├── auth_test.exs │ ├── binding_test.exs │ ├── channel_bind_test.exs │ ├── channel_data_test.exs │ ├── create_permission_test.exs │ ├── data_test.exs │ ├── refresh_test.exs │ ├── send_test.exs │ └── server_test.exs ├── mongooseice_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/", "test/"], 7 | excluded: [~r"/_build/", ~r"/deps/"] 8 | }, 9 | requires: [], 10 | strict: true, 11 | checks: [ 12 | {Credo.Check.Consistency.ExceptionNames}, 13 | {Credo.Check.Consistency.LineEndings}, 14 | {Credo.Check.Consistency.SpaceAroundOperators}, 15 | {Credo.Check.Consistency.SpaceInParentheses}, 16 | {Credo.Check.Consistency.TabsOrSpaces}, 17 | {Credo.Check.Design.DuplicatedCode, excluded_macros: [:setup, :test]}, 18 | {Credo.Check.Design.TagTODO, exit_status: 0}, 19 | {Credo.Check.Design.TagFIXME}, 20 | {Credo.Check.Design.AliasUsage, false}, 21 | {Credo.Check.Readability.FunctionNames}, 22 | {Credo.Check.Readability.LargeNumbers}, 23 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 90}, 24 | {Credo.Check.Readability.ModuleAttributeNames}, 25 | {Credo.Check.Readability.ModuleDoc}, 26 | {Credo.Check.Readability.ModuleNames}, 27 | {Credo.Check.Readability.ParenthesesInCondition}, 28 | {Credo.Check.Readability.PredicateFunctionNames}, 29 | {Credo.Check.Readability.TrailingBlankLine}, 30 | {Credo.Check.Readability.TrailingWhiteSpace}, 31 | {Credo.Check.Readability.VariableNames}, 32 | 33 | {Credo.Check.Refactor.ABCSize}, 34 | {Credo.Check.Refactor.CondStatements}, 35 | {Credo.Check.Refactor.FunctionArity}, 36 | {Credo.Check.Refactor.MatchInCondition}, 37 | {Credo.Check.Refactor.PipeChainStart}, 38 | {Credo.Check.Refactor.CyclomaticComplexity}, 39 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 40 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 41 | {Credo.Check.Refactor.Nesting}, 42 | {Credo.Check.Refactor.UnlessWithElse}, 43 | 44 | {Credo.Check.Warning.IExPry}, 45 | {Credo.Check.Warning.IoInspect}, 46 | {Credo.Check.Warning.NameRedeclarationByAssignment}, 47 | {Credo.Check.Warning.NameRedeclarationByCase}, 48 | {Credo.Check.Warning.NameRedeclarationByDef}, 49 | {Credo.Check.Warning.NameRedeclarationByFn}, 50 | {Credo.Check.Warning.OperationOnSameValues}, 51 | {Credo.Check.Warning.BoolOperationOnSameValues}, 52 | {Credo.Check.Warning.UnusedEnumOperation}, 53 | {Credo.Check.Warning.UnusedKeywordOperation}, 54 | {Credo.Check.Warning.UnusedListOperation}, 55 | {Credo.Check.Warning.UnusedStringOperation}, 56 | {Credo.Check.Warning.UnusedTupleOperation}, 57 | {Credo.Check.Warning.OperationWithConstantResult}, 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | _build 3 | cover 4 | deps 5 | doc 6 | test 7 | tmp 8 | erl_crash.dump 9 | *.ez 10 | -------------------------------------------------------------------------------- /.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 3rd-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 | # Emacs temporary files 23 | **/*~ 24 | **/#* 25 | **/.#* 26 | 27 | # Dialyzer PLTs 28 | /.dialyzer 29 | 30 | # Apple... 31 | **/.DS_Store 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | dist: trusty 4 | sudo: required 5 | services: 6 | - docker 7 | 8 | install: 9 | - mix local.rebar --force 10 | - mix local.hex --force 11 | - mix deps.get 12 | - mix compile 13 | - MIX_ENV=test mix compile 14 | 15 | branches: 16 | only: 17 | - master 18 | - stable 19 | - /^rel\-\d+\.\d+$/ 20 | - /^\d+\.\d+\.\d+([a-z0-9\-\+])*/ 21 | 22 | script: 23 | .travis/script.sh $PRESET 24 | 25 | after_script: 26 | mix inch.report 27 | 28 | after_success: 29 | - .travis/docker.sh 30 | 31 | elixir: 32 | - 1.8.0 33 | otp_release: 34 | - 22.0 35 | - 20.3 36 | env: 37 | - PRESET=test 38 | 39 | matrix: 40 | include: 41 | 42 | - otp_release: 20.3 43 | elixir: 1.6.0 44 | env: PRESET=test 45 | - otp_release: 22.0 46 | elixir: 1.9.0 47 | env: PRESET=test 48 | - otp_release: 22.0 49 | elixir: 1.8.0 50 | env: PRESET=dialyzer 51 | 52 | cache: 53 | directories: 54 | - .dialyzer 55 | -------------------------------------------------------------------------------- /.travis/docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | APP_NAME='mongooseice' 5 | 6 | # Skip this step for jobs that don't run exunit 7 | test "${PRESET}" == "test" || exit 0 8 | 9 | MIX_ENV=prod mix docker.build 10 | MIX_ENV=prod mix docker.release 11 | 12 | DOCKERHUB_TAG="${TRAVIS_BRANCH//\//-}" 13 | 14 | if [ "${TRAVIS_PULL_REQUEST}" != 'false' ]; then 15 | DOCKERHUB_TAG="PR-${TRAVIS_PULL_REQUEST}" 16 | elif [ "${TRAVIS_BRANCH}" == 'master' ]; then 17 | DOCKERHUB_TAG="latest"; 18 | fi 19 | 20 | TARGET_IMAGE="${DOCKERHUB_REPOSITORY}/${APP_NAME}:${DOCKERHUB_TAG}" 21 | 22 | if [ "${TRAVIS_SECURE_ENV_VARS}" == 'true' ]; then 23 | docker login -u "${DOCKERHUB_USER}" -p "${DOCKERHUB_PASS}" 24 | docker tag ${APP_NAME}:release "${TARGET_IMAGE}" 25 | docker push "${TARGET_IMAGE}" 26 | fi 27 | -------------------------------------------------------------------------------- /.travis/script.sh: -------------------------------------------------------------------------------- 1 | PRESET=$1 2 | 3 | case $PRESET in 4 | test) 5 | MIX_ENV=test mix coveralls.travis --include system 6 | ;; 7 | dialyzer) 8 | mix dialyzer 9 | ;; 10 | *) 11 | echo "Invalid preset: $PRESET" 12 | exit 1 13 | ;; 14 | esac 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## v0.4.0 - 2017-09-25 9 | 10 | ### Added 11 | * support for TURN channels mechanism 12 | 13 | ### Changed 14 | * renamed project to MongooseICE - a part of [MongooseIM](https://github.com/esl/MongooseIM) platform 15 | 16 | ## v0.3.0 - 2017-05-26 17 | 18 | ### Added 19 | * support for building MongooseICE as a standalone release 20 | * support for building MongooseICE as a Docker container 21 | * configuration of releases via system environment variables 22 | 23 | ## v0.2.0 - 2017-05-15 24 | 25 | ### Added 26 | * TURN relay over UDP with global secret authentication, support for reservations, 27 | and without channels mechanism 28 | 29 | ## v0.1.0 - 2017-02-21 30 | 31 | ### Added 32 | * STUN server using UDP transport 33 | * processing of Binding requests and indications 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Erlang Solutions Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MongooseICE 2 | 3 | [![Build Status][BUILD BADGE]][BUILD LINK] 4 | [![Coverage Status][COVERAGE BADGE]][COVERAGE LINK] 5 | 6 | [Documentation](https://hexdocs.pm/mongooseice/0.4.0) 7 | 8 | MongooseICE is a STUN server by [Erlang Solutions][OUR SITE] whose internals aim to be well written and tested. 9 | 10 | ## Rationale 11 | 12 | Many modern applications (mobile and web) are media intensive like those involving audio, video, gaming, and file transfer. 13 | MongooseICE helps to get communication done peer-to-peer (without going through a server) so your **bandwidth and server-side costs don't need to be as much of a concern**. 14 | 15 | ## Resources 16 | 17 | Some helpful technical material: 18 | 19 | * For the bigger picture see the **RTCPeerConnection plus servers** section under [this][OVERVIEW] tutorial 20 | * MongooseICE alone isn't enough to get peer-to-peer communication going. 21 | The reason why is described in [this][SIGNALING] tutorial. 22 | Our [XMPP server][MONGOOSE], MongooseIM, is perfect for building a combination of signaling and chat applications 23 | * Find the STUN, TURN, and ICE RFCs (at the IETF site) 24 | 25 | ### Installation as part of other application 26 | 27 | MongooseICE is available on [Hex](https://hex.pm/packages/mongooseice). To use it, just add it to your dependencies: 28 | 29 | ```elixir 30 | def deps do 31 | [ 32 | {:mongooseice, "~> 0.4.0"} 33 | ] 34 | end 35 | ``` 36 | 37 | ### Installation as standalone service 38 | 39 | For now there are two ways of starting `MongooseICE` as standalone application. Via release built from 40 | source or via prebuilt docker image. The docker image could be used for production system with a proper 41 | network setup (the easiest one would be `--net=host` docker option). For developement on non-docker-native platforms 42 | it is probably easier to start the built release then setup docker container to work correctly. 43 | This is due to the fact that TURN server uses system ephemeral port pool for allocations, which is 44 | not so easy to map to the host machine. This issue is not visible on Linux systems, since you 45 | can allow docker to use its private virtual network and just use the docker container's IP address 46 | as the relay IP (which is set this way in `MongooseICE` by default when using the docker image). 47 | 48 | #### Building and using a release 49 | 50 | You may build the release and use it on production system. In order to do that, just type: 51 | 52 | ```bash 53 | MIX_ENV=prod mix do deps.get, release 54 | ``` 55 | 56 | The release can be configured by environment variables described in **Configuration** section below. 57 | 58 | #### Using docker prebuilt container 59 | 60 | You can use our prebuilt docker images on our dockerhub: 61 | 62 | ```bash 63 | docker run -it -p 3478:3478/udp -e "MONGOOSEICE_STUN_SECRET=very_secret" mongooseim/mongooseice 64 | ``` 65 | 66 | This command will start the *MongooseICE* server with default configuration and with STUN secret set 67 | to *very_secret*. If you are using this on Linux, the part with `-p 3478:3478/udp` is not needed, since 68 | you can access the server directly using the container's IP. You can configure the server by passing 69 | environment variables to the container. All those variables are described in **Configuration** section below. 70 | 71 | #### Building docker container 72 | 73 | Well, that's gonna be quite simple and short: 74 | 75 | ```bash 76 | MIX_ENV=prod mix do deps.get, docker.build, docker.release 77 | ``` 78 | 79 | And that's it. You have just built `MongooseICE's` docker image. The name of the image should be 80 | visible at the end of the output of the command you've just run. You can configure the container by 81 | setting environment variables that are described in **Configuration** section below. 82 | 83 | #### Configuration 84 | 85 | Assuming you are using release built with env `prod` or the docker image, you will have access to 86 | the following system's environment viaribles: 87 | 88 | ##### General configuration 89 | 90 | * `MONGOOSEICE_LOGLEVEL` - `debug`/`info`/`warn`/`error` - Log level of the application. `info` is the default one 91 | * `MONGOOSEICE_UDP_ENABLED` - `true`/`false` - Enable or disable UDP STUN/TURN interface. Enabled by default 92 | * `MONGOOSEICE_TCP_ENABLED` - `true`/`false` - *Not yet supported* - Enable or disable TCP STUN/TURN interface. Disabled by default. 93 | * `MONGOOSEICE_STUN_SECRET` - Secret that STUN/TURN clients have to use to authorize with the server 94 | 95 | ##### UDP configuration 96 | 97 | The following variables configure UDP STUN/TURN interface. It must be enabled via `MONGOOSEICE_UDP_ENABLED=true` in order for those options to take effect. 98 | 99 | * `MONGOOSEICE_UDP_BIND_IP` - IP address on which MongooseICE listens for requests. Release default is `127.0.0.1`, but in case of docker container the default is `0.0.0.0` 100 | * `MONGOOSEICE_UDP_PORT` - Port which server listens on for STUN/TURN requests. Default is `3478` 101 | * `MONGOOSEICE_UDP_REALM` - Realm name for this MongooseICE server as defined in [TURN RFC](https://tools.ietf.org/rfc/rfc5766.txt). Default: `udp.localhost.local` 102 | * `MONGOOSEICE_UDP_RELAY_IP` - IP of the relay interface. All `allocate` requests will return this IP address to the client, therefore this cannot be set to `0.0.0.0`. Release default is `127.0.0.1`, but in case of docker container the default is set to the first IP address returned by `hostname -i` on the container. 103 | 104 | ##### TCP configuration 105 | 106 | TCP is not yet supported. 107 | 108 | ### Checklist of STUN/TURN methods supported by MongooseICE 109 | 110 | - [x] Binding 111 | - [x] Allocate 112 | - [x] Refresh 113 | - [x] Send 114 | - [x] Data 115 | - [x] CreatePermission 116 | - [x] ChannelBind 117 | 118 | ### Checklist of STUN/TURN attributes supported by MongooseICE 119 | 120 | #### Comprehension Required 121 | 122 | - [x] XOR-MAPPED-ADDRESS 123 | - [x] MESSAGE-INTEGRITY 124 | - [x] ERROR-CODE 125 | - [x] UNKNOWN-ATTRIBUTES 126 | - [x] REALM 127 | - [x] NONCE 128 | - [x] CHANNEL-NUMBER 129 | - [x] LIFETIME 130 | - [x] XOR-PEER-ADDRESS 131 | - [x] DATA 132 | - [x] XOR-RELAYED-ADDRESS 133 | - [x] EVEN-PORT 134 | - [x] REQUESTED-TRANSPORT 135 | - [ ] DONT-FRAGMENT 136 | - [x] RESERVATION-TOKEN 137 | - [ ] PRIORITY 138 | - [ ] USE-CANDIDATE 139 | - [ ] ICE-CONTROLLED 140 | - [ ] ICE-CONTROLLING 141 | 142 | #### Comprehension Optional 143 | 144 | - [ ] SOFTWARE 145 | - [ ] ALTERNATE-SERVER 146 | - [ ] FINGERPRINT 147 | 148 | ## License 149 | 150 | Copyright 2017 Erlang Solutions Ltd. 151 | 152 | Licensed under the Apache License, Version 2.0 (the "License"); 153 | you may not use this file except in compliance with the License. 154 | You may obtain a copy of the License at 155 | 156 | http://www.apache.org/licenses/LICENSE-2.0 157 | 158 | Unless required by applicable law or agreed to in writing, software 159 | distributed under the License is distributed on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 161 | See the License for the specific language governing permissions and 162 | limitations under the License. 163 | 164 | [BUILD BADGE]: https://travis-ci.org/esl/MongooseICE.svg?branch=master 165 | [BUILD LINK]: https://travis-ci.org/esl/MongooseICE 166 | 167 | [COVERAGE BADGE]: https://coveralls.io/repos/github/esl/MongooseICE/badge.svg 168 | [COVERAGE LINK]: https://coveralls.io/github/esl/MongooseICE 169 | 170 | [OUR SITE]: https://www.erlang-solutions.com/ 171 | 172 | [OVERVIEW]: https://www.html5rocks.com/en/tutorials/webrtc/basics/#toc-rtcpeerconnection 173 | [SIGNALING]: https://www.html5rocks.com/en/tutorials/webrtc/basics/#toc-rtcpeerconnection 174 | 175 | [MONGOOSE]: https://github.com/esl/MongooseIM 176 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :mix_docker, image: "mongooseice" 4 | config :mix_docker, 5 | dockerfile_build: "docker/Dockerfile.build", 6 | dockerfile_release: "docker/Dockerfile.release" 7 | 8 | import_config "#{Mix.env}.exs" 9 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :mongooseice, secret: "abc" 4 | config :mongooseice, servers: [ 5 | {:udp, [ 6 | ip: {127, 0, 0, 1}, 7 | port: 12_100, 8 | realm: "turn1.localhost" 9 | ]}, 10 | {:udp, [ 11 | ip: {127, 0, 0, 1}, 12 | port: 12_200, 13 | realm: "turn2.localhost" 14 | ]} 15 | ] 16 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :mongooseice, loglevel: 4 | {:system, :atom, "MONGOOSEICE_LOGLEVEL", :info} 5 | 6 | config :mongooseice, udp_enabled: 7 | {:system, :boolean, "MONGOOSEICE_UDP_ENABLED", true} 8 | 9 | # TCP is NOT supported yet anyway, so don't enable this option 10 | config :mongooseice, tcp_enabled: 11 | {:system, :boolean, "MONGOOSEICE_TCP_ENABLED", false} 12 | 13 | config :mongooseice, secret: 14 | {:system, :string, "MONGOOSEICE_STUN_SECRET", :base64.encode(:crypto.strong_rand_bytes(128))} 15 | 16 | config :mongooseice, servers: [ 17 | {:udp, [ 18 | ip: {:system, :string, "MONGOOSEICE_UDP_BIND_IP", "127.0.0.1"}, 19 | port: {:system, :integer, "MONGOOSEICE_UDP_PORT", 3478}, 20 | realm: {:system, :string, "MONGOOSEICE_UDP_REALM", "udp.localhost.local"}, 21 | relay_ip: {:system, :string, "MONGOOSEICE_UDP_RELAY_IP", "127.0.0.1"}, 22 | ]}, 23 | {:tcp, [ 24 | ip: {:system, :string, "MONGOOSEICE_TCP_BIND_IP", "127.0.0.1"}, 25 | port: {:system, :integer, "MONGOOSEICE_TCP_PORT", 3478}, 26 | realm: {:system, :string, "MONGOOSEICE_TCP_REALM", "tcp.localhost.local"}, 27 | relay_ip: {:system, :string, "MONGOOSEICE_TCP_RELAY_IP", "127.0.0.1"}, 28 | ]}, 29 | ] 30 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, 4 | level: :debug 5 | -------------------------------------------------------------------------------- /docker/Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage 2 | 3 | ENV HOME=/opt/app/ TERM=xterm 4 | ENV LANG en_US.UTF-8 5 | ENV LANGUAGE en_US.UTF-8 6 | ENV LC_ALL en_US.UTF-8 7 | 8 | # Install Elixir and basic build dependencies 9 | RUN apt-get update && apt-get install -y \ 10 | git \ 11 | wget \ 12 | gcc \ 13 | g++ \ 14 | make \ 15 | wget && \ 16 | wget http://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && \ 17 | dpkg -i erlang-solutions_1.0_all.deb && \ 18 | apt-get update && \ 19 | apt-get install -y esl-erlang elixir && \ 20 | apt-get clean 21 | 22 | 23 | # Install Hex+Rebar 24 | RUN mix local.hex --force && \ 25 | mix local.rebar --force 26 | 27 | WORKDIR /opt/app 28 | 29 | ENV MIX_ENV=prod 30 | 31 | # Cache elixir deps 32 | COPY mix.exs mix.lock ./ 33 | RUN mix do deps.get, deps.compile 34 | 35 | COPY . . 36 | 37 | RUN mix release --env=prod --verbose 38 | -------------------------------------------------------------------------------- /docker/Dockerfile.release: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage 2 | 3 | ENV LANG en_US.UTF-8 4 | ENV LANGUAGE en_US.UTF-8 5 | ENV LC_ALL en_US.UTF-8 6 | 7 | # required packages 8 | RUN apt-get update && apt-get upgrade -y && apt-get install -y \ 9 | bash \ 10 | bash-completion \ 11 | curl \ 12 | dnsutils \ 13 | vim && \ 14 | apt-get clean 15 | 16 | ENV MONGOOSEICE_UDP_BIND_IP=0.0.0.0 MONGOOSEICE_UDP_PORT=3478 MIX_ENV=prod \ 17 | MONGOOSEICE_TCP_BIND_IP=0.0.0.0 MONGOOSEICE_TCP_PORT=3479 MIX_ENV=prod \ 18 | REPLACE_OS_VARS=true SHELL=/bin/bash 19 | 20 | WORKDIR /opt/app 21 | 22 | ADD mongooseice.tar.gz ./ 23 | ADD docker/start.sh /opt/app/start.sh 24 | RUN chmod +x /opt/app/start.sh 25 | 26 | # Move priv dir 27 | RUN mv $(find lib -name mongooseice-*)/priv . 28 | RUN ln -s $(pwd)/priv $(find lib -name mongooseice-*)/priv 29 | 30 | VOLUME /opt/app/priv 31 | 32 | CMD ["foreground"] 33 | ENTRYPOINT ["/opt/app/start.sh"] 34 | -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setting default relay IP to `hostname -I` is much better then leaving 127.0.0.1 in docker container. 4 | # Especially on linux system, where docker container IP is effectively exclusive for MongooseICE 5 | # On Windows and macOS it will not be accessible since the container is hidden in ~VM 6 | 7 | DEFAULT_RELAY_IP=`hostname -I | awk '{print $1}'` 8 | 9 | export MONGOOSEICE_UDP_RELAY_IP=${MONGOOSEICE_UDP_RELAY_IP:=$DEFAULT_RELAY_IP} 10 | export MONGOOSEICE_TCP_RELAY_IP=${MONGOOSEICE_TCP_RELAY_IP:=$DEFAULT_RELAY_IP} 11 | 12 | /opt/app/bin/mongooseice "$@" 13 | -------------------------------------------------------------------------------- /lib/mongooseice.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE do 2 | @moduledoc ~S""" 3 | STUN/TURN servers 4 | 5 | MongooseICE allows you to start multiple listeners (later called "servers") 6 | which react to STUN/TURN messages. The only difference between the servers 7 | is a transport protocol and interface/port pair which a server uses for 8 | listening to STUN packets. 9 | 10 | Each server is independent, i.e. if one of them crashes the other one should not 11 | be affected (note: this applies when starting servers using the recommended method - 12 | via application's configuration. If you hook up a server to your supervision tree 13 | the behaviour will depend on a server's supervisor configuration). 14 | 15 | Currently only UDP transport is supported. Read more about it in the documentation 16 | of `MongooseICE.UDP`. 17 | 18 | ## Global configuration 19 | 20 | The only parameter configured globally is a shared secret used for TURN 21 | authentication: 22 | 23 | config :mongooseice, secret: "my_secret" 24 | 25 | Currently it is not possible to configure it per listening TURN port. 26 | """ 27 | 28 | @type ip :: :inet.ip_address 29 | @type portn :: :inet.port_number 30 | @type address :: {ip, portn} 31 | 32 | @type client_info :: %{socket: MongooseICE.UDP.socket, 33 | ip: MongooseICE.ip, port: MongooseICE.portn} 34 | end 35 | -------------------------------------------------------------------------------- /lib/mongooseice/application.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Application do 2 | @moduledoc false 3 | 4 | @app :mongooseice 5 | @ip_addr_config_keys [:ip, :relay_ip] 6 | 7 | use Application 8 | 9 | def start(_type, _args) do 10 | loglevel = Confex.get_env(@app, :loglevel, :info) 11 | Logger.configure(level: loglevel) 12 | 13 | opts = [strategy: :one_for_one, name: MongooseICE.Supervisor] 14 | Supervisor.start_link([MongooseICE.ReservationLog.child_spec()] ++ servers(), opts) 15 | end 16 | 17 | defp servers do 18 | @app 19 | |> Confex.get_env(:servers, []) 20 | |> Enum.filter(fn({type, _}) -> is_proto_enabled(type) end) 21 | |> Enum.map(&make_server/1) 22 | end 23 | 24 | defp is_proto_enabled(type) do 25 | @app 26 | |> Confex.get_env(String.to_atom(~s"#{type}_enabled"), true) 27 | end 28 | 29 | defp make_server({type, config}) do 30 | config 31 | |> normalize_server_config() 32 | |> server_mod(type).child_spec() 33 | end 34 | 35 | defp normalize_server_config(config) do 36 | Enum.map(config, fn({key, value}) -> 37 | case Enum.member?(@ip_addr_config_keys, key) do 38 | false -> {key, value} 39 | true -> {key, normalize_ip_addr(value)} 40 | end 41 | end) 42 | end 43 | 44 | defp normalize_ip_addr(addr) when is_binary(addr) do 45 | MongooseICE.Helper.string_to_inet(addr) 46 | end 47 | defp normalize_ip_addr(addr), do: addr 48 | 49 | defp server_mod(:udp), do: MongooseICE.UDP 50 | end 51 | -------------------------------------------------------------------------------- /lib/mongooseice/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Auth do 2 | @moduledoc false 3 | # This module implements authentication and authorization of 4 | # the STUN and TURN protocol. 5 | 6 | alias Jerboa.Format.Body.Attribute 7 | alias Jerboa.Format.Body.Attribute.{Username, Nonce, Realm} 8 | alias Jerboa.Params 9 | alias MongooseICE.TURN 10 | 11 | @nonce_bytes 48 12 | @nonce_lifetime_seconds 60 * 60 # 1h 13 | @authorized_methods [:allocate, :refresh, :create_permission, :channel_bind] 14 | 15 | def get_secret do 16 | Confex.get_env(:mongooseice, :secret) 17 | end 18 | 19 | def nonce_lifetime() do 20 | @nonce_lifetime_seconds 21 | end 22 | 23 | def gen_nonce do 24 | @nonce_bytes 25 | |> :crypto.strong_rand_bytes() 26 | |> :binary.decode_unsigned() 27 | |> Integer.to_string(16) 28 | end 29 | 30 | def authorize(params, server, turn_state) do 31 | # All authorized requests must have matching username with one that 32 | # created an allocation, if any 33 | %Username{value: username} = Params.get_attr(params, Username) 34 | case turn_state do 35 | %TURN{allocation: %TURN.Allocation{owner_username: ^username}} -> 36 | {:ok, params} 37 | %TURN{allocation: nil} -> 38 | {:ok, params} 39 | _ -> 40 | {:error, error_params(:wrong_credentials, params, server, turn_state)} 41 | end 42 | end 43 | 44 | def authenticate(params, server, turn_state) do 45 | nonce = turn_state.nonce 46 | signed? = params.signed? 47 | 48 | with %Username{} <- Params.get_attr(params, Username), 49 | %Realm{} <- Params.get_attr(params, Realm), 50 | n = %Nonce{value: ^nonce} <- Params.get_attr(params, Nonce), 51 | true <- params.verified? do 52 | {:ok, %Params{params | attributes: params.attributes -- [n]}} 53 | else 54 | false -> # Not verified -> error code 401 55 | {:error, error_params(:unauthorized, params, server, turn_state)} 56 | %Nonce{} -> # Invalid nonce, error code 438 57 | {:error, error_params(:stale_nonce, params, server, turn_state)} 58 | nil when not signed? -> 59 | {:error, error_params(:unauthorized, params, server, turn_state)} 60 | _ when signed? -> # If message is signed and there are some attributes 61 | # missing, we need to respond with error code 400 62 | {:error, error_params(:bad_request, params, server, turn_state)} 63 | end 64 | end 65 | 66 | def maybe(action_fun, params, server, turn_state) do 67 | case should_authorize?(params) do 68 | true -> 69 | action_fun.(params, server, turn_state) 70 | false -> 71 | {:ok, params} 72 | end 73 | end 74 | 75 | defp should_authorize?(params) do 76 | should_authorize?(Params.get_class(params), Params.get_method(params)) 77 | end 78 | 79 | defp should_authorize?(:request, method) 80 | when method in @authorized_methods, do: true 81 | defp should_authorize?(_, _), do: false 82 | 83 | defp error_params(code_or_name, params, server, turn_state) do 84 | %Params{params | attributes: [ 85 | Attribute.ErrorCode.new(code_or_name), 86 | %Attribute.Realm{value: server[:realm]}, 87 | %Attribute.Nonce{value: turn_state.nonce} 88 | ]} 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator do 2 | @moduledoc false 3 | 4 | require Logger 5 | 6 | alias Jerboa.Params 7 | alias MongooseICE.TURN 8 | alias MongooseICE.Auth 9 | alias Jerboa.Format.Body.Attribute 10 | 11 | @doc """ 12 | This function implements the second phase of the message processing. Here, 13 | message gets authenticated, authorized and passed to specific request handler. 14 | The response from the specific request handler gets normalized before 15 | leaving this function (i.e. gets :success or :failure class or is 16 | changed to :void if this is an response to :indication message). 17 | """ 18 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 19 | :: {Params.t, TURN.t} | {:void, TURN.t} 20 | def service(params, client, server, turn_state) do 21 | case service_(params, client, server, turn_state) do 22 | {new_params, new_turn_state} -> 23 | {response(new_params), new_turn_state} 24 | new_params -> 25 | {response(new_params), turn_state} 26 | end 27 | end 28 | 29 | defp service_(params, client, server, turn_state) do 30 | with {:ok, params} <- Auth.maybe(&Auth.authenticate/3, params, server, turn_state), 31 | {:ok, params} <- Auth.maybe(&Auth.authorize/3, params, server, turn_state) do 32 | handler(class(params)).service(params, client, server, turn_state) 33 | else 34 | {:error, error_params} -> 35 | {error_params, turn_state} 36 | end 37 | end 38 | 39 | # This function is run every time the params are returned from processing. 40 | # The Params have already set message class to either :failure or :success. 41 | # Return value of this function will be the message that will be send back to the client. 42 | @spec on_result(:request | :indication, Params.t) :: Params.t | :void 43 | def on_result(:request, result) do 44 | if Params.get_class(result) == :failure do 45 | e = error(result) 46 | Logger.debug ~s"Request #{Params.get_method(result)} failed due " <> 47 | ~s"to error #{e.name} (#{e.code})..." 48 | end 49 | 50 | result 51 | end 52 | def on_result(:indication, result) do 53 | if Params.get_class(result) == :failure do 54 | e = error(result) 55 | Logger.debug ~s"Indication #{Params.get_method(result)} dropped due " <> 56 | ~s"to error #{e.name} (#{e.code})..." 57 | end 58 | 59 | :void 60 | end 61 | 62 | defp class(params) do 63 | Params.get_class(params) 64 | end 65 | 66 | # Puts :success or :failure message class and runs `on_result/2` hook 67 | defp response(:void), do: :void 68 | defp response(params) do 69 | result = 70 | case errors?(params) do 71 | false -> 72 | success(params) 73 | true -> 74 | failure(params) 75 | end 76 | __MODULE__.on_result(class(params), result) 77 | end 78 | 79 | 80 | defp errors?(params) do 81 | error(params) != nil 82 | end 83 | 84 | defp error(params), do: Params.get_attr(params, Attribute.ErrorCode) 85 | 86 | defp success(params), do: Params.put_class(params, :success) 87 | 88 | defp failure(params), do: Params.put_class(params, :failure) 89 | 90 | defp handler(:request), do: MongooseICE.Evaluator.Request 91 | defp handler(:indication), do: MongooseICE.Evaluator.Indication 92 | 93 | end 94 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/allocate/request.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.Allocate.Request do 2 | @moduledoc false 3 | 4 | import MongooseICE.Evaluator.Helper, only: [ 5 | family: 1, 6 | maybe: 2, maybe: 3 7 | ] 8 | 9 | alias Jerboa.Format.Body.Attribute 10 | alias Jerboa.Format.Body.Attribute.ErrorCode 11 | alias Jerboa.Params 12 | alias MongooseICE.TURN 13 | alias MongooseICE.TURN.Reservation 14 | alias MongooseICE.UDP 15 | 16 | require Integer 17 | require Logger 18 | 19 | @create_relays_max_retries 100 20 | 21 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 22 | :: {Params.t, TURN.t} 23 | def service(params, client, server, turn_state) do 24 | request_status = 25 | {:continue, params, %{}} 26 | |> maybe(&verify_existing_allocation/5, [client, server, turn_state]) 27 | |> maybe(&verify_requested_transport/2) 28 | |> maybe(&verify_dont_fragment/2) 29 | |> maybe(&verify_reservation_token/2) 30 | |> maybe(&verify_even_port/2) 31 | |> maybe(&allocate/5, [client, server, turn_state]) 32 | 33 | case request_status do 34 | {:error, error_code} -> 35 | {%{params | attributes: [error_code]}, turn_state} 36 | {:respond, {new_params, new_turn_state}} -> 37 | {new_params, new_turn_state} 38 | end 39 | end 40 | 41 | defp allocation_params(params, %{ip: a, port: p}, server, 42 | turn_state = %TURN{allocation: allocation}, 43 | reservation_token) do 44 | %TURN.Allocation{socket: socket, expire_at: expire_at} = allocation 45 | {:ok, {socket_addr, port}} = :inet.sockname(socket) 46 | addr = server[:relay_ip] || socket_addr 47 | lifetime = max(0, expire_at - MongooseICE.Time.system_time(:second)) 48 | attrs = case reservation_token do 49 | :not_requested -> [] 50 | %Attribute.ReservationToken{} -> [reservation_token] 51 | end ++ [ 52 | %Attribute.XORMappedAddress{ 53 | family: family(a), 54 | address: a, 55 | port: p 56 | }, 57 | %Attribute.XORRelayedAddress{ 58 | family: family(addr), 59 | address: addr, 60 | port: port 61 | }, 62 | %Attribute.Lifetime{ 63 | duration: lifetime 64 | } 65 | ] 66 | {%{params | attributes: attrs}, turn_state} 67 | end 68 | 69 | defp allocate(params, state, client, server, turn_state) do 70 | case create_relays(params, state, server) do 71 | {:error, error_code} -> 72 | {:error, error_code} 73 | {:ok, socket, reservation_token} -> 74 | allocation = %MongooseICE.TURN.Allocation{ 75 | socket: socket, 76 | expire_at: MongooseICE.Time.system_time(:second) + TURN.Allocation.default_lifetime(), 77 | req_id: Params.get_id(params), 78 | owner_username: owner_username(params) 79 | } 80 | new_turn_state = %{turn_state | allocation: allocation} 81 | {:respond, allocation_params(params, client, server, new_turn_state, 82 | reservation_token)} 83 | end 84 | end 85 | 86 | defp create_relays(params, state, server) do 87 | status = 88 | {:continue, params, create_relays_state(state)} 89 | |> maybe(&requests_reserved_port?/2) 90 | |> maybe(&open_this_relay/3, [server]) 91 | |> maybe(&reserve_another_relay/3, [server]) 92 | case status do 93 | {:respond, state} -> 94 | {:ok, state.this_socket, state.new_reservation_token} 95 | {:continue, _params, state} -> 96 | {:ok, state.this_socket, state.new_reservation_token} 97 | {:error, error_code} -> 98 | {:error, error_code} 99 | end 100 | end 101 | 102 | defp create_relays_state(allocate_state) do 103 | %{this_socket: nil, 104 | this_port: nil, 105 | new_reservation_token: :not_requested, 106 | retries: Map.get(allocate_state, :retries) || @create_relays_max_retries} 107 | end 108 | 109 | defp requests_reserved_port?(params, state) do 110 | case Params.get_attr(params, Attribute.ReservationToken) do 111 | nil -> {:continue, params, state} 112 | %Attribute.ReservationToken{} = token -> 113 | case MongooseICE.ReservationLog.take(token) do 114 | nil -> 115 | Logger.info fn -> "no reservation: #{inspect(token)}" end 116 | {:error, ErrorCode.new(:insufficient_capacity)} 117 | %Reservation{} = r -> 118 | {:respond, %{state | this_socket: r.socket}} 119 | end 120 | end 121 | end 122 | 123 | defp open_this_relay(_params, %{retries: r}, _server) when r < 0 do 124 | Logger.warn(:max_retries) 125 | {:error, ErrorCode.new(:insufficient_capacity)} 126 | end 127 | defp open_this_relay(params, state, server) do 128 | opts = udp_opts(server) 129 | case {Params.get_attr(params, Attribute.EvenPort), 130 | :gen_udp.open(0, opts)} do 131 | {nil, {:ok, socket}} -> 132 | {:continue, params, %{state | this_socket: socket}} 133 | {%Attribute.EvenPort{}, {:ok, socket}} -> 134 | {:ok, {_, port}} = :inet.sockname(socket) 135 | if Integer.is_even(port) do 136 | {:continue, params, 137 | %{state | this_socket: socket, this_port: port}} 138 | else 139 | :gen_udp.close(socket) 140 | new_state = %{state | retries: state.retries - 1} 141 | open_this_relay(params, new_state, server) 142 | end 143 | {_, {:error, reason}} -> 144 | Logger.warn(":gen_udp.open/2 error: #{reason}, port: 0, opts: #{opts}") 145 | {:error, ErrorCode.new(:insufficient_capacity)} 146 | end 147 | end 148 | 149 | defp reserve_another_relay(params, state, server) do 150 | case Params.get_attr(params, Attribute.EvenPort) do 151 | nil -> {:continue, params, state} 152 | %Attribute.EvenPort{reserved?: false} -> {:continue, params, state} 153 | %Attribute.EvenPort{reserved?: true} -> 154 | port = state.this_port + 1 155 | opts = udp_opts(server) 156 | case :gen_udp.open(port, opts) do 157 | {:error, :eaddrinuse} -> 158 | ## We can't allocate a pair of consecutive ports. 159 | ## We're jumping back to before we opened state.this_socket! 160 | :gen_udp.close(state.this_socket) 161 | create_relays(params, %{retries: state.retries - 1}, server) 162 | {:error, reason} -> 163 | Logger.warn(":gen_udp.open/2 error: #{reason}, port: #{port}, opts: #{opts}") 164 | {:error, ErrorCode.new(:insufficient_capacity)} 165 | {:ok, socket} -> 166 | token = do_reserve_another_relay(socket) 167 | {:continue, params, %{state | new_reservation_token: token}} 168 | end 169 | end 170 | end 171 | 172 | defp do_reserve_another_relay(socket) do 173 | r = Reservation.new(socket) 174 | :ok = MongooseICE.ReservationLog.register(r, MongooseICE.TURN.Reservation.default_timeout()) 175 | r.token 176 | end 177 | 178 | defp udp_opts(server) do 179 | [:binary, active: UDP.Worker.burst_length(), ip: server[:relay_ip]] 180 | end 181 | 182 | defp verify_existing_allocation(params, state, client, server, turn_state) do 183 | req_id = Params.get_id(params) 184 | case turn_state do 185 | %TURN{allocation: %TURN.Allocation{req_id: ^req_id}} -> 186 | {:respond, allocation_params(params, client, server, turn_state, :not_requested)} 187 | %TURN{allocation: %TURN.Allocation{}} -> 188 | {:error, ErrorCode.new(:allocation_mismatch)} 189 | %TURN{allocation: nil} -> 190 | {:continue, params, state} 191 | end 192 | end 193 | 194 | defp verify_requested_transport(params, state) do 195 | case Params.get_attr(params, Attribute.RequestedTransport) do 196 | %Attribute.RequestedTransport{protocol: :udp} = t -> 197 | {:continue, %{params | attributes: params.attributes -- [t]}, state} 198 | %Attribute.RequestedTransport{} -> 199 | {:error, ErrorCode.new(:allocation_mismatch)} 200 | _ -> 201 | {:error, ErrorCode.new(:bad_request)} 202 | end 203 | end 204 | 205 | defp verify_dont_fragment(params, state) do 206 | case Params.get_attr(params, Attribute.DontFragment) do 207 | %Attribute.DontFragment{} -> 208 | {:error, ErrorCode.new(:unknown_attribute)} # Currently unsupported 209 | _ -> 210 | {:continue, params, state} 211 | end 212 | end 213 | 214 | defp verify_reservation_token(params, state) do 215 | even_port = Params.get_attr(params, Attribute.EvenPort) 216 | case Params.get_attr(params, Attribute.ReservationToken) do 217 | %Attribute.ReservationToken{} when even_port != nil -> 218 | {:error, ErrorCode.new(:bad_request)} 219 | _ -> 220 | {:continue, params, state} 221 | end 222 | end 223 | 224 | defp verify_even_port(params, state) do 225 | reservation_token = Params.get_attr(params, Attribute.ReservationToken) 226 | case Params.get_attr(params, Attribute.EvenPort) do 227 | %Attribute.EvenPort{} when reservation_token != nil -> 228 | {:error, ErrorCode.new(:bad_request)} 229 | _ -> 230 | {:continue, params, state} 231 | end 232 | end 233 | 234 | defp owner_username(params) do 235 | case Params.get_attr(params, Attribute.Username) do 236 | %Attribute.Username{value: owner_username} -> 237 | owner_username 238 | _ -> 239 | nil 240 | end 241 | end 242 | 243 | end 244 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/binding/request.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.Binding.Request do 2 | @moduledoc false 3 | 4 | import MongooseICE.Evaluator.Helper 5 | alias Jerboa.Format 6 | alias Jerboa.Params 7 | alias MongooseICE.TURN 8 | 9 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 10 | :: Params.t 11 | def service(params, %{ip: a, port: p}, _server, _turn_state) do 12 | %{params | attributes: [attribute(family(a), a, p)]} 13 | end 14 | 15 | defp attribute(f, a, p) do 16 | %Format.Body.Attribute.XORMappedAddress{ 17 | family: f, 18 | address: a, 19 | port: p 20 | } 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/channel_bind/request.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.ChannelBind.Request do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute.ErrorCode 6 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 7 | alias Jerboa.Format.Body.Attribute.ChannelNumber 8 | alias MongooseICE.UDP 9 | alias MongooseICE.TURN 10 | 11 | import MongooseICE.Evaluator.Helper, only: [maybe: 3, maybe: 2] 12 | 13 | require Logger 14 | 15 | @spec service(Params.t, MongooseICE.client_info, UDP.server_opts, TURN.t) 16 | :: {Params.t, TURN.t} 17 | def service(params, _, _, turn_state) do 18 | request_status = 19 | {:continue, params, %{}} 20 | |> maybe(&verify_allocation/3, [turn_state]) 21 | |> maybe(&verify_xor_peer_address/2) 22 | |> maybe(&verify_channel_number/2) 23 | |> maybe(&verify_channel_binding/3, [turn_state]) 24 | |> maybe(&bind_channel/3, [turn_state]) 25 | 26 | case request_status do 27 | {:error, error_code} -> 28 | {%{params | attributes: [error_code]}, turn_state} 29 | {:respond, {new_params, new_turn_state}} -> 30 | {new_params, new_turn_state} 31 | end 32 | end 33 | 34 | @spec verify_allocation(Params.t, context :: map, TURN.t) 35 | :: {:error, ErrorCode.t} | {:continue, Params.t, map} 36 | defp verify_allocation(params, context, turn_state) do 37 | case turn_state do 38 | %TURN{allocation: %TURN.Allocation{}} -> 39 | {:continue, params, context} 40 | _ -> 41 | {:error, ErrorCode.new(:allocation_mismatch)} 42 | end 43 | end 44 | 45 | @spec verify_xor_peer_address(Params.t, context :: map) 46 | :: {:error, ErrorCode.t} | {:continue, Params.t, map} 47 | defp verify_xor_peer_address(params, context) do 48 | with %XPA{} = xor_peer_addr <- Params.get_attr(params, XPA), 49 | :ipv4 <- xor_peer_addr.family do 50 | peer = {xor_peer_addr.address, xor_peer_addr.port} 51 | {:continue, params, Map.put(context, :peer, peer)} 52 | else 53 | _ -> {:error, ErrorCode.new(:bad_request)} 54 | end 55 | end 56 | 57 | @spec verify_channel_number(Params.t, context :: map) 58 | :: {:error, ErrorCode.t} | {:continue, Params.t, map} 59 | defp verify_channel_number(params, context) do 60 | with %ChannelNumber{} = cn <- Params.get_attr(params, ChannelNumber), 61 | true <- valid_channel_number?(cn) do 62 | {:continue, params, Map.put(context, :channel_number, cn.number)} 63 | else 64 | _ -> {:error, ErrorCode.new(:bad_request)} 65 | end 66 | end 67 | 68 | @spec verify_channel_binding(Params.t, context :: map, TURN.t) 69 | :: {:error, ErrorCode.t} | {:continue, Params.t, map} 70 | defp verify_channel_binding(params, context, turn_state) do 71 | %{peer: peer, channel_number: channel_number} = context 72 | cond do 73 | channel_bound?(turn_state, peer, channel_number) -> 74 | {:continue, params, Map.put(context, :refresh?, true)} 75 | peer_bound?(turn_state, peer) -> 76 | {:error, ErrorCode.new(:bad_request)} 77 | channel_number_bound?(turn_state, channel_number) -> 78 | {:error, ErrorCode.new(:bad_request)} 79 | true -> 80 | {:continue, params, Map.put(context, :refresh?, false)} 81 | end 82 | end 83 | 84 | @spec bind_channel(Params.t, context :: map, TURN.t) 85 | :: {:respond, {Params.t, TURN.t}} 86 | defp bind_channel(params, context, turn_state) do 87 | %{peer: peer, channel_number: channel_number, refresh?: refresh?} = context 88 | {ip, port} = peer 89 | _ = if refresh? do 90 | Logger.debug fn -> 91 | "Refreshing channel ##{channel_number} bound to peer #{ip}:#{port}" 92 | end 93 | else 94 | Logger.debug fn -> 95 | "Binding channel ##{channel_number} to peer #{ip}:#{port}" 96 | end 97 | end 98 | new_turn_state = 99 | turn_state 100 | |> TURN.put_channel(peer, channel_number) 101 | |> TURN.put_permission(ip) 102 | {:respond, {Params.set_attrs(params, []), new_turn_state}} 103 | end 104 | 105 | @spec valid_channel_number?(ChannelNumber.t) :: boolean 106 | defp valid_channel_number?(%ChannelNumber{number: number}) do 107 | number in 0x4000..0x7FFF 108 | end 109 | 110 | @spec channel_bound?(TURN.t, MongooseICE.address, Jerboa.Format.channel_number) 111 | :: boolean 112 | defp channel_bound?(turn_state, peer, channel_number) do 113 | with {:ok, channel} <- TURN.get_channel(turn_state, peer), 114 | true <- channel.number == channel_number do 115 | true 116 | else 117 | _ -> false 118 | end 119 | end 120 | 121 | @spec peer_bound?(TURN.t, MongooseICE.address) :: boolean 122 | defp peer_bound?(turn_state, peer) do 123 | case TURN.get_channel(turn_state, peer) do 124 | {:ok, _} -> true 125 | _ -> false 126 | end 127 | end 128 | 129 | @spec channel_number_bound?(TURN.t, Jerboa.Format.channel_number) :: boolean 130 | defp channel_number_bound?(turn_state, channel_number) do 131 | case TURN.get_channel(turn_state, channel_number) do 132 | {:ok, _} -> true 133 | _ -> false 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/channel_data.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.ChannelData do 2 | @moduledoc false 3 | 4 | alias MongooseICE.TURN 5 | alias MongooseICE.TURN.Channel 6 | alias Jerboa.ChannelData 7 | 8 | require Logger 9 | 10 | @spec service(ChannelData.t, TURN.t) :: TURN.t 11 | def service(channel_data, turn_state) do 12 | case TURN.has_channel(turn_state, channel_data.channel_number) do 13 | {:ok, turn_state, channel} -> 14 | send(turn_state, channel, channel_data) 15 | turn_state 16 | {:error, turn_state} -> 17 | Logger.debug fn -> 18 | "Dropping data sent over channel ##{channel_data.channel_number}. " <> 19 | "Channel may not exist or there is no permission for the bound peer" 20 | end 21 | turn_state 22 | end 23 | end 24 | 25 | @spec send(TURN.t, Channel.t, ChannelData.t) :: :ok 26 | defp send(turn_state, channel, channel_data) do 27 | sock = turn_state.allocation.socket 28 | {peer_ip, peer_port} = channel.peer 29 | data = channel_data.data 30 | :ok = :gen_udp.send(sock, peer_ip, peer_port, data) 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/create_permission/request.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.CreatePermission.Request do 2 | @moduledoc false 3 | 4 | import MongooseICE.Evaluator.Helper 5 | alias Jerboa.Format.Body.Attribute 6 | alias Jerboa.Format.Body.Attribute.ErrorCode 7 | alias Jerboa.Params 8 | alias MongooseICE.TURN 9 | 10 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 11 | :: {Params.t, TURN.t} 12 | def service(params, _client, _server, turn_state) do 13 | request_status = 14 | {:continue, params, %{}} 15 | |> maybe(&verify_allocation/3, [turn_state]) 16 | |> maybe(&verify_xor_peer_address/2, []) 17 | |> maybe(&create_permissions/3, [turn_state]) 18 | 19 | case request_status do 20 | {:error, error_code} -> 21 | {%{params | attributes: [error_code]}, turn_state} 22 | {:respond, {new_params, new_turn_state}} -> 23 | {new_params, new_turn_state} 24 | end 25 | end 26 | 27 | defp verify_allocation(params, state, turn_state) do 28 | case turn_state do 29 | %TURN{allocation: %TURN.Allocation{}} -> 30 | {:continue, params, state} 31 | _ -> 32 | {:error, ErrorCode.new(:allocation_mismatch)} 33 | end 34 | end 35 | 36 | defp verify_xor_peer_address(params, state) do 37 | # The request MUST have at least one XORPeerAddress 38 | case Params.get_attr(params, Attribute.XORPeerAddress) do 39 | %Attribute.XORPeerAddress{} -> 40 | {:continue, params, state} 41 | _ -> 42 | {:error, ErrorCode.new(:bad_request)} 43 | end 44 | end 45 | 46 | defp create_permissions(params, _state, turn_state) do 47 | peers = Params.get_attrs(params, Attribute.XORPeerAddress) 48 | new_turn_state = 49 | peers 50 | |> Enum.map(& Map.fetch!(&1, :address)) 51 | |> Enum.reduce(turn_state, & TURN.put_permission(&2, &1)) 52 | new_params = %Params{params | attributes: []} 53 | {:respond, {new_params, new_turn_state}} 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/helper.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.Helper do 2 | @moduledoc false 3 | # This module defines several helper functions commonly used in 4 | # request/indication implementations 5 | 6 | def maybe(result, check), do: maybe(result, check, []) 7 | 8 | def maybe({:continue, params, state}, check, args) do 9 | apply(check, [params, state | args]) 10 | end 11 | def maybe({:respond, resp}, _check, _args), do: {:respond, resp} 12 | def maybe({:error, error_code}, _check, _x), do: {:error, error_code} 13 | 14 | @spec family(MongooseICE.ip) :: :ipv4 | :ipv6 15 | def family(addr) when tuple_size(addr) == 4, do: :ipv4 16 | def family(addr) when tuple_size(addr) == 8, do: :ipv6 17 | end 18 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/indication.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.Indication do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias MongooseICE.TURN 6 | alias MongooseICE.Evaluator 7 | 8 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 9 | :: :void 10 | def service(params, client, server, turn_state) do 11 | case method(params) do 12 | :binding -> 13 | ## This call is external to be mockable. 14 | __MODULE__.void() 15 | :send -> 16 | Evaluator.Send.Indication.service(params, client, server, turn_state) 17 | end 18 | end 19 | 20 | defp method(params) do 21 | Params.get_method(params) 22 | end 23 | 24 | @doc false 25 | def void, do: :void 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/refresh/request.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.Refresh.Request do 2 | @moduledoc false 3 | # Implements Refresh request as defined by [RFC 5766 Section 7: Refreshing an Allocation][rfc5766-sec7]. 4 | # [rfc5766-sec7]: https://tools.ietf.org/html/rfc5766#section-7 5 | 6 | import MongooseICE.Evaluator.Helper, only: [maybe: 3] 7 | alias MongooseICE.{TURN, TURN.Allocation} 8 | alias Jerboa.Format.Body.Attribute.{ErrorCode, Lifetime} 9 | alias Jerboa.Params 10 | 11 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 12 | :: {Params.t, TURN.t} 13 | def service(params, _client, _server, turn_state) do 14 | status = 15 | {:continue, params, %{}} 16 | |> maybe(&allocation_mismatch/3, [turn_state]) 17 | |> maybe(&refresh/3, [turn_state]) 18 | case status do 19 | {:error, code} -> 20 | {%{params | attributes: [code]}, turn_state} 21 | {:respond, {new_params, new_turn_state}} -> 22 | {new_params, new_turn_state} 23 | end 24 | end 25 | 26 | defp allocation_mismatch(_params, _state, %TURN{allocation: nil}) do 27 | {:error, ErrorCode.new(:allocation_mismatch)} 28 | end 29 | defp allocation_mismatch(params, state, %TURN{allocation: _}) do 30 | {:continue, params, state} 31 | end 32 | 33 | defp refresh(params, _state, %TURN{allocation: a} = t) do 34 | case Params.get_attr(params, Lifetime) do 35 | %Lifetime{duration: 0} -> 36 | new_a = %Allocation{ a | expire_at: 0 } 37 | {:respond, {params, %TURN{ t | allocation: new_a }}} 38 | %Lifetime{duration: d} -> 39 | refresh_(params, t, d) 40 | nil -> 41 | refresh_(params, t, Allocation.default_lifetime()) 42 | end 43 | end 44 | 45 | defp refresh_(params, %TURN{allocation: a} = t, requested_lifetime) do 46 | lifetime = 47 | requested_lifetime 48 | |> min(Allocation.maximum_lifetime()) 49 | |> max(Allocation.default_lifetime()) 50 | new_params = %Params{ params | attributes: [%Lifetime{duration: lifetime}] } 51 | now = MongooseICE.Time.system_time(:second) 52 | new_a = %Allocation{ a | expire_at: now + lifetime } 53 | {:respond, {new_params, %TURN{ t | allocation: new_a }}} 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/request.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.Request do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias MongooseICE.Evaluator 6 | alias MongooseICE.TURN 7 | 8 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 9 | :: {Params.t, TURN.t} 10 | def service(params, client, server, turn_state) do 11 | service_(params, client, server, turn_state) 12 | end 13 | 14 | defp service_(p, client, server, turn_state) do 15 | handler(p).service(p, client, server, turn_state) 16 | end 17 | 18 | defp handler(params) do 19 | case Params.get_method(params) do 20 | :binding -> Evaluator.Binding.Request 21 | :allocate -> Evaluator.Allocate.Request 22 | :create_permission -> Evaluator.CreatePermission.Request 23 | :refresh -> Evaluator.Refresh.Request 24 | :channel_bind -> Evaluator.ChannelBind.Request 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /lib/mongooseice/evaluator/send/indication.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Evaluator.Send.Indication do 2 | @moduledoc false 3 | 4 | require Logger 5 | import MongooseICE.Evaluator.Helper 6 | alias Jerboa.Format.Body.Attribute 7 | alias Jerboa.Format.Body.Attribute.ErrorCode 8 | alias Jerboa.Params 9 | alias MongooseICE.TURN 10 | 11 | @spec service(Params.t, MongooseICE.client_info, MongooseICE.UDP.server_opts, TURN.t) 12 | :: {Params.t, TURN.t} 13 | def service(params, _client, _server, turn_state) do 14 | request_status = 15 | {:continue, params, %{}} 16 | |> maybe(&verify_allocation/3, [turn_state]) 17 | |> maybe(&verify_dont_fragment/2, []) 18 | |> maybe(&verify_xor_peer_address/2, []) 19 | |> maybe(&verify_data/2, []) 20 | |> maybe(&verify_permissions/3, [turn_state]) 21 | |> maybe(&send/3, [turn_state]) 22 | 23 | case request_status do 24 | {:error, error_code} -> 25 | {%Params{params | attributes: [error_code]}, turn_state} 26 | {:respond, :void} -> 27 | {%Params{params | attributes: []}, turn_state} 28 | end 29 | end 30 | 31 | defp verify_allocation(params, state, turn_state) do 32 | case turn_state do 33 | %TURN{allocation: %TURN.Allocation{}} -> 34 | {:continue, params, state} 35 | _ -> 36 | {:error, ErrorCode.new(:allocation_mismatch)} 37 | end 38 | end 39 | 40 | defp verify_dont_fragment(params, state) do 41 | case Params.get_attr(params, Attribute.DontFragment) do 42 | %Attribute.DontFragment{} -> 43 | {:error, ErrorCode.new(:unknown_attribute)} # Currently unsupported 44 | _ -> 45 | {:continue, params, state} 46 | end 47 | end 48 | 49 | defp verify_xor_peer_address(params, state) do 50 | # The request MUST have exactly one XORPeerAddress 51 | case Params.get_attrs(params, Attribute.XORPeerAddress) do 52 | [%Attribute.XORPeerAddress{}] -> 53 | {:continue, params, state} 54 | _ -> 55 | {:error, ErrorCode.new(:bad_request)} 56 | end 57 | end 58 | 59 | defp verify_data(params, state) do 60 | # The request MUST have exactly one Data attribute 61 | case Params.get_attrs(params, Attribute.Data) do 62 | [%Attribute.Data{}] -> 63 | {:continue, params, state} 64 | _ -> 65 | {:error, ErrorCode.new(:bad_request)} 66 | end 67 | end 68 | 69 | defp verify_permissions(params, state, turn_state) do 70 | peer = Params.get_attr(params, Attribute.XORPeerAddress) 71 | case MongooseICE.TURN.has_permission(turn_state, peer.address) do 72 | {_, false} -> 73 | {:error, ErrorCode.new(:forbidden)} 74 | {_, true} -> 75 | {:continue, params, state} 76 | end 77 | end 78 | 79 | defp send(params, _state, turn_state) do 80 | sock = turn_state.allocation.socket 81 | peer = Params.get_attr(params, Attribute.XORPeerAddress) 82 | data = Params.get_attr(params, Attribute.Data) 83 | :ok = :gen_udp.send(sock, peer.address, peer.port, data.content) 84 | {:respond, :void} 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/mongooseice/helper.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Helper do 2 | @moduledoc false 3 | # Helper module that defines some commonly used functions in order to make 4 | # code cleaner and testing easier. 5 | 6 | import Kernel, except: [to_string: 1] 7 | defimpl String.Chars, for: Tuple do 8 | def to_string({a, b, c, d} = addr) when 9 | is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) do 10 | MongooseICE.Helper.inet_to_string(addr) 11 | end 12 | 13 | def to_string({a, b, c, d, e, f, g, h} = addr) when 14 | is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) 15 | and is_integer(e) and is_integer(f) and is_integer(g) and is_integer(h) do 16 | MongooseICE.Helper.inet_to_string(addr) 17 | end 18 | end 19 | 20 | @spec inet_to_string(MongooseICE.ip) :: String.t 21 | def inet_to_string(addr) do 22 | Kernel.to_string(:inet.ntoa(addr)) 23 | end 24 | 25 | @spec string_to_inet(String.t) :: MongooseICE.ip 26 | def string_to_inet(addr) do 27 | {:ok, inet_addr} = 28 | addr 29 | |> String.trim() 30 | |> String.to_charlist() 31 | |> :inet.parse_address() 32 | inet_addr 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mongooseice/reservation_log.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.ReservationLog do 2 | @moduledoc false 3 | 4 | ## Runtime support for storing and fetching pending reservations. 5 | ## ReservationLog is just a process registry of processes implemented in 6 | ## MongooseICE.TURN.Reservation.Instance module. Each of those processes corresponds to single 7 | ## reservation in order to easly invalidate itself when needed. 8 | 9 | alias MongooseICE.TURN.Reservation 10 | alias Jerboa.Format.Body.Attribute.ReservationToken 11 | 12 | require Logger 13 | 14 | def start_link() do 15 | Registry.start_link(keys: :unique, name: __MODULE__) 16 | end 17 | 18 | def child_spec() do 19 | Supervisor.Spec.worker(__MODULE__, []) 20 | end 21 | 22 | @spec register(Reservation.t, timeout :: MongooseICE.Time.seconds) :: :ok 23 | def register(%Reservation{} = reservation, timeout) do 24 | {:ok, reservation_pid} = GenServer.start_link(Reservation.Instance, [ 25 | __MODULE__, self(), reservation, :timer.seconds(timeout) 26 | ]) 27 | :ok = :gen_udp.controlling_process(reservation.socket, reservation_pid) 28 | end 29 | 30 | @spec take(ReservationToken.t) :: Reservation.t | nil 31 | def take(%ReservationToken{} = token) do 32 | case Registry.lookup(__MODULE__, token) do 33 | [] -> nil 34 | [{_owner, pid}] -> 35 | GenServer.call(pid, :take) 36 | end 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/mongooseice/stun.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.STUN do 2 | @moduledoc false 3 | # Processing of STUN messages 4 | 5 | alias MongooseICE.TURN 6 | alias MongooseICE.Evaluator 7 | alias MongooseICE.UDP 8 | alias MongooseICE.Auth 9 | alias Jerboa.Params 10 | alias Jerboa.ChannelData 11 | 12 | @doc """ 13 | This function implements phase 1 of message processing which is encoding/decoding. 14 | The decoded message is evaluated by `MongooseICE.Evaluator` and the return value 15 | of the `MongooseICE.Evaluator.service/4` is then encoded and returned from this function. 16 | """ 17 | @spec process_message(binary, MongooseICE.client_info, UDP.server_opts, TURN.t) :: 18 | {:ok, {binary, TURN.t}} | {:ok, {:void, TURN.t}} | {:error, reason :: term} 19 | def process_message(data, client, server, turn_state) do 20 | with secret = Auth.get_secret(), 21 | {:ok, %Params{} = params} <- Jerboa.Format.decode(data, [secret: secret]), 22 | {%Params{} = resp, new_turn_state} <- Evaluator.service(params, client, server, turn_state) do 23 | {:ok, {Jerboa.Format.encode(resp), new_turn_state}} 24 | else 25 | {:ok, %ChannelData{} = channel_data} -> 26 | process_channel_data(channel_data, turn_state) 27 | {:void, new_turn_state} -> 28 | {:ok, {:void, new_turn_state}} 29 | {:error, reason} -> 30 | {:error, reason} 31 | end 32 | end 33 | 34 | @spec process_channel_data(ChannelData.t, TURN.t) :: {:ok, {:void, TURN.t}} 35 | defp process_channel_data(channel_data, turn_state) do 36 | new_turn_state = Evaluator.ChannelData.service(channel_data, turn_state) 37 | {:ok, {:void, new_turn_state}} 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /lib/mongooseice/time.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Time do 2 | @moduledoc false 3 | # Abstract time(r)-related functions for mocking / overriding them in tests. 4 | # This should allow for testing timeouts without actually waiting. 5 | 6 | @type seconds :: integer 7 | 8 | @spec system_time(System.time_unit) :: integer 9 | def system_time(unit) do 10 | System.system_time(unit) 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/mongooseice/turn.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.TURN do 2 | @moduledoc false 3 | # This module defines a struct used as TURN protocol state. 4 | 5 | alias Jerboa.Format 6 | alias MongooseICE.TURN.Channel 7 | 8 | defstruct allocation: nil, permissions: %{}, channels: [], 9 | nonce: nil, realm: nil 10 | 11 | @permission_lifetime 5 * 60 # MUST be 5mins 12 | @channel_lifetime 10 * 60 # MUST be 10 minutes 13 | 14 | @type t :: %__MODULE__{ 15 | allocation: nil | MongooseICE.TURN.Allocation.t, 16 | permissions: %{peer_addr :: MongooseICE.ip => expiration_time :: integer}, 17 | channels: [Channel.t], 18 | nonce: String.t, 19 | realm: String.t 20 | } 21 | 22 | @spec has_permission(state :: t, MongooseICE.ip) :: {new_state :: t, boolean} 23 | def has_permission(state, ip) do 24 | now = MongooseICE.Time.system_time(:second) 25 | perms = state.permissions 26 | case Map.get(perms, ip) do 27 | nil -> 28 | {state, false} 29 | expire_at when expire_at <= now -> 30 | new_perms = Map.delete(perms, ip) 31 | new_state = %__MODULE__{state | permissions: new_perms} 32 | {new_state, false} 33 | _ -> 34 | {state, true} 35 | end 36 | end 37 | 38 | @spec put_permission(t, peer_ip :: MongooseICE.ip) :: t 39 | def put_permission(turn_state, peer) do 40 | expire_at = MongooseICE.Time.system_time(:second) + @permission_lifetime 41 | new_permissions = Map.put(turn_state.permissions, peer, expire_at) 42 | %__MODULE__{turn_state | permissions: new_permissions} 43 | end 44 | 45 | @spec get_channel(t, peer_or_number :: MongooseICE.address | Format.channel_number) 46 | :: {:ok, Channel.t} | :error 47 | def get_channel(turn, number) when is_integer(number) do 48 | find_channel turn, & &1.number == number 49 | end 50 | def get_channel(turn, peer) do 51 | find_channel turn, & &1.peer == peer 52 | end 53 | 54 | @spec find_channel(t, (Channel.t -> boolean)) :: {:ok, Channel.t} | :error 55 | defp find_channel(turn, pred) do 56 | case Enum.find(turn.channels, pred) do 57 | nil -> :error 58 | c -> {:ok, c} 59 | end 60 | end 61 | 62 | @spec put_channel(t, MongooseICE.address, Format.channel_number) :: t 63 | def put_channel(turn, peer, channel_number) do 64 | now = MongooseICE.Time.system_time(:second) 65 | channel = %Channel{peer: peer, number: channel_number, 66 | expiration_time: now + @channel_lifetime} 67 | channels = 68 | case get_channel(turn, peer) do 69 | {:ok, _} -> 70 | Enum.reject(turn.channels, & &1.peer == peer) 71 | :error -> 72 | turn.channels 73 | end 74 | %{turn | channels: [channel | channels]} 75 | end 76 | 77 | @spec has_channel(t, peer_or_number :: MongooseICE.address | Format.channel_number) 78 | :: {:ok, t, Channel.t} | {:error, t} 79 | def has_channel(turn_state, peer_or_number) do 80 | now = MongooseICE.Time.system_time(:second) 81 | with {:ok, channel} <- get_channel(turn_state, peer_or_number), 82 | {peer_ip, _} = channel.peer, 83 | true <- channel.expiration_time > now, 84 | {turn_state, true} <- has_permission(turn_state, peer_ip) do 85 | {:ok, turn_state, channel} 86 | else 87 | :error -> 88 | {:error, turn_state} 89 | false -> 90 | {:error, remove_channel(turn_state, peer_or_number)} 91 | {turn_state, false} -> 92 | {:error, turn_state} 93 | end 94 | end 95 | 96 | @spec remove_channel(t, peer_or_number :: MongooseICE.address | Format.channel_number) 97 | :: t 98 | defp remove_channel(turn_state, peer_or_number) do 99 | {:ok, channel} = get_channel(turn_state, peer_or_number) 100 | new_channels = 101 | Enum.reject(turn_state.channels, & &1.peer == channel.peer) 102 | %__MODULE__{turn_state | channels: new_channels} 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/mongooseice/turn/allocation.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.TURN.Allocation do 2 | @moduledoc false 3 | # This module defines a struct that is used to represent active TURN allocation 4 | # made by a client. 5 | 6 | defstruct socket: nil, owner_username: nil, req_id: nil, expire_at: 0 7 | 8 | @type t :: %__MODULE__{ 9 | socket: :gen_udp.socket, 10 | req_id: binary, 11 | owner_username: binary, 12 | expire_at: integer # system time in seconds 13 | } 14 | 15 | @doc false 16 | def default_lifetime, do: 10 * 60 17 | 18 | @doc false 19 | def maximum_lifetime, do: 10 * 60 * 6 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lib/mongooseice/turn/channel.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.TURN.Channel do 2 | @moduledoc false 3 | # Defines a struct representing a TRUN channel 4 | 5 | defstruct [:peer, :number, :expiration_time] 6 | 7 | @type t :: %__MODULE__{ 8 | peer: MongooseICE.address, 9 | number: Jerboa.Format.ChannelNumber, 10 | expiration_time: pos_integer 11 | } 12 | end 13 | -------------------------------------------------------------------------------- /lib/mongooseice/turn/reservation.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.TURN.Reservation do 2 | @moduledoc false 3 | 4 | ## A Reservation represents a relay address reserved 5 | ## by an allocation request with a positive `EvenPort.reserved?`. 6 | ## The relay address is created along with a RESERVATION-TOKEN 7 | ## which is returned to the client requesting the allocation, 8 | ## and is stored until another allocation request with the same 9 | ## reservation token is sent by the client. 10 | ## The Reservation is then turned into a full-blown Allocation. 11 | ## This mechanism is used to allocate consecutive port pairs, 12 | ## for example for RTP and RTCP transmissions. 13 | 14 | alias Jerboa.Format.Body.Attribute.ReservationToken 15 | 16 | defstruct [:token, :socket] 17 | 18 | @type t :: %__MODULE__{ 19 | token: ReservationToken.t, 20 | socket: MongooseICE.UDP.socket 21 | } 22 | 23 | @spec new(MongooseICE.UDP.socket) :: t 24 | def new(socket) do 25 | %__MODULE__{token: ReservationToken.new(), 26 | socket: socket} 27 | end 28 | 29 | @spec default_timeout :: MongooseICE.Time.seconds 30 | def default_timeout, do: 30 31 | 32 | ## Only intended for storing in ETS 33 | def to_tuple(%__MODULE__{} = r) do 34 | %__MODULE__{token: %ReservationToken{value: token}} = r 35 | {token, r.socket} 36 | end 37 | 38 | ## Only intended for storing in ETS 39 | def from_tuple({token, socket}) when is_binary(token) and is_port(socket) do 40 | %__MODULE__{token: %ReservationToken{value: token}, 41 | socket: socket} 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/mongooseice/turn/reservation/instance.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.TURN.Reservation.Instance do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | def init([registry, allocation_worker, reservation, timeout]) do 7 | {:ok, _owner} = Registry.register(registry, reservation.token, self()) 8 | monitor_ref = Process.monitor(allocation_worker) 9 | {:ok, %{reservation: reservation, monitor_ref: monitor_ref}, timeout} 10 | end 11 | 12 | def handle_call(:take, {from, _tag}, %{reservation: reservation} = state) do 13 | :ok = :gen_udp.controlling_process(reservation.socket, from) 14 | # Reply, clear reservation and timout right away 15 | {:reply, reservation, %{state | reservation: nil}, 0} 16 | end 17 | 18 | def handle_info(:timeout, state), do: do_stop(state) 19 | def handle_info({:DOWN, monitor_ref, :process, _obj, _info}, 20 | %{monitor_ref: monitor_ref} = state) do 21 | do_stop(state) 22 | end 23 | 24 | defp do_stop(state) do 25 | if state.reservation != nil do 26 | :ok = :inet.close(state.reservation.socket) 27 | end 28 | 29 | Process.demonitor(state.monitor_ref) 30 | {:stop, :normal, %{state | reservation: nil}} 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/mongooseice/udp.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP do 2 | @moduledoc """ 3 | UDP STUN server 4 | 5 | The easiest way to start a server is to spawn it under MongooseICE's 6 | supervision tree providing the following configuration: 7 | 8 | config :mongooseice, :servers, 9 | udp: [ip: {192,168, 1, 21}, port: 32323] 10 | 11 | ...or hook it up to your supervision tree: 12 | 13 | children = [ 14 | MongooseICE.UDP.child_spec([port: 3478]), 15 | MongooseICE.UDP.child_spec([port: 1234]), 16 | ... 17 | ] 18 | 19 | You can also start a server under MongooseICE's supervision tree 20 | using `start/1`. 21 | 22 | ## Options 23 | 24 | All methods of starting a server accept the same configuration options 25 | passed as a keyword list: 26 | * `:port` - the port which server should be bound to 27 | * `:ip` - the address of an interface which server should listen on 28 | * `:relay_ip` - the address of an interface which relay should listen on 29 | * `:realm` - public name of the server used as context of authorization. 30 | Does not have to be same as the server's hostname, yet in very basic configuration it may be. 31 | 32 | You may start multiple UDP servers at a time. 33 | """ 34 | 35 | @type socket :: :gen_udp.socket 36 | @type server_opts :: [option] 37 | @type option :: {:ip, MongooseICE.ip} | {:port, MongooseICE.portn} | 38 | {:relay_ip, MongooseICE.ip} | {:realm, String.t} 39 | 40 | @default_opts [ip: {127, 0, 0, 1}, port: 3478, relay_ip: {127, 0, 0, 1}, 41 | realm: "localhost"] 42 | @allowed_opts [:ip, :port, :relay_ip, :realm] 43 | 44 | @doc """ 45 | Starts UDP STUN server under MongooseICE's supervisor 46 | 47 | Accepts the same options as `start_link/1`. 48 | """ 49 | @spec start(server_opts) :: Supervisor.on_start_child 50 | def start(opts \\ @default_opts) do 51 | child = child_spec(opts) 52 | Supervisor.start_child(MongooseICE.Supervisor, child) 53 | end 54 | 55 | @doc """ 56 | Stops UDP server started with start/1 57 | 58 | It accepts the *port number* server is running on as argument 59 | """ 60 | @spec stop(MongooseICE.portn) :: :ok | :error 61 | def stop(port) do 62 | name = base_name(port) 63 | with :ok <- Supervisor.terminate_child(MongooseICE.Supervisor, name), 64 | :ok <- Supervisor.delete_child(MongooseICE.Supervisor, name) do 65 | :ok 66 | else 67 | _ -> :error 68 | end 69 | end 70 | 71 | @doc """ 72 | Starts UDP STUN server with given options 73 | 74 | Default options are: 75 | #{inspect @default_opts} 76 | 77 | Links the server to the calling process. 78 | """ 79 | @spec start_link(server_opts) :: Supervisor.on_start 80 | def start_link(opts \\ @default_opts) do 81 | opts = normalize_opts(opts) 82 | MongooseICE.UDP.Supervisor.start_link(opts) 83 | end 84 | 85 | @doc """ 86 | Returns child specification of UDP server which can be hooked 87 | up into supervision tree 88 | """ 89 | @spec child_spec(server_opts) :: Supervisor.Spec.spec 90 | def child_spec(opts) do 91 | opts = normalize_opts(opts) 92 | name = base_name(opts[:port]) 93 | Supervisor.Spec.supervisor(MongooseICE.UDP.Supervisor, [opts], id: name) 94 | end 95 | 96 | defp normalize_opts(opts) do 97 | @default_opts 98 | |> Keyword.merge(opts) 99 | |> Keyword.take(@allowed_opts) 100 | end 101 | 102 | @doc false 103 | def base_name(port) do 104 | "#{__MODULE__}.#{port}" |> String.to_atom() 105 | end 106 | 107 | @doc false 108 | def sup_name(base_name) do 109 | build_name(base_name, "Supervisor") 110 | end 111 | 112 | @doc false 113 | def receiver_name(base_name) do 114 | build_name(base_name, "Receiver") 115 | end 116 | 117 | @doc false 118 | def dispatcher_name(base_name) do 119 | build_name(base_name, "Dispatcher") 120 | end 121 | 122 | @doc false 123 | def worker_sup_name(base_name) do 124 | build_name(base_name, "WorkerSupervisor") 125 | end 126 | 127 | @doc false 128 | defp build_name(base, suffix) do 129 | "#{base}.#{suffix}" |> String.to_atom() 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/mongooseice/udp/dispatcher.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.Dispatcher do 2 | @moduledoc false 3 | # Process dispatching UDP datagrams to workers associated 4 | # with source IP address and port 5 | 6 | alias MongooseICE.UDP 7 | alias MongooseICE.UDP.Worker 8 | 9 | def start_link(base_name) do 10 | name = UDP.dispatcher_name(base_name) 11 | Registry.start_link(keys: :unique, name: name) 12 | end 13 | 14 | # Dispatches data to worker associated with client's 15 | # server-reflexive IP and port number 16 | @spec dispatch(atom, atom, MongooseICE.client_info, binary) :: term 17 | def dispatch(dispatcher, worker_sup, client, data) do 18 | case find_or_start_worker(dispatcher, worker_sup, client) do 19 | {:ok, pid} -> 20 | Worker.process_data(pid, data) 21 | _ -> 22 | nil 23 | end 24 | end 25 | 26 | # Registers worker in dispatcher's registry 27 | # 28 | # This functions should be called only by workers, 29 | # because keys in the registry are bound to the calling process. 30 | # When the registering process dies, the keys are automatically 31 | # deregistered. 32 | @spec register_worker(atom, pid, MongooseICE.ip, MongooseICE.portn) :: term 33 | def register_worker(dispatcher, worker_pid, ip, port) do 34 | Registry.register(dispatcher, key(ip, port), worker_pid) 35 | end 36 | 37 | @spec lookup_worker(atom, MongooseICE.ip, MongooseICE.portn) :: [{pid, pid}] 38 | def lookup_worker(dispatcher, ip, port) do 39 | Registry.lookup(dispatcher, key(ip, port)) 40 | end 41 | 42 | defp find_or_start_worker(dispatcher, worker_sup, client) do 43 | %{ip: ip, port: port} = client 44 | case lookup_worker(dispatcher, ip, port) do 45 | [{_owner, pid}] -> {:ok, pid} 46 | [] -> 47 | start_worker(worker_sup, client) 48 | end 49 | end 50 | 51 | defp start_worker(worker_sup, client) do 52 | case Worker.start(worker_sup, client) do 53 | {:ok, pid} -> 54 | {:ok, pid} 55 | _ -> 56 | :error 57 | end 58 | end 59 | 60 | defp key(ip, port), do: {ip, port} 61 | end 62 | -------------------------------------------------------------------------------- /lib/mongooseice/udp/receiver.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.Receiver do 2 | @moduledoc false 3 | # STUN UDP receiver process 4 | 5 | use GenServer 6 | 7 | @type state :: %{dispatcher: atom, 8 | worker_sup: atom, 9 | socket: MongooseICE.UDP.socket} 10 | 11 | def start_link(base_name, opts) do 12 | name = MongooseICE.UDP.receiver_name(base_name) 13 | GenServer.start_link(__MODULE__, [base_name, opts], name: name) 14 | end 15 | 16 | def init([base_name, opts]) do 17 | worker_sup = MongooseICE.UDP.worker_sup_name(base_name) 18 | dispatcher = MongooseICE.UDP.dispatcher_name(base_name) 19 | state = %{dispatcher: dispatcher, worker_sup: worker_sup, socket: nil} 20 | socket_opts = [:binary, active: burst_length(), ip: opts[:ip]] 21 | case :gen_udp.open(opts[:port], socket_opts) do 22 | {:ok, socket} -> 23 | {:ok, %{state | socket: socket}} 24 | {:error, reason} -> 25 | {:stop, "Failed to open UDP(#{opts[:ip]}:#{opts[:port]}) socket. Reason: #{inspect reason}"} 26 | end 27 | end 28 | 29 | def handle_info({:udp, socket, ip, port, data}, %{socket: socket} = state) do 30 | ## TODO: refactor to a proper struct? 31 | client = %{socket: socket, ip: ip, port: port} 32 | _ = MongooseICE.UDP.Dispatcher.dispatch(state.dispatcher, state.worker_sup, client, data) 33 | {:noreply, state} 34 | end 35 | 36 | def handle_info({:udp_passive, socket}, state) do 37 | require Logger 38 | 39 | n = burst_length() 40 | Logger.debug(~s"Processed #{n} peer packets") 41 | :inet.setopts(socket, [active: n]) 42 | {:noreply, state} 43 | end 44 | 45 | defp burst_length do 46 | 500 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/mongooseice/udp/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.Supervisor do 2 | @moduledoc false 3 | # Supervisor of UDP listener, dispatcher and workers 4 | 5 | require Logger 6 | 7 | @spec start_link(MongooseICE.UDP.server_opts) :: Supervisor.on_start 8 | def start_link(opts) do 9 | import Supervisor.Spec, warn: false 10 | 11 | base_name = MongooseICE.UDP.base_name(opts[:port]) 12 | name = MongooseICE.UDP.sup_name(base_name) 13 | 14 | children = [ 15 | supervisor(MongooseICE.UDP.Dispatcher, [base_name]), 16 | supervisor(MongooseICE.UDP.WorkerSupervisor, [base_name, opts]), 17 | worker(MongooseICE.UDP.Receiver, [base_name, opts]) 18 | ] 19 | 20 | Logger.info(~s"Starting STUN/TURN server (#{opts[:ip]}:#{opts[:port]}) " <> 21 | ~s"with relay_ip: #{opts[:relay_ip]}") 22 | 23 | opts = [strategy: :one_for_all, name: name] 24 | Supervisor.start_link(children, opts) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mongooseice/udp/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.Worker do 2 | @moduledoc false 3 | # Process handling STUN messages received over UDP 4 | # 5 | # Currently when worker receives a message which can't 6 | # be decoded or doesn't know how to process a message 7 | # it simply crashes. 8 | 9 | alias MongooseICE.UDP 10 | alias MongooseICE.TURN 11 | alias MongooseICE.STUN 12 | alias MongooseICE.UDP.{WorkerSupervisor, Dispatcher} 13 | 14 | use GenServer 15 | require Logger 16 | 17 | # should be configurable 18 | @timeout 5_000 19 | 20 | # how many packets should we accept per one :inet.setopts(socket, {:active, N}) call? 21 | @burst_length 500 22 | 23 | @type state :: %{socket: UDP.socket, 24 | nonce_updated_at: integer, 25 | client: MongooseICE.client_info, 26 | server: UDP.server_opts, 27 | turn: TURN.t 28 | } 29 | 30 | # Starts a UDP worker 31 | @spec start(atom, MongooseICE.client_info) :: {:ok, pid} | :error 32 | def start(worker_sup, client) do 33 | WorkerSupervisor.start_worker(worker_sup, client) 34 | end 35 | 36 | # Process UDP datagram which might be STUN message 37 | @spec process_data(pid, binary) :: :ok 38 | def process_data(pid, data) do 39 | GenServer.cast(pid, {:process_data, data}) 40 | end 41 | 42 | def start_link(dispatcher, server_opts, client) do 43 | GenServer.start_link(__MODULE__, [dispatcher, server_opts, client]) 44 | end 45 | 46 | ## GenServer callbacks 47 | 48 | def init([dispatcher, server_opts, client]) do 49 | _ = Dispatcher.register_worker(dispatcher, self(), client.ip, client.port) 50 | state = %{client: client, nonce_updated_at: 0, 51 | server: server_opts, turn: %TURN{}} 52 | {:ok, state, timeout(state)} 53 | end 54 | 55 | def handle_call(:get_permissions, _from, state) do 56 | {:reply, state.turn.permissions, state, timeout(state)} 57 | end 58 | def handle_call(:get_channels, _from, state) do 59 | {:reply, state.turn.channels, state} 60 | end 61 | 62 | def handle_cast({:process_data, data}, state) do 63 | state = maybe_update_nonce(state) 64 | next_state = 65 | case STUN.process_message(data, state.client, state.server, state.turn) do 66 | {:ok, {:void, new_turn_state}} -> 67 | %{state | turn: new_turn_state} 68 | {:ok, {resp, new_turn_state}} -> 69 | :ok = :gen_udp.send(state.client.socket, state.client.ip, 70 | state.client.port, resp) 71 | %{state | turn: new_turn_state} 72 | end 73 | {:noreply, next_state, timeout(next_state)} 74 | end 75 | 76 | def handle_info({:udp, socket, ip, port, data}, state = %{turn: 77 | %TURN{allocation: %TURN.Allocation{socket: socket}}}) do 78 | turn_state = state.turn 79 | next_state = 80 | case TURN.has_permission(turn_state, ip) do 81 | {^turn_state, false} -> 82 | Logger.debug(~s"Dropped data from peer #{ip}:#{port} due to no permission") 83 | __MODULE__.handle_peer_data(:no_permission, ip, port, data, state) 84 | {new_turn_state, false} -> 85 | Logger.debug(~s"Dropped data from peer #{ip}:#{port} due to stale permission") 86 | next_state = %{state | turn: new_turn_state} 87 | __MODULE__.handle_peer_data(:stale_permission, ip, port, data, next_state) 88 | {^turn_state, true} -> 89 | Logger.debug(~s"Processing data from peer #{ip}:#{port}") 90 | __MODULE__.handle_peer_data(:allowed, ip, port, data, state) 91 | end 92 | {:noreply, next_state, timeout(next_state)} 93 | end 94 | 95 | def handle_info({:udp_passive, socket}, 96 | %{turn: %TURN{allocation: %TURN.Allocation{socket: socket}}} = state) do 97 | n = burst_length() 98 | Logger.debug(~s"Processed #{n} peer packets") 99 | :inet.setopts(socket, [active: n]) 100 | {:noreply, state, timeout(state)} 101 | end 102 | 103 | def handle_info(:timeout, state) do 104 | handle_timeout(state) 105 | end 106 | 107 | def handle_peer_data(:allowed, ip, port, data, state) do 108 | {turn, payload} = 109 | case TURN.has_channel(state.turn, {ip, port}) do 110 | {:ok, turn_state, channel} -> 111 | {turn_state, channel_data(channel.number, data)} 112 | {:error, turn_state} -> 113 | {turn_state, data_params(ip, port, data)} 114 | end 115 | :ok = :gen_udp.send(state.client.socket, state.client.ip, state.client.port, 116 | Jerboa.Format.encode(payload)) 117 | %{state | turn: turn} 118 | end 119 | # This function clause is for (not) handling rejected peer's data. 120 | # It exists to make testing easier and to delete expired channels. 121 | def handle_peer_data(_, ip, port, _data, state) do 122 | turn_state = 123 | case TURN.has_channel(state.turn, {ip, port}) do 124 | {:ok, turn, _} -> turn 125 | {:error, turn} -> turn 126 | end 127 | %{state | turn: turn_state} 128 | end 129 | 130 | # Extracted as a separate function, 131 | # as it's easier to trace for side effects this way. 132 | defp handle_timeout(state) do 133 | {:stop, :normal, state} 134 | end 135 | 136 | defp maybe_update_nonce(state) do 137 | %{nonce_updated_at: last_update, turn: turn_state} = state 138 | expire_at = last_update + MongooseICE.Auth.nonce_lifetime() 139 | now = MongooseICE.Time.system_time(:second) 140 | case expire_at < now do 141 | true -> 142 | new_turn_state = %TURN{turn_state | nonce: MongooseICE.Auth.gen_nonce()} 143 | %{state | turn: new_turn_state, nonce_updated_at: now} 144 | false -> 145 | state 146 | end 147 | end 148 | 149 | defp timeout(%{turn: %TURN{allocation: nil}}), do: @timeout 150 | defp timeout(%{turn: %TURN{allocation: allocation}}) do 151 | %TURN.Allocation{expire_at: expire_at} = allocation 152 | now = MongooseICE.Time.system_time(:second) 153 | timeout_ms = (expire_at - now) * 1000 154 | max(0, timeout_ms) 155 | end 156 | 157 | defp data_params(ip, port, data) do 158 | alias Jerboa.Params, as: P 159 | alias Jerboa.Format.Body.Attribute.{Data, XORPeerAddress} 160 | P.new() 161 | |> P.put_class(:indication) 162 | |> P.put_method(:data) 163 | |> P.put_attr(%Data{content: data}) 164 | |> P.put_attr(XORPeerAddress.new(ip, port)) 165 | end 166 | 167 | defp channel_data(channel_number, data) do 168 | alias Jerboa.ChannelData 169 | %ChannelData{channel_number: channel_number, data: data} 170 | end 171 | 172 | def burst_length, do: @burst_length 173 | end 174 | -------------------------------------------------------------------------------- /lib/mongooseice/udp/worker_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.WorkerSupervisor do 2 | @moduledoc false 3 | # Supervisor of `MongooseICE.UDP.Worker` processes 4 | 5 | alias MongooseICE.UDP 6 | 7 | def start_link(base_name, server_opts) do 8 | import Supervisor.Spec, warn: false 9 | 10 | name = UDP.worker_sup_name(base_name) 11 | dispatcher = UDP.dispatcher_name(base_name) 12 | 13 | children = [ 14 | worker(MongooseICE.UDP.Worker, [dispatcher, server_opts], restart: :temporary) 15 | ] 16 | 17 | opts = [strategy: :simple_one_for_one, name: name] 18 | Supervisor.start_link(children, opts) 19 | end 20 | 21 | # Starts worker under WorkerSupervisor 22 | @spec start_worker(atom, MongooseICE.client_info) :: {:ok, pid} | :error 23 | def start_worker(worker_sup, client) do 24 | case Supervisor.start_child(worker_sup, [client]) do 25 | {:ok, pid} -> 26 | {:ok, pid} 27 | _ -> 28 | :error 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :mongooseice, 6 | version: "0.4.1", 7 | name: "MongooseICE", 8 | description: "STUN/TURN server", 9 | source_url: "https://github.com/esl/mongooseice", 10 | homepage_url: "http://mongooseim.readthedocs.io", 11 | elixir: "~> 1.6", 12 | elixirc_paths: elixirc_paths(Mix.env), 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | deps: deps(), 16 | package: package(), 17 | docs: docs(), 18 | dialyzer: dialyzer(), 19 | test_coverage: test_coverage(), 20 | preferred_cli_env: preferred_cli_env()] 21 | end 22 | 23 | def application do 24 | [extra_applications: [:logger, :runtime_tools, :crypto], 25 | mod: {MongooseICE.Application, []}] 26 | end 27 | 28 | defp elixirc_paths(:test), do: ["lib", "test/helper.ex"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | defp deps do 32 | [{:confex, "~> 3.4.0"}, 33 | {:mix_docker, "~> 0.5.0", runtime: false}, 34 | {:ex_doc, "~> 0.14", runtime: false, only: :dev}, 35 | {:credo, "~> 1.0", runtime: false, only: :dev}, 36 | {:dialyxir, "~> 1.0-pre", runtime: false, only: :dev}, 37 | {:excoveralls, "~> 0.5", runtime: false, only: :test}, 38 | {:inch_ex, "~> 0.5", runtime: false, only: :dev}, 39 | {:mock, "~> 0.3.3", only: :test}, 40 | {:distillery, "~> 2.0", override: true}, 41 | {:jerboa, "~> 0.3"}] 42 | end 43 | 44 | defp package do 45 | [licenses: ["Apache 2.0"], 46 | maintainers: ["Erlang Solutions"], 47 | links: %{"GitHub" => "https://github.com/esl/mongooseice"}] 48 | end 49 | 50 | defp docs do 51 | [main: "MongooseICE", 52 | logo: "static/mongooseim_logo.png", 53 | extras: ["README.md": [title: "MongooseICE"]]] 54 | end 55 | 56 | defp dialyzer do 57 | [plt_core_path: ".dialyzer/", 58 | flags: ["-Wunmatched_returns", "-Werror_handling", 59 | "-Wrace_conditions", "-Wunderspecs"]] 60 | end 61 | 62 | defp test_coverage do 63 | [tool: ExCoveralls] 64 | end 65 | 66 | defp preferred_cli_env do 67 | [coveralls: :test, "coveralls.detail": :test, 68 | "coveralls.travis": :test, "coveralls.html": :test] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "artificery": {:hex, :artificery, "0.4.2", "3ded6e29e13113af52811c72f414d1e88f711410cac1b619ab3a2666bbd7efd4", [:mix], [], "hexpm"}, 3 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, 4 | "certifi": {:hex, :certifi, "1.1.0", "c9b71a547016c2528a590ccfc28de786c7edb74aafa17446b84f54e04efc00ee", [:rebar3], []}, 5 | "confex": {:hex, :confex, "3.4.0", "8b1c3cc7a93320291abb31223a178df19d7f722ee816c05a8070c8c9a054560d", [:mix], [], "hexpm"}, 6 | "credo": {:hex, :credo, "1.1.0", "e0c07b2fd7e2109495f582430a1bc96b2c71b7d94c59dfad120529f65f19872f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "distillery": {:hex, :distillery, "2.0.14", "25fc1cdad06282334dbf4a11b6e869cc002855c4e11825157498491df2eed594", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], []}, 10 | "erlex": {:hex, :erlex, "0.2.2", "cb0e6878fdf86dc63509eaf2233a71fa73fc383c8362c8ff8e8b6f0c2bb7017c", [:mix], [], "hexpm"}, 11 | "ex_doc": {:hex, :ex_doc, "0.16.1", "b4b8a23602b4ce0e9a5a960a81260d1f7b29635b9652c67e95b0c2f7ccee5e81", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, 12 | "excoveralls": {:hex, :excoveralls, "0.6.3", "894bf9254890a4aac1d1165da08145a72700ff42d8cb6ce8195a584cb2a4b374", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, 13 | "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, 14 | "hackney": {:hex, :hackney, "1.8.0", "8388a22f4e7eb04d171f2cf0285b217410f266d6c13a4c397a6c22ab823a486c", [:rebar3], [{:certifi, "1.1.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 15 | "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, 16 | "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]}, 17 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 18 | "jerboa": {:hex, :jerboa, "0.3.0", "5e9017347b699285ef6f2b7d3bd4b77d9d15706e2f8096ded9ba309d8d64a0c0", [:mix], [], "hexpm"}, 19 | "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], []}, 20 | "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, 21 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 22 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 23 | "mix_docker": {:hex, :mix_docker, "0.5.0", "c7ad34008c43d4a949d69721f39c4d2a2afc509c179926a683117ea8dff8af59", [:mix], [{:distillery, "~> 1.2", [hex: :distillery, repo: "hexpm", optional: false]}], "hexpm"}, 24 | "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, 25 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, 26 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, 27 | } 28 | -------------------------------------------------------------------------------- /priv/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esl/MongooseICE/358ba7087a6e19cd95313c365567663f4cab848f/priv/.keep -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | # Import all plugins from `rel/plugins` 2 | # They can then be used by adding `plugin MyPlugin` to 3 | # either an environment, or release definition, where 4 | # `MyPlugin` is the name of the plugin module. 5 | ~w(rel plugins *.exs) 6 | |> Path.join() 7 | |> Path.wildcard() 8 | |> Enum.map(&Code.eval_file(&1)) 9 | 10 | use Mix.Releases.Config, 11 | # This sets the default release built by `mix release` 12 | default_release: :default, 13 | # This sets the default environment used by `mix release` 14 | default_environment: Mix.env() 15 | 16 | # For a full list of config options for both releases 17 | # and environments, visit https://hexdocs.pm/distillery/config/distillery.html 18 | 19 | 20 | # You may define one or more environments in this file, 21 | # an environment's settings will override those of a release 22 | # when building in that environment, this combination of release 23 | # and environment configuration is called a profile 24 | 25 | environment :dev do 26 | # If you are running Phoenix, you should make sure that 27 | # server: true is set and the code reloader is disabled, 28 | # even in dev mode. 29 | # It is recommended that you build with MIX_ENV=prod and pass 30 | # the --env flag to Distillery explicitly if you want to use 31 | # dev mode. 32 | set dev_mode: true 33 | set include_erts: false 34 | set cookie: :"X9qx~N1zN2?c$HBR1=nR~Bt8[CIEYiqq;kKnB2c%i1@xc{lsUPb%`p8p$)eU;Ss<" 35 | end 36 | 37 | environment :prod do 38 | set include_erts: true 39 | set include_src: false 40 | set cookie: :"x~f_nWyKxy3WdIt2F`2J1MgM0HnLBb;wLm/n2t3Ned[AeVDEeX>Vd|.12.ISvq20" 41 | set vm_args: "rel/vm.args" 42 | end 43 | 44 | # You may define one or more releases in this file. 45 | # If you have not set a default release, or selected one 46 | # when running `mix release`, the first release in the file 47 | # will be used by default 48 | 49 | release :mongooseice do 50 | set version: current_version(:mongooseice) 51 | set applications: [ 52 | :runtime_tools 53 | ] 54 | end 55 | 56 | -------------------------------------------------------------------------------- /rel/plugins/.gitignore: -------------------------------------------------------------------------------- 1 | *.* 2 | !*.exs 3 | !.gitignore 4 | 5 | -------------------------------------------------------------------------------- /rel/vm.args: -------------------------------------------------------------------------------- 1 | ## This file provide the arguments provided to the VM at startup 2 | ## You can find a full list of flags and their behaviours at 3 | ## http://erlang.org/doc/man/erl.html 4 | 5 | ## Name of the node 6 | -name <%= release_name %>@127.0.0.1 7 | 8 | ## Cookie for distributed erlang 9 | -setcookie <%= release.profile.cookie %> 10 | 11 | ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive 12 | ## (Disabled by default..use with caution!) 13 | ##-heart 14 | 15 | ## Enable kernel poll and a few async threads 16 | ##+K true 17 | ##+A 5 18 | ## For OTP21+, the +A flag is not used anymore, 19 | ## +SDio replace it to use dirty schedulers 20 | ##+SDio 5 21 | 22 | ## Increase number of concurrent ports/sockets 23 | ##-env ERL_MAX_PORTS 4096 24 | 25 | ## Tweak GC to run more often 26 | ##-env ERL_FULLSWEEP_AFTER 10 27 | 28 | # Enable SMP automatically based on availability 29 | # On OTP21+, this is not needed anymore. 30 | -smp auto 31 | -------------------------------------------------------------------------------- /static/mongooseim_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esl/MongooseICE/358ba7087a6e19cd95313c365567663f4cab848f/static/mongooseim_logo.png -------------------------------------------------------------------------------- /test/helper.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.Test.Helper do 2 | @moduledoc false 3 | 4 | defmodule Server do 5 | @moduledoc false 6 | 7 | def configuration(name) when is_binary(name) do 8 | Enum.find(configuration(), select(name)) 9 | end 10 | 11 | defp configuration do 12 | Keyword.fetch!(environment(), :server) 13 | end 14 | 15 | defp select(name) do 16 | fn %{name: ^name} -> 17 | true 18 | %{name: _} -> 19 | false 20 | end 21 | end 22 | 23 | defp environment do 24 | Application.fetch_env!(:jerboa, :test) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/helper/allocation.ex: -------------------------------------------------------------------------------- 1 | defmodule Helper.Allocation do 2 | 3 | def monitor_owner(ctx) do 4 | Process.monitor(owner(ctx)) 5 | end 6 | 7 | def owner(ctx) do 8 | [{_, relay_pid, _, _}] = Supervisor.which_children(udp_worker_sup(ctx.udp.server_port)) 9 | relay_pid 10 | end 11 | 12 | defp udp_worker_sup(port) do 13 | port 14 | |> MongooseICE.UDP.base_name() 15 | |> MongooseICE.UDP.worker_sup_name() 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/helper/macros.ex: -------------------------------------------------------------------------------- 1 | defmodule Helper.Macros do 2 | defmacro __using__(_opts) do 3 | quote do 4 | @eventually_timeout 5_000 5 | import unquote(__MODULE__) 6 | import Mock 7 | end 8 | end 9 | 10 | defmacro no_auth(do_something) do 11 | quote do 12 | with_mock MongooseICE.Auth, [:passthrough], [ 13 | maybe: fn(_, p, _, _) -> {:ok, p} end 14 | ] do 15 | unquote do_something 16 | end 17 | end 18 | end 19 | 20 | defmacro eventually(truly) do 21 | quote do 22 | Helper.Macros.wait_for(fn -> unquote truly end, @eventually_timeout) 23 | end 24 | end 25 | 26 | # This macro sends the binary request and return binary response. 27 | # The response will be returned for both :request and :indication but 28 | # using different methods. For :request, normal UDP communication is used, 29 | # while for :indication, this macro uses Mock history to get final response params 30 | # since those are dropped just before sending via UDP due to nature of :indications 31 | defmacro communicate_all(udp, client_id, req) do 32 | alias Helper.UDP 33 | alias Jerboa.Params 34 | quote do 35 | # First, we need to mock MongooseICE.Evaluator.on_result to gather results 36 | with_mock MongooseICE.Evaluator, [:passthrough], [] do 37 | # Then we send the request 38 | :ok = UDP.send(unquote(udp), unquote(client_id), unquote(req)) 39 | case Params.get_class(Jerboa.Format.decode!(unquote(req))) do 40 | :request -> unquote(receive_request_response(udp, client_id)) 41 | :indication -> unquote(receive_indication_response()) 42 | end 43 | end 44 | end 45 | end 46 | 47 | defp receive_request_response(udp, client_id) do 48 | # In case of the request, get response via UDP 49 | quote do 50 | Helper.UDP.recv(unquote(udp), unquote(client_id)) 51 | end 52 | end 53 | 54 | defp receive_indication_response() do 55 | # For indication we need to get result from MongooseICE.Evaluator.on_result 56 | # call history 57 | quote do 58 | assert eventually called MongooseICE.Evaluator.on_result(:indication, :_) 59 | history = :meck.history(MongooseICE.Evaluator) 60 | history = # Filter only calls to on_result/2 61 | Enum.filter(history, fn(entry) -> 62 | case entry do 63 | {_caller, {_mod, :on_result, _args}, _ret} -> true 64 | _ -> false 65 | end 66 | end) 67 | # Get last call 68 | {_caller, {_mod, _fun, [_calss, params]}, _ret} = List.last(history) 69 | Jerboa.Format.encode(params) 70 | end 71 | end 72 | 73 | def wait_for(fun, timeout) when timeout > 0 do 74 | timestep = 100 75 | case fun.() do 76 | true -> true 77 | false -> 78 | Process.sleep(timestep) 79 | wait_for(fun, timeout - timestep) 80 | end 81 | end 82 | def wait_for(fun, _timeout), do: fun.() 83 | 84 | end 85 | -------------------------------------------------------------------------------- /test/helper/port_master.ex: -------------------------------------------------------------------------------- 1 | defmodule Helper.PortMaster do 2 | use GenServer 3 | 4 | @base_client_port 32_000 5 | @base_server_port 12_000 6 | @server_name :port_master 7 | 8 | def start_link do 9 | GenServer.start_link(__MODULE__, [], name: @server_name) 10 | end 11 | 12 | def checkout_port(type) do 13 | GenServer.call(@server_name, {:checkout_port, type}) 14 | end 15 | 16 | def init(_opts) do 17 | {:ok, %{ 18 | next_client: @base_client_port, 19 | next_server: @base_server_port 20 | }} 21 | end 22 | 23 | def handle_call({:checkout_port, :client}, _from, state) do 24 | new_state = %{state | next_client: state.next_client + 1} 25 | {:reply, state.next_client, new_state} 26 | end 27 | 28 | def handle_call({:checkout_port, :server}, _from, state) do 29 | new_state = %{state | next_server: state.next_server + 1} 30 | {:reply, state.next_server, new_state} 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /test/helper/udp.ex: -------------------------------------------------------------------------------- 1 | defmodule Helper.UDP do 2 | use ExUnit.Case 3 | use Helper.Macros 4 | 5 | alias Jerboa.Params 6 | alias Jerboa.ChannelData 7 | alias Jerboa.Format 8 | alias Jerboa.Format.Body.Attribute.{Username, RequestedTransport, 9 | XORPeerAddress, ChannelNumber} 10 | 11 | @recv_timeout 5_000 12 | @default_user "user" 13 | 14 | ## Requests definitions 15 | 16 | def binding_request(id) do 17 | %Params{class: :request, method: :binding, identifier: id} |> Format.encode() 18 | end 19 | 20 | def binding_indication(id) do 21 | %Params{class: :indication, method: :binding, identifier: id} |> Format.encode() 22 | end 23 | 24 | def allocate_request(id) do 25 | allocate_request(id, [%RequestedTransport{protocol: :udp}]) 26 | end 27 | 28 | def allocate_request(id, attrs) do 29 | allocate_params(id, attrs) 30 | |> Format.encode() 31 | end 32 | 33 | def allocate_params(id, attrs) do 34 | %Params{class: :request, method: :allocate, identifier: id, 35 | attributes: attrs} 36 | end 37 | 38 | def send_indication(id, attrs) do 39 | send_params(id, attrs) |> Format.encode() 40 | end 41 | 42 | def send_params(id, attrs) do 43 | %Params{class: :indication, method: :send, identifier: id, 44 | attributes: attrs} 45 | end 46 | 47 | def create_permission_request(id, attrs) do 48 | create_permission_params(id, attrs) 49 | |> Format.encode() 50 | end 51 | 52 | def create_permission_params(id, attrs) do 53 | %Params{class: :request, method: :create_permission, identifier: id, 54 | attributes: attrs} 55 | end 56 | 57 | def refresh_request(id, attrs) do 58 | refresh_params(id, attrs) 59 | |> Format.encode() 60 | end 61 | 62 | def refresh_params(id, attrs) do 63 | %Params{class: :request, method: :refresh, identifier: id, 64 | attributes: attrs} 65 | end 66 | 67 | def channel_bind_request(id, attrs) do 68 | channel_bind_params(id, attrs) 69 | |> Format.encode() 70 | end 71 | 72 | def channel_bind_params(id, attrs) do 73 | %Params{identifier: id} 74 | |> Params.put_class(:request) 75 | |> Params.put_method(:channel_bind) 76 | |> Params.set_attrs(attrs) 77 | end 78 | 79 | def channel_data(channel_number, data) do 80 | %ChannelData{channel_number: channel_number, data: data} 81 | |> Format.encode() 82 | end 83 | 84 | def peers(peers) do 85 | for ip <- peers do 86 | %XORPeerAddress{ 87 | address: ip, 88 | port: 0, 89 | family: MongooseICE.Evaluator.Helper.family(ip) 90 | } 91 | end 92 | end 93 | 94 | ## UDP Client 95 | 96 | def allocate(udp, opts \\ []) do 97 | opts = Keyword.merge([username: @default_user, 98 | client_id: 0, 99 | attributes: []], opts) 100 | id = Params.generate_id() 101 | req = allocate_request(id, opts[:attributes] ++ [ 102 | %RequestedTransport{protocol: :udp}, 103 | %Username{value: opts[:username]} 104 | ]) 105 | resp = no_auth(communicate(udp, opts[:client_id], req)) 106 | params = Format.decode!(resp) 107 | %Params{class: :success, 108 | method: :allocate, 109 | identifier: ^id} = params 110 | end 111 | 112 | def create_permissions(udp, ips, username \\ @default_user, client_id \\ 0) do 113 | id = Params.generate_id() 114 | req = create_permission_request(id, peers(ips) ++ [ 115 | %Username{value: username} 116 | ]) 117 | resp = no_auth(communicate(udp, client_id, req)) 118 | params = Format.decode!(resp) 119 | %Params{class: :success, 120 | method: :create_permission, 121 | identifier: ^id} = params 122 | end 123 | 124 | def refresh(udp, attrs \\ [], username \\ @default_user, client_id \\ 0) do 125 | id = Params.generate_id() 126 | req = refresh_request(id, attrs ++ [%Username{value: username}]) 127 | resp = no_auth(communicate(udp, client_id, req)) 128 | params = Format.decode!(resp) 129 | %Params{class: :success, 130 | method: :refresh, 131 | identifier: ^id} = params 132 | end 133 | 134 | def channel_bind(udp, channel_number, peer_ip, peer_port) do 135 | id = Params.generate_id() 136 | attrs = [XORPeerAddress.new(peer_ip, peer_port), 137 | %ChannelNumber{number: channel_number}] 138 | req = channel_bind_request(id, attrs) 139 | resp = no_auth(communicate(udp, 0, req)) 140 | params = Format.decode!(resp) 141 | %Params{class: :success, 142 | method: :channel_bind, 143 | identifier: ^id} = params 144 | end 145 | 146 | ## Communication 147 | 148 | def setup_connection(_ctx, family \\ :ipv4) do 149 | addr = 150 | case family do 151 | :ipv4 -> {127, 0, 0, 1} 152 | :ipv6 -> {0, 0, 0, 0, 0, 0, 0, 1} 153 | end 154 | udp = connect(addr, addr, 1) 155 | on_exit fn -> close(udp) end 156 | udp 157 | end 158 | 159 | def connect(server_address, client_address, client_count) do 160 | server_port = Helper.PortMaster.checkout_port(:server) 161 | client_port = Helper.PortMaster.checkout_port(:client) 162 | Application.put_env(:mongooseice, :relay_addr, server_address) 163 | MongooseICE.UDP.start_link(ip: server_address, port: server_port, 164 | relay_ip: server_address) 165 | 166 | sockets = 167 | for i <- 1..client_count do 168 | {:ok, sock} = 169 | :gen_udp.open(client_port + i, 170 | [:binary, active: false, ip: client_address]) 171 | sock 172 | end 173 | 174 | %{ 175 | server_address: server_address, 176 | server_port: server_port, 177 | client_address: client_address, 178 | client_port_base: client_port, 179 | sockets: sockets 180 | } 181 | end 182 | 183 | def socket(udp, client_id) do 184 | Enum.at(udp.sockets, client_id) 185 | end 186 | 187 | def port(udp, client_id) do 188 | udp.client_port_base + client_id + 1 189 | end 190 | 191 | def close(%{sockets: sockets}) do 192 | for sock <- sockets do 193 | :gen_udp.close(sock) 194 | end 195 | end 196 | 197 | def send(udp, client_id, req) do 198 | sock = Enum.at(udp.sockets, client_id) 199 | :ok = :gen_udp.send(sock, udp.server_address, udp.server_port, req) 200 | end 201 | 202 | def recv(udp, client_id) do 203 | %{server_address: server_address, server_port: server_port} = udp 204 | {sock, _} = List.pop_at(udp.sockets, client_id) 205 | {:ok, {^server_address, 206 | ^server_port, 207 | resp}} = :gen_udp.recv(sock, 0, @recv_timeout) 208 | resp 209 | end 210 | 211 | def communicate(udp, client_id, req) do 212 | :ok = send(udp, client_id, req) 213 | recv(udp, client_id) 214 | end 215 | 216 | def client_port(udp, client_id) do 217 | udp.client_port_base + client_id + 1 218 | end 219 | 220 | def worker(udp, client_id) do 221 | alias MongooseICE.UDP.Dispatcher 222 | 223 | base_name = MongooseICE.UDP.base_name(udp.server_port) 224 | dispatcher = MongooseICE.UDP.dispatcher_name(base_name) 225 | [{_, worker}] = Dispatcher.lookup_worker(dispatcher, udp.client_address, 226 | client_port(udp, client_id)) 227 | worker 228 | end 229 | 230 | end 231 | -------------------------------------------------------------------------------- /test/mongooseice/udp/allocate_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.AllocateTest do 2 | use ExUnit.Case 3 | use Helper.Macros 4 | 5 | alias Helper.UDP 6 | alias Jerboa.Params 7 | alias Jerboa.Format 8 | alias Jerboa.Format.Body.Attribute.{XORMappedAddress, Lifetime, 9 | XORRelayedAddress, ErrorCode, 10 | RequestedTransport, EvenPort, 11 | ReservationToken, Lifetime} 12 | 13 | require Integer 14 | 15 | describe "allocate request" do 16 | 17 | setup do 18 | {:ok, [udp: UDP.setup_connection([], :ipv4)]} 19 | end 20 | 21 | test "fails without RequestedTransport attribute", ctx do 22 | udp = ctx.udp 23 | id = Params.generate_id() 24 | req = UDP.allocate_request(id, []) 25 | 26 | resp = no_auth(UDP.communicate(udp, 0, req)) 27 | 28 | params = Format.decode!(resp) 29 | assert %Params{class: :failure, 30 | method: :allocate, 31 | identifier: ^id, 32 | attributes: [error]} = params 33 | 34 | assert %ErrorCode{code: 400} = error 35 | end 36 | 37 | test "fails if EvenPort and ReservationToken are supplied", ctx do 38 | udp = ctx.udp 39 | id = Params.generate_id() 40 | req = UDP.allocate_request(id, [ 41 | %RequestedTransport{protocol: :udp}, 42 | %EvenPort{}, 43 | %ReservationToken{value: "12345678"} 44 | ]) 45 | 46 | resp = no_auth(UDP.communicate(udp, 0, req)) 47 | 48 | params = Format.decode!(resp) 49 | assert %Params{class: :failure, 50 | method: :allocate, 51 | identifier: ^id, 52 | attributes: [error]} = params 53 | 54 | assert %ErrorCode{code: 400} = error 55 | end 56 | 57 | test "returns response with IPv4 XOR relayed address attribute", ctx do 58 | udp = ctx.udp 59 | %{server_address: server_address, client_address: client_address} = udp 60 | client_port = UDP.client_port(udp, 0) 61 | id = Params.generate_id() 62 | req = UDP.allocate_request(id) 63 | 64 | resp = no_auth(UDP.communicate(udp, 0, req)) 65 | 66 | params = Format.decode!(resp) 67 | assert %Params{class: :success, 68 | method: :allocate, 69 | identifier: ^id, 70 | attributes: attrs} = params 71 | [lifetime, mapped, relayed] = Enum.sort(attrs) 72 | 73 | assert %Lifetime{duration: 600} = lifetime 74 | 75 | assert %XORMappedAddress{address: ^client_address, 76 | port: ^client_port, 77 | family: :ipv4} = mapped 78 | 79 | assert %XORRelayedAddress{address: ^server_address, 80 | port: relayed_port, 81 | family: :ipv4} = relayed 82 | assert relayed_port != udp.server_port 83 | end 84 | 85 | test "returns error after second allocation with different id", ctx do 86 | udp = ctx.udp 87 | id1 = Params.generate_id() 88 | id2 = Params.generate_id() 89 | req1 = UDP.allocate_request(id1) 90 | req2 = UDP.allocate_request(id2) 91 | 92 | resp1 = no_auth(UDP.communicate(udp, 0, req1)) 93 | params1 = Format.decode!(resp1) 94 | assert %Params{class: :success, 95 | method: :allocate, 96 | identifier: ^id1} = params1 97 | 98 | resp2 = no_auth(UDP.communicate(udp, 0, req2)) 99 | 100 | params2 = Format.decode!(resp2) 101 | assert %Params{class: :failure, 102 | method: :allocate, 103 | identifier: ^id2, 104 | attributes: [error]} = params2 105 | assert %ErrorCode{code: 437} = error 106 | end 107 | 108 | test "returns success after second allocation with the same id", ctx do 109 | udp = ctx.udp 110 | id = Params.generate_id() 111 | req = UDP.allocate_request(id) 112 | 113 | resp1 = no_auth(UDP.communicate(udp, 0, req)) 114 | params1 = Format.decode!(resp1) 115 | assert %Params{class: :success, 116 | method: :allocate, 117 | identifier: ^id} = params1 118 | 119 | resp2 = no_auth(UDP.communicate(udp, 0, req)) 120 | 121 | params2 = Format.decode!(resp2) 122 | assert %Params{class: :success, 123 | method: :allocate, 124 | identifier: ^id, 125 | attributes: attrs} = params2 126 | assert 3 = length(attrs) 127 | end 128 | 129 | end 130 | 131 | describe "allocate request with EVEN-PORT attribute" do 132 | 133 | test "allocates an even port" do 134 | addr = {127, 0, 0, 1} 135 | for _ <- 1..100 do 136 | udp = UDP.connect(addr, addr, 1) 137 | id = Params.generate_id() 138 | req = UDP.allocate_request(id, [ 139 | %RequestedTransport{protocol: :udp}, 140 | %EvenPort{} 141 | ]) 142 | 143 | resp = no_auth(UDP.communicate(udp, 0, req)) 144 | params = Format.decode!(resp) 145 | assert %Params{class: :success, 146 | method: :allocate, 147 | identifier: ^id} = params 148 | %XORRelayedAddress{port: relay_port} = Params.get_attr(params, XORRelayedAddress) 149 | assert Integer.is_even(relay_port) 150 | UDP.close(udp) 151 | end 152 | end 153 | 154 | test "reserves a higher port if requested" do 155 | ## given a TURN server 156 | addr = {127, 0, 0, 1} 157 | ## when allocating a UDP relay address with an even port 158 | ## and reserving the next port 159 | udp1 = UDP.connect(addr, addr, 1) 160 | on_exit fn -> UDP.close(udp1) end 161 | params1 = UDP.allocate(udp1, attributes: [ 162 | %RequestedTransport{protocol: :udp}, 163 | %EvenPort{reserved?: true} 164 | ]) 165 | %XORRelayedAddress{port: relay_port1} = Params.get_attr(params1, XORRelayedAddress) 166 | reservation_token = Params.get_attr(params1, ReservationToken) 167 | ## then the next allocation with a RESERVATION-TOKEN 168 | ## allocates a relay address with the reserved port 169 | udp2 = UDP.connect(addr, addr, 1) 170 | on_exit fn -> UDP.close(udp2) end 171 | params2 = UDP.allocate(udp2, attributes: [reservation_token]) 172 | %XORRelayedAddress{port: relay_port2} = Params.get_attr(params2, XORRelayedAddress) 173 | assert Integer.is_even(relay_port1) 174 | assert relay_port2 == relay_port1 + 1 175 | end 176 | 177 | end 178 | 179 | describe "allocation" do 180 | 181 | import Mock 182 | 183 | setup ctx do 184 | {:ok, [udp: UDP.setup_connection(ctx)]} 185 | end 186 | 187 | test "expires after timeout", ctx do 188 | ## given an existing allocation 189 | client_id = 0 190 | UDP.allocate(ctx.udp) 191 | ## when its timeout is reached 192 | mref = Helper.Allocation.monitor_owner(ctx) 193 | now = MongooseICE.Time.system_time(:second) 194 | future = now + 10_000 195 | with_mock MongooseICE.Time, [system_time: fn(:second) -> future end] do 196 | ## send indication to trigger timeout 197 | :ok = UDP.send(ctx.udp, client_id, UDP.binding_indication(Params.generate_id())) 198 | ## then the allocation is deleted 199 | assert_receive {:DOWN, ^mref, :process, _pid, _info}, 3_000 200 | assert called MongooseICE.Time.system_time(:second) 201 | end 202 | end 203 | 204 | end 205 | 206 | describe "reservation" do 207 | import Mock 208 | 209 | test "expires after timeout", _ctx do 210 | ## Set reservation timeout to 1 second 211 | with_mock MongooseICE.TURN.Reservation, [:passthrough], [default_timeout: fn() -> 1 end] do 212 | ## given a TURN server 213 | addr = {127, 0, 0, 1} 214 | ## given the allocation 215 | udp1 = UDP.connect(addr, addr, 1) 216 | on_exit fn -> UDP.close(udp1) end 217 | params1 = UDP.allocate(udp1, attributes: [ 218 | %RequestedTransport{protocol: :udp}, 219 | %EvenPort{reserved?: true} 220 | ]) 221 | reservation_token = Params.get_attr(params1, ReservationToken) 222 | 223 | ## when reservation lifetime ends 224 | Process.sleep(1500) 225 | 226 | ## then the reservation expires 227 | udp2 = UDP.connect(addr, addr, 1) 228 | on_exit fn -> UDP.close(udp2) end 229 | id = Params.generate_id() 230 | req = UDP.allocate_request(id, [ 231 | reservation_token, 232 | %RequestedTransport{protocol: :udp} 233 | ]) 234 | resp = no_auth(UDP.communicate(udp2, 0, req)) 235 | params = Format.decode!(resp) 236 | assert %Params{class: :failure, 237 | method: :allocate, 238 | identifier: ^id, 239 | attributes: [error]} = params 240 | assert %ErrorCode{name: :insufficient_capacity} = error 241 | end 242 | end 243 | 244 | test "expires if original allocation is deleted" do 245 | ## given a TURN server 246 | addr = {127, 0, 0, 1} 247 | ## given the allocation 248 | udp1 = UDP.connect(addr, addr, 1) 249 | on_exit fn -> UDP.close(udp1) end 250 | params1 = UDP.allocate(udp1, attributes: [ 251 | %RequestedTransport{protocol: :udp}, 252 | %EvenPort{reserved?: true} 253 | ]) 254 | reservation_token = Params.get_attr(params1, ReservationToken) 255 | 256 | ## when the reservation is manually removed 257 | UDP.refresh(udp1, [%Lifetime{duration: 0}]) 258 | 259 | ## when cleanups have finished 260 | Process.sleep(100) 261 | 262 | ## then the reservation expires 263 | udp2 = UDP.connect(addr, addr, 1) 264 | on_exit fn -> UDP.close(udp2) end 265 | id = Params.generate_id() 266 | req = UDP.allocate_request(id, [ 267 | reservation_token, 268 | %RequestedTransport{protocol: :udp} 269 | ]) 270 | resp = no_auth(UDP.communicate(udp2, 0, req)) 271 | params = Format.decode!(resp) 272 | assert %Params{class: :failure, 273 | method: :allocate, 274 | identifier: ^id, 275 | attributes: [error]} = params 276 | assert %ErrorCode{name: :insufficient_capacity} = error 277 | end 278 | 279 | test "expires if original allocation expires" do 280 | ## given a TURN server 281 | addr = {127, 0, 0, 1} 282 | ## given the allocation 283 | udp1 = UDP.connect(addr, addr, 1) 284 | on_exit fn -> UDP.close(udp1) end 285 | params1 = UDP.allocate(udp1, attributes: [ 286 | %RequestedTransport{protocol: :udp}, 287 | %EvenPort{reserved?: true} 288 | ]) 289 | reservation_token = Params.get_attr(params1, ReservationToken) 290 | 291 | ## when the allocation timeouts 292 | now = MongooseICE.Time.system_time(:second) 293 | future = now + 10_000 294 | with_mock MongooseICE.Time, [system_time: fn(:second) -> future end] do 295 | ## send indication to trigger timeout 296 | :ok = UDP.send(udp1, 0, UDP.binding_indication(Params.generate_id())) 297 | assert eventually called MongooseICE.Time.system_time(:second) 298 | end 299 | 300 | ## when cleanups have finished 301 | Process.sleep(100) 302 | 303 | ## then the reservation expires 304 | udp2 = UDP.connect(addr, addr, 1) 305 | on_exit fn -> UDP.close(udp2) end 306 | id = Params.generate_id() 307 | req = UDP.allocate_request(id, [ 308 | reservation_token, 309 | %RequestedTransport{protocol: :udp} 310 | ]) 311 | resp = no_auth(UDP.communicate(udp2, 0, req)) 312 | params = Format.decode!(resp) 313 | assert %Params{class: :failure, 314 | method: :allocate, 315 | identifier: ^id, 316 | attributes: [error]} = params 317 | assert %ErrorCode{name: :insufficient_capacity} = error 318 | end 319 | end 320 | 321 | end 322 | -------------------------------------------------------------------------------- /test/mongooseice/udp/auth_template.ex: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.AuthTemplate do 2 | use ExUnit.Case 3 | 4 | defmacro __using__(_opts) do 5 | quote do 6 | import unquote __MODULE__ 7 | import Mock 8 | use Helper.Macros 9 | alias Jerboa.Format.Body.Attribute.Nonce 10 | 11 | @max_value_bytes 763 - 1 12 | @max_value_chars 128 - 1 13 | @valid_secret "abc" 14 | @invalid_secret "abcd" 15 | 16 | defp get_nonce(udp) do 17 | id = Jerboa.Params.generate_id() 18 | req = Helper.UDP.allocate_request(id) 19 | resp = communicate_all(udp, 0, req) 20 | Jerboa.Params.get_attr(Jerboa.Format.decode!(resp), Nonce) 21 | end 22 | end 23 | end 24 | 25 | defmacro test_auth_for(request, base_attrs) do 26 | alias Helper.UDP 27 | alias Jerboa.Params 28 | alias Jerboa.Format 29 | alias Jerboa.Format.Body.Attribute.{ 30 | Username, 31 | ErrorCode, 32 | Nonce, 33 | Realm 34 | } 35 | 36 | request_str = Atom.to_string(request) 37 | params_fun = String.to_atom(~s"#{request}_params") 38 | quote do 39 | describe unquote(request_str) <> " request" do 40 | setup do 41 | Application.put_env(:mongooseice, :secret, @valid_secret) 42 | udp = 43 | UDP.connect({127,0,0,1}, {127,0,0,1}, 1) 44 | on_exit fn -> 45 | UDP.close(udp) 46 | end 47 | 48 | username = "bob" 49 | unquote( 50 | case request do 51 | :allocate -> nil 52 | _ -> 53 | quote do 54 | UDP.allocate(udp, username: username) 55 | UDP.create_permissions(udp, [{127,0,0,1}], username) 56 | end 57 | end 58 | ) 59 | {:ok, [udp: udp, username: username]} 60 | end 61 | 62 | test "without auth attributes returns nonce and realm", ctx do 63 | udp = ctx.udp 64 | id = Params.generate_id() 65 | req = 66 | UDP. unquote(params_fun)(id, unquote(base_attrs)) 67 | |> Format.encode() 68 | resp = communicate_all(udp, 0, req) 69 | 70 | params = Format.decode!(resp) 71 | assert %Params{class: :failure, 72 | method: unquote(request), 73 | identifier: ^id} = params 74 | 75 | assert %ErrorCode{code: 401} = Params.get_attr(params, ErrorCode) 76 | assert %Nonce{value: nonce} = Params.get_attr(params, Nonce) 77 | assert %Realm{value: realm} = Params.get_attr(params, Realm) 78 | 79 | assert String.length(nonce) > 0 80 | assert String.length(nonce) <= @max_value_chars 81 | assert byte_size(nonce) <= @max_value_bytes 82 | 83 | assert String.length(realm) > 0 84 | assert String.length(realm) <= @max_value_chars 85 | assert byte_size(realm) <= @max_value_bytes 86 | end 87 | 88 | test "with all missing attributes fails to authenticate", ctx do 89 | udp = ctx.udp 90 | id = Params.generate_id() 91 | req = 92 | UDP. unquote(params_fun)(id, unquote(base_attrs)) 93 | |> Format.encode(secret: @valid_secret, realm: "realm", username: ctx.username) 94 | 95 | resp = communicate_all(udp, 0, req) 96 | 97 | params = Format.decode!(resp) 98 | assert %Params{class: :failure, 99 | method: unquote(request), 100 | identifier: ^id} = params 101 | 102 | assert %ErrorCode{code: 400} = Params.get_attr(params, ErrorCode) 103 | end 104 | 105 | test "with missing nonce attribute fails to authenticate", ctx do 106 | udp = ctx.udp 107 | id = Params.generate_id() 108 | attrs = [ 109 | %Realm{value: "localhost"}, 110 | %Username{value: ctx.username} 111 | ] 112 | req = 113 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 114 | |> Format.encode(secret: @valid_secret) 115 | 116 | resp = communicate_all(udp, 0, req) 117 | 118 | params = Format.decode!(resp) 119 | assert %Params{class: :failure, 120 | method: unquote(request), 121 | identifier: ^id} = params 122 | 123 | assert %ErrorCode{code: 400} = Params.get_attr(params, ErrorCode) 124 | end 125 | 126 | test "with missing username attributes fails to authenticate", ctx do 127 | udp = ctx.udp 128 | id = Params.generate_id() 129 | attrs = [ 130 | %Realm{value: "localhost"}, 131 | %Nonce{value: "nonce"} 132 | ] 133 | req = 134 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 135 | |> Format.encode(secret: @valid_secret, username: ctx.username) 136 | 137 | resp = communicate_all(udp, 0, req) 138 | 139 | params = Format.decode!(resp) 140 | assert %Params{class: :failure, 141 | method: unquote(request), 142 | identifier: ^id} = params 143 | 144 | assert %ErrorCode{code: 400} = Params.get_attr(params, ErrorCode) 145 | end 146 | 147 | test "with missing realm attributes fails to authenticate", ctx do 148 | udp = ctx.udp 149 | id = Params.generate_id() 150 | attrs = [ 151 | %Username{value: ctx.username} 152 | ] 153 | req = 154 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 155 | |> Format.encode(secret: @valid_secret, realm: "localhost") 156 | 157 | resp = communicate_all(udp, 0, req) 158 | 159 | params = Format.decode!(resp) 160 | assert %Params{class: :failure, 161 | method: unquote(request), 162 | identifier: ^id} = params 163 | 164 | assert %ErrorCode{code: 400} = Params.get_attr(params, ErrorCode) 165 | end 166 | 167 | test "with invalid secret fails to authenticate", ctx do 168 | udp = ctx.udp 169 | nonce_attr = get_nonce(udp) 170 | id = Params.generate_id() 171 | attrs = [ 172 | %Username{value: ctx.username}, 173 | %Realm{value: "localhost"}, 174 | nonce_attr 175 | ] 176 | req = 177 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 178 | |> Format.encode(secret: @invalid_secret) 179 | 180 | resp = communicate_all(udp, 0, req) 181 | 182 | params = Format.decode!(resp) 183 | assert %Params{class: :failure, 184 | method: unquote(request), 185 | identifier: ^id} = params 186 | 187 | assert %ErrorCode{code: 401} = Params.get_attr(params, ErrorCode) 188 | end 189 | 190 | test "with no message integrity fails to authenticate", ctx do 191 | udp = ctx.udp 192 | nonce_attr = get_nonce(udp) 193 | id = Params.generate_id() 194 | attrs = [ 195 | %Username{value: ctx.username}, 196 | %Realm{value: "localhost"}, 197 | nonce_attr 198 | ] 199 | req = 200 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 201 | |> Format.encode() 202 | 203 | resp = communicate_all(udp, 0, req) 204 | 205 | params = Format.decode!(resp) 206 | assert %Params{class: :failure, 207 | method: unquote(request), 208 | identifier: ^id} = params 209 | 210 | assert %ErrorCode{code: 401} = Params.get_attr(params, ErrorCode) 211 | end 212 | 213 | test "with invalid nonce fails to authenticate", ctx do 214 | udp = ctx.udp 215 | id = Params.generate_id() 216 | attrs = [ 217 | %Username{value: ctx.username}, 218 | %Realm{value: "localhost"}, 219 | %Nonce{value: "some_invalid_nonce...hopefully"} 220 | ] 221 | req = 222 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 223 | |> Format.encode(secret: @valid_secret) 224 | 225 | resp = communicate_all(udp, 0, req) 226 | 227 | params = Format.decode!(resp) 228 | assert %Params{class: :failure, 229 | method: unquote(request), 230 | identifier: ^id} = params 231 | 232 | assert %ErrorCode{code: 438} = Params.get_attr(params, ErrorCode) 233 | assert %Nonce{value: nonce} = Params.get_attr(params, Nonce) 234 | assert %Realm{value: realm} = Params.get_attr(params, Realm) 235 | 236 | assert String.length(nonce) > 0 237 | assert String.length(nonce) <= @max_value_chars 238 | assert byte_size(nonce) <= @max_value_bytes 239 | 240 | assert String.length(realm) > 0 241 | assert String.length(realm) <= @max_value_chars 242 | assert byte_size(realm) <= @max_value_bytes 243 | end 244 | 245 | test "with valid nonce authenticate successfully", ctx do 246 | udp = ctx.udp 247 | id = Params.generate_id() 248 | nonce_attr = get_nonce(udp) 249 | attrs = [ 250 | %Username{value: ctx.username}, 251 | %Realm{value: "localhost"}, 252 | nonce_attr 253 | ] 254 | req = 255 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 256 | |> Format.encode(secret: @valid_secret) 257 | 258 | resp = communicate_all(udp, 0, req) 259 | 260 | params = Format.decode!(resp) 261 | assert %Params{class: :success, 262 | method: unquote(request), 263 | identifier: ^id} = params 264 | end 265 | 266 | unquote( 267 | case request do 268 | :allocate -> 269 | quote do 270 | @tag :skip 271 | end 272 | _ -> 273 | nil 274 | end 275 | ) 276 | test "with different username fails to authorize", ctx do 277 | udp = ctx.udp 278 | id = Params.generate_id() 279 | nonce_attr = get_nonce(udp) 280 | attrs = [ 281 | %Username{value: ctx.username <> "_ish"}, 282 | %Realm{value: "localhost"}, 283 | nonce_attr 284 | ] 285 | req = 286 | UDP. unquote(params_fun)(id, unquote(base_attrs) ++ attrs) 287 | |> Format.encode(secret: @valid_secret) 288 | 289 | resp = communicate_all(udp, 0, req) 290 | 291 | params = Format.decode!(resp) 292 | assert %Params{class: :failure, 293 | method: unquote(request), 294 | identifier: ^id, 295 | attributes: attrs} = params 296 | assert %ErrorCode{code: 441} = Params.get_attr(params, ErrorCode) 297 | end 298 | end 299 | end 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /test/mongooseice/udp/auth_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.AuthTest do 2 | use ExUnit.Case 3 | use MongooseICE.UDP.AuthTemplate 4 | 5 | alias Jerboa.Format.Body.Attribute.{Lifetime, RequestedTransport, XORPeerAddress} 6 | 7 | test_auth_for(:allocate, [%RequestedTransport{protocol: :udp}]) 8 | 9 | test_auth_for(:create_permission, [%XORPeerAddress{ 10 | address: {127, 0, 0, 1}, 11 | port: 0, 12 | family: :ipv4 13 | }]) 14 | 15 | test_auth_for(:refresh, [%Lifetime{duration: 1020}]) 16 | end 17 | -------------------------------------------------------------------------------- /test/mongooseice/udp/binding_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.BindingTest do 2 | use ExUnit.Case 3 | use Helper.Macros 4 | 5 | alias Helper.UDP 6 | alias Jerboa.Params 7 | alias Jerboa.Format 8 | alias Jerboa.Format.Body.Attribute.XORMappedAddress 9 | 10 | describe "binding request" do 11 | 12 | setup do 13 | {:ok, [udp: UDP.setup_connection([], :ipv4)]} 14 | end 15 | 16 | test "returns response with IPv4 XOR mapped address attribute", ctx do 17 | udp = ctx.udp 18 | client_address = udp.client_address 19 | client_port = UDP.client_port(udp, 0) 20 | 21 | id = Params.generate_id() 22 | req = UDP.binding_request(id) 23 | 24 | resp = no_auth(UDP.communicate(udp, 0, req)) 25 | 26 | params = Format.decode!(resp) 27 | assert %Params{class: :success, 28 | method: :binding, 29 | identifier: ^id, 30 | attributes: [a]} = params 31 | assert %XORMappedAddress{address: ^client_address, 32 | port: ^client_port, 33 | family: :ipv4} = a 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/mongooseice/udp/channel_bind_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.ChannelBindTest do 2 | use ExUnit.Case, async: false 3 | 4 | use Helper.Macros 5 | 6 | alias Helper.UDP 7 | alias Jerboa.Params 8 | alias Jerboa.Format 9 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 10 | alias Jerboa.Format.Body.Attribute.ChannelNumber 11 | 12 | import Mock 13 | 14 | setup ctx do 15 | udp = UDP.connect({127, 0, 0, 1}, {127, 0, 0, 1}, 1) 16 | allocate_time = MongooseICE.Time.system_time(:second) 17 | if ctx[:allocate], do: UDP.allocate(udp) 18 | on_exit fn -> 19 | UDP.close(udp) 20 | end 21 | {:ok, udp: udp, allocate_time: allocate_time} 22 | end 23 | 24 | test "fails without active allocation", %{udp: udp} do 25 | id = Params.generate_id() 26 | req = UDP.channel_bind_request(id, []) 27 | 28 | resp = no_auth(UDP.communicate(udp, 0, req)) 29 | params = Format.decode!(resp) 30 | 31 | assert Params.get_class(params) == :failure 32 | assert Params.get_method(params) == :channel_bind 33 | assert Params.get_id(params) == id 34 | assert [error] = Params.get_attrs(params) 35 | assert error.name == :allocation_mismatch 36 | end 37 | 38 | @tag :allocate 39 | test "fails without XOR-PEER-ADDRESS attribute", %{udp: udp} do 40 | id = Params.generate_id() 41 | attrs = [ 42 | %ChannelNumber{number: 0x4000} 43 | ] 44 | req = UDP.channel_bind_request(id, attrs) 45 | 46 | resp = no_auth(UDP.communicate(udp, 0, req)) 47 | params = Format.decode!(resp) 48 | 49 | assert Params.get_class(params) == :failure 50 | assert Params.get_method(params) == :channel_bind 51 | assert Params.get_id(params) == id 52 | assert [error] = Params.get_attrs(params) 53 | assert error.name == :bad_request 54 | end 55 | 56 | @tag :allocate 57 | test "fails without CHANNEL-NUMBER attribute", %{udp: udp} do 58 | id = Params.generate_id() 59 | attrs = [ 60 | XPA.new({127, 0, 0, 1}, 12_345) 61 | ] 62 | req = UDP.channel_bind_request(id, attrs) 63 | 64 | resp = no_auth(UDP.communicate(udp, 0, req)) 65 | params = Format.decode!(resp) 66 | 67 | assert Params.get_class(params) == :failure 68 | assert Params.get_method(params) == :channel_bind 69 | assert Params.get_id(params) == id 70 | assert [error] = Params.get_attrs(params) 71 | assert error.name == :bad_request 72 | end 73 | 74 | @tag :allocate 75 | test "succeeds with CHANNEL-NUMBER and XOR-PEER-ADDRESS attributes", 76 | %{udp: udp} do 77 | worker = UDP.worker(udp, 0) 78 | id = Params.generate_id() 79 | peer_ip = {127, 0, 0, 1} 80 | peer_port = 12_345 81 | channel_number = 0x4000 82 | attrs = [ 83 | %ChannelNumber{number: channel_number}, 84 | XPA.new(peer_ip, peer_port) 85 | ] 86 | req = UDP.channel_bind_request(id, attrs) 87 | 88 | resp = no_auth(UDP.communicate(udp, 0, req)) 89 | assert [channel] = GenServer.call(worker, :get_channels) 90 | assert channel.peer == {peer_ip, peer_port} 91 | assert channel.number == channel_number 92 | permissions = GenServer.call(worker, :get_permissions) 93 | assert peer_ip in Map.keys(permissions) 94 | params = Format.decode!(resp) 95 | 96 | assert Params.get_class(params) == :success 97 | assert Params.get_method(params) == :channel_bind 98 | assert Params.get_id(params) == id 99 | end 100 | 101 | @tag :allocate 102 | test "refreshes given previously bound peer and channel number", 103 | %{udp: udp, allocate_time: allocate_time} do 104 | worker = UDP.worker(udp, 0) 105 | peer_ip = {127, 0, 0, 1} 106 | peer_port = 12_345 107 | channel_number = 0x4000 108 | id1 = Params.generate_id() 109 | id2 = Params.generate_id() 110 | attrs = [ 111 | %ChannelNumber{number: channel_number}, 112 | XPA.new(peer_ip, peer_port) 113 | ] 114 | req1 = UDP.channel_bind_request(id1, attrs) 115 | req2 = UDP.channel_bind_request(id2, attrs) 116 | 117 | base_time = allocate_time + 10 118 | expiration_time1 = 119 | with_mock MongooseICE.Time, [:passthrough], [system_time: fn (:second) -> 120 | base_time end] do 121 | # 1st request 122 | no_auth(UDP.communicate(udp, 0, req1)) 123 | assert [channel] = GenServer.call(worker, :get_channels) 124 | assert channel.peer == {peer_ip, peer_port} 125 | assert channel.number == channel_number 126 | channel.expiration_time 127 | end 128 | 129 | # 2nd request 130 | time_passed = 2 * 60 131 | with_mock MongooseICE.Time, [:passthrough], [system_time: fn (:second) -> 132 | base_time + time_passed 133 | end] do 134 | resp = no_auth(UDP.communicate(udp, 0, req2)) 135 | params = Format.decode!(resp) 136 | 137 | assert Params.get_class(params) == :success 138 | assert Params.get_method(params) == :channel_bind 139 | assert Params.get_id(params) == id2 140 | assert [channel] = GenServer.call(worker, :get_channels) 141 | assert channel.peer == {peer_ip, peer_port} 142 | assert channel.number == channel_number 143 | expiration_time2 = channel.expiration_time 144 | assert expiration_time2 == expiration_time1 + time_passed 145 | end 146 | end 147 | 148 | @tag :allocate 149 | test "fails given already bound peer address", %{udp: udp} do 150 | worker = UDP.worker(udp, 0) 151 | peer_ip = {127, 0, 0, 1} 152 | peer_port = 12_345 153 | xor_peer_addr = XPA.new(peer_ip, peer_port) 154 | 155 | channel_number1 = 0x4000 156 | id1 = Params.generate_id() 157 | attrs1 = [ 158 | %ChannelNumber{number: channel_number1}, 159 | xor_peer_addr 160 | ] 161 | req1 = UDP.channel_bind_request(id1, attrs1) 162 | 163 | channel_number2 = 0x4001 164 | id2 = Params.generate_id() 165 | attrs2 = [ 166 | %ChannelNumber{number: channel_number2}, 167 | xor_peer_addr 168 | ] 169 | req2 = UDP.channel_bind_request(id2, attrs2) 170 | 171 | # 1st request 172 | no_auth(UDP.communicate(udp, 0, req1)) 173 | assert [channel] = GenServer.call(worker, :get_channels) 174 | assert channel.number == channel_number1 175 | assert channel.peer == {peer_ip, peer_port} 176 | 177 | # 2nd request 178 | resp = no_auth(UDP.communicate(udp, 0, req2)) 179 | params = Format.decode!(resp) 180 | 181 | assert Params.get_class(params) == :failure 182 | assert Params.get_method(params) == :channel_bind 183 | assert Params.get_id(params) == id2 184 | assert [error] = Params.get_attrs(params) 185 | assert error.name == :bad_request 186 | assert [^channel] = GenServer.call(worker, :get_channels) 187 | end 188 | 189 | @tag :allocate 190 | test "fails given already bound channel number", %{udp: udp} do 191 | peer_ip1 = {127, 0, 0, 1} 192 | peer_port1 = 12_345 193 | worker = UDP.worker(udp, 0) 194 | channel_number = 0x4000 195 | id1 = Params.generate_id() 196 | attrs1 = [ 197 | %ChannelNumber{number: channel_number}, 198 | XPA.new(peer_ip1, peer_port1) 199 | ] 200 | req1 = UDP.channel_bind_request(id1, attrs1) 201 | 202 | peer_ip2 = {127, 0, 0, 2} 203 | peer_port2 = 54_321 204 | id2 = Params.generate_id() 205 | attrs2 = [ 206 | %ChannelNumber{number: channel_number}, 207 | XPA.new(peer_ip2, peer_port2) 208 | ] 209 | req2 = UDP.channel_bind_request(id2, attrs2) 210 | 211 | # 1st request 212 | no_auth(UDP.communicate(udp, 0, req1)) 213 | assert [channel] = GenServer.call(worker, :get_channels) 214 | assert channel.number == channel_number 215 | assert channel.peer == {peer_ip1, peer_port1} 216 | 217 | # 2nd request 218 | resp = no_auth(UDP.communicate(udp, 0, req2)) 219 | params = Format.decode!(resp) 220 | 221 | assert Params.get_class(params) == :failure 222 | assert Params.get_method(params) == :channel_bind 223 | assert Params.get_id(params) == id2 224 | assert [error] = Params.get_attrs(params) 225 | assert error.name == :bad_request 226 | assert [^channel] = GenServer.call(worker, :get_channels) 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /test/mongooseice/udp/channel_data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.ChannelDataTest do 2 | use ExUnit.Case, async: false 3 | use Helper.Macros 4 | 5 | alias Helper.UDP 6 | alias Jerboa.{Params, ChannelData} 7 | alias Jerboa.Format.Body.Attribute.{Lifetime, XORRelayedAddress, 8 | XORPeerAddress} 9 | 10 | import Mock 11 | 12 | @recv_timeout 1_000 13 | 14 | setup ctx do 15 | sockets_count = ctx[:sockets] || 2 16 | udp = UDP.connect({127, 0, 0, 1}, {127, 0, 0, 1}, sockets_count) 17 | peer_port = UDP.port(udp, 1) 18 | peer_socket = UDP.socket(udp, 1) 19 | client_socket = UDP.socket(udp, 0) 20 | 21 | allocate_ctx = 22 | if ctx[:allocate] do 23 | resp = UDP.allocate(udp) 24 | xor_relayed_addr = Params.get_attr(resp, XORRelayedAddress) 25 | [relay_ip: xor_relayed_addr.address, 26 | relay_port: xor_relayed_addr.port] 27 | else 28 | [] 29 | end 30 | 31 | on_exit fn -> 32 | UDP.close(udp) 33 | end 34 | 35 | {:ok, allocate_ctx ++ [udp: udp, 36 | peer_socket: peer_socket, 37 | peer_port: peer_port, 38 | client_socket: client_socket]} 39 | end 40 | 41 | describe "from TURN client to peer" do 42 | test "doesn't relay without an allocation", %{udp: udp, peer_socket: peer} do 43 | channel_data = UDP.channel_data(0x4000, "hello") 44 | 45 | UDP.send(udp, 0, channel_data) 46 | 47 | assert {:error, :timeout} = :gen_udp.recv peer, 0, @recv_timeout 48 | end 49 | 50 | @tag :allocate 51 | test "doesn't relay without channel bound", 52 | %{udp: udp, peer_socket: peer} do 53 | channel_data = UDP.channel_data(0x4000, "hello") 54 | 55 | UDP.send(udp, 0, channel_data) 56 | 57 | assert {:error, :timeout} = :gen_udp.recv peer, 0, @recv_timeout 58 | end 59 | 60 | @tag :allocate 61 | test "doesn't relay without channel bound but with permission", 62 | %{udp: udp, peer_socket: peer} do 63 | UDP.create_permissions(udp, [{127, 0, 0, 1}]) 64 | channel_data = UDP.channel_data(0x4000, "hello") 65 | 66 | UDP.send(udp, 0, channel_data) 67 | 68 | assert {:error, :timeout} = :gen_udp.recv peer, 0, @recv_timeout 69 | worker = UDP.worker(udp, 0) 70 | assert %{{127, 0, 0, 1} => _} = GenServer.call(worker, :get_permissions) 71 | end 72 | 73 | @tag :allocate 74 | test "relays data with channel bound", 75 | %{udp: udp, peer_socket: peer, peer_port: port} do 76 | channel_number = 0x4000 77 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, port) 78 | data = "hello" 79 | channel_data = UDP.channel_data(channel_number, data) 80 | 81 | UDP.send(udp, 0, channel_data) 82 | 83 | assert {:ok, {_, _, ^data}} = :gen_udp.recv peer, 0, @recv_timeout 84 | end 85 | 86 | @tag :allocate 87 | test "doesn't relay if permission expired but channel is still bound", 88 | %{udp: udp, peer_socket: peer, peer_port: port} do 89 | channel_number = 0x4000 90 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, port) 91 | data = "hello" 92 | channel_data = UDP.channel_data(channel_number, data) 93 | 94 | time_passed = 8 * 60 # permission expires after 5 minutes, channel after 10 95 | with_mock MongooseICE.Time, [:passthrough], [ 96 | system_time: fn (:second) -> :meck.passthrough([:second]) + time_passed end 97 | ] do 98 | UDP.send(udp, 0, channel_data) 99 | 100 | assert {:error, :timeout} = :gen_udp.recv peer, 0, @recv_timeout 101 | worker = UDP.worker(udp, 0) 102 | assert %{} == GenServer.call(worker, :get_permissions) 103 | assert [_] = GenServer.call(worker, :get_channels) 104 | end 105 | end 106 | 107 | @tag :allocate 108 | test "doesn't relay if channel expired", 109 | %{udp: udp, peer_socket: peer, peer_port: port} do 110 | channel_number = 0x4000 111 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, port) 112 | data = "hello" 113 | channel_data = UDP.channel_data(channel_number, data) 114 | 115 | # we need to refresh the allocation so that it doesn't time out 116 | UDP.refresh(udp, [%Lifetime{duration: 20 * 60}]) 117 | time_passed = 11 * 60 # channel expires after 10 minutes 118 | with_mock MongooseICE.Time, [:passthrough], [ 119 | system_time: fn (:second) -> :meck.passthrough([:second]) + time_passed end 120 | ] do 121 | UDP.send(udp, 0, channel_data) 122 | 123 | assert {:error, :timeout} = :gen_udp.recv peer, 0, @recv_timeout 124 | worker = UDP.worker(udp, 0) 125 | assert [] == GenServer.call(worker, :get_channels) 126 | end 127 | end 128 | 129 | @tag allocate: true, sockets: 3 130 | test "relays data only to peer bound to channel", 131 | %{udp: udp, peer_socket: peer_socket1, peer_port: peer_port1} do 132 | peer_socket2 = UDP.socket(udp, 2) 133 | channel_number = 0x4000 134 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, peer_port1) 135 | data = "hello" 136 | channel_data = UDP.channel_data(channel_number, data) 137 | 138 | UDP.send(udp, 0, channel_data) 139 | 140 | assert {:ok, {_, _, ^data}} = :gen_udp.recv peer_socket1, 0, @recv_timeout 141 | assert {:error, :timeout} = :gen_udp.recv peer_socket2, 0, @recv_timeout 142 | end 143 | end 144 | 145 | describe "from peer to TURN client" do 146 | @tag :allocate 147 | test "doesn't relay without channel bound", ctx do 148 | %{client_socket: client, peer_socket: peer, 149 | relay_ip: relay_ip, relay_port: relay_port} = ctx 150 | data = "hello" 151 | 152 | :ok = :gen_udp.send peer, relay_ip, relay_port, data 153 | 154 | assert {:error, :timeout} = :gen_udp.recv client, 0, @recv_timeout 155 | end 156 | 157 | @tag :allocate 158 | test "doesn't relay over channel without channel bound but with permission", 159 | ctx do 160 | %{udp: udp, client_socket: client, peer_socket: peer, 161 | relay_ip: relay_ip, relay_port: relay_port} = ctx 162 | UDP.create_permissions(udp, [{127, 0, 0, 1}]) 163 | data = "hello" 164 | 165 | :ok = :gen_udp.send peer, relay_ip, relay_port, data 166 | 167 | assert {:ok, {_, _, payload}} = :gen_udp.recv client, 0, @recv_timeout 168 | assert {:ok, %Params{method: :data}} = Jerboa.Format.decode(payload) 169 | worker = UDP.worker(udp, 0) 170 | assert %{{127, 0, 0, 1} => _} = GenServer.call(worker, :get_permissions) 171 | end 172 | 173 | @tag :allocate 174 | test "relays data with channel bound", ctx do 175 | %{udp: udp, client_socket: client, peer_socket: peer, 176 | peer_port: peer_port, relay_ip: relay_ip, relay_port: relay_port} = ctx 177 | channel_number = 0x4000 178 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, peer_port) 179 | data = "hello" 180 | 181 | :ok = :gen_udp.send peer, relay_ip, relay_port, data 182 | 183 | assert {:ok, {_, _, payload}} = :gen_udp.recv client, 0, @recv_timeout 184 | assert {:ok, %ChannelData{data: data, channel_number: channel_number}} == 185 | Jerboa.Format.decode(payload) 186 | end 187 | 188 | @tag :allocate 189 | test "doesn't relay if permission expired but channel is still bound", ctx do 190 | %{udp: udp, client_socket: client, peer_socket: peer, 191 | peer_port: peer_port, relay_ip: relay_ip, relay_port: relay_port} = ctx 192 | channel_number = 0x4000 193 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, peer_port) 194 | data = "hello" 195 | 196 | time_passed = 8 * 60 # permission expires after 5 minutes, channel after 10 197 | with_mock MongooseICE.Time, [:passthrough], [ 198 | system_time: fn (:second) -> :meck.passthrough([:second]) + time_passed end 199 | ] do 200 | :ok = :gen_udp.send peer, relay_ip, relay_port, data 201 | 202 | assert {:error, :timeout} = :gen_udp.recv client, 0, @recv_timeout 203 | worker = UDP.worker(udp, 0) 204 | assert %{} == GenServer.call(worker, :get_permissions) 205 | assert [_] = GenServer.call(worker, :get_channels) 206 | end 207 | end 208 | 209 | @tag :allocate 210 | test "doesn't relay over channel if channel expired", ctx do 211 | %{udp: udp, client_socket: client, peer_socket: peer, 212 | peer_port: peer_port, relay_ip: relay_ip, relay_port: relay_port} = ctx 213 | channel_number = 0x4000 214 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, peer_port) 215 | data = "hello" 216 | 217 | # we need to refresh the allocation so that it doesn't time out 218 | UDP.refresh(udp, [%Lifetime{duration: 20 * 60}]) 219 | time_passed = 11 * 60 # channel expires after 10 minutes 220 | with_mock MongooseICE.Time, [:passthrough], [ 221 | system_time: fn (:second) -> :meck.passthrough([:second]) + time_passed end 222 | ] do 223 | :ok = :gen_udp.send peer, relay_ip, relay_port, data 224 | 225 | assert {:error, :timeout} = :gen_udp.recv client, 0, @recv_timeout 226 | worker = UDP.worker(udp, 0) 227 | assert [] == GenServer.call(worker, :get_channels) 228 | end 229 | end 230 | 231 | @tag allocate: true, sockets: 3 232 | test "relays data over channel only from peer bound to channel", ctx do 233 | %{udp: udp, client_socket: client, peer_socket: peer_socket1, 234 | peer_port: peer_port1, relay_ip: relay_ip, relay_port: relay_port} = ctx 235 | peer_socket2 = UDP.socket(udp, 2) 236 | peer_port2 = UDP.port(udp, 2) 237 | channel_number = 0x4000 238 | UDP.channel_bind(udp, channel_number, {127, 0, 0, 1}, peer_port1) 239 | data = "hello" 240 | 241 | :ok = :gen_udp.send peer_socket1, relay_ip, relay_port, data 242 | :ok = :gen_udp.send peer_socket2, relay_ip, relay_port, data 243 | 244 | {:ok, {_, _, recv1}} = :gen_udp.recv client, 0, @recv_timeout 245 | {:ok, {_, _, recv2}} = :gen_udp.recv client, 0, @recv_timeout 246 | 247 | assert {[channel_data], [params]} = 248 | [recv1, recv2] 249 | |> Enum.map(&Jerboa.Format.decode!/1) 250 | |> Enum.split_with(fn msg -> 251 | case msg do 252 | %ChannelData{} -> true 253 | _ -> false 254 | end 255 | end) 256 | assert %ChannelData{data: data, channel_number: channel_number} == 257 | channel_data 258 | assert Params.get_method(params) == :data 259 | xor_peer_addr = Params.get_attr(params, XORPeerAddress) 260 | assert xor_peer_addr.port == peer_port2 261 | end 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /test/mongooseice/udp/create_permission_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.CreatePermissionTest do 2 | use ExUnit.Case, async: false 3 | use Helper.Macros 4 | import Mock 5 | 6 | alias Helper.UDP 7 | alias Jerboa.Params 8 | alias Jerboa.Format 9 | alias Jerboa.Format.Body.Attribute.{ErrorCode, XORRelayedAddress} 10 | alias MongooseICE.UDP.Worker 11 | 12 | setup do 13 | udp = 14 | UDP.connect({127, 0, 0, 1}, {127, 0, 0, 1}, 2) 15 | on_exit fn -> 16 | UDP.close(udp) 17 | end 18 | 19 | {:ok, [udp: udp]} 20 | end 21 | 22 | describe "worker's permissions state" do 23 | setup ctx do 24 | UDP.allocate(ctx.udp) 25 | {:ok, []} 26 | end 27 | 28 | test "contains no permissions after allocate", ctx do 29 | udp = ctx.udp 30 | worker = UDP.worker(udp, 0) 31 | 32 | assert %{} == GenServer.call(worker, :get_permissions) 33 | end 34 | 35 | test "contains permission after create_permission request", ctx do 36 | udp = ctx.udp 37 | worker = UDP.worker(udp, 0) 38 | 39 | UDP.create_permissions(udp, [{127, 0, 10, 0}]) 40 | assert %{{127, 0, 10, 0} => expire_at} = 41 | GenServer.call(worker, :get_permissions) 42 | 43 | later_5min = MongooseICE.Time.system_time(:second) + 5 * 60 44 | assert_in_delta expire_at, later_5min, 5 45 | end 46 | 47 | test "contains several permission after create_permission request", ctx do 48 | udp = ctx.udp 49 | worker = UDP.worker(udp, 0) 50 | 51 | UDP.create_permissions(udp, [{127, 0, 10, 0}, {127, 0, 10, 1}]) 52 | 53 | # Time passes 54 | time_passed = 2 * 60 55 | with_mock MongooseICE.Time, [:passthrough], [ 56 | system_time: fn (:second) -> :meck.passthrough([:second]) + time_passed end 57 | ] do 58 | UDP.create_permissions(udp, [{127, 0, 10, 2}, {127, 0, 10, 3}]) 59 | 60 | assert %{ 61 | {127, 0, 10, 0} => expire_at_0, 62 | {127, 0, 10, 1} => expire_at_1, 63 | {127, 0, 10, 2} => expire_at_2, 64 | {127, 0, 10, 3} => expire_at_3 65 | } = GenServer.call(worker, :get_permissions) 66 | 67 | assert expire_at_0 == expire_at_1 68 | assert expire_at_2 == expire_at_3 69 | 70 | assert expire_at_2 >= expire_at_0 + time_passed 71 | end 72 | end 73 | 74 | test "contains refreshed permission after second create_permission", ctx do 75 | udp = ctx.udp 76 | worker = UDP.worker(udp, 0) 77 | 78 | UDP.create_permissions(udp, [{127, 0, 10, 0}]) 79 | assert %{{127, 0, 10, 0} => expire_at_1} = 80 | GenServer.call(worker, :get_permissions) 81 | 82 | # Time passes 83 | time_passed = 2 * 60 84 | with_mock MongooseICE.Time, [:passthrough], [ 85 | system_time: fn (:second) -> :meck.passthrough([:second]) + time_passed end 86 | ] do 87 | UDP.create_permissions(udp, [{127, 0, 10, 0}]) 88 | 89 | assert %{{127, 0, 10, 0} => expire_at_2} = 90 | GenServer.call(worker, :get_permissions) 91 | 92 | assert expire_at_2 - expire_at_1 >= time_passed 93 | assert expire_at_2 - expire_at_1 < 2 * time_passed 94 | end 95 | end 96 | end 97 | 98 | describe "peer's data" do 99 | test "gets rejected without correct permission", ctx do 100 | udp = ctx.udp 101 | with_mock Worker, [:passthrough], [] do 102 | # Allocate 103 | allocate_res = UDP.allocate(udp) 104 | %XORRelayedAddress{ 105 | address: relay_ip, 106 | port: relay_port 107 | } = Params.get_attr(allocate_res, XORRelayedAddress) 108 | 109 | # Invalied CreatePermission 110 | UDP.create_permissions(udp, [{127, 0, 0, 2}]) 111 | 112 | # Peer sends data 113 | {:ok, sock} = :gen_udp.open(0) 114 | :ok = :gen_udp.send(sock, relay_ip, relay_port, "some_bytes") 115 | 116 | assert eventually called Worker.handle_peer_data(:no_permission, :_, :_, :_, :_) 117 | end 118 | end 119 | 120 | test "gets rejected with stale permission", ctx do 121 | udp = ctx.udp 122 | with_mock Worker, [:passthrough], [] do 123 | # Allocate 124 | allocate_res = UDP.allocate(udp) 125 | %XORRelayedAddress{ 126 | address: relay_ip, 127 | port: relay_port 128 | } = Params.get_attr(allocate_res, XORRelayedAddress) 129 | 130 | # CreatePermission 131 | UDP.create_permissions(udp, [{127, 0, 0, 1}]) 132 | 133 | # Time passes 134 | with_mock MongooseICE.Time, [:passthrough], [ 135 | system_time: fn (:second) -> :meck.passthrough([:second]) + 5 * 60 end 136 | ] do 137 | # Peer sends data 138 | {:ok, sock} = :gen_udp.open(0) 139 | :ok = :gen_udp.send(sock, relay_ip, relay_port, "some_bytes") 140 | 141 | assert eventually called Worker.handle_peer_data(:stale_permission, :_, :_, :_, :_) 142 | end 143 | end 144 | end 145 | 146 | test "is accepted with valid permission", ctx do 147 | udp = ctx.udp 148 | with_mock Worker, [:passthrough], [] do 149 | # Allocate 150 | allocate_res = UDP.allocate(udp) 151 | %XORRelayedAddress{ 152 | address: relay_ip, 153 | port: relay_port 154 | } = Params.get_attr(allocate_res, XORRelayedAddress) 155 | 156 | # CreatePermission 157 | UDP.create_permissions(udp, [{127, 0, 0, 1}]) 158 | 159 | # Peer sends data 160 | {:ok, sock} = :gen_udp.open(0) 161 | :ok = :gen_udp.send(sock, relay_ip, relay_port, "some_bytes") 162 | 163 | assert eventually called Worker.handle_peer_data(:allowed, :_, :_, :_, :_) 164 | end 165 | end 166 | end 167 | 168 | describe "create_permission request" do 169 | 170 | test "fails without XORPeerAddress attribute", ctx do 171 | udp = ctx.udp 172 | UDP.allocate(udp) 173 | 174 | id = Params.generate_id() 175 | req = UDP.create_permission_request(id, []) 176 | 177 | resp = no_auth(UDP.communicate(udp, 0, req)) 178 | 179 | params = Format.decode!(resp) 180 | assert %Params{class: :failure, 181 | method: :create_permission, 182 | identifier: ^id, 183 | attributes: [error]} = params 184 | 185 | assert %ErrorCode{code: 400} = error 186 | end 187 | 188 | test "fails without active allocation", ctx do 189 | udp = ctx.udp 190 | id = Params.generate_id() 191 | req = UDP.create_permission_request(id, []) 192 | 193 | resp = no_auth(UDP.communicate(udp, 0, req)) 194 | 195 | params = Format.decode!(resp) 196 | assert %Params{class: :failure, 197 | method: :create_permission, 198 | identifier: ^id, 199 | attributes: [error]} = params 200 | 201 | assert %ErrorCode{code: 437} = error 202 | end 203 | 204 | test "succeeds with one XORPeerAddress", ctx do 205 | udp = ctx.udp 206 | UDP.allocate(udp) 207 | 208 | id = Params.generate_id() 209 | peers = UDP.peers([{123, 123, 6, 1}]) 210 | req = UDP.create_permission_request(id, peers) 211 | 212 | resp = no_auth(UDP.communicate(udp, 0, req)) 213 | 214 | params = Format.decode!(resp) 215 | assert %Params{class: :success, 216 | method: :create_permission, 217 | identifier: ^id} = params 218 | end 219 | 220 | test "succeeds with multiple XORPeerAddress", ctx do 221 | udp = ctx.udp 222 | UDP.allocate(udp) 223 | 224 | id = Params.generate_id() 225 | peers = UDP.peers([ 226 | {123, 123, 6, 1}, 227 | {123, 123, 6, 2}, 228 | {123, 123, 6, 3}, 229 | ]) 230 | req = UDP.create_permission_request(id, peers) 231 | 232 | resp = no_auth(UDP.communicate(udp, 0, req)) 233 | 234 | params = Format.decode!(resp) 235 | assert %Params{class: :success, 236 | method: :create_permission, 237 | identifier: ^id} = params 238 | end 239 | 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /test/mongooseice/udp/data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.DataTest do 2 | use ExUnit.Case, async: false 3 | use Helper.Macros 4 | 5 | alias Helper.UDP 6 | alias Jerboa.Params 7 | alias Jerboa.Format 8 | alias Jerboa.Format.Body.Attribute.{Data, XORPeerAddress, XORRelayedAddress} 9 | 10 | import Mock 11 | 12 | @peer_addr {127, 0, 0, 1} 13 | 14 | setup ctx do 15 | {:ok, [udp: UDP.setup_connection(ctx, :ipv4)]} 16 | end 17 | 18 | describe "incoming peer datagram" do 19 | 20 | setup ctx do 21 | params = UDP.allocate(ctx.udp) 22 | {:ok, [relay_sock: Params.get_attr(params, XORRelayedAddress)]} 23 | end 24 | 25 | test "gets discarded when there's no permission for peer", ctx do 26 | ## given a relay address for a peer with no permission 27 | %XORRelayedAddress{address: relay_addr, port: relay_port} = ctx.relay_sock 28 | {:ok, peer} = :gen_udp.open(0, [{:active, :false}, :binary]) 29 | {:ok, peer_port} = :inet.port(peer) 30 | with_mock MongooseICE.UDP.Worker, [:passthrough], [] do 31 | ## when the peer sends a datagram 32 | data = "arbitrary data" 33 | :ok = :gen_udp.send(peer, relay_addr, relay_port, data) 34 | ## then the datagram gets silently discarded 35 | assert eventually called \ 36 | MongooseICE.UDP.Worker.handle_peer_data(:no_permission, @peer_addr, peer_port, data, :_) 37 | end 38 | end 39 | 40 | end 41 | 42 | describe "incoming peer datagram with permission" do 43 | 44 | setup ctx do 45 | params = UDP.allocate(ctx.udp) 46 | UDP.create_permissions(ctx.udp, [@peer_addr]) 47 | {:ok, [relay_sock: Params.get_attr(params, XORRelayedAddress)]} 48 | end 49 | 50 | test "is relayed as a Data indication", ctx do 51 | ## given a relay address for a peer 52 | %XORRelayedAddress{address: relay_addr, port: relay_port} = ctx.relay_sock 53 | {:ok, peer} = :gen_udp.open(0, [{:active, :false}, :binary]) 54 | {:ok, peer_port} = :inet.port(peer) 55 | ## when the peer sends a datagram 56 | data = "arbitrary data" 57 | :ok = :gen_udp.send(peer, relay_addr, relay_port, data) 58 | ## then the datagram payload gets delivered as a Data indication 59 | raw = UDP.recv(ctx.udp, _client_id = 0) 60 | params = Format.decode!(raw) 61 | assert %Params{class: :indication, 62 | method: :data} = params 63 | assert %XORPeerAddress{address: @peer_addr, 64 | port: peer_port, 65 | family: :ipv4} == Params.get_attr(params, XORPeerAddress) 66 | assert %Data{content: data} == Params.get_attr(params, Data) 67 | end 68 | 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /test/mongooseice/udp/refresh_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.RefreshTest do 2 | use ExUnit.Case 3 | use Helper.Macros 4 | 5 | describe "refresh request" do 6 | 7 | alias Helper.{Allocation, UDP} 8 | alias Jerboa.Format 9 | alias Jerboa.Format.Body.Attribute 10 | alias Jerboa.Params 11 | 12 | import Mock 13 | 14 | setup ctx do 15 | {:ok, [udp: UDP.setup_connection(ctx)]} 16 | end 17 | 18 | test "with a lifetime of 0 deletes the allocation", ctx do 19 | ## given allocation 20 | UDP.allocate(ctx.udp) 21 | ## when sending Refresh with lifetime = 0 22 | mref = Allocation.monitor_owner(ctx) 23 | %Params{class: :success} = UDP.refresh(ctx.udp, [%Attribute.Lifetime{duration: 0}]) 24 | ## then the allocation is deleted 25 | assert_receive {:DOWN, ^mref, :process, _pid, _info}, 5_000 26 | end 27 | 28 | test "errors with allocation mismatch if there's no allocation", ctx do 29 | ## given no allocation or expired allocation 30 | client_id = 0 31 | ## when Refresh is sent 32 | req_id = Params.generate_id() 33 | req = UDP.refresh_request(req_id, []) 34 | resp = no_auth(UDP.communicate(ctx.udp, client_id, req)) 35 | params = Format.decode!(resp) 36 | ## then the result is an allocation mismatch error 37 | %Params{class: :failure, 38 | attributes: [%Attribute.ErrorCode{name: :allocation_mismatch}], 39 | identifier: ^req_id} = params 40 | end 41 | 42 | test "extends the allocation", ctx do 43 | ## given allocation 44 | non_standard_lifetime = 17 * 60 45 | client_id = 0 46 | now = MongooseICE.Time.system_time(:second) 47 | future_after_expiry = now + 14 * 60 48 | future_after_second_expiry = now + 153 * 60 49 | UDP.allocate(ctx.udp) 50 | ## when Refresh is sent 51 | allocation_owner = Helper.Allocation.owner(ctx) 52 | mref = Helper.Allocation.monitor_owner(ctx) 53 | params = UDP.refresh(ctx.udp, [%Attribute.Lifetime{duration: non_standard_lifetime}]) 54 | ## then the allocation gets extended 55 | %Params{class: :success, 56 | attributes: [%Attribute.Lifetime{duration: ^non_standard_lifetime}]} = params 57 | ## time travel 1: future_after_expiry is the point at which the allocation 58 | ## would have expired if it hadn't been extended 59 | with_mocks [ 60 | {MongooseICE.Time, [], 61 | [system_time: fn (:second) -> future_after_expiry end]}, 62 | {MongooseICE.Evaluator.Indication, [:passthrough], []} 63 | ] do 64 | ## First indication triggers reading the new time. 65 | ## If the allocation timed out, we would trigger the process exit here. 66 | ## However, the owner process **might exit after we assert its existence.** 67 | :ok = UDP.send(ctx.udp, client_id, UDP.binding_indication(Params.generate_id())) 68 | assert eventually called MongooseICE.Evaluator.Indication.void() 69 | assert Process.alive?(allocation_owner) 70 | ## Second indication asserts the allocation did not expire even 71 | ## if the assertion above was a false positive. 72 | :ok = UDP.send(ctx.udp, client_id, UDP.binding_indication(Params.generate_id())) 73 | assert eventually called MongooseICE.Evaluator.Indication.void() 74 | assert Process.alive?(allocation_owner) 75 | end 76 | ## time travel 2: future_after_second_expiry is the point at which the allocation 77 | ## would expire even though it had been extended 78 | with_mock MongooseICE.Time, [ 79 | system_time: fn (:second) -> future_after_second_expiry end 80 | ] do 81 | :ok = UDP.send(ctx.udp, client_id, UDP.binding_indication(Params.generate_id())) 82 | assert_receive {:DOWN, ^mref, :process, _pid, _info}, 3_000 83 | assert called MongooseICE.Time.system_time(:second) 84 | end 85 | end 86 | 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /test/mongooseice/udp/send_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.SendTest do 2 | use ExUnit.Case, async: false 3 | use Helper.Macros 4 | 5 | alias Helper.UDP 6 | alias Jerboa.Params 7 | alias Jerboa.Format 8 | alias Jerboa.Format.Body.Attribute.{ErrorCode, XORPeerAddress, Data, 9 | XORRelayedAddress} 10 | 11 | @peer_addr {127, 0, 0, 1} 12 | 13 | setup ctx do 14 | {:ok, [udp: UDP.setup_connection(ctx, :ipv4)]} 15 | end 16 | 17 | describe "send request with no allocation" do 18 | test "gets rejected", ctx do 19 | udp = ctx.udp 20 | 21 | id = Params.generate_id() 22 | peer = XORPeerAddress.new(@peer_addr, 12345) 23 | data = %Data{content: ""} 24 | req = UDP.send_indication(id, [peer, data]) 25 | 26 | resp = no_auth(communicate_all(udp, 0, req)) 27 | 28 | params = Format.decode!(resp) 29 | assert %Params{class: :failure, 30 | method: :send, 31 | identifier: ^id} = params 32 | 33 | assert %ErrorCode{code: 437} = Params.get_attr(params, ErrorCode) 34 | end 35 | end 36 | 37 | describe "send request with allocation" do 38 | setup ctx do 39 | UDP.allocate(ctx.udp) 40 | {:ok, []} 41 | end 42 | 43 | test "gets rejected when there's no permission", ctx do 44 | udp = ctx.udp 45 | 46 | id = Params.generate_id() 47 | peer = XORPeerAddress.new(@peer_addr, 12345) 48 | data = %Data{content: ""} 49 | req = UDP.send_indication(id, [peer, data]) 50 | 51 | resp = no_auth(communicate_all(udp, 0, req)) 52 | 53 | params = Format.decode!(resp) 54 | assert %Params{class: :failure, 55 | method: :send, 56 | identifier: ^id} = params 57 | 58 | assert %ErrorCode{code: 403} = Params.get_attr(params, ErrorCode) 59 | end 60 | 61 | test "gets rejected when no peer is given", ctx do 62 | udp = ctx.udp 63 | 64 | id = Params.generate_id() 65 | req = UDP.send_indication(id, [%Data{content: ""}]) 66 | 67 | resp = no_auth(communicate_all(udp, 0, req)) 68 | 69 | params = Format.decode!(resp) 70 | assert %Params{class: :failure, 71 | method: :send, 72 | identifier: ^id} = params 73 | 74 | assert %ErrorCode{code: 400} = Params.get_attr(params, ErrorCode) 75 | end 76 | 77 | test "gets rejected when no data is given", ctx do 78 | udp = ctx.udp 79 | 80 | id = Params.generate_id() 81 | peer = XORPeerAddress.new(@peer_addr, 12345) 82 | req = UDP.send_indication(id, [peer]) 83 | 84 | resp = no_auth(communicate_all(udp, 0, req)) 85 | 86 | params = Format.decode!(resp) 87 | assert %Params{class: :failure, 88 | method: :send, 89 | identifier: ^id} = params 90 | 91 | assert %ErrorCode{code: 400} = Params.get_attr(params, ErrorCode) 92 | end 93 | end 94 | 95 | describe "send request with allocation and permission" do 96 | setup ctx do 97 | allocate_params = UDP.allocate(ctx.udp) 98 | 99 | UDP.create_permissions(ctx.udp, [@peer_addr]) 100 | {:ok, [relay_sock: Params.get_attr(allocate_params, XORRelayedAddress)]} 101 | end 102 | 103 | test "delivers data to peer", ctx do 104 | udp = ctx.udp 105 | {:ok, sock} = :gen_udp.open(0, [{:active, true}, :binary]) 106 | {:ok, port} = :inet.port(sock) 107 | 108 | id = Params.generate_id() 109 | peer = XORPeerAddress.new(@peer_addr, port) 110 | data = %Data{content: "some content"} 111 | req = UDP.send_indication(id, [peer, data]) 112 | 113 | resp = no_auth(communicate_all(udp, 0, req)) 114 | 115 | # Check whether indication was processed with success 116 | params = Format.decode!(resp) 117 | assert %Params{class: :success, 118 | method: :send, 119 | identifier: ^id} = params 120 | 121 | # Check whether peer got the data 122 | relay_ip = ctx.relay_sock.address 123 | relay_port = ctx.relay_sock.port 124 | assert_receive {:udp, ^sock, ^relay_ip, ^relay_port, "some content"} 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/mongooseice/udp/server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICE.UDP.ServerTest do 2 | use ExUnit.Case 3 | 4 | test "start/1 and stop/1 a UDP server linked to MongooseICE.Supervisor" do 5 | port = Helper.PortMaster.checkout_port(:server) 6 | {:ok, _} = MongooseICE.UDP.start(ip: {127, 0, 0, 1}, port: port) 7 | 8 | expected_name = String.to_atom(~s"Elixir.MongooseICE.UDP.#{port}") 9 | assert [{MongooseICE.ReservationLog, _, _, _}, {^expected_name, _, _, _}] = 10 | Enum.sort(Supervisor.which_children(MongooseICE.Supervisor)) 11 | assert :ok = MongooseICE.UDP.stop(port) 12 | assert [{MongooseICE.ReservationLog, _, _, _}] = 13 | Supervisor.which_children(MongooseICE.Supervisor) 14 | end 15 | 16 | test "start/1 allows to start multiple servers on different ports" do 17 | port1 = 1234 18 | port2 = 4321 19 | 20 | assert {:ok, _} = MongooseICE.UDP.start(port: port1) 21 | assert {:ok, _} = MongooseICE.UDP.start(port: port2) 22 | 23 | MongooseICE.UDP.stop port1 24 | MongooseICE.UDP.stop port2 25 | end 26 | 27 | test "start/1 does not allow to start multiple servers on the same port" do 28 | port = 3478 29 | 30 | assert {:ok, _} = MongooseICE.UDP.start(port: port) 31 | assert {:error, {:already_started, _}} = MongooseICE.UDP.start(port: port) 32 | 33 | MongooseICE.UDP.stop(port) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/mongooseice_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MongooseICETest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Helper.PortMaster 5 | 6 | @moduletag :system 7 | @server_addr {127, 0, 0, 1} 8 | 9 | setup do 10 | 11 | ## Given: 12 | port = PortMaster.checkout_port(:server) 13 | MongooseICE.UDP.start_link(ip: @server_addr, port: port) 14 | Application.put_env(:mongooseice, :secret, "abc") 15 | {:ok, alice} = Jerboa.Client.start(server: {@server_addr, port}, 16 | username: "alice", secret: "abc") 17 | on_exit fn -> 18 | :ok = Jerboa.Client.stop(alice) 19 | end 20 | {:ok, 21 | client: alice} 22 | end 23 | 24 | describe "(IPv4) MongooseICE over UDP Transport" do 25 | 26 | test "send allocate request; receive success response", %{client: alice} do 27 | 28 | ## When: 29 | x = Jerboa.Client.allocate(alice) 30 | 31 | ## Then: 32 | assert family(x) == "IPv4" 33 | end 34 | 35 | test "send binding request; receive success response", %{client: alice} do 36 | 37 | ## When: 38 | x = Jerboa.Client.bind(alice) 39 | 40 | ## Then: 41 | assert family(x) == "IPv4" 42 | end 43 | 44 | test "send binding indication", %{client: alice} do 45 | 46 | ## When: 47 | x = for _ <- 1..3 do 48 | Jerboa.Client.persist(alice) 49 | end 50 | 51 | ## Then: 52 | assert Enum.all?(x, &ok?/1) == true 53 | end 54 | end 55 | 56 | defp family({:ok, {address, _}}) when tuple_size(address) == 4, do: "IPv4" 57 | defp family({:ok, {address, _}}) when tuple_size(address) == 8, do: "IPv6" 58 | defp family(r), do: r 59 | 60 | defp ok?(x) do 61 | x == :ok 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(exclude: [:system], capture_log: true) 2 | # These files have to be required here, since the code requires ExUnit 3 | # application to be running while loading (use ExUnit.Case). 4 | # But first we need to load Helper.Macros module, since the other ones 5 | # use the macros defined in it. 6 | Code.require_file "helper/macros.ex", __DIR__ 7 | 8 | {:ok, files} = File.ls("./test/helper") 9 | Enum.each files, fn(file) -> 10 | Code.require_file "helper/#{file}", __DIR__ 11 | end 12 | 13 | Code.require_file "mongooseice/udp/auth_template.ex", __DIR__ 14 | 15 | {:ok, _pid} = Helper.PortMaster.start_link() 16 | --------------------------------------------------------------------------------