implement a bunch of utility functions on tun device primitive

This commit is contained in:
lilly 2026-05-21 16:44:01 +02:00
commit d94f20d042
Signed by: lilly
SSH key fingerprint: SHA256:y9T5GFw2A20WVklhetIxG1+kcg/Ce0shnQmbu1LQ37g
9 changed files with 248 additions and 82 deletions

2
Cargo.lock generated
View file

@ -407,7 +407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "p2pchat_transport_gen_tun" name = "p2pchat_transport_prim_tun"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"rustler", "rustler",

View file

@ -1,4 +1,4 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["native/p2pchat_transport_gen_tun"] members = ["native/p2pchat_transport_prim_tun"]

View file

@ -1,7 +1,7 @@
defmodule P2pChat do defmodule P2pChat do
def hello do def hello do
{:ok, pid1} = P2pChat.Transport.start_link(port: 12345) {: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) {:ok, pid2} = P2pChat.Transport.start_link(port: 12346)
netid2 = P2pChat.Transport.get_netid(pid2) netid2 = P2pChat.Transport.get_netid(pid2)

View file

@ -1,6 +1,6 @@
defmodule P2pChat.Transport.GenTun do defmodule P2pChat.Transport.GenTun do
require Logger require Logger
use Rustler, otp_app: :p2p_chat, crate: "p2pchat_transport_gen_tun" require P2pChat.Transport.PrimTun, as: PrimTun
@behaviour GenServer @behaviour GenServer
@ -10,15 +10,15 @@ defmodule P2pChat.Transport.GenTun do
# Server API # Server API
# #
@impl true @impl true
def init(args) do def init(_args) do
{:ok, tun_handle} = make_tun_device() {:ok, tun_handle} = PrimTun.make_tun_device(true)
state = %__MODULE__{tun_handle: tun_handle} state = %__MODULE__{tun_handle: tun_handle}
{ :ok, state, { :continue, :recv } } { :ok, state, { :continue, :recv } }
end end
@impl true @impl true
def handle_continue(:recv, state) do 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} -> {:ok, buf} ->
Logger.debug("Received #{byte_size(buf)} bytes") Logger.debug("Received #{byte_size(buf)} bytes")
{:error, :would_block} -> {} {:error, :would_block} -> {}
@ -26,13 +26,13 @@ defmodule P2pChat.Transport.GenTun do
Logger.error("Error during receive: #{e}") Logger.error("Error during receive: #{e}")
end end
{:noreply, state, { :continue, :recv }} {:noreply, state}
end end
@impl true @impl true
def handle_call({:recv}, _from, state) do def handle_call({:get_addrs}, _from, state) do
data = tun_recv(state.tun_handle) result = PrimTun.get_addrs(state.tun_handle)
{ :reply, data, state } {:reply, result, state}
end end
# #
@ -40,13 +40,10 @@ defmodule P2pChat.Transport.GenTun do
# #
def open() do def open() do
GenServer.start_link(__MODULE__, nil) GenServer.start_link(__MODULE__, nil)
end end
# def get_addrs(pid) do
# NIFs GenServer.call(pid, {:get_addrs})
# end
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)
end end

View file

@ -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

View file

@ -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<Term> {
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<TunHandle>, bufsize: usize) -> NifResult<Term> {
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<T: Encoder>(env: rustler::Env, value: T) -> Term {
make_tuple(env, &[atoms::ok().encode(env), value.encode(env)])
}
rustler::init!("Elixir.P2pChat.Transport.GenTun");

View file

@ -1,5 +1,5 @@
[package] [package]
name = "p2pchat_transport_gen_tun" name = "p2pchat_transport_prim_tun"
version = "0.1.0" version = "0.1.0"
authors = [] authors = []
edition = "2021" edition = "2021"

View file

@ -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<IpAddr> 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::<Vec<_>>();
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<Self> {
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<Term> {
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<TunHandle>, bufsize: usize) -> NifResult<Term> {
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<TunHandle>, 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<TunHandle>) -> NifResult<Vec<WrappedIpAddr>> {
let addrs = handle.device.addresses().map_err(map_io_error)?;
Ok(addrs.into_iter().map(|i| i.into()).collect::<Vec<_>>())
}
#[rustler::nif]
fn get_broadcast(handle: ResourceArc<TunHandle>) -> NifResult<WrappedIpAddr> {
let broadcast = handle.device.broadcast().map_err(map_io_error)?;
Ok(broadcast.into())
}
#[rustler::nif]
fn get_name(handle: ResourceArc<TunHandle>) -> NifResult<String> {
handle.device.name().map_err(map_io_error)
}
#[rustler::nif]
fn set_name(handle: ResourceArc<TunHandle>, value: &str) -> NifResult<()> {
handle.device.set_name(value).map_err(map_io_error)
}
#[rustler::nif]
fn is_running(handle: ResourceArc<TunHandle>) -> NifResult<bool> {
handle.device.is_running().map_err(map_io_error)
}
#[rustler::nif]
fn set_running(handle: ResourceArc<TunHandle>, should_run: bool) -> NifResult<()> {
handle.device.enabled(should_run).map_err(map_io_error)
}
#[rustler::nif]
fn add_address(
handle: ResourceArc<TunHandle>,
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<TunHandle>, addr: WrappedIpAddr) -> NifResult<()> {
handle.device.remove_address(*addr).map_err(map_io_error)
}
#[rustler::nif]
fn get_mtu(handle: ResourceArc<TunHandle>) -> NifResult<u16> {
handle.device.mtu().map_err(map_io_error)
}
#[rustler::nif]
fn set_mtu(handle: ResourceArc<TunHandle>, 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<T: Encoder>(env: rustler::Env, value: T) -> Term {
make_tuple(env, &[atoms::ok().encode(env), value.encode(env)])
}
rustler::init!("Elixir.P2pChat.Transport.PrimTun");