From d94f20d04242009b521113e94f732270f5807b89 Mon Sep 17 00:00:00 2001 From: lilly Date: Thu, 21 May 2026 16:44:01 +0200 Subject: [PATCH] implement a bunch of utility functions on tun device primitive --- Cargo.lock | 2 +- Cargo.toml | 2 +- lib/p2p_chat.ex | 2 +- lib/p2p_chat/transport/gen_tun.ex | 27 +-- lib/p2p_chat/transport/prim_tun.ex | 17 ++ native/p2pchat_transport_gen_tun/src/lib.rs | 63 ----- .../Cargo.toml | 2 +- .../README.md | 0 native/p2pchat_transport_prim_tun/src/lib.rs | 215 ++++++++++++++++++ 9 files changed, 248 insertions(+), 82 deletions(-) create mode 100644 lib/p2p_chat/transport/prim_tun.ex delete mode 100644 native/p2pchat_transport_gen_tun/src/lib.rs rename native/{p2pchat_transport_gen_tun => p2pchat_transport_prim_tun}/Cargo.toml (86%) rename native/{p2pchat_transport_gen_tun => p2pchat_transport_prim_tun}/README.md (100%) create mode 100644 native/p2pchat_transport_prim_tun/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 796b5da..1495f33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,7 +407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "p2pchat_transport_gen_tun" +name = "p2pchat_transport_prim_tun" version = "0.1.0" dependencies = [ "rustler", diff --git a/Cargo.toml b/Cargo.toml index 28fe060..5d4615c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [workspace] resolver = "2" -members = ["native/p2pchat_transport_gen_tun"] +members = ["native/p2pchat_transport_prim_tun"] diff --git a/lib/p2p_chat.ex b/lib/p2p_chat.ex index 17aae51..2d940a3 100644 --- a/lib/p2p_chat.ex +++ b/lib/p2p_chat.ex @@ -1,7 +1,7 @@ defmodule P2pChat do def hello do {:ok, pid1} = P2pChat.Transport.start_link(port: 12345) - netid1 = P2pChat.Transport.get_netid(pid1) + _netid1 = P2pChat.Transport.get_netid(pid1) {:ok, pid2} = P2pChat.Transport.start_link(port: 12346) netid2 = P2pChat.Transport.get_netid(pid2) diff --git a/lib/p2p_chat/transport/gen_tun.ex b/lib/p2p_chat/transport/gen_tun.ex index 98113a3..50137ae 100644 --- a/lib/p2p_chat/transport/gen_tun.ex +++ b/lib/p2p_chat/transport/gen_tun.ex @@ -1,6 +1,6 @@ defmodule P2pChat.Transport.GenTun do require Logger - use Rustler, otp_app: :p2p_chat, crate: "p2pchat_transport_gen_tun" + require P2pChat.Transport.PrimTun, as: PrimTun @behaviour GenServer @@ -10,15 +10,15 @@ defmodule P2pChat.Transport.GenTun do # Server API # @impl true - def init(args) do - {:ok, tun_handle} = make_tun_device() + def init(_args) do + {:ok, tun_handle} = PrimTun.make_tun_device(true) state = %__MODULE__{tun_handle: tun_handle} { :ok, state, { :continue, :recv } } end @impl true def handle_continue(:recv, state) do - case tun_recv(state.tun_handle) do + case PrimTun.tun_recv(state.tun_handle, 2**16) do {:ok, buf} -> Logger.debug("Received #{byte_size(buf)} bytes") {:error, :would_block} -> {} @@ -26,13 +26,13 @@ defmodule P2pChat.Transport.GenTun do Logger.error("Error during receive: #{e}") end - {:noreply, state, { :continue, :recv }} + {:noreply, state} end @impl true - def handle_call({:recv}, _from, state) do - data = tun_recv(state.tun_handle) - { :reply, data, state } + def handle_call({:get_addrs}, _from, state) do + result = PrimTun.get_addrs(state.tun_handle) + {:reply, result, state} end # @@ -40,13 +40,10 @@ defmodule P2pChat.Transport.GenTun do # def open() do GenServer.start_link(__MODULE__, nil) - end + end - # - # NIFs - # - - defp make_tun_device(), do: :erlang.nif_error(:nif_not_loaded) - defp tun_recv(_handle, _bufsize \\ 2*16), do: :erlang.nif_error(:nif_not_loaded) + def get_addrs(pid) do + GenServer.call(pid, {:get_addrs}) + end end diff --git a/lib/p2p_chat/transport/prim_tun.ex b/lib/p2p_chat/transport/prim_tun.ex new file mode 100644 index 0000000..1510d37 --- /dev/null +++ b/lib/p2p_chat/transport/prim_tun.ex @@ -0,0 +1,17 @@ +defmodule P2pChat.Transport.PrimTun do + use Rustler, otp_app: :p2p_chat, crate: "p2pchat_transport_prim_tun" + + def make_tun_device(_packet_info), do: :erlang.nif_error(:nif_not_loaded) + def recv(_handle, _bufsize), do: :erlang.nif_error(:nif_not_loaded) + def send(_handle, _buf), do: :erlang.nif_error(:nif_not_loaded) + def get_addrs(_handle), do: :erlang.nif_error(:nif_not_loaded) + def get_broadcast(_handle), do: :erlang.nif_error(:nif_not_loaded) + def get_name(_handle), do: :erlang.nif_error(:nif_not_loaded) + def set_name(_handle, _name), do: :erlang.nif_error(:nif_not_loaded) + def is_running(_handle), do: :erlang.nif_error(:nif_not_loaded) + def set_running(_handle, _should_run), do: :erlang.nif_error(:nif_not_loaded) + def add_address(_handle, _address, _prefix_length), do: :erlang.nif_error(:nif_not_loaded) + def remove_address(_handle, _address), do: :erlang.nif_error(:nif_not_loaded) + def get_mtu(_handle), do: :erlang.nif_error(:nif_not_loaded) + def set_mtu(_handle, _mtu), do: :erlang.nif_error(:nif_not_loaded) +end diff --git a/native/p2pchat_transport_gen_tun/src/lib.rs b/native/p2pchat_transport_gen_tun/src/lib.rs deleted file mode 100644 index bcd3d31..0000000 --- a/native/p2pchat_transport_gen_tun/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::io; -use std::net::Ipv6Addr; - -use rustler::types::tuple::make_tuple; -use rustler::{resource_impl, Encoder, NifResult, OwnedBinary, Resource, ResourceArc, Term}; -use tun_rs::{DeviceBuilder, SyncDevice}; - -mod atoms { - rustler::atoms! { - ok, - resource_busy, - permission_denied, - would_block, - } -} - -pub struct TunHandle { - device: SyncDevice, -} - -#[resource_impl] -impl Resource for TunHandle {} - -#[rustler::nif] -fn make_tun_device(env: rustler::Env) -> NifResult { - let device = DeviceBuilder::new() - .name("tunP2P") - .ipv6(Ipv6Addr::new(0x2001, 0x2f, 0, 0, 0, 0, 0, 1), 28) - .packet_information(true) - .build_sync() - .map_err(map_io_error)?; - device.set_nonblocking(true).map_err(map_io_error)?; - - let handle = ResourceArc::new(TunHandle { device }); - Ok(make_ok_tuple(env, handle)) -} - -#[rustler::nif] -fn tun_recv(env: rustler::Env, handle: ResourceArc, bufsize: usize) -> NifResult { - let mut buf = OwnedBinary::new(bufsize).expect("Could not allocate receive buffer"); - let n = handle.device.recv(&mut buf).map_err(map_io_error)?; - assert!(buf.realloc(n)); - let erl_buf = buf.release(env); - - Ok(make_ok_tuple(env, erl_buf)) -} - -#[inline] -fn map_io_error(error: io::Error) -> rustler::Error { - rustler::Error::Term(match error.kind() { - io::ErrorKind::ResourceBusy => Box::new(atoms::resource_busy()), - io::ErrorKind::PermissionDenied => Box::new(atoms::permission_denied()), - io::ErrorKind::WouldBlock => Box::new(atoms::would_block()), - e => Box::new(e.to_string()), - }) -} - -#[inline] -fn make_ok_tuple(env: rustler::Env, value: T) -> Term { - make_tuple(env, &[atoms::ok().encode(env), value.encode(env)]) -} - -rustler::init!("Elixir.P2pChat.Transport.GenTun"); diff --git a/native/p2pchat_transport_gen_tun/Cargo.toml b/native/p2pchat_transport_prim_tun/Cargo.toml similarity index 86% rename from native/p2pchat_transport_gen_tun/Cargo.toml rename to native/p2pchat_transport_prim_tun/Cargo.toml index e7a6510..f043ec6 100644 --- a/native/p2pchat_transport_gen_tun/Cargo.toml +++ b/native/p2pchat_transport_prim_tun/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "p2pchat_transport_gen_tun" +name = "p2pchat_transport_prim_tun" version = "0.1.0" authors = [] edition = "2021" diff --git a/native/p2pchat_transport_gen_tun/README.md b/native/p2pchat_transport_prim_tun/README.md similarity index 100% rename from native/p2pchat_transport_gen_tun/README.md rename to native/p2pchat_transport_prim_tun/README.md diff --git a/native/p2pchat_transport_prim_tun/src/lib.rs b/native/p2pchat_transport_prim_tun/src/lib.rs new file mode 100644 index 0000000..7d13c82 --- /dev/null +++ b/native/p2pchat_transport_prim_tun/src/lib.rs @@ -0,0 +1,215 @@ +use std::io; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::ops::Deref; + +use rustler::types::tuple::{get_tuple, make_tuple}; +use rustler::{ + resource_impl, Binary, Decoder, Encoder, NifResult, OwnedBinary, Resource, ResourceArc, Term, +}; +use tun_rs::{DeviceBuilder, SyncDevice}; + +mod atoms { + rustler::atoms! { + ok, + resource_busy, + permission_denied, + would_block, + address_not_available, + } +} + +/// Opaque handle which the BEAM an use to refer to an open TUN Device +pub struct TunHandle { + device: SyncDevice, +} + +/// A utility wrapper around native IP addresses that has BEAM encoding/decoding rules implemented +#[repr(transparent)] +struct WrappedIpAddr(IpAddr); + +impl From for WrappedIpAddr { + fn from(value: IpAddr) -> Self { + Self(value) + } +} + +impl Deref for WrappedIpAddr { + type Target = IpAddr; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Encoder for WrappedIpAddr { + fn encode<'a>(&self, env: rustler::Env<'a>) -> Term<'a> { + match self.0 { + IpAddr::V6(addr) => { + let octets = addr + .segments() + .into_iter() + .map(|i| i.encode(env)) + .collect::>(); + make_tuple(env, &octets) + } + IpAddr::V4(addr) => { + let octets = addr.octets(); + (octets[0], octets[1], octets[2], octets[3]).encode(env) + } + } + } +} + +impl<'a> Decoder<'a> for WrappedIpAddr { + fn decode(term: Term<'a>) -> NifResult { + let term_tuple = get_tuple(term)?; + match term_tuple.len() { + 8 => { + let segments: [u16; 8] = [ + term_tuple[0].decode()?, + term_tuple[1].decode()?, + term_tuple[2].decode()?, + term_tuple[3].decode()?, + term_tuple[4].decode()?, + term_tuple[5].decode()?, + term_tuple[6].decode()?, + term_tuple[7].decode()?, + ]; + Ok(IpAddr::V6(Ipv6Addr::from_segments(segments)).into()) + } + 4 => { + let octets: [u8; 4] = [ + term_tuple[0].decode()?, + term_tuple[1].decode()?, + term_tuple[2].decode()?, + term_tuple[3].decode()?, + ]; + Ok(IpAddr::V4(Ipv4Addr::from_octets(octets)).into()) + } + _ => Err(rustler::Error::BadArg), + } + } +} + +#[resource_impl] +impl Resource for TunHandle {} + +#[rustler::nif] +fn make_tun_device(env: rustler::Env, packet_info: bool) -> NifResult { + let device = DeviceBuilder::new() + .name("tunP2P") + .packet_information(packet_info) + .build_sync() + .map_err(map_io_error)?; + device.set_nonblocking(true).map_err(map_io_error)?; + + let handle = ResourceArc::new(TunHandle { device }); + Ok(make_ok_tuple(env, handle)) +} + +#[rustler::nif] +fn recv(env: rustler::Env, handle: ResourceArc, bufsize: usize) -> NifResult { + handle.device.set_nonblocking(true).map_err(map_io_error)?; + + let mut buf = OwnedBinary::new(bufsize).expect("Could not allocate receive buffer"); + let n = handle.device.recv(&mut buf).map_err(map_io_error)?; + assert!(buf.realloc(n)); + let erl_buf = buf.release(env); + + Ok(make_ok_tuple(env, erl_buf)) +} + +#[rustler::nif] +fn send(handle: ResourceArc, buf: Binary) -> NifResult<()> { + handle.device.set_nonblocking(false).map_err(map_io_error)?; + + let mut n = 0; + while n < buf.len() { + n += handle.device.send(&buf[n..]).map_err(map_io_error)?; + } + + Ok(()) +} + +#[rustler::nif] +fn get_addrs(handle: ResourceArc) -> NifResult> { + let addrs = handle.device.addresses().map_err(map_io_error)?; + Ok(addrs.into_iter().map(|i| i.into()).collect::>()) +} + +#[rustler::nif] +fn get_broadcast(handle: ResourceArc) -> NifResult { + let broadcast = handle.device.broadcast().map_err(map_io_error)?; + Ok(broadcast.into()) +} + +#[rustler::nif] +fn get_name(handle: ResourceArc) -> NifResult { + handle.device.name().map_err(map_io_error) +} + +#[rustler::nif] +fn set_name(handle: ResourceArc, value: &str) -> NifResult<()> { + handle.device.set_name(value).map_err(map_io_error) +} + +#[rustler::nif] +fn is_running(handle: ResourceArc) -> NifResult { + handle.device.is_running().map_err(map_io_error) +} + +#[rustler::nif] +fn set_running(handle: ResourceArc, should_run: bool) -> NifResult<()> { + handle.device.enabled(should_run).map_err(map_io_error) +} + +#[rustler::nif] +fn add_address( + handle: ResourceArc, + addr: WrappedIpAddr, + prefix_length: u8, +) -> NifResult<()> { + match addr.0 { + IpAddr::V6(addr) => handle + .device + .add_address_v6(addr, prefix_length) + .map_err(map_io_error)?, + IpAddr::V4(addr) => handle + .device + .add_address_v4(addr, prefix_length) + .map_err(map_io_error)?, + } + + Ok(()) +} + +#[rustler::nif] +fn remove_address(handle: ResourceArc, addr: WrappedIpAddr) -> NifResult<()> { + handle.device.remove_address(*addr).map_err(map_io_error) +} + +#[rustler::nif] +fn get_mtu(handle: ResourceArc) -> NifResult { + handle.device.mtu().map_err(map_io_error) +} + +#[rustler::nif] +fn set_mtu(handle: ResourceArc, mtu: u16) -> NifResult<()> { + handle.device.set_mtu(mtu).map_err(map_io_error) +} + +fn map_io_error(error: io::Error) -> rustler::Error { + rustler::Error::Term(match error.kind() { + io::ErrorKind::ResourceBusy => Box::new(atoms::resource_busy()), + io::ErrorKind::PermissionDenied => Box::new(atoms::permission_denied()), + io::ErrorKind::WouldBlock => Box::new(atoms::would_block()), + io::ErrorKind::AddrNotAvailable => Box::new(atoms::address_not_available()), + e => Box::new(e.to_string()), + }) +} + +fn make_ok_tuple(env: rustler::Env, value: T) -> Term { + make_tuple(env, &[atoms::ok().encode(env), value.encode(env)]) +} + +rustler::init!("Elixir.P2pChat.Transport.PrimTun");