-
-
Notifications
You must be signed in to change notification settings - Fork 556
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
feat(elixir): udp hole punching example #6588
Open
ckhrysze
wants to merge
2
commits into
build-trust:develop
Choose a base branch
from
ckhrysze:develop
base: develop
Could not load branches
Branch not found: {{ refName }}
Could not load tags
Nothing to show
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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(:punch_hole) | ||
|
||
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,11 @@ | ||
["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.setup() | ||
{:ok, _udp_t} = Ockam.Transport.UDP.start(ip: {0, 0, 0, 0}, port: port) | ||
|
||
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,34 @@ | ||
# 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 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,71 @@ | ||
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(:punch_hole, _, state) do | ||
Logger.debug("Request address from rendezvous server") | ||
|
||
Router.route(%{ | ||
payload: "my address", | ||
onward_route: [state.attributes.rendezvous_address, @rendezvous_node], | ||
return_route: [state.address] | ||
}) | ||
|
||
{:reply, :ok, state} | ||
end | ||
|
||
@impl true | ||
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, 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,86 @@ | ||
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} | ||
|
||
@rendezvous_node "rendezvous" | ||
|
||
def setup do | ||
__MODULE__.create( | ||
address: @rendezvous_node, | ||
attributes: %{addresses: %{}, pending_connections: []} | ||
) | ||
end | ||
|
||
@impl true | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the intention is to let unmatched connection request on the |
||
|
||
target_address -> | ||
Logger.debug("Target #{target} address found: #{inspect(target_address)}") | ||
|
||
pending = | ||
if {target, source} in state.attributes.pending_connections do | ||
Logger.debug("Pending connection found") | ||
|
||
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] | ||
}) | ||
|
||
state.attributes.pending_connections | ||
|> Enum.reject(&(&1 == {target, source})) | ||
else | ||
Logger.debug("Pending connection not found") | ||
|
||
[{source, target} | state.attributes.pending_connections] | ||
end | ||
|
||
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) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have expected here to send a msg to the other end, and wait till receiving a message from the other end as well, to show how user code can make use of the "Puncher" to send messages traversing NAT. Not necessary like this, but this is what would come to my mind:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad you brought this one up, I saw that self registration and receive block in other examples, but in practice I've never gone to production with code containing an explicit receive block. As such I just made the worker module (Puncher in this case) handle receiving since its already wired to do so. But as the other examples are more along the lines as yours, I'm curious if that is providing additional benefit that I'm not seeing.