-
-
Notifications
You must be signed in to change notification settings - Fork 556
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(elixir): udp hole punching example
A simple Elixir example demonstrating how UDP hole punching could be done
- Loading branch information
Showing
7 changed files
with
249 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["*.exs"], | ||
locals_without_parens: [step: 1, step: 2] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
["setup.exs", "puncher.exs"] |> Enum.map(&Code.require_file/1) | ||
|
||
[my_name, their_name, addr] = System.argv() | ||
[rendezvous_host, port_s] = String.split(addr, ":") | ||
{rendezvous_port, ""} = Integer.parse(port_s) | ||
|
||
rendezvous_address = Ockam.Transport.UDPAddress.new(rendezvous_host, rendezvous_port) | ||
|
||
{:ok, _name} = | ||
Puncher.create( | ||
address: my_name, | ||
attributes: %{ | ||
target: their_name, | ||
rendezvous_address: rendezvous_address | ||
} | ||
) | ||
|
||
# Port 0 will cause the OS to assign a random port | ||
{:ok, _udp} = Ockam.Transport.UDP.start(port: 0) | ||
|
||
Ockam.Node.whereis(my_name) | ||
|> GenServer.call(:ping_rendezvous_server) | ||
|
||
Process.sleep(30_000) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
["setup.exs", "rendezvous_worker.exs"] |> Enum.map(&Code.require_file/1) | ||
|
||
require Logger | ||
|
||
port = 5000 | ||
Logger.info("Starting Rendezvous Worker on port #{port}") | ||
|
||
{:ok, _rendezvous} = | ||
RendezvousWorker.create( | ||
address: "rendezvous", | ||
attributes: %{addresses: %{}, pending_connections: []} | ||
) | ||
|
||
{:ok, _udp_t} = Ockam.Transport.UDP.start(ip: {0, 0, 0, 0}, port: port) | ||
|
||
Ockam.Node.list_workers() | ||
|> IO.inspect(label: "Workers") | ||
|
||
Process.sleep(:infinity) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# UDP Hole Punching Setup | ||
|
||
This directory contains examples of how UDP hole punching might be | ||
achieved using the Ockam Elixir UDP transport and simple workers. | ||
|
||
## Overview | ||
|
||
UDP hole punching is a technique used to establish a direct communication channel between two devices that are behind NAT (Network Address Translation) routers. This is achieved by sending UDP packets to a third-party rendezvous server that provides | ||
each node with the other's external facing address. | ||
|
||
## Usage | ||
|
||
- Start the rendezvous server | ||
``` | ||
elixir 01-rendezvous.exs | ||
``` | ||
|
||
- Start two (or more) clients (punchers) | ||
``` | ||
elixir 01-puncher.exs my_name their_name rendezvous_host:port | ||
elixir 01-puncher.exs my_name their_name rendezvous_host:port | ||
``` | ||
|
||
## Known Issues | ||
|
||
- Generally unresilient | ||
- doesn't handle address changes | ||
- doesn't ensure both sides intend to connect to the other | ||
- doesn't implement keep alives | ||
- doesn't handle multi hop | ||
- Hard coded rendezvous node name | ||
- Identifiers are just simple strings | ||
- Uses underlying Elixir UDP transport which is currently incompatible with the Rust impl | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
defmodule Puncher do | ||
@moduledoc """ | ||
An Ockam Worker that acts as a client in a UDP hole punching scenario. | ||
""" | ||
|
||
use Ockam.Worker | ||
|
||
require Logger | ||
|
||
alias Ockam.{Message, Router} | ||
|
||
@rendezvous_node "rendezvous" | ||
|
||
@impl true | ||
def handle_call(:ping_rendezvous_server, _, state) do | ||
Logger.debug("Ensure rendezvous server is listening") | ||
|
||
Ockam.Router.route(%{ | ||
payload: "ping", | ||
onward_route: [state.attributes.rendezvous_address, @rendezvous_node], | ||
return_route: [state.address] | ||
}) | ||
|
||
{:reply, :ok, state} | ||
end | ||
|
||
@impl true | ||
def handle_message(%{payload: "pong"} = message, state) do | ||
Logger.debug("Rendezvous server is up, request address") | ||
|
||
Router.route(%{ | ||
payload: "my address", | ||
onward_route: [state.attributes.rendezvous_address, @rendezvous_node], | ||
return_route: [state.address] | ||
}) | ||
|
||
{:ok, state} | ||
end | ||
|
||
def handle_message(%{payload: "address: " <> address} = message, state) do | ||
Logger.debug("Received address: #{inspect(address)}") | ||
|
||
state = put_in(state, [:attributes, :external_address], address) | ||
|
||
Router.route(%{ | ||
payload: "connect", | ||
onward_route: [ | ||
state.attributes.rendezvous_address, | ||
@rendezvous_node, | ||
state.attributes.target | ||
], | ||
return_route: [state.address] | ||
}) | ||
|
||
{:ok, state} | ||
end | ||
|
||
def handle_message(%{payload: "connected"} = message, state) do | ||
Logger.debug("Received connected message") | ||
|
||
Router.route(%{ | ||
payload: "hello", | ||
onward_route: message.return_route |> tl(), | ||
return_route: [state.attributes.external_address, state.address] | ||
}) | ||
|
||
{:ok, state} | ||
end | ||
|
||
def handle_message(%{payload: "hello"} = message, state) do | ||
Logger.info("Received hello message! Hole punching successful!") | ||
|
||
Router.route(Message.reply(message, state.attributes.external_address, "hello")) | ||
|
||
{:ok, state} | ||
end | ||
|
||
def handle_message(message, %{address: address} = state) do | ||
Logger.warning("Unknown message #{inspect(message)}}") | ||
|
||
{:ok, state} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
defmodule RendezvousWorker do | ||
@moduledoc """ | ||
An Ockam Worker that acts as a rendezvous server for UDP hole punching. | ||
""" | ||
|
||
use Ockam.Worker | ||
|
||
require Logger | ||
|
||
alias Ockam.{Message, Router} | ||
|
||
@impl true | ||
def handle_message(%{payload: "ping"} = message, state) do | ||
Router.route(Message.reply(message, state.address, "pong")) | ||
|
||
{:ok, state} | ||
end | ||
|
||
def handle_message(%{payload: "my address"} = message, state) do | ||
[external_address, source] = message.return_route | ||
|
||
Logger.debug("Replying with address: #{external_address.value}") | ||
|
||
Router.route(Message.reply(message, state.address, "address: #{external_address.value}")) | ||
|
||
state = put_in(state, [:attributes, :addresses, source], external_address) | ||
{:ok, state} | ||
end | ||
|
||
def handle_message(%{payload: "connect"} = message, state) do | ||
source = message.return_route |> Enum.reverse() |> hd() | ||
target = message.onward_route |> Enum.reverse() |> hd() | ||
|
||
Logger.debug("Received connect message from #{inspect(source)} to #{inspect(target)}") | ||
|
||
state = | ||
state.attributes.addresses | ||
|> Map.get(target) | ||
|> case do | ||
nil -> | ||
Logger.debug("Target #{target} not found") | ||
pending = [{source, target} | state.attributes.pending_connections] | ||
put_in(state, [:attributes, :pending_connections], pending) | ||
|
||
target_address -> | ||
Logger.debug("Target #{target} address found: #{inspect(target_address)}") | ||
|
||
Router.route(%{ | ||
payload: "connected", | ||
onward_route: [target_address, target], | ||
return_route: message.return_route | ||
}) | ||
|
||
Router.route(%{ | ||
payload: "connected", | ||
onward_route: message.return_route, | ||
return_route: [target_address, target] | ||
}) | ||
|
||
pending = | ||
state.attributes.pending_connections | ||
|> Enum.reject(&(&1 == {source, target})) | ||
|
||
put_in(state, [:attributes, :pending_connections], pending) | ||
end | ||
|
||
{:ok, state} | ||
end | ||
|
||
def handle_message(message, state) do | ||
Logger.warning("Unknown message #{inspect(message)}") | ||
|
||
{:ok, state} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
Mix.install([ | ||
{:ockam, path: "../../../implementations/elixir/ockam/ockam"}, | ||
{:ockam_typed_cbor, | ||
override: true, path: "../../../implementations/elixir/ockam/ockam_typed_cbor"}, | ||
{:ranch, "~> 2.1"} | ||
]) | ||
|
||
Application.load(:ockam) |