mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 17:45:44 +00:00
feat(socket): add Linux SO_MARK (fwmark) support for underlay sockets (#2288)
Adds a Linux-only socket_mark u32 config flag (CLI: --socket-mark, env:
ET_SOCKET_MARK, TOML/proto: flags.socket_mark, 0 = disabled) that is
applied as SO_MARK to every outbound underlay socket EasyTier creates:
TCP, UDP, QUIC, WebSocket, WireGuard connectors and listeners, plus the
FakeTCP decoy socket. Lets the host policy-route or filter EasyTier
underlay traffic with 'ip rule fwmark ...' or iptables -m mark.
Plumbing mirrors the existing bind_device pattern:
- FlagsInConfig.socket_mark (proto) + default 0 in gen_default_flags
- bind() builder gets a socket_mark arg; setup_socket2_ext calls
apply_socket_mark which is a no-op for mark=0 and on non-Linux
- TunnelConnector trait gets set_socket_mark(u32) default-no-op method
- IP-based connectors override; create_listener_by_url and the connector
factory pass mark from global_ctx flags
- QUIC threads mark through QuicEndpointManager::{server,connect}
- WebSocket/FakeTCP/TCP default-bind bypass paths apply mark via
socket2::SockRef::from(&tokio_socket)
- ForeignNetworkEntry propagates parent socket_mark into its derived ctx
Includes a Linux smoke test plus a CAP_NET_ADMIN-gated test that does a
getsockopt(SO_MARK) round-trip to confirm the kernel applied the value.
SO_MARK requires CAP_NET_ADMIN; ignored silently on non-Linux. FakeTCP's
TUN-written segments are not covered (kernel doesn't tag raw TUN
writes); operators relying on fwmark for FakeTCP must apply an iptables
rule on the FakeTCP TUN device separately.
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -417,6 +417,7 @@ fn setup_socket2_ext(
|
||||
bind_addr: &SocketAddr,
|
||||
#[allow(unused_variables)] bind_dev: Option<String>,
|
||||
only_v6: bool,
|
||||
socket_mark: Option<u32>,
|
||||
) -> Result<(), TunnelError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -430,6 +431,12 @@ fn setup_socket2_ext(
|
||||
|
||||
socket2_socket.set_nonblocking(true)?;
|
||||
socket2_socket.set_reuse_address(!cfg!(target_os = "windows"))?;
|
||||
|
||||
// SO_MARK must be set before bind() so the kernel applies the mark to
|
||||
// any source-address selection bind() triggers on unspecified binds.
|
||||
// Accepted child sockets inherit the mark from the listener on Linux.
|
||||
apply_socket_mark(socket2_socket, socket_mark)?;
|
||||
|
||||
if let Err(e) = socket2_socket.bind(&socket2::SockAddr::from(*bind_addr)) {
|
||||
if bind_addr.is_ipv4() {
|
||||
return Err(e.into());
|
||||
@@ -476,6 +483,29 @@ fn setup_socket2_ext(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply Linux SO_MARK (a.k.a. fwmark) to a `socket2::Socket`. `None` leaves
|
||||
/// SO_MARK untouched (kernel default 0); `Some(mark)` applies that exact value
|
||||
/// — including `Some(0)`, which is a legitimate mark. On non-Linux platforms
|
||||
/// this is unconditionally a no-op.
|
||||
///
|
||||
/// Exposed so transports that bypass [`bind`] (currently the WebSocket
|
||||
/// default-bind path and FakeTCP) can apply the same mark.
|
||||
pub fn apply_socket_mark(
|
||||
socket: &socket2::Socket,
|
||||
socket_mark: Option<u32>,
|
||||
) -> Result<(), TunnelError> {
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
if let Some(mark) = socket_mark {
|
||||
tracing::trace!(socket_mark = mark, "set SO_MARK on socket");
|
||||
socket.set_mark(mark)?;
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))]
|
||||
{
|
||||
let _ = (socket, socket_mark);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum BindDev {
|
||||
#[default]
|
||||
@@ -529,6 +559,9 @@ pub fn bind<B: Bindable>(
|
||||
#[builder(default, into)] dev: BindDev,
|
||||
net_ns: Option<NetNS>,
|
||||
#[builder(default)] only_v6: bool,
|
||||
/// Linux SO_MARK (fwmark) to apply to the socket. `None` leaves SO_MARK
|
||||
/// untouched; `Some(mark)` applies that exact value, including `Some(0)`.
|
||||
socket_mark: Option<u32>,
|
||||
) -> Result<B, TunnelError> {
|
||||
let _g = net_ns.map(|n| n.guard());
|
||||
let dev = match dev {
|
||||
@@ -537,7 +570,7 @@ pub fn bind<B: Bindable>(
|
||||
BindDev::Custom(s) => Some(s),
|
||||
};
|
||||
let socket = socket2::Socket::new(socket2::Domain::for_address(addr), B::TYPE, B::PROTOCOL)?;
|
||||
setup_socket2_ext(&socket, &addr, dev, only_v6)?;
|
||||
setup_socket2_ext(&socket, &addr, dev, only_v6, socket_mark)?;
|
||||
B::finalize(socket)
|
||||
}
|
||||
|
||||
@@ -567,6 +600,59 @@ pub mod tests {
|
||||
packet_def::{PEER_MANAGER_HEADER_SIZE, TCP_TUNNEL_HEADER_SIZE},
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
#[test]
|
||||
fn apply_socket_mark_none_is_noop_and_does_not_error() {
|
||||
// The contract for `None` is "no syscall is made and no error is
|
||||
// returned" — must not require CAP_NET_ADMIN to call.
|
||||
let socket = socket2::Socket::new(
|
||||
socket2::Domain::IPV4,
|
||||
socket2::Type::DGRAM,
|
||||
Some(socket2::Protocol::UDP),
|
||||
)
|
||||
.unwrap();
|
||||
super::apply_socket_mark(&socket, None).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
#[ignore = "requires CAP_NET_ADMIN; run as root or with sudo -E cargo test"]
|
||||
fn apply_socket_mark_sets_so_mark_when_capable() {
|
||||
use nix::libc;
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
fn read_so_mark(s: &socket2::Socket) -> u32 {
|
||||
let mut value: libc::c_int = 0;
|
||||
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
let r = unsafe {
|
||||
libc::getsockopt(
|
||||
s.as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_MARK,
|
||||
&mut value as *mut _ as *mut libc::c_void,
|
||||
&mut len,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0, "getsockopt(SO_MARK) failed");
|
||||
value as u32
|
||||
}
|
||||
|
||||
let socket = socket2::Socket::new(
|
||||
socket2::Domain::IPV4,
|
||||
socket2::Type::DGRAM,
|
||||
Some(socket2::Protocol::UDP),
|
||||
)
|
||||
.unwrap();
|
||||
super::apply_socket_mark(&socket, Some(0x1234))
|
||||
.expect("set_mark failed; need CAP_NET_ADMIN");
|
||||
assert_eq!(read_so_mark(&socket), 0x1234);
|
||||
|
||||
// Some(0) is a legitimate value: it must reach setsockopt and clear
|
||||
// the mark, distinct from None which makes no syscall.
|
||||
super::apply_socket_mark(&socket, Some(0)).expect("set_mark(0) failed");
|
||||
assert_eq!(read_so_mark(&socket), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framed_reader_rejects_short_peer_manager_body() {
|
||||
let mut buf = BytesMut::new();
|
||||
|
||||
Reference in New Issue
Block a user