Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Fly.io deployments #167

Merged
merged 2 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion guides/advanced/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,4 @@ You can also configure PeerConnection to use a specific port range by doing

Otherwise, the connection won't be established at all, or just in one direction.

Read more in our [Deploying tutorial](./deploying.md#allow-udp-traffic-in-your-firewall)!
Read more in our [Deploying tutorial](../deploying/bare.md#allow-udp-traffic-in-your-firewall)!
14 changes: 4 additions & 10 deletions guides/advanced/deploying.md → guides/deploying/bare.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Deploying
# Deploying on bare machines

Deploying WebRTC applications can be cumbersome.
Here are a few details you should keep in mind when trying to push your project into production.
Here are a few details you should keep in mind when trying to push your project into production on a bare machine.

## Allow UDP traffic in your firewall

Expand Down Expand Up @@ -37,12 +37,6 @@ docker run -p 50000-50010/udp myapp
Keep in mind that exporting a lot of ports might take a lot of time or even cause the Docker daemon to timeout.
That's why we recommend using host's network.

## Choose your cloud provider wisely

Many cloud providers do not offer good support for UDP traffic.
In such cases, deploying a WebRTC-based application might be impossible.
We recommend using bare machines that you can configure as you need.

## Enable HTTPS in your frontend

The server hosting your frontend site must have HTTPS enabled.
Expand All @@ -66,7 +60,7 @@ Read more [here](https://nginx.org/en/docs/http/websocket.html).

## Configure STUN servers

If you are deploying your application behind a NAT, you have to configure a STUN
If you are deploying your application behind a NAT, you have to configure a STUN
server that will allow it to discover its public IP address.
In Elixir WebRTC this will be:

Expand All @@ -86,6 +80,6 @@ And as a TURN server, you can always use our [Rel](https://github.com/elixir-web

If your application is deployed behind a very restrictive NAT, which should be very rare (e.g. a symmetric NAT),
you will need to configure a TURN server.
In most cases, TURN servers are needed on the client side as you don't have any control
In most cases, TURN servers are needed on the client side as you don't have any control
over a network your clients connect from.
For testing and experimental purposes, you can use our publicly available TURN called [Rel](https://github.com/elixir-webrtc/rel)!
50 changes: 50 additions & 0 deletions guides/deploying/fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Deploying on Fly.io

Elixir WebRTC-based apps can be easily deployed on [Fly.io](https://fly.io)!

There are just two things you need to do:

- configure a STUN server both on the client and server side
- use custom Fly.io IP filter on the server side
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- use custom Fly.io IP filter on the server side
- use a custom Fly.io IP filter on the server side


In theory, configuring a STUN server just on a one side should be enough but we recommend to do it on both sides.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In theory, configuring a STUN server just on a one side should be enough but we recommend to do it on both sides.
In theory, configuring a STUN server just on one side should be enough but we recommend doing it on both sides.


In JavaScript code:

```js
pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
```

in Elixir code:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
in Elixir code:
In Elixir code:


```elixir
ip_filter = Application.get_env(:your_app, :ice_ip_filter)

{:ok, pc} =
PeerConnection.start_link(
ice_ip_filter: ip_filter,
ice_servers: [%{urls: "stun:stun.l.google.com:19302"}]
)
```
Comment on lines +22 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] When :ice_ip_filter is unset in the application, we're passing ice_ip_filter: nil to PeerConnection, which means

|> Keyword.put_new(:ice_ip_filter, fn _ -> true end)

doesn't insert the default filter.

When we pass it to ICE, however, this line
https://github.com/elixir-webrtc/ex_ice/blob/4c5190687e11b28a8209f2c930895647d0b2a64c/lib/ex_ice/priv/ice_agent.ex#L137
makes it work again, as it gets the nil stored in the keyword list and overrides it with the default filter.

I feel like this creates a kind of ambiguity and depends on ExICE not changing its behaviour of parsing options, which can potentially lead to hard-to-find errors in the future. To avoid this in this example, I would explicitly not insert ice_ip_filter to the PC options when it's missing from the application's environment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!


in `runtime.exs`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
in `runtime.exs`:
In `runtime.exs`:


```elixir
if System.get_env("FLY_IO") do
config :your_app, ice_ip_filter: &ExWebRTC.ICE.FlyIpFilter.ip_filter/1
end
```

in fly.toml:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
in fly.toml:
In fly.toml:


```toml
[env]
# add one additional env
FLY_IO = 'true'
```

That's it!
No special UDP port exports or dedicated IP address needed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
No special UDP port exports or dedicated IP address needed.
No special UDP port exports or dedicated IP addresses are needed.

Just run `fly launch` and enjoy your deployment :)
22 changes: 22 additions & 0 deletions lib/ex_webrtc/ice/fly_ip_filter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule ExWebRTC.ICE.FlyIpFilter do
@moduledoc """
ICE IP filter for Fly.io deployments.

This module defines a single function, which filters out IP addresses,
which ICE Agent will use as its host candidates.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. "filters out" to me sounds like it's rejecting these addresses rather than accepting them
  2. I feel like adding a bit more info about the function could be helpful
Suggested change
This module defines a single function, which filters out IP addresses,
which ICE Agent will use as its host candidates.
This module defines a single function, which filters IP addresses,
which ICE Agent will use as its host candidates.
It accepts only the IPv4 address that `fly-global-services` resolves to.

"""

@spec ip_filter(:inet.ip_address()) :: boolean()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] We could add a behaviour in ExICE. Then again, it's only one function -- up to you

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's stay with the current version for know. In debugging and testing it's verry convenient to pass anonymous function instead of creating a module with a single function.

I thought about returning an anonymous function from ip_filter and use ICEAgent.ip_filter type in typespec but sounds like an overkill 🤔

def ip_filter(ip_address) do
case :inet.gethostbyname(~c"fly-global-services") do
# Assume that fly-global-services has to resolve
# to a single ipv4 address.
# In other case, don't even try to connect.
{:ok, {:hostent, _, _, :inet, 4, [addr]}} ->
addr == ip_address

_ ->
false
end
end
end
3 changes: 2 additions & 1 deletion lib/ex_webrtc/peer_connection/configuration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule ExWebRTC.PeerConnection.Configuration do

require Logger

alias ExICE.ICEAgent
alias ExWebRTC.{RTPCodecParameters, SDPUtils}
alias ExSDP.Attribute.{Extmap, FMTP, RTCPFeedback}

Expand Down Expand Up @@ -148,7 +149,7 @@ defmodule ExWebRTC.PeerConnection.Configuration do
controlling_process: Process.dest(),
ice_servers: [ice_server()],
ice_transport_policy: :relay | :all,
ice_ip_filter: (:inet.ip_address() -> boolean()),
ice_ip_filter: ICEAgent.ip_filter(),
ice_port_range: Enumerable.t(non_neg_integer()),
audio_codecs: [RTPCodecParameters.t()],
video_codecs: [RTPCodecParameters.t()],
Expand Down
11 changes: 7 additions & 4 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ defmodule ExWebRTC.MixProject do
defp deps do
[
{:ex_sdp, "~> 1.0"},
{:ex_ice, "~> 0.8.0"},
{:ex_ice, github: "elixir-webrtc/ex_ice"},
{:ex_dtls, "~> 0.16.0"},
{:ex_libsrtp, "~> 0.7.1"},
{:ex_rtp, "~> 0.4.0"},
Expand All @@ -80,25 +80,28 @@ defmodule ExWebRTC.MixProject do
"simulcast",
"modifying",
"mastering_transceivers",
"deploying",
"debugging"
]

deploying_guides = ["bare", "fly"]

[
main: "readme",
logo: "logo.svg",
extras:
["README.md"] ++
Enum.map(intro_guides, &"guides/introduction/#{&1}.md") ++
Enum.map(advanced_guides, &"guides/advanced/#{&1}.md"),
Enum.map(advanced_guides, &"guides/advanced/#{&1}.md") ++
Enum.map(deploying_guides, &"guides/deploying/#{&1}.md"),
assets: "guides/assets",
source_ref: "v#{@version}",
formatters: ["html"],
before_closing_body_tag: &before_closing_body_tag/1,
nest_modules_by_prefix: [ExWebRTC],
groups_for_extras: [
Introduction: Path.wildcard("guides/introduction/*.md"),
Advanced: Path.wildcard("guides/advanced/*.md")
Advanced: Path.wildcard("guides/advanced/*.md"),
Deploying: Path.wildcard("guides/deploying/*.md")
],
groups_for_modules: [
MEDIA: ~r"ExWebRTC\.Media\..*",
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"},
"ex_dtls": {:hex, :ex_dtls, "0.16.0", "3ae38025ccc77f6db573e2e391602fa9bbc02253c137d8d2d59469a66cbe806b", [:mix], [{:bundlex, "~> 1.5.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2a4e30d74c6ddf95cc5b796423293c06a0da295454c3823819808ff031b4b361"},
"ex_ice": {:hex, :ex_ice, "0.8.1", "4d5c911766ce92e13323b632a55d9ab821092f13fc2ebf236dc233c8c1f9a64c", [:mix], [{:elixir_uuid, "~> 1.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}, {:ex_turn, "~> 0.1.0", [hex: :ex_turn, repo: "hexpm", optional: false]}], "hexpm", "8f10134e2eb7e6aebbf8fba0d5fcec56d8f8db3e94c3dde045feb463979c2dda"},
"ex_ice": {:git, "https://github.com/elixir-webrtc/ex_ice.git", "4c5190687e11b28a8209f2c930895647d0b2a64c", []},
"ex_libsrtp": {:hex, :ex_libsrtp, "0.7.2", "211bd89c08026943ce71f3e2c0231795b99cee748808ed3ae7b97cd8d2450b6b", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2e20645d0d739a4ecdcf8d4810a0c198120c8a2f617f2b75b2e2e704d59f492a"},
"ex_rtcp": {:hex, :ex_rtcp, "0.4.0", "f9e515462a9581798ff6413583a25174cfd2101c94a2ebee871cca7639886f0a", [:mix], [], "hexpm", "28956602cf210d692fcdaf3f60ca49681634e1deb28ace41246aee61ee22dc3b"},
"ex_rtp": {:hex, :ex_rtp, "0.4.0", "1f1b5c1440a904706011e3afbb41741f5da309ce251cb986690ce9fd82636658", [:mix], [], "hexpm", "0f72d80d5953a62057270040f0f1ee6f955c08eeae82ac659c038001d7d5a790"},
Expand Down