Skip to content

Commit

Permalink
feat(elixir): udp hole punching example
Browse files Browse the repository at this point in the history
A simple Elixir example demonstrating how UDP hole punching could be done
  • Loading branch information
ckhrysze committed Oct 18, 2023
1 parent 0d12cdc commit 55e0813
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 0 deletions.
5 changes: 5 additions & 0 deletions examples/elixir/udp_hole_punching/.formatter.exs
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]
]
24 changes: 24 additions & 0 deletions examples/elixir/udp_hole_punching/01-puncher.exs
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)
19 changes: 19 additions & 0 deletions examples/elixir/udp_hole_punching/01-rendezvous.exs
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)
35 changes: 35 additions & 0 deletions examples/elixir/udp_hole_punching/README.md
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


83 changes: 83 additions & 0 deletions examples/elixir/udp_hole_punching/puncher.exs
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
75 changes: 75 additions & 0 deletions examples/elixir/udp_hole_punching/rendezvous_worker.exs
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
8 changes: 8 additions & 0 deletions examples/elixir/udp_hole_punching/setup.exs
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)

0 comments on commit 55e0813

Please sign in to comment.