l2tp-rs
High-level async L2TPv3 control API for Linux Generic Netlink.
This crate focuses on L2TP tunnel/session lifecycle and query operations only.
It does not configure interfaces/bridges/routes (rtnetlink tasks belong in a separate crate).
Requirements
- Linux. On other platforms the crate compiles but exposes no API, and the
l2tpbinary only reports that the platform is unsupported. - Rust 1.89 or newer (required by the netlink dependencies).
- Linux kernel with L2TP Generic Netlink support (
l2tpfamily). - Tokio runtime with its I/O driver enabled.
L2tpHandle::newreturns an error without one. CAP_NET_ADMIN(or root) for every L2TP netlink command, includinggetandlist; the kernel marks them allGENL_UNS_ADMIN_PERM.
Installation
[]
= "0.3"
= { = "1", = ["rt-multi-thread", "macros"] }
Cargo features
serde: derivesSerialize/DeserializeforTunnelStatsandSessionStats.cli: builds thel2tpcommand-line tool (impliesserde).
No features are enabled by default, so library users do not pull in the CLI's dependencies.
Command-Line Tool
The l2tp binary is behind the cli feature since 0.3. If you previously
installed it with cargo install l2tp, install it with:
From a checkout, use cargo run --features cli -- <args>.
Quick Start
Managed UDP tunnel + Ethernet session
use ;
use ;
async
Managed IP-backed tunnel (IPv4 or IPv6)
use ;
use Ipv4Addr;
async
Link-local IPv6 endpoints
The kernel refuses to bind or connect a link-local address without an
interface. Scope the endpoint with an interface name; it is resolved to the
interface index each time the socket binds, connects or reconnects, so the
tunnel keeps working with an interface that was re-created under a new index.
The scopes of both endpoints and the device must name the same interface.
use ;
use Ipv6Addr;
async
UDP endpoints carry the numeric scope ID of SocketAddrV6. Resolve the name
with IfName::index() right before creating or reconnecting the socket:
let scope_id = new?.index?;
let local = V6;
A link-local remote may omit the scope ID once the local endpoint, the
device, or an earlier scoped connect has pinned the socket to an interface.
Both socket types report a missing scope as Error::ScopeRequired before
the socket is created.
Unmanaged tunnels cannot use link-local endpoints, because the kernel creates
their socket without an interface scope; create_unmanaged_tunnel returns
Error::UnmanagedLinkLocal before contacting the kernel. Re-creating a tunnel
after its interface disappears is left to the application; this crate does
not watch rtnetlink link events.
Interface Names
IfName is a checked, Copy value for Linux interface names, stored
NUL-padded like the kernel's char ifname[IFNAMSIZ]. IfName::new,
str::parse and the TryFrom conversions from &str, String, &[u8],
Vec<u8>, &OsStr, &CStr and [u8; IFNAMSIZ] validate the name with the
kernel's dev_valid_name() rules: non-empty, at most IFNAMSIZ - 1 bytes,
not . or .., and no NUL, /, : or whitespace. A rejected name yields
Error::InvalidIfName with an IfNameError that names the rule.
Names are byte strings and need not be UTF-8. Read one back with as_bytes,
as_array, to_str (None for a non-UTF-8 name), to_string_lossy,
as_os_str, as_c_str, Display (lossy) or the From<IfName> conversions
into [u8; IFNAMSIZ], Vec<u8> and OsString. IfName::index and
IfName::from_index convert between names and the kernel's interface
indexes. Interface names appear in the public API only as IfName; the CLI
validates --ifname and --device while parsing and accepts non-UTF-8
arguments.
netlink-packet-l2tp 0.3 carries L2TP_ATTR_IFNAME as a String. Until a
release that carries the raw bytes is adopted, creating a session with a
non-UTF-8 interface name fails with Error::Netlink, and a session listing
that contains such a name fails to decode.
Binding Managed Sockets to an Interface
Use IfName with managed sockets:
- At creation:
TunnelSocket::udp(..., Some(&ifname))TunnelSocket::ip(..., Some(&ifname), ...)
- After creation:
tunnel.socket().unwrap().bind_to_device(&ifname)?
SO_BINDTODEVICE requires CAP_NET_RAW. A link-local scope only selects the
interface of a link-local address and needs no extra capability.
Kernel Semantics Notes
L2TP_ATTR_IFNAMEapplies to sessions (SESSION_CREATE/session lookups), not tunnels. Tunnels do not create a netdevice.- The kernel reports the IPv4 UDP checksum setting (
L2TP_ATTR_UDP_CSUM) only for IPv4 sockets.TunnelInfotherefore showsudp_csum: falsefor every IPv6 tunnel, even when its IPv4-mapped peer receives checksums. - The kernel does not report IPv6 scope IDs or flow labels, so
TunnelInfoUDP endpoints have them set to0and IP endpoints have no scope. - Managed tunnels are fd-backed. When the managed tunnel socket is closed, the kernel
tears down that tunnel and its sessions. Sockets are created with
SOCK_CLOEXECso child processes do not keep tunnels alive. - The kernel cannot change tunnel parameters after creation, so there is no tunnel
modify operation. Session parameters can be changed with
SessionHandle::modify. - The kernel's session dump ignores the tunnel filter;
list_sessionsfilters the dump on the client side.
Unmanaged Tunnels
For kernel-managed tunnels without L2TP_ATTR_FD:
let cfg = new?;
let tunnel = handle.create_unmanaged_tunnel.await?;
Common Operations
let tunnels = handle.list_tunnels.await?;
let sessions = handle.list_all_sessions.await?;
let tstats = handle.tunnel_stats.await?;
let sstats = handle.session_stats.await?;
Handle Lifetime
TunnelHandleandSessionHandledefault toauto_delete = true.- Tunnel and session creation keeps ownership through the kernel acknowledgement even if the caller is cancelled. Successful creations that the caller no longer receives are cleaned up. Keep the Tokio runtime running until this work finishes.
- On drop, they spawn best-effort async delete calls onto the current Tokio runtime. These run after the drop returns, so they can race with re-creating the same ID.
- Prefer
tunnel.delete().await?/session.delete().await?, which delete deterministically and report errors. - Use
set_auto_delete(false)if you want to avoid those best-effort netlink delete calls. - For managed tunnels, dropping the handle still closes the owned tunnel socket fd, which causes the kernel to remove the tunnel/session datapath.
Error Handling
Match kernel errors by kind without depending on libc:
match handle.create_unmanaged_tunnel.await
Error::raw_os_error() returns the raw errno when one is available.
Upgrading from 0.2
- The
l2tpbinary now requires--features cli(see above). serdederives on the statistics types now require theserdefeature.- Modules are private; import everything from the crate root (e.g.
l2tp::L2tpHandle). TunnelModify,TunnelHandle::modifyandL2tpHandle::modify_tunnelwere removed: the kernel accepted these requests but did not apply their UDP checksum setting.TunnelSocket::iptakes aTunnelIdinstead of au32.IpEndpoint::V6is a struct variant with anaddrand an optional interfacescope; useIpEndpoint::v6(addr)orIpEndpoint::v6_scoped(addr, ifname). Link-local endpoints without a scope are rejected withError::ScopeRequiredbyTunnelSocket::ipandTunnelSocket::udp, and unmanaged tunnels reject link-local endpoints withError::UnmanagedLinkLocal.L2tpHandle::newreturnsError::Ioinstead of panicking when the Tokio runtime has no I/O driver.IfNameis aCopyvalue holding the kernel's NUL-padded[u8; IFNAMSIZ]buffer, and names need not be UTF-8.as_stris replaced byto_str,to_string_lossyandas_bytes;AsRef<str>andFrom<IfName> for StringbyAsRef<[u8]>,AsRef<OsStr>,AsRef<CStr>andFrom<IfName>for[u8; IFNAMSIZ],Vec<u8>andOsString.Error::InvalidIfNamecarries anIfNameErrorreason instead of the rejected string.Erroris#[non_exhaustive].Error::Netlinkholds a message instead of anetlink-packet-coretype. Malformed kernel replies are reported asError::UnexpectedReplyinstead of a syntheticKernelError, and socket encapsulation mismatches asError::EncapsulationMismatchinstead ofError::UnmanagedSocket.TunnelSocket::set_ipv6_dontfragerrors areError::Io.PseudowireTypeandL2SpecTypeare#[non_exhaustive]and haveOther(..)variants for values this crate does not name.TunnelInfo,SessionInfo,TunnelStatsandSessionStatsare#[non_exhaustive].
Running Tests
Standard tests:
Privileged tests (require root/CAP_NET_ADMIN and matching kernel modules):
CLI tunnel lifetime
l2tp tunnel create creates an unmanaged, kernel-owned tunnel that survives
command exit. Remove it with l2tp tunnel delete. Checksum flags apply to the
kernel-created socket. The socket-only flags --device and --ipv6-dontfrag
are unsupported by persistent CLI tunnels and return an error; use the library's
managed tunnel API when those socket options are needed.
--no-udp-csum is rejected when the remote peer is IPv4-mapped on an IPv6
socket, including when an IPv4 --remote is automatically mapped to match an
IPv6 --local. Use IPv4 endpoints for both addresses, or the library's managed
tunnel API, to disable IPv4 checksums. Mapped tunnels with checksums enabled
remain supported.
Link-local addresses, with or without a %ifname zone suffix, are rejected
for the same reason: the kernel-created socket has no interface scope. Use the
library's managed tunnel API for link-local endpoints.
Automatically chosen tunnel IDs come from getrandom(2); when that fails the
command reports the error instead of falling back to a predictable source.
Pass --tunnel-id to choose the ID yourself.