From c3f779f88d44e8efd9dffe224ed3ec98f55f4d51 Mon Sep 17 00:00:00 2001 From: yuming Date: Wed, 5 Aug 2026 20:12:42 +0800 Subject: [PATCH 01/10] feat(net): allow multiple TCP sockets to share a port in port manager Replace the single-owner tcp_port_table (HashMap) with multi-binding records mirroring the UDP side. A new binding is accepted when every conflicting binding shares SO_REUSEPORT or SO_REUSEADDR with the requester. Each record is identified by (iface_nic_id, smoltcp handle), so unbinding is exact without threading an extra id through the socket state. The generic bind_port/unbind_port/bind_ephemeral_port helpers are superseded by per-protocol variants. --- kernel/src/net/socket/inet/common/port.rs | 96 ++++++++++++++++++----- kernel/src/net/socket/inet/mod.rs | 2 +- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/kernel/src/net/socket/inet/common/port.rs b/kernel/src/net/socket/inet/common/port.rs index 5f8446730f..f761931eda 100644 --- a/kernel/src/net/socket/inet/common/port.rs +++ b/kernel/src/net/socket/inet/common/port.rs @@ -1,11 +1,13 @@ +use alloc::vec::Vec; use core::sync::atomic::{AtomicU16, Ordering}; use hashbrown::HashMap; +use smoltcp::wire::IpAddress; use system_error::SystemError; use crate::{ arch::rand::rand, libs::mutex::Mutex, - process::{ProcessManager, RawPid}, + process::ProcessManager, }; use super::Types::{self, *}; @@ -16,8 +18,8 @@ use super::Types::{self, *}; /// because Linux device-bound sockets can legally share a port across ifaces. #[derive(Debug)] pub struct PortManager { - // TCP 端口记录表 - tcp_port_table: Mutex>, + // TCP 端口记录表。一个端口可以有多条绑定记录(SO_REUSEPORT/SO_REUSEADDR 共享)。 + tcp_port_table: Mutex>>, } impl Default for PortManager { @@ -85,7 +87,14 @@ impl PortManager { } #[inline] - pub fn bind_ephemeral_port(&self, socket_type: Types) -> Result { + pub fn bind_tcp_ephemeral_port( + &self, + addr: IpAddress, + reuseaddr: bool, + reuseport: bool, + iface_nic_id: usize, + handle: smoltcp::iface::SocketHandle, + ) -> Result { let (min, max) = Self::local_port_range(); let range = (max - min) as u32 + 1; if range == 0 { @@ -93,8 +102,8 @@ impl PortManager { } let mut remaining = range; while remaining > 0 { - let port = self.get_ephemeral_port(socket_type)?; - match self.bind_port(socket_type, port) { + let port = self.get_ephemeral_port(Types::Tcp)?; + match self.bind_tcp_port(port, addr, reuseaddr, reuseport, iface_nic_id, handle) { Ok(()) => return Ok(port), Err(SystemError::EADDRINUSE) => { // Race: another thread grabbed the port after we checked. @@ -107,24 +116,73 @@ impl PortManager { Err(SystemError::EADDRINUSE) } - /// @brief 检测给定端口是否已被占用,如果未被占用则在 TCP 对应的表中记录 + /// TCP: 绑定端口,支持 SO_REUSEADDR/SO_REUSEPORT。 /// - pub fn bind_port(&self, socket_type: Types, port: u16) -> Result<(), SystemError> { - if port > 0 && socket_type == Tcp { - let mut guard = self.tcp_port_table.lock(); - if guard.get(&port).is_some() { + /// 一条绑定记录以 `(iface_nic_id, handle)` 唯一标识(BoundInner 身份), + /// 因此多个进程/多个 socket 可以共享同一端口而不需要调用方保存额外 id。 + pub fn bind_tcp_port( + &self, + port: u16, + addr: IpAddress, + reuseaddr: bool, + reuseport: bool, + iface_nic_id: usize, + handle: smoltcp::iface::SocketHandle, + ) -> Result<(), SystemError> { + if port == 0 { + return Err(SystemError::EINVAL); + } + let mut guard = self.tcp_port_table.lock(); + let bindings = guard.entry(port).or_default(); + for binding in bindings.iter() { + if !addrs_conflict(addr, binding.addr) { + continue; + } + let share_ok = (reuseport && binding.reuseport) || (reuseaddr && binding.reuseaddr); + if !share_ok { return Err(SystemError::EADDRINUSE); } - guard.insert(port, ProcessManager::current_pid()); } - return Ok(()); + bindings.push(TcpPortBinding { + addr, + reuseaddr, + reuseport, + iface_nic_id, + handle, + }); + Ok(()) + } + + /// TCP: 解绑端口(按 BoundInner 身份) + pub fn unbind_tcp_port(&self, port: u16, iface_nic_id: usize, handle: smoltcp::iface::SocketHandle) { + let mut guard = self.tcp_port_table.lock(); + if let Some(list) = guard.get_mut(&port) { + list.retain(|b| b.iface_nic_id != iface_nic_id || b.handle != handle); + if list.is_empty() { + guard.remove(&port); + } + } } - /// @brief 在对应的端口记录表中将端口和 socket 解绑 - /// should call this function when socket is closed or aborted - pub fn unbind_port(&self, socket_type: Types, port: u16) { - if socket_type == Tcp { - self.tcp_port_table.lock().remove(&port); - }; +} + +/// TCP 端口绑定记录。`(iface_nic_id, handle)` 是绑定的 BoundInner 身份。 +#[derive(Debug, Clone)] +struct TcpPortBinding { + addr: IpAddress, + reuseaddr: bool, + reuseport: bool, + iface_nic_id: usize, + handle: smoltcp::iface::SocketHandle, +} + +#[inline] +fn addrs_conflict(a: IpAddress, b: IpAddress) -> bool { + if a.version() != b.version() { + return false; + } + if a.is_unspecified() || b.is_unspecified() { + return true; } + a == b } diff --git a/kernel/src/net/socket/inet/mod.rs b/kernel/src/net/socket/inet/mod.rs index aa79de1876..e778006b8e 100644 --- a/kernel/src/net/socket/inet/mod.rs +++ b/kernel/src/net/socket/inet/mod.rs @@ -8,7 +8,7 @@ pub mod stream; pub mod syscall; pub use common::BoundInner; -pub use common::Types; + pub use datagram::UdpSocket; pub use raw::RawSocket; From 9e332432c273fb9ad2c9d5c1b9446005715ebefa Mon Sep 17 00:00:00 2001 From: yuming Date: Wed, 5 Aug 2026 20:13:24 +0800 Subject: [PATCH 02/10] feat(net): add SO_REUSEPORT socket option to TCP sockets Store the flag in TcpSocketOptions and wire it into setsockopt/getsockopt, mirroring the existing SO_REUSEADDR handling. The value is snapshotted at bind() time, matching Linux semantics where SO_REUSEPORT must be set before bind() to participate in port sharing. --- kernel/src/net/socket/inet/stream/option.rs | 2 ++ kernel/src/net/socket/inet/stream/stream_core.rs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/kernel/src/net/socket/inet/stream/option.rs b/kernel/src/net/socket/inet/stream/option.rs index 475106ebea..bf55fe4ffc 100644 --- a/kernel/src/net/socket/inet/stream/option.rs +++ b/kernel/src/net/socket/inet/stream/option.rs @@ -311,6 +311,7 @@ impl super::TcpSocket { Ok(()) }), PSO::REUSEADDR => Self::set_bool_option(self.so_reuseaddr(), val, |_| Ok(())), + PSO::REUSEPORT => Self::set_bool_option(self.so_reuseport(), val, |_| Ok(())), PSO::BROADCAST => Self::set_bool_option(self.so_broadcast(), val, |_| Ok(())), PSO::PASSCRED => Self::set_bool_option(self.so_passcred(), val, |_| Ok(())), PSO::NO_CHECK => Self::set_bool_option(self.so_no_check(), val, |_| Ok(())), @@ -575,6 +576,7 @@ impl super::TcpSocket { } PSO::KEEPALIVE => Self::write_bool_opt_i32(value, self.so_keepalive_enabled()), PSO::REUSEADDR => Self::write_bool_opt_i32(value, self.so_reuseaddr()), + PSO::REUSEPORT => Self::write_bool_opt_i32(value, self.so_reuseport()), PSO::BROADCAST => Self::write_bool_opt_i32(value, self.so_broadcast()), PSO::PASSCRED => Self::write_bool_opt_i32(value, self.so_passcred()), PSO::NO_CHECK => Self::write_bool_opt_i32(value, self.so_no_check()), diff --git a/kernel/src/net/socket/inet/stream/stream_core.rs b/kernel/src/net/socket/inet/stream/stream_core.rs index c0521348ed..a136ec6d52 100644 --- a/kernel/src/net/socket/inet/stream/stream_core.rs +++ b/kernel/src/net/socket/inet/stream/stream_core.rs @@ -52,6 +52,8 @@ pub struct TcpSocketOptions { pub(crate) so_filter_attached: AtomicBool, /// SO_REUSEADDR pub(crate) so_reuseaddr: AtomicBool, + /// SO_REUSEPORT + pub(crate) so_reuseport: AtomicBool, /// SO_BROADCAST pub(crate) so_broadcast: AtomicBool, /// SO_PASSCRED @@ -108,6 +110,7 @@ impl TcpSocketOptions { tcp_user_timeout: AtomicI32::new(0), so_filter_attached: AtomicBool::new(false), so_reuseaddr: AtomicBool::new(false), + so_reuseport: AtomicBool::new(false), so_broadcast: AtomicBool::new(false), so_passcred: AtomicBool::new(false), so_no_check: AtomicBool::new(false), @@ -344,6 +347,11 @@ impl TcpSocket { &self.options.so_reuseaddr } + #[inline] + pub(crate) fn so_reuseport(&self) -> &AtomicBool { + &self.options.so_reuseport + } + #[inline] pub(crate) fn so_broadcast(&self) -> &AtomicBool { &self.options.so_broadcast From 9aadd1b3e514a128b649718eba0e91c81a926728 Mon Sep 17 00:00:00 2001 From: yuming Date: Wed, 5 Aug 2026 20:14:03 +0800 Subject: [PATCH 03/10] feat(net): thread reuseport flags through TCP bind/listen Pass SO_REUSEPORT/SO_REUSEADDR from TcpSocket options into Init::bind and Init::listen, routing port registration through bind_tcp_port and bind_tcp_ephemeral_port. Every listening smoltcp socket that belongs to a reuseport listener - the primary socket, per-interface and backlog sockets, and the replacement socket created on accept - is marked with set_reuseport so the smoltcp dispatcher can distribute SYNs across the group. All unbind call sites now release by (iface_nic_id, handle). --- kernel/src/net/socket/inet/stream/inner.rs | 71 +++++++++++++------ .../src/net/socket/inet/stream/lifecycle.rs | 56 ++++++++++----- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/kernel/src/net/socket/inet/stream/inner.rs b/kernel/src/net/socket/inet/stream/inner.rs index 0a28a28674..af8f3f4257 100644 --- a/kernel/src/net/socket/inet/stream/inner.rs +++ b/kernel/src/net/socket/inet/stream/inner.rs @@ -5,7 +5,7 @@ use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crate::filesystem::epoll::EPollEventType; use crate::libs::mutex::Mutex; use crate::libs::rwsem::RwSem; -use crate::net::socket::{self, inet::Types}; +use crate::net::socket::{self}; use crate::process::namespace::net_namespace::NetNamespace; use crate::syscall::user_buffer::UserBuffer; use alloc::boxed::Box; @@ -55,11 +55,13 @@ fn new_smoltcp_socket() -> smoltcp::socket::tcp::Socket<'static> { fn new_listen_smoltcp_socket( local_endpoint: T, + reuseport: bool, ) -> Result, SystemError> where T: Into, { let mut socket = new_smoltcp_socket(); + socket.set_reuseport(reuseport); socket.listen(local_endpoint).map_err(|e| match e { tcp::ListenError::InvalidState => SystemError::EINVAL, // TODO: Check is right impl tcp::ListenError::Unaddressable => SystemError::EADDRINUSE, @@ -116,6 +118,8 @@ impl Init { self, local_endpoint: smoltcp::wire::IpEndpoint, netns: Arc, + reuseaddr: bool, + reuseport: bool, ) -> Result { match self { Init::Unbound((socket, ver)) => { @@ -132,7 +136,13 @@ impl Init { // Handle ephemeral port assignment (port 0) let bind_port = if local_endpoint.port == 0 { - match bound.port_manager().bind_ephemeral_port(Types::Tcp) { + match bound.port_manager().bind_tcp_ephemeral_port( + local_endpoint.addr, + reuseaddr, + reuseport, + bound.iface().nic_id(), + bound.handle(), + ) { Ok(port) => port, Err(err) => { let smoltcp::socket::Socket::Tcp(socket) = bound.into_socket() else { @@ -142,10 +152,14 @@ impl Init { } } } else { - if let Err(err) = bound - .port_manager() - .bind_port(Types::Tcp, local_endpoint.port) - { + if let Err(err) = bound.port_manager().bind_tcp_port( + local_endpoint.port, + local_endpoint.addr, + reuseaddr, + reuseport, + bound.iface().nic_id(), + bound.handle(), + ) { let smoltcp::socket::Socket::Tcp(socket) = bound.into_socket() else { unreachable!("TCP BoundInner should contain a TCP socket"); }; @@ -182,7 +196,13 @@ impl Init { return Err((Self::Unbound((Box::new(socket), ver)), err)) } }; - let bound_port = match bound.port_manager().bind_ephemeral_port(Types::Tcp) { + let bound_port = match bound.port_manager().bind_tcp_ephemeral_port( + address, + false, + false, + bound.iface().nic_id(), + bound.handle(), + ) { Ok(port) => port, Err(err) => { let smoltcp::socket::Socket::Tcp(socket) = bound.into_socket() else { @@ -243,6 +263,8 @@ impl Init { self, backlog: usize, netns: Arc, + reuseaddr: bool, + reuseport: bool, ) -> Result { // If unbound, auto-bind to INADDR_ANY:ephemeral (Linux compat). let bound_self = if matches!(self, Init::Unbound(_)) { @@ -259,7 +281,7 @@ impl Init { } }; let auto_bind_ep = smoltcp::wire::IpEndpoint::new(unspec_addr, 0); - match self.bind(auto_bind_ep, netns.clone()) { + match self.bind(auto_bind_ep, netns.clone(), reuseaddr, reuseport) { Ok(bound) => bound, Err((init, err)) => return Err((init, err)), } @@ -311,7 +333,7 @@ impl Init { continue; // primary inner already covers this iface } let new_listen = socket::inet::BoundInner::bind_on_iface( - new_listen_smoltcp_socket(listen_addr)?, + new_listen_smoltcp_socket(listen_addr, reuseport)?, iface.clone(), inner.netns(), )?; @@ -321,7 +343,7 @@ impl Init { let remaining = backlog.saturating_sub(1 + inners.len()); for _ in 0..remaining { let new_listen = socket::inet::BoundInner::bind_on_iface( - new_listen_smoltcp_socket(listen_addr)?, + new_listen_smoltcp_socket(listen_addr, reuseport)?, inner.iface().clone(), inner.netns(), )?; @@ -332,7 +354,7 @@ impl Init { let additional_sockets = backlog.saturating_sub(1); for _ in 0..additional_sockets { let new_listen = socket::inet::BoundInner::bind( - new_listen_smoltcp_socket(listen_addr)?, + new_listen_smoltcp_socket(listen_addr, reuseport)?, listen_addr .addr .as_ref() @@ -350,6 +372,7 @@ impl Init { } if let Err(err) = inner.with_mut::(|socket| { + socket.set_reuseport(reuseport); socket.listen(listen_addr).map_err(|err| match err { tcp::ListenError::InvalidState => SystemError::EINVAL, tcp::ListenError::Unaddressable => SystemError::EINVAL, @@ -363,6 +386,7 @@ impl Init { inners, connect: AtomicUsize::new(0), listen_addr, + reuseport, }); } @@ -370,7 +394,9 @@ impl Init { match self { Init::Unbound((_, version)) => Closed::new(version), Init::Bound((inner, endpoint)) => { - inner.port_manager().unbind_port(Types::Tcp, endpoint.port); + inner + .port_manager() + .unbind_tcp_port(endpoint.port, inner.iface().nic_id(), inner.handle()); let version = match endpoint.addr { smoltcp::wire::IpAddress::Ipv4(_) => smoltcp::wire::IpVersion::Ipv4, smoltcp::wire::IpAddress::Ipv6(_) => smoltcp::wire::IpVersion::Ipv6, @@ -466,9 +492,11 @@ impl Connecting { | ConnectResult::ShutdownReset | ConnectResult::ShutdownResetConsumed => { // unbind port - self.inner - .port_manager() - .unbind_port(Types::Tcp, self.local.port); + self.inner.port_manager().unbind_tcp_port( + self.local.port, + self.inner.iface().nic_id(), + self.inner.handle(), + ); let socket = self.inner.into_socket(); let socket = match socket { smoltcp::socket::Socket::Tcp(s) => s, @@ -734,6 +762,7 @@ pub struct Listening { pub inners: Vec, connect: AtomicUsize, listen_addr: smoltcp::wire::IpListenEndpoint, + reuseport: bool, } impl Listening { @@ -760,13 +789,13 @@ impl Listening { // where each interface has its own listen socket in the smoltcp SocketSet. let mut new_listen = if self.listen_addr.addr.is_none() { socket::inet::BoundInner::bind_on_iface( - new_listen_smoltcp_socket(self.listen_addr)?, + new_listen_smoltcp_socket(self.listen_addr, self.reuseport)?, connected.iface().clone(), connected.netns(), )? } else { socket::inet::BoundInner::bind( - new_listen_smoltcp_socket(self.listen_addr)?, + new_listen_smoltcp_socket(self.listen_addr, self.reuseport)?, self.listen_addr .addr .as_ref() @@ -826,12 +855,14 @@ impl Listening { // (pushed last during listen() construction). We must unbind from its // port_manager, not inners[0] which may belong to a different iface for // INADDR_ANY listeners. - self.inners + let owner = self + .inners .last() - .expect("Listening socket must have at least one inner") + .expect("Listening socket must have at least one inner"); + owner .iface() .port_manager() - .unbind_port(Types::Tcp, port); + .unbind_tcp_port(port, owner.iface().nic_id(), owner.handle()); } pub fn release(&mut self) { diff --git a/kernel/src/net/socket/inet/stream/lifecycle.rs b/kernel/src/net/socket/inet/stream/lifecycle.rs index 1cfdcd1b24..cbc7f3c2bb 100644 --- a/kernel/src/net/socket/inet/stream/lifecycle.rs +++ b/kernel/src/net/socket/inet/stream/lifecycle.rs @@ -1,6 +1,5 @@ use crate::net::socket::common::ShutdownBit; use crate::net::socket::inet::InetSocket; -use crate::net::socket::inet::Types; use crate::net::tcp_close_defer::{ DeferredTcpCloseKind, DeferredTcpCloseReason, DeferredTcpCloseRequest, }; @@ -143,20 +142,28 @@ impl TcpSocket { pub fn do_bind(&self, local_endpoint: smoltcp::wire::IpEndpoint) -> Result<(), SystemError> { let mut writer = self.inner.write(); match writer.take().expect("Tcp inner::Inner is None") { - inner::Inner::Init(inner) => match inner.bind(local_endpoint, self.netns()) { - Ok(bound) => { - if let inner::Init::Bound((ref bound, _)) = bound { - bound - .iface() - .common() - .bind_socket(self.self_ref.upgrade().unwrap()); + inner::Inner::Init(inner) => { + let reuseaddr = self + .so_reuseaddr() + .load(core::sync::atomic::Ordering::Relaxed); + let reuseport = self + .so_reuseport() + .load(core::sync::atomic::Ordering::Relaxed); + match inner.bind(local_endpoint, self.netns(), reuseaddr, reuseport) { + Ok(bound) => { + if let inner::Init::Bound((ref bound, _)) = bound { + bound + .iface() + .common() + .bind_socket(self.self_ref.upgrade().unwrap()); + } + writer.replace(inner::Inner::Init(bound)); + Ok(()) + } + Err((inner, err)) => { + writer.replace(inner::Inner::Init(inner)); + Err(err) } - writer.replace(inner::Inner::Init(bound)); - Ok(()) - } - Err((inner, err)) => { - writer.replace(inner::Inner::Init(inner)); - Err(err) } }, any => { @@ -172,7 +179,13 @@ impl TcpSocket { let inner = writer.take().expect("Tcp inner::Inner is None"); let (listening, err) = match inner { inner::Inner::Init(init) => { - let listen_result = init.listen(backlog, self.netns()); + let reuseaddr = self + .so_reuseaddr() + .load(core::sync::atomic::Ordering::Relaxed); + let reuseport = self + .so_reuseport() + .load(core::sync::atomic::Ordering::Relaxed); + let listen_result = init.listen(backlog, self.netns(), reuseaddr, reuseport); match listen_result { Ok(listening) => { // DragonOS backlog emulation: listener is represented by multiple @@ -674,7 +687,9 @@ impl TcpSocket { conn.with_mut(|socket| socket.abort()); let initial_state = conn.with(|socket| socket.state()); if conn.owns_port() { - iface.port_manager().unbind_port(Types::Tcp, local_port); + iface + .port_manager() + .unbind_tcp_port(local_port, iface.nic_id(), handle); } iface.common().defer_tcp_close(DeferredTcpCloseRequest { handle, @@ -704,7 +719,9 @@ impl TcpSocket { es.with_mut(|socket| Self::apply_close_action(socket, close_action)); let initial_state = es.with(|socket| socket.state()); if es.owns_port() { - iface.port_manager().unbind_port(Types::Tcp, local_port); + iface + .port_manager() + .unbind_tcp_port(local_port, iface.nic_id(), es.handle()); } iface.common().defer_tcp_close(DeferredTcpCloseRequest { handle, @@ -728,8 +745,11 @@ impl TcpSocket { }; let port = sc.get_name().port; let iface = sc.iface().clone(); + let handle = sc.handle(); sc.release(); - iface.port_manager().unbind_port(Types::Tcp, port); + iface + .port_manager() + .unbind_tcp_port(port, iface.nic_id(), handle); writer.replace(inner::Inner::Closed(inner::Closed::new(ver))); } inner::Inner::Listening(mut ls) => { From a237c10ed86fd44aaa75a45196702bb4ed156521 Mon Sep 17 00:00:00 2001 From: yuming Date: Thu, 3 Sep 2026 17:08:02 +0800 Subject: [PATCH 04/10] build(kernel): add smoltcp as a submodule --- .github/workflows/dunitest.yml | 2 ++ .github/workflows/makefile.yml | 11 ++++++----- .github/workflows/nightly-build.yml | 2 ++ .github/workflows/test-x86.yml | 2 ++ .gitmodules | 3 +++ kernel/Cargo.lock | 1 - kernel/Cargo.toml | 3 ++- kernel/submodules/smoltcp | 1 + 8 files changed, 18 insertions(+), 7 deletions(-) create mode 160000 kernel/submodules/smoltcp diff --git a/.github/workflows/dunitest.yml b/.github/workflows/dunitest.yml index a5f69f23e3..5f066a5004 100644 --- a/.github/workflows/dunitest.yml +++ b/.github/workflows/dunitest.yml @@ -27,6 +27,8 @@ jobs: steps: - name: Checkout DragonOS code uses: actions/checkout@v4 + with: + submodules: recursive - name: Change source run: | diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index 9fc79b26b9..6b6d4f11dc 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -37,6 +37,8 @@ jobs: steps: - run: echo "Running in dragonos/dragonos-dev:v1.23" - uses: actions/checkout@v3 + with: + submodules: recursive - name: Change source run: | @@ -68,6 +70,8 @@ jobs: - run: echo "Running in dragonos/dragonos-dev:v1.23" - uses: actions/checkout@v3 + with: + submodules: recursive - name: Change source run: | @@ -90,20 +94,17 @@ jobs: include: - arch: x86_64 make_target: all - checkout_params: {} - arch: riscv64 make_target: all - checkout_params: - submodules: "recursive" - arch: loongarch64 make_target: all - checkout_params: {} steps: - run: echo "Running in dragonos/dragonos-dev:v1.23" - uses: actions/checkout@v3 - with: ${{ matrix.checkout_params }} + with: + submodules: recursive - name: Change source run: | diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 29c320d70b..c0bdeb7c45 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Checkout DragonOS code uses: actions/checkout@v4 + with: + submodules: recursive - name: Change source run: | diff --git a/.github/workflows/test-x86.yml b/.github/workflows/test-x86.yml index 77bbec21b5..92f71c03e7 100644 --- a/.github/workflows/test-x86.yml +++ b/.github/workflows/test-x86.yml @@ -28,6 +28,8 @@ jobs: steps: - name: Checkout DragonOS code uses: actions/checkout@v4 + with: + submodules: recursive - name: Change source run: | diff --git a/.gitmodules b/.gitmodules index 9e0b3597a4..4fc9b62de5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "kernel/submodules/DragonStub"] path = kernel/submodules/DragonStub url = https://github.com/DragonOS-Community/DragonStub.git +[submodule "kernel/submodules/smoltcp"] + path = kernel/submodules/smoltcp + url = https://github.com/DragonOS-Community/smoltcp.git diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index 6a2891a5fb..b243cc5279 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -1561,7 +1561,6 @@ checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "smoltcp" version = "0.12.0" -source = "git+https://github.com/DragonOS-Community/smoltcp?rev=ebeaec6#ebeaec612ccdd36239588e4d8987fc14d623a52b" dependencies = [ "bitflags 1.3.2", "byteorder", diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 989734c47e..e417469475 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["staticlib"] [workspace] members = ["crates/*"] +exclude = ["submodules/smoltcp"] [features] default = ["fatfs", "kvm", "fatfs-secure", "static_keys_test"] @@ -62,7 +63,7 @@ linkme = "=0.3.27" num = { version = "=0.4.0", default-features = false } num-derive = "=0.3" num-traits = { git = "https://git.mirrors.dragonos.org.cn/DragonOS-Community/num-traits.git", rev = "1597c1c", default-features = false } -smoltcp = { version = "=0.12.0", git = "https://github.com/DragonOS-Community/smoltcp", rev = "ebeaec6", default-features = false, features = [ +smoltcp = { version = "=0.12.0", path = "submodules/smoltcp", default-features = false, features = [ "alloc", "medium-ethernet", "socket-raw", diff --git a/kernel/submodules/smoltcp b/kernel/submodules/smoltcp new file mode 160000 index 0000000000..3933571d21 --- /dev/null +++ b/kernel/submodules/smoltcp @@ -0,0 +1 @@ +Subproject commit 3933571d212b5c10cca7222c2e9a20fb2050870b From 7edf3e485e9a881d9c73130bcdd9054689de154e Mon Sep 17 00:00:00 2001 From: yuming Date: Thu, 3 Sep 2026 17:08:15 +0800 Subject: [PATCH 05/10] docs(build): document submodule initialization --- docs/introduction/develop_nix.md | 2 ++ docs/zh/introduction/develop_nix.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/introduction/develop_nix.md b/docs/introduction/develop_nix.md index ae9af294bb..3ef62a18fb 100644 --- a/docs/introduction/develop_nix.md +++ b/docs/introduction/develop_nix.md @@ -36,6 +36,8 @@ DragonOS now has repository mirrors on multiple hosting platforms: ```shell git clone https://atomgit.com/DragonOS-Community/DragonOS.git cd DragonOS +# 初始化内核依赖的子模块 +make update-submodules-by-mirror ``` ## Activating the Kernel Compilation Environment diff --git a/docs/zh/introduction/develop_nix.md b/docs/zh/introduction/develop_nix.md index e8e14c67fb..85ca132b6b 100644 --- a/docs/zh/introduction/develop_nix.md +++ b/docs/zh/introduction/develop_nix.md @@ -36,6 +36,8 @@ DragonOS 现在在多个托管平台上都有仓库镜像 ```shell git clone https://atomgit.com/DragonOS-Community/DragonOS.git cd DragonOS +# 初始化内核依赖的子模块 +make update-submodules-by-mirror ``` ## 激活内核编译环境 From 8d32371a9ca25acf332906af4189895fa70eee75 Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 12 Sep 2026 18:03:44 +0800 Subject: [PATCH 06/10] feat(net): implement namespace-wide TCP reuseport groups Track stable TCP binding identities, owner credentials and live socket options in the network namespace. Refresh logical listener selection before ingress and preserve binding ownership across connect, accept and shutdown transitions. Replace port-wide backlog bookkeeping with per-listener slots and atomic accept handoff. Pin smoltcp with the matching logical listener dispatch API. --- kernel/src/driver/net/iface.rs | 12 - kernel/src/driver/net/iface_common.rs | 42 +- kernel/src/driver/net/local_output.rs | 8 - kernel/src/driver/net/loopback.rs | 39 +- kernel/src/net/mod.rs | 1 - kernel/src/net/socket/inet/common/mod.rs | 38 ++ kernel/src/net/socket/inet/common/port.rs | 557 ++++++++++++++++-- kernel/src/net/socket/inet/stream/inner.rs | 507 +++++++++++----- .../src/net/socket/inet/stream/lifecycle.rs | 118 ++-- kernel/src/net/socket/inet/stream/option.rs | 21 +- .../src/net/socket/inet/stream/stream_core.rs | 3 + kernel/src/net/tcp_listener_backlog.rs | 227 ------- kernel/src/process/namespace/net_namespace.rs | 8 + kernel/submodules/smoltcp | 2 +- 14 files changed, 1021 insertions(+), 562 deletions(-) delete mode 100644 kernel/src/net/tcp_listener_backlog.rs diff --git a/kernel/src/driver/net/iface.rs b/kernel/src/driver/net/iface.rs index bf2f5d6dcc..e13263b147 100644 --- a/kernel/src/driver/net/iface.rs +++ b/kernel/src/driver/net/iface.rs @@ -381,18 +381,6 @@ pub trait Iface: crate::driver::base::device::Device { Ok(()) } - /// # `should_drop_rx_packet` - /// 驱动收包入口可选调用:询问“上层(协议栈/Socket 语义)”是否需要丢弃该包。 - /// - /// 说明: - /// - 默认不丢弃; - /// - 驱动层不应理解 TCP/UDP 等协议语义,这个 hook 用于实现 Linux 兼容语义(如 backlog 满丢 SYN) - /// 且不修改 smoltcp。 - #[inline] - fn should_drop_rx_packet(&self, _packet: &[u8]) -> bool { - false - } - /// @brief 获取smoltcp的网卡接口类型 #[inline(always)] fn smol_iface(&self) -> &Mutex { diff --git a/kernel/src/driver/net/iface_common.rs b/kernel/src/driver/net/iface_common.rs index f1517586a5..db4f59c04a 100644 --- a/kernel/src/driver/net/iface_common.rs +++ b/kernel/src/driver/net/iface_common.rs @@ -52,8 +52,6 @@ pub struct IfaceCommon { pub(super) bootstrap_routes: Mutex>, /// TCP close(2) 语义辅助:延迟回收 smoltcp TCP socket(Linux-like)。 pub(super) tcp_close_defer: crate::net::tcp_close_defer::TcpCloseDefer, - /// TCP listener/backlog 语义辅助(Linux-like 丢 SYN 等)。 - pub(super) tcp_listener_backlog: crate::net::tcp_listener_backlog::TcpListenerBacklog, pub(super) ipv4_multicast_refcnt: Mutex>, /// Serializes configured receive-mode flags with AF_PACKET references. pub(super) receive_mode: Mutex, @@ -125,7 +123,6 @@ impl IfaceCommon { address_metadata: Mutex::new(address_metadata), bootstrap_routes: Mutex::new(Vec::new()), tcp_close_defer: crate::net::tcp_close_defer::TcpCloseDefer::new(), - tcp_listener_backlog: crate::net::tcp_listener_backlog::TcpListenerBacklog::new(), ipv4_multicast_refcnt: Mutex::new(Vec::new()), receive_mode: Mutex::new(ReceiveModeState { configured_flags: flags.bits(), @@ -135,17 +132,6 @@ impl IfaceCommon { } } - /// Register an active TCP listener port on this iface. - pub fn register_tcp_listen_port(&self, port: u16, backlog: usize) { - self.tcp_listener_backlog - .register_tcp_listen_port(port, backlog); - } - - /// Unregister an active TCP listener port on this iface. - pub fn unregister_tcp_listen_port(&self, port: u16) { - self.tcp_listener_backlog.unregister_tcp_listen_port(port); - } - pub fn ipv4_multicast_join_ref( &self, group: smoltcp::wire::Ipv4Address, @@ -178,13 +164,6 @@ impl IfaceCommon { .leave_multicast_group(smoltcp::wire::IpAddress::Ipv4(group)); } - /// 驱动收包入口使用的通用丢包策略(避免驱动理解 L4 语义)。 - #[inline] - pub fn should_drop_rx_packet(&self, packet: &[u8]) -> bool { - self.tcp_listener_backlog - .should_drop_backlog_full_tcp_syn_ip(packet) - } - pub(super) fn enqueue_local_input(&self, packet: LocalInputPacket) -> Result<(), SystemError> { self.local_input_queue.enqueue(packet) } @@ -577,9 +556,14 @@ impl IfaceCommon { authoritative_ipv4_output, }); - // 刷新 listener 缓存:必须在持有 sockets 锁的前提下进行,且不得额外分配。 - self.tcp_listener_backlog - .refresh_listen_socket_present(&sockets); + // Refresh logical listener eligibility under SocketSet serialization + // before either namespace-local or device ingress. Port-table methods + // never acquire SocketSet, preserving the one-way lock order. + if let Some(netns) = netns.as_ref() { + netns + .tcp_port_manager() + .refresh_tcp_listener_selection(&mut sockets); + } let (has_events, poll_again, deadline_rearm) = { let local_result = if routed_this_round @@ -765,9 +749,13 @@ impl IfaceCommon { authoritative_ipv4_output, }); - // 刷新 listener 缓存:必须在持有 sockets 锁的前提下进行,且不得额外分配。 - self.tcp_listener_backlog - .refresh_listen_socket_present(&sockets); + // Apply the same selection to both local handoff and device ingress + // for the entire serialized NAPI batch. + if let Some(netns) = netns.as_ref() { + netns + .tcp_port_manager() + .refresh_tcp_listener_selection(&mut sockets); + } let mut processed = 0usize; let mut had_packet = false; diff --git a/kernel/src/driver/net/local_output.rs b/kernel/src/driver/net/local_output.rs index 34a4d175c2..cc6d73a66b 100644 --- a/kernel/src/driver/net/local_output.rs +++ b/kernel/src/driver/net/local_output.rs @@ -506,14 +506,6 @@ impl SmolDevice for LocalInputDevice<'_, D> { // input remains queued for a later poll. let tx_token = self.tx_token()?; let packet = self.common.local_input_queue.pop()?; - // Namespace-local delivery is an ingress path in its own right. - // Apply the same pre-stack policy as a driver receive queue so a - // routed local packet cannot bypass listener/backlog semantics. Stop - // this ingress round after one policy drop to keep NAPI work bounded; - // the non-empty local queue schedules the next round. - if self.common.should_drop_rx_packet(&packet.ip_packet) { - return None; - } let ingress_ifindex = packet.ingress_ifindex; let frame = packet.into_frame(self.device.capabilities().medium).ok()?; let mut meta = PacketMeta::default(); diff --git a/kernel/src/driver/net/loopback.rs b/kernel/src/driver/net/loopback.rs index d46270c09f..f65bf55e10 100644 --- a/kernel/src/driver/net/loopback.rs +++ b/kernel/src/driver/net/loopback.rs @@ -223,29 +223,20 @@ impl phy::Device for LoopbackDriver { &mut self, _timestamp: smoltcp::time::Instant, ) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { - loop { - let buffer = self.inner.lock().loopback_receive(); - // receive 队列为空,返回 None 以通知上层没有可以 receive 的包 - if buffer.is_empty() { - return None; - } - - if let Some(iface) = self.iface() { - if iface.should_drop_rx_packet(&buffer) { - // Drop this packet and try the next one in the queue. - continue; - } - } - - let rx = LoopbackRxToken { - buffer, - driver: self.clone(), - }; - let tx = LoopbackTxToken { - driver: self.clone(), - }; - return Some((rx, tx)); + let buffer = self.inner.lock().loopback_receive(); + // receive 队列为空,返回 None 以通知上层没有可以 receive 的包 + if buffer.is_empty() { + return None; } + + let rx = LoopbackRxToken { + buffer, + driver: self.clone(), + }; + let tx = LoopbackTxToken { + driver: self.clone(), + }; + return Some((rx, tx)); } /// ## Loopback驱动处理发送数据包事件 /// Loopback驱动在需要发送数据时会调用这个函数来获取一个发送令牌。 @@ -615,10 +606,6 @@ impl Iface for LoopbackInterface { .map_err(Into::into) } - fn should_drop_rx_packet(&self, packet: &[u8]) -> bool { - self.common.should_drop_rx_packet(packet) - } - fn addr_assign_type(&self) -> u8 { return self.inner().netdevice_common.addr_assign_type; } diff --git a/kernel/src/net/mod.rs b/kernel/src/net/mod.rs index 3b00a8cfcb..01d7e70956 100644 --- a/kernel/src/net/mod.rs +++ b/kernel/src/net/mod.rs @@ -16,7 +16,6 @@ pub(crate) mod rtnl; pub mod socket; pub mod syscall; pub mod tcp_close_defer; -pub mod tcp_listener_backlog; /// Linux reserves interface index 1 for the loopback device in every netns. pub const LOOPBACK_IFINDEX: usize = 1; diff --git a/kernel/src/net/socket/inet/common/mod.rs b/kernel/src/net/socket/inet/common/mod.rs index c1af52c3f5..6ee8414bc7 100644 --- a/kernel/src/net/socket/inet/common/mod.rs +++ b/kernel/src/net/socket/inet/common/mod.rs @@ -153,6 +153,44 @@ impl BoundInner { }) } + /// Atomically hand off a completed TCP listener slot and install its + /// replacement on the same interface. The replacement is prepared outside + /// SocketSet, so a failed state check cannot expose or leak a new listener. + pub(super) fn accept_tcp( + &mut self, + mut replacement: smoltcp::socket::tcp::Socket<'static>, + ) -> Result<(Self, smoltcp::wire::IpEndpoint, smoltcp::wire::IpEndpoint), SystemError> { + use smoltcp::socket::tcp::{Socket, State}; + + let iface = self.iface.clone(); + let mut sockets = iface.sockets().lock(); + let socket = sockets.get_mut::(self.handle); + if !matches!(socket.state(), State::Established | State::CloseWait) { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + let (Some(local), Some(remote)) = (socket.local_endpoint(), socket.remote_endpoint()) + else { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + }; + replacement.set_listener_enabled(socket.listener_enabled()); + socket.set_listener_id(None); + + // No ingress can observe the handoff between these operations. Keep the + // accepted transport's handle and move this listener to the new slot. + let accepted_handle = self.handle; + self.handle = sockets.add(replacement); + drop(sockets); + Ok(( + Self { + handle: accepted_handle, + iface, + netns: self.netns.clone(), + }, + local, + remote, + )) + } + pub fn bind_ephemeral( socket: T, // socket_type: Types, diff --git a/kernel/src/net/socket/inet/common/port.rs b/kernel/src/net/socket/inet/common/port.rs index f761931eda..6f92911c22 100644 --- a/kernel/src/net/socket/inet/common/port.rs +++ b/kernel/src/net/socket/inet/common/port.rs @@ -1,25 +1,19 @@ use alloc::vec::Vec; -use core::sync::atomic::{AtomicU16, Ordering}; +use core::sync::atomic::{AtomicU16, AtomicU64, Ordering}; use hashbrown::HashMap; use smoltcp::wire::IpAddress; use system_error::SystemError; -use crate::{ - arch::rand::rand, - libs::mutex::Mutex, - process::ProcessManager, -}; +use crate::{arch::rand::rand, libs::mutex::Mutex, process::ProcessManager}; use super::Types::{self, *}; -/// Per-interface TCP port manager. -/// -/// UDP reservations are network-namespace-wide and live in `UdpBindingTable`, -/// because Linux device-bound sockets can legally share a port across ifaces. +/// Network-namespace-wide TCP port manager. +/// UDP reservations are managed separately by `UdpBindingTable`. #[derive(Debug)] pub struct PortManager { - // TCP 端口记录表。一个端口可以有多条绑定记录(SO_REUSEPORT/SO_REUSEADDR 共享)。 - tcp_port_table: Mutex>>, + // TCP port table. One port may have multiple bindings shared by SO_REUSEPORT/SO_REUSEADDR. + tcp_port_table: Mutex>, } impl Default for PortManager { @@ -41,7 +35,7 @@ impl PortManager { ProcessManager::current_netns().set_local_port_range(min, max) } - /// @brief 自动分配一个相对应协议中未被使用的PORT,如果动态端口均已被占用,返回错误码 EADDRINUSE + /// @brief Automatically allocate an unused port for the requested protocol. Returns EADDRINUSE if all ephemeral ports are occupied. pub fn get_ephemeral_port(&self, socket_type: Types) -> Result { // TODO: selects non-conflict high port static EPHEMERAL_PORT: AtomicU16 = AtomicU16::new(0); @@ -70,7 +64,7 @@ impl PortManager { old + 1 }; - // 使用 ListenTable 检查端口是否被占用 + // Check whether the port is already occupied through the port table match socket_type { Tcp => { let guard = self.tcp_port_table.lock(); @@ -92,8 +86,8 @@ impl PortManager { addr: IpAddress, reuseaddr: bool, reuseport: bool, - iface_nic_id: usize, - handle: smoltcp::iface::SocketHandle, + uid: u32, + id: TcpBindId, ) -> Result { let (min, max) = Self::local_port_range(); let range = (max - min) as u32 + 1; @@ -103,7 +97,7 @@ impl PortManager { let mut remaining = range; while remaining > 0 { let port = self.get_ephemeral_port(Types::Tcp)?; - match self.bind_tcp_port(port, addr, reuseaddr, reuseport, iface_nic_id, handle) { + match self.bind_tcp_port(port, addr, reuseaddr, reuseport, uid, id) { Ok(()) => return Ok(port), Err(SystemError::EADDRINUSE) => { // Race: another thread grabbed the port after we checked. @@ -116,64 +110,303 @@ impl PortManager { Err(SystemError::EADDRINUSE) } - /// TCP: 绑定端口,支持 SO_REUSEADDR/SO_REUSEPORT。 - /// - /// 一条绑定记录以 `(iface_nic_id, handle)` 唯一标识(BoundInner 身份), - /// 因此多个进程/多个 socket 可以共享同一端口而不需要调用方保存额外 id。 + /// Bind a TCP endpoint in the network namespace using a stable socket identity. pub fn bind_tcp_port( &self, port: u16, addr: IpAddress, reuseaddr: bool, reuseport: bool, - iface_nic_id: usize, - handle: smoltcp::iface::SocketHandle, + uid: u32, + id: TcpBindId, ) -> Result<(), SystemError> { if port == 0 { return Err(SystemError::EINVAL); } + let member = TcpPortBinding { + addr, + reuseaddr, + reuseport, + uid, + id, + listening: false, + group: None, + listen_order: 0, + hash_tail: false, + }; let mut guard = self.tcp_port_table.lock(); - let bindings = guard.entry(port).or_default(); - for binding in bindings.iter() { - if !addrs_conflict(addr, binding.addr) { - continue; + if let Some(bucket) = guard.get(&port) { + if bucket.members.iter().any(|b| b.id == id) { + return Err(SystemError::EINVAL); } - let share_ok = (reuseport && binding.reuseport) || (reuseaddr && binding.reuseaddr); - if !share_ok { - return Err(SystemError::EADDRINUSE); + bucket.check_conflict(&member)?; + } + // Do not leave empty buckets behind on failed admission. + let bucket = guard.entry(port).or_default(); + bucket.update_fastreuse(&member); + bucket.members.push(member); + Ok(()) + } + + /// Reserve admission before publishing LISTEN slots. On construction failure, + /// the caller must stop the reservation before returning to Bound. + pub fn listen_tcp_port(&self, port: u16, id: TcpBindId) -> Result<(), SystemError> { + let mut guard = self.tcp_port_table.lock(); + let bucket = guard.get_mut(&port).ok_or(SystemError::EINVAL)?; + let index = bucket + .members + .iter() + .position(|b| b.id == id) + .ok_or(SystemError::EINVAL)?; + let mut candidate = bucket.members[index].clone(); + candidate.listening = true; + bucket.check_conflict(&candidate)?; + bucket.update_fastreuse(&candidate); + if !bucket.members[index].listening { + static LISTEN_ORDER: AtomicU64 = AtomicU64::new(1); + candidate.listen_order = LISTEN_ORDER.fetch_add(1, Ordering::Relaxed); + candidate.hash_tail = + candidate.addr.version() == smoltcp::wire::IpVersion::Ipv6 && candidate.reuseport; + if candidate.reuseport { + // Linux chooses an existing group through a currently reuseport- + // enabled listener; membership itself survives option changes. + if let Some(peer) = bucket + .members + .iter_mut() + .filter(|m| { + m.id != id + && m.listening + && m.reuseport + && m.uid == candidate.uid + && m.addr == candidate.addr + }) + .max_by_key(|m| m.lookup_order()) + { + let group = *peer.group.get_or_insert(peer.id); + candidate.group = Some(group); + } else { + candidate.group = Some(id); + } } } - bindings.push(TcpPortBinding { - addr, - reuseaddr, - reuseport, - iface_nic_id, - handle, - }); + bucket.members[index] = candidate; Ok(()) } - /// TCP: 解绑端口(按 BoundInner 身份) - pub fn unbind_tcp_port(&self, port: u16, iface_nic_id: usize, handle: smoltcp::iface::SocketHandle) { + pub fn stop_tcp_listen(&self, port: u16, id: TcpBindId) { let mut guard = self.tcp_port_table.lock(); - if let Some(list) = guard.get_mut(&port) { - list.retain(|b| b.iface_nic_id != iface_nic_id || b.handle != handle); - if list.is_empty() { + if let Some(bucket) = guard.get_mut(&port) { + if let Some(member) = bucket.members.iter_mut().find(|b| b.id == id) { + member.listening = false; + member.group = None; + member.listen_order = 0; + } + } + } + + /// Linux updates sk_reuse/sk_reuseport without rewriting the bind bucket's + /// fastreuse cache. Admission refreshes that cache at bind/listen time. + pub fn update_tcp_options( + &self, + port: u16, + id: TcpBindId, + reuseaddr: Option, + reuseport: Option, + ) -> Result<(), SystemError> { + let mut guard = self.tcp_port_table.lock(); + let member = guard + .get_mut(&port) + .and_then(|b| b.members.iter_mut().find(|m| m.id == id)) + .ok_or(SystemError::EINVAL)?; + if let Some(value) = reuseaddr { + member.reuseaddr = value; + } + if let Some(value) = reuseport { + member.reuseport = value; + } + Ok(()) + } + + /// Called with the interface's SocketSet locked, immediately before ingress. + /// No port-manager operation acquires SocketSet, so this lock order is one-way. + /// The protocol stack sees only opaque IDs and admission, never Linux groups. + pub fn refresh_tcp_listener_selection(&self, sockets: &mut smoltcp::iface::SocketSet<'_>) { + let guard = self.tcp_port_table.lock(); + for item in sockets.items_mut() { + if let smoltcp::socket::Socket::Tcp(socket) = &mut item.socket { + if let Some(id) = socket.listener_id() { + let enabled = guard + .get(&socket.listen_endpoint().port) + .map_or(false, |bucket| bucket.listener_is_selected(id)); + socket.set_listener_enabled(enabled); + } + } + } + } + + /// Remove exactly this binding, independent of accept's handle replacements. + pub fn unbind_tcp_port(&self, port: u16, id: TcpBindId) { + let mut guard = self.tcp_port_table.lock(); + if let Some(bucket) = guard.get_mut(&port) { + bucket.members.retain(|b| b.id != id); + if bucket.members.is_empty() { guard.remove(&port); } } } +} + +/// A socket binding's identity does not change when a backlog slot is accepted. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TcpBindId(u64); + +impl TcpBindId { + pub fn new() -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(1); + Self( + NEXT_ID + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1)) + .expect("TCP binding identity space exhausted"), + ) + } + pub fn get(self) -> u64 { + self.0 + } } -/// TCP 端口绑定记录。`(iface_nic_id, handle)` 是绑定的 BoundInner 身份。 #[derive(Debug, Clone)] struct TcpPortBinding { addr: IpAddress, reuseaddr: bool, reuseport: bool, - iface_nic_id: usize, - handle: smoltcp::iface::SocketHandle, + uid: u32, + id: TcpBindId, + listening: bool, + group: Option, + listen_order: u64, + // Linux inserts IPv6 reuseport listeners at the tail; other listeners at the head. + hash_tail: bool, +} + +impl TcpPortBinding { + fn lookup_order(&self) -> (bool, u64) { + ( + !self.hash_tail, + if self.hash_tail { + u64::MAX - self.listen_order + } else { + self.listen_order + }, + ) + } +} + +#[derive(Debug, Clone, Copy)] +struct FastReusePort { + uid: u32, + addr: IpAddress, + strict: bool, +} + +#[derive(Debug, Default)] +struct TcpPortBucket { + members: Vec, + fast_reuseaddr: bool, + fast_reuseport: Option, +} + +impl TcpPortBucket { + fn listener_is_selected(&self, id: u64) -> bool { + let Some(member) = self + .members + .iter() + .find(|m| m.id.get() == id && m.listening) + else { + return false; + }; + // Exact and wildcard address layers are resolved by the stack. Within + // one layer use the first listener in Linux hash-list order. + let first = self + .members + .iter() + .filter(|m| m.listening && m.addr == member.addr) + .max_by_key(|m| m.lookup_order()) + .expect("active member must have a listener in its address layer"); + if first.reuseport { + match first.group { + Some(group) => member.group == Some(group), + None => member.id == first.id, + } + } else { + member.id == first.id + } + } + + fn fast_reuseport_matches(&self, candidate: &TcpPortBinding) -> bool { + self.fast_reuseport.map_or(false, |cached| { + candidate.reuseport + && candidate.uid == cached.uid + && (!cached.strict + || (cached.addr.version() == candidate.addr.version() + && (cached.addr.is_unspecified() || cached.addr == candidate.addr))) + }) + } + + fn check_conflict(&self, candidate: &TcpPortBinding) -> Result<(), SystemError> { + // Mirrors inet_csk_get_port's cached admission. Current member options + // and cached bucket admission are intentionally separate state. + if (self.fast_reuseaddr && candidate.reuseaddr && !candidate.listening) + || self.fast_reuseport_matches(candidate) + { + return Ok(()); + } + for other in &self.members { + if candidate.id == other.id || !tcp_addrs_conflict(candidate.addr, other.addr) { + continue; + } + let reuseaddr_ok = candidate.reuseaddr && other.reuseaddr && !other.listening; + let reuseport_ok = candidate.reuseport && other.reuseport && candidate.uid == other.uid; + if !reuseaddr_ok && !reuseport_ok { + return Err(SystemError::EADDRINUSE); + } + } + Ok(()) + } + + fn update_fastreuse(&mut self, candidate: &TcpPortBinding) { + let reuseaddr = candidate.reuseaddr && !candidate.listening; + if self.members.is_empty() { + self.fast_reuseaddr = reuseaddr; + self.fast_reuseport = candidate.reuseport.then_some(FastReusePort { + uid: candidate.uid, + addr: candidate.addr, + strict: false, + }); + } else { + if !reuseaddr { + self.fast_reuseaddr = false; + } + if !candidate.reuseport { + self.fast_reuseport = None; + } else if !self.fast_reuseport_matches(candidate) { + self.fast_reuseport = Some(FastReusePort { + uid: candidate.uid, + addr: candidate.addr, + strict: true, + }); + } + } + } +} + +/// TCP currently exposes default dual-stack IPv6 sockets. A native IPv6 +/// wildcard therefore reserves IPv4 addresses too; concrete native IPv6 +/// addresses remain disjoint. Keep UDP's existing policy separate. +fn tcp_addrs_conflict(a: IpAddress, b: IpAddress) -> bool { + addrs_conflict(a, b) + || (a.version() == smoltcp::wire::IpVersion::Ipv6 && a.is_unspecified()) + || (b.version() == smoltcp::wire::IpVersion::Ipv6 && b.is_unspecified()) } #[inline] @@ -186,3 +419,237 @@ fn addrs_conflict(a: IpAddress, b: IpAddress) -> bool { } a == b } + +#[cfg(test)] +mod tests { + use super::*; + + const PORT: u16 = 43001; + fn loopback() -> IpAddress { + IpAddress::v4(127, 0, 0, 1) + } + fn bind( + pm: &PortManager, + addr: IpAddress, + ra: bool, + rp: bool, + uid: u32, + ) -> Result { + let id = TcpBindId::new(); + pm.bind_tcp_port(PORT, addr, ra, rp, uid, id)?; + Ok(id) + } + + #[test] + fn reuseaddr_shares_bound_sockets_but_not_listeners() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), true, false, 1000).unwrap(); + let b = bind(&pm, loopback(), true, false, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + assert_eq!(pm.listen_tcp_port(PORT, b), Err(SystemError::EADDRINUSE)); + pm.stop_tcp_listen(PORT, a); + pm.listen_tcp_port(PORT, b).unwrap(); + } + + #[test] + fn reuseport_requires_the_same_owner() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + assert_eq!( + bind(&pm, loopback(), false, true, 1001), + Err(SystemError::EADDRINUSE) + ); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + } + + #[test] + fn removing_one_binding_preserves_the_other() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.unbind_tcp_port(PORT, a); + assert_eq!( + bind(&pm, loopback(), false, false, 1000), + Err(SystemError::EADDRINUSE) + ); + pm.unbind_tcp_port(PORT, a); // stale release must not remove a different member + pm.unbind_tcp_port(PORT, b); + assert!(bind(&pm, loopback(), false, false, 1000).is_ok()); + } + + #[test] + fn enabling_reuseport_after_bind_is_visible_to_admission() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, false, 1000).unwrap(); + pm.update_tcp_options(PORT, a, None, Some(true)).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + } + + #[test] + fn disabling_before_listen_revalidates_port_cache() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.update_tcp_options(PORT, a, None, Some(false)).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + assert_eq!( + bind(&pm, loopback(), false, true, 1000), + Err(SystemError::EADDRINUSE) + ); + } + + #[test] + fn disabling_after_listen_keeps_linux_fastreuse_admission() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + pm.update_tcp_options(PORT, a, None, Some(false)).unwrap(); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + } + + #[test] + fn wildcard_conflicts_across_local_addresses() { + let pm = PortManager::default(); + let a = bind(&pm, IpAddress::v4(0, 0, 0, 0), true, false, 1000).unwrap(); + let b = bind(&pm, loopback(), true, false, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + assert_eq!(pm.listen_tcp_port(PORT, b), Err(SystemError::EADDRINUSE)); + } + + #[test] + fn different_addresses_and_native_families_do_not_conflict() { + let pm = PortManager::default(); + bind(&pm, loopback(), false, false, 1000).unwrap(); + bind(&pm, IpAddress::v4(127, 0, 0, 2), false, false, 1001).unwrap(); + bind( + &pm, + IpAddress::v6(0, 0, 0, 0, 0, 0, 0, 1), + false, + false, + 1001, + ) + .unwrap(); + } + + #[test] + fn disabling_a_group_member_does_not_remove_its_membership() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + pm.update_tcp_options(PORT, a, None, Some(false)).unwrap(); + let table = pm.tcp_port_table.lock(); + assert!(table[&PORT].listener_is_selected(a.get())); + assert!(table[&PORT].listener_is_selected(b.get())); + } + + #[test] + fn newest_non_reuseport_listener_takes_precedence_over_its_old_group() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + pm.update_tcp_options(PORT, b, None, Some(false)).unwrap(); + { + let table = pm.tcp_port_table.lock(); + assert!(!table[&PORT].listener_is_selected(a.get())); + assert!(table[&PORT].listener_is_selected(b.get())); + } + pm.unbind_tcp_port(PORT, b); + assert!(pm.tcp_port_table.lock()[&PORT].listener_is_selected(a.get())); + } + + #[test] + fn disabled_singleton_does_not_join_a_new_group() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + pm.update_tcp_options(PORT, a, None, Some(false)).unwrap(); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + let table = pm.tcp_port_table.lock(); + assert!(!table[&PORT].listener_is_selected(a.get())); + assert!(table[&PORT].listener_is_selected(b.get())); + } + + #[test] + fn strict_cache_does_not_allow_a_wildcard_to_bypass_an_exact_owner() { + let pm = PortManager::default(); + bind(&pm, loopback(), false, false, 1000).unwrap(); + bind(&pm, IpAddress::v4(127, 0, 0, 2), false, true, 1000).unwrap(); + assert_eq!( + bind(&pm, IpAddress::v4(0, 0, 0, 0), false, true, 1000), + Err(SystemError::EADDRINUSE) + ); + } + + #[test] + fn ipv6_reuseport_listeners_are_looked_up_in_insertion_order() { + let pm = PortManager::default(); + let addr = IpAddress::v6(0, 0, 0, 0, 0, 0, 0, 1); + let a = bind(&pm, addr, false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + let b = bind(&pm, addr, false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + pm.update_tcp_options(PORT, a, None, Some(false)).unwrap(); + let table = pm.tcp_port_table.lock(); + assert!(table[&PORT].listener_is_selected(a.get())); + assert!(!table[&PORT].listener_is_selected(b.get())); + } + + #[test] + fn group_join_uses_listen_order_instead_of_bind_order() { + let pm = PortManager::default(); + let a = bind(&pm, loopback(), false, true, 1000).unwrap(); + let b = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + pm.update_tcp_options(PORT, b, None, Some(false)).unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + pm.update_tcp_options(PORT, b, None, Some(true)).unwrap(); + let c = bind(&pm, loopback(), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, c).unwrap(); + let table = pm.tcp_port_table.lock(); + assert!(table[&PORT].listener_is_selected(a.get())); + assert!(!table[&PORT].listener_is_selected(b.get())); + assert!(table[&PORT].listener_is_selected(c.get())); + } + + #[test] + fn default_ipv6_wildcard_owns_the_ipv4_port_too() { + let any6 = IpAddress::v6(0, 0, 0, 0, 0, 0, 0, 0); + for (first, second) in [(any6, loopback()), (loopback(), any6)] { + let pm = PortManager::default(); + bind(&pm, first, false, false, 1000).unwrap(); + assert_eq!( + bind(&pm, second, false, false, 1000), + Err(SystemError::EADDRINUSE) + ); + } + } + + #[test] + fn dual_stack_and_ipv4_reuseport_members_have_separate_address_groups() { + let pm = PortManager::default(); + let a = bind( + &pm, + IpAddress::v6(0, 0, 0, 0, 0, 0, 0, 0), + false, + true, + 1000, + ) + .unwrap(); + pm.listen_tcp_port(PORT, a).unwrap(); + let b = bind(&pm, IpAddress::v4(0, 0, 0, 0), false, true, 1000).unwrap(); + pm.listen_tcp_port(PORT, b).unwrap(); + let table = pm.tcp_port_table.lock(); + assert!(table[&PORT].listener_is_selected(a.get())); + assert!(table[&PORT].listener_is_selected(b.get())); + assert_ne!(table[&PORT].members[0].group, table[&PORT].members[1].group); + } +} diff --git a/kernel/src/net/socket/inet/stream/inner.rs b/kernel/src/net/socket/inet/stream/inner.rs index af8f3f4257..4a84460568 100644 --- a/kernel/src/net/socket/inet/stream/inner.rs +++ b/kernel/src/net/socket/inet/stream/inner.rs @@ -5,6 +5,7 @@ use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crate::filesystem::epoll::EPollEventType; use crate::libs::mutex::Mutex; use crate::libs::rwsem::RwSem; +use crate::net::socket::inet::common::port::TcpBindId; use crate::net::socket::{self}; use crate::process::namespace::net_namespace::NetNamespace; use crate::syscall::user_buffer::UserBuffer; @@ -22,12 +23,13 @@ use super::registration::{ pub const DEFAULT_RX_BUF_SIZE: usize = 128 * 1024; pub const DEFAULT_TX_BUF_SIZE: usize = 128 * 1024; -/// 显式的“已关闭”状态:不再绑定/访问 smoltcp SocketSet 中的任何 handle。 +/// Explicit closed state: no longer bound to or accessing any handle in smoltcp SocketSet. /// -/// 目的: -/// - 语义上表示 socket 已经 close; -/// - 并发上避免在 handle 已被 remove 后仍通过 update_events()/poll/notify 触达 SocketSet, -/// 触发 smoltcp 的 "handle does not refer to a valid socket" panic。 +/// Purpose: +/// - Semantically marks the socket as closed. +/// - Prevents concurrent update_events()/poll/notify paths from touching SocketSet +/// after the handle has been removed, which would trigger smoltcp's +/// "handle does not refer to a valid socket" panic. #[derive(Debug, Clone, Copy)] pub struct Closed { ver: smoltcp::wire::IpVersion, @@ -55,20 +57,133 @@ fn new_smoltcp_socket() -> smoltcp::socket::tcp::Socket<'static> { fn new_listen_smoltcp_socket( local_endpoint: T, - reuseport: bool, + listener_id: Option, ) -> Result, SystemError> where T: Into, { let mut socket = new_smoltcp_socket(); - socket.set_reuseport(reuseport); socket.listen(local_endpoint).map_err(|e| match e { tcp::ListenError::InvalidState => SystemError::EINVAL, // TODO: Check is right impl tcp::ListenError::Unaddressable => SystemError::EADDRINUSE, })?; + socket.set_listener_id(listener_id); Ok(socket) } +/// Port ownership is independent of the transport slots used by a listener. +/// Moving this resource through states preserves identity; release is idempotent. +#[derive(Debug)] +pub struct TcpBinding { + pub id: TcpBindId, + pub local: smoltcp::wire::IpEndpoint, + netns: Arc, + released: AtomicBool, + /// Linux SOCK_BINDPORT_LOCK: only an explicit nonzero bind preserves the + /// reservation when a listener is shut down. + explicit_port: bool, +} + +impl TcpBinding { + fn new( + id: TcpBindId, + local: smoltcp::wire::IpEndpoint, + netns: Arc, + explicit_port: bool, + ) -> Self { + Self { + id, + local, + netns, + released: AtomicBool::new(false), + explicit_port, + } + } + + pub fn release(&self) { + if !self.released.swap(true, Ordering::Relaxed) { + self.netns + .tcp_port_manager() + .unbind_tcp_port(self.local.port, self.id); + } + } + + pub fn stop_listening(&self) { + self.netns + .tcp_port_manager() + .stop_tcp_listen(self.local.port, self.id); + } + + pub fn is_released(&self) -> bool { + self.released.load(Ordering::Relaxed) + } + + pub fn shutdown_listener(&self) { + self.stop_listening(); + if !self.explicit_port { + // Keep the last visible endpoint for getsockname(), but release the + // auto-assigned port as tcp_set_state(TCP_CLOSE) does on Linux. + self.release(); + } + } + + fn renew_if_released( + &mut self, + owner_uid: u32, + reuseaddr: bool, + reuseport: bool, + ) -> Result<(), SystemError> { + if !self.is_released() { + return Ok(()); + } + let id = TcpBindId::new(); + let port = self.netns.tcp_port_manager().bind_tcp_ephemeral_port( + self.local.addr, + reuseaddr, + reuseport, + owner_uid, + id, + )?; + *self = Self::new( + id, + smoltcp::wire::IpEndpoint::new(self.local.addr, port), + self.netns.clone(), + false, + ); + Ok(()) + } + + fn update_options( + &self, + reuseaddr: Option, + reuseport: Option, + ) -> Result<(), SystemError> { + if self.released.load(Ordering::Relaxed) { + return Ok(()); + } + self.netns.tcp_port_manager().update_tcp_options( + self.local.port, + self.id, + reuseaddr, + reuseport, + ) + } +} + +impl Drop for TcpBinding { + fn drop(&mut self) { + self.release(); + } +} + +#[derive(Debug)] +pub struct Bound { + pub inner: socket::inet::BoundInner, + // A stopped auto-bound listener retains its last local endpoint here after + // releasing ownership. The next listen/connect renews its reservation. + pub binding: TcpBinding, +} + #[derive(Debug)] pub enum Init { Unbound( @@ -77,7 +192,7 @@ pub enum Init { smoltcp::wire::IpVersion, ), ), - Bound((socket::inet::BoundInner, smoltcp::wire::IpEndpoint)), + Bound(Bound), } impl Init { @@ -104,7 +219,7 @@ impl Init { **socket = new_sock; Ok(()) } - Init::Bound((inner, _)) => { + Init::Bound(Bound { inner, .. }) => { inner.with_mut::(|socket| { socket.set_send_buffer_size(tx_size); socket.set_recv_buffer_size(rx_size); @@ -120,13 +235,57 @@ impl Init { netns: Arc, reuseaddr: bool, reuseport: bool, + owner_uid: u32, ) -> Result { match self { + Init::Bound(Bound { inner, mut binding }) if binding.is_released() => { + let old_iface = inner.iface().clone(); + let ver = match binding.local.addr { + smoltcp::wire::IpAddress::Ipv4(_) => smoltcp::wire::IpVersion::Ipv4, + smoltcp::wire::IpAddress::Ipv6(_) => smoltcp::wire::IpVersion::Ipv6, + }; + let smoltcp::socket::Socket::Tcp(socket) = inner.into_socket() else { + unreachable!("TCP BoundInner should contain a TCP socket"); + }; + match Init::Unbound((Box::new(socket), ver)).bind( + local_endpoint, + netns.clone(), + reuseaddr, + reuseport, + owner_uid, + ) { + Ok(init) => Ok(init), + Err((Init::Unbound((socket, _)), err)) => { + // inet_bind preserves inet_sport on failure. A failed + // get_port clears the address, while an earlier address + // validation error preserves it (Linux 6.6 __inet_bind). + if err == SystemError::EADDRINUSE { + binding.local.addr = match ver { + smoltcp::wire::IpVersion::Ipv4 => { + smoltcp::wire::Ipv4Address::UNSPECIFIED.into() + } + smoltcp::wire::IpVersion::Ipv6 => { + smoltcp::wire::Ipv6Address::UNSPECIFIED.into() + } + }; + } + // bind_on_iface only adds a socket to a known SocketSet; + // unlike address selection, it cannot fail. + let inner = + socket::inet::BoundInner::bind_on_iface(*socket, old_iface, netns) + .expect( + "restoring a TCP socket on its existing interface cannot fail", + ); + Err((Init::Bound(Bound { inner, binding }), err)) + } + Err(_) => unreachable!("binding an unbound socket only recovers Unbound"), + } + } Init::Unbound((socket, ver)) => { let bound = match socket::inet::BoundInner::bind_recoverable( *socket, &local_endpoint.addr, - netns, + netns.clone(), ) { Ok(bound) => bound, Err((socket, err)) => { @@ -134,14 +293,15 @@ impl Init { } }; + let id = TcpBindId::new(); // Handle ephemeral port assignment (port 0) let bind_port = if local_endpoint.port == 0 { - match bound.port_manager().bind_tcp_ephemeral_port( + match netns.tcp_port_manager().bind_tcp_ephemeral_port( local_endpoint.addr, reuseaddr, reuseport, - bound.iface().nic_id(), - bound.handle(), + owner_uid, + id, ) { Ok(port) => port, Err(err) => { @@ -152,13 +312,13 @@ impl Init { } } } else { - if let Err(err) = bound.port_manager().bind_tcp_port( + if let Err(err) = netns.tcp_port_manager().bind_tcp_port( local_endpoint.port, local_endpoint.addr, reuseaddr, reuseport, - bound.iface().nic_id(), - bound.handle(), + owner_uid, + id, ) { let smoltcp::socket::Socket::Tcp(socket) = bound.into_socket() else { unreachable!("TCP BoundInner should contain a TCP socket"); @@ -170,7 +330,10 @@ impl Init { // Create endpoint with actual assigned port let final_endpoint = smoltcp::wire::IpEndpoint::new(local_endpoint.addr, bind_port); - Ok(Init::Bound((bound, final_endpoint))) + Ok(Init::Bound(Bound { + inner: bound, + binding: TcpBinding::new(id, final_endpoint, netns, local_endpoint.port != 0), + })) } Init::Bound(_) => { log::debug!("Already Bound"); @@ -183,26 +346,27 @@ impl Init { self, remote_endpoint: smoltcp::wire::IpEndpoint, netns: Arc, - ) -> Result<(socket::inet::BoundInner, smoltcp::wire::IpEndpoint), (Self, SystemError)> { + owner_uid: u32, + reuseaddr: bool, + reuseport: bool, + ) -> Result { match self { Init::Unbound((socket, ver)) => { let (bound, address) = match socket::inet::BoundInner::bind_ephemeral_recoverable( *socket, remote_endpoint.addr, - netns, + netns.clone(), ) { Ok(result) => result, Err((socket, err)) => { return Err((Self::Unbound((Box::new(socket), ver)), err)) } }; - let bound_port = match bound.port_manager().bind_tcp_ephemeral_port( - address, - false, - false, - bound.iface().nic_id(), - bound.handle(), - ) { + let id = TcpBindId::new(); + let bound_port = match netns + .tcp_port_manager() + .bind_tcp_ephemeral_port(address, reuseaddr, reuseport, owner_uid, id) + { Ok(port) => port, Err(err) => { let smoltcp::socket::Socket::Tcp(socket) = bound.into_socket() else { @@ -212,7 +376,10 @@ impl Init { } }; let endpoint = smoltcp::wire::IpEndpoint::new(address, bound_port); - Ok((bound, endpoint)) + Ok(Bound { + inner: bound, + binding: TcpBinding::new(id, endpoint, netns, false), + }) } Init::Bound(_) => Err((self, SystemError::EINVAL)), } @@ -223,13 +390,22 @@ impl Init { remote_endpoint: smoltcp::wire::IpEndpoint, netns: Arc, wrapper: Weak, + owner_uid: u32, + reuseaddr: bool, + reuseport: bool, ) -> Result { - let (inner, local) = match self { - Init::Unbound(_) => self.bind_to_ephemeral(remote_endpoint, netns)?, + let Bound { inner, mut binding } = match self { + Init::Unbound(_) => { + self.bind_to_ephemeral(remote_endpoint, netns, owner_uid, reuseaddr, reuseport)? + } Init::Bound(inner) => inner, }; + if let Err(err) = binding.renew_if_released(owner_uid, reuseaddr, reuseport) { + return Err((Init::Bound(Bound { inner, binding }), err)); + } + let local = binding.local; if local.addr.is_unspecified() { - return Err((Init::Bound((inner, local)), SystemError::EINVAL)); + return Err((Init::Bound(Bound { inner, binding }), SystemError::EINVAL)); } // Publish before taking the SocketSet lock and making the first SYN // visible to poll. The RAII reservation survives until Connecting is @@ -237,7 +413,7 @@ impl Init { // socket lifetime. let registration = match ConnectingRegistration::try_new(inner.iface().clone(), wrapper) { Ok(registration) => registration, - Err(err) => return Err((Init::Bound((inner, local)), err)), + Err(err) => return Err((Init::Bound(Bound { inner, binding }), err)), }; let result = inner.with_mut::(|socket| { socket @@ -249,8 +425,13 @@ impl Init { .map_err(|_| SystemError::ECONNREFUSED) }); match result { - Ok(_) => Ok(Connecting::new(inner, registration, local, remote_endpoint)), - Err(err) => Err((Init::Bound((inner, local)), err)), + Ok(_) => Ok(Connecting::new( + inner, + registration, + binding, + remote_endpoint, + )), + Err(err) => Err((Init::Bound(Bound { inner, binding }), err)), } } @@ -265,6 +446,7 @@ impl Init { netns: Arc, reuseaddr: bool, reuseport: bool, + owner_uid: u32, ) -> Result { // If unbound, auto-bind to INADDR_ANY:ephemeral (Linux compat). let bound_self = if matches!(self, Init::Unbound(_)) { @@ -281,25 +463,34 @@ impl Init { } }; let auto_bind_ep = smoltcp::wire::IpEndpoint::new(unspec_addr, 0); - match self.bind(auto_bind_ep, netns.clone(), reuseaddr, reuseport) { + match self.bind(auto_bind_ep, netns.clone(), reuseaddr, reuseport, owner_uid) { Ok(bound) => bound, Err((init, err)) => return Err((init, err)), } } else { self }; - let (inner, local) = match bound_self { + let Bound { inner, mut binding } = match bound_self { Init::Bound(inner) => inner, Init::Unbound(_) => unreachable!(), }; - let listen_addr = if local.addr.is_unspecified() { - smoltcp::wire::IpListenEndpoint::from(local.port) - } else { - smoltcp::wire::IpListenEndpoint::from(local) + if let Err(err) = binding.renew_if_released(owner_uid, reuseaddr, reuseport) { + return Err((Init::Bound(Bound { inner, binding }), err)); + } + let local = binding.local; + // IPv6 wildcard sockets are dual-stack by default. An unqualified + // transport wildcard accepts both families; the binding still retains + // IPv6 :: for getsockname and Linux port admission. IPv4 wildcard + // sockets keep their family and must not accept IPv6 traffic. + let listen_addr = match local.addr { + smoltcp::wire::IpAddress::Ipv6(addr) if addr.is_unspecified() => { + smoltcp::wire::IpListenEndpoint::from(local.port) + } + _ => smoltcp::wire::IpListenEndpoint::from(local), }; if listen_addr.port == 0 { // Invalid port number - return Err((Init::Bound((inner, local)), SystemError::EINVAL)); + return Err((Init::Bound(Bound { inner, binding }), SystemError::EINVAL)); } // log::debug!("listen at {:?}, backlog {}", listen_addr, backlog); // @@ -307,7 +498,7 @@ impl Init { // one pending connection in the accept queue (see sk_acceptq_is_full logic). // DragonOS uses multiple smoltcp TCP sockets to emulate accept queue slots. if backlog > u16::MAX as usize { - return Err((Init::Bound((inner, local)), SystemError::EINVAL)); + return Err((Init::Bound(Bound { inner, binding }), SystemError::EINVAL)); } // Backlog emulation: @@ -315,8 +506,15 @@ impl Init { // - cap to avoid excessive socket allocations (FIXME: refactor backlog mechanism) let backlog = core::cmp::min(if backlog == 0 { 1 } else { backlog }, 8); + if let Err(err) = netns + .tcp_port_manager() + .listen_tcp_port(local.port, binding.id) + { + return Err((Init::Bound(Bound { inner, binding }), err)); + } + let listener_id = Some(binding.id.get()); let mut inners = Vec::new(); - let is_any_addr = listen_addr.addr.is_none(); + let is_any_addr = local.addr.is_unspecified(); if let Err(err) = || -> Result<(), SystemError> { if is_any_addr { @@ -333,7 +531,7 @@ impl Init { continue; // primary inner already covers this iface } let new_listen = socket::inet::BoundInner::bind_on_iface( - new_listen_smoltcp_socket(listen_addr, reuseport)?, + new_listen_smoltcp_socket(listen_addr, listener_id)?, iface.clone(), inner.netns(), )?; @@ -343,7 +541,7 @@ impl Init { let remaining = backlog.saturating_sub(1 + inners.len()); for _ in 0..remaining { let new_listen = socket::inet::BoundInner::bind_on_iface( - new_listen_smoltcp_socket(listen_addr, reuseport)?, + new_listen_smoltcp_socket(listen_addr, listener_id)?, inner.iface().clone(), inner.netns(), )?; @@ -354,7 +552,7 @@ impl Init { let additional_sockets = backlog.saturating_sub(1); for _ in 0..additional_sockets { let new_listen = socket::inet::BoundInner::bind( - new_listen_smoltcp_socket(listen_addr, reuseport)?, + new_listen_smoltcp_socket(listen_addr, listener_id)?, listen_addr .addr .as_ref() @@ -368,17 +566,26 @@ impl Init { } Ok(()) }() { - return Err((Init::Bound((inner, local)), err)); + for slot in &inners { + slot.release(); + } + binding.stop_listening(); + return Err((Init::Bound(Bound { inner, binding }), err)); } if let Err(err) = inner.with_mut::(|socket| { - socket.set_reuseport(reuseport); socket.listen(listen_addr).map_err(|err| match err { tcp::ListenError::InvalidState => SystemError::EINVAL, tcp::ListenError::Unaddressable => SystemError::EINVAL, - }) + })?; + socket.set_listener_id(listener_id); + Ok::<(), SystemError>(()) }) { - return Err((Init::Bound((inner, local)), err)); + for slot in &inners { + slot.release(); + } + binding.stop_listening(); + return Err((Init::Bound(Bound { inner, binding }), err)); } inners.push(inner); @@ -386,18 +593,17 @@ impl Init { inners, connect: AtomicUsize::new(0), listen_addr, - reuseport, + binding, + listener_id, }); } pub(super) fn close(self) -> Closed { match self { Init::Unbound((_, version)) => Closed::new(version), - Init::Bound((inner, endpoint)) => { - inner - .port_manager() - .unbind_tcp_port(endpoint.port, inner.iface().nic_id(), inner.handle()); - let version = match endpoint.addr { + Init::Bound(Bound { inner, binding }) => { + binding.release(); + let version = match binding.local.addr { smoltcp::wire::IpAddress::Ipv4(_) => smoltcp::wire::IpVersion::Ipv4, smoltcp::wire::IpAddress::Ipv6(_) => smoltcp::wire::IpVersion::Ipv6, }; @@ -421,6 +627,7 @@ enum ConnectResult { #[derive(Debug)] pub struct Connecting { + binding: TcpBinding, inner: socket::inet::BoundInner, registration: ConnectingRegistration, result: RwSem, @@ -437,12 +644,14 @@ impl Connecting { fn new( inner: socket::inet::BoundInner, registration: ConnectingRegistration, - local: smoltcp::wire::IpEndpoint, + binding: TcpBinding, remote: smoltcp::wire::IpEndpoint, ) -> Self { + let local = binding.local; Connecting { inner, registration, + binding, result: RwSem::new(ConnectResult::Connecting), was_established: AtomicBool::new(false), local, @@ -481,7 +690,7 @@ impl Connecting { ( Inner::Established(Established::new_with_connecting_registration( self.inner, - true, + Some(self.binding), registration, )), Ok(()), @@ -491,12 +700,7 @@ impl Connecting { | ConnectResult::RefusedConsumed | ConnectResult::ShutdownReset | ConnectResult::ShutdownResetConsumed => { - // unbind port - self.inner.port_manager().unbind_tcp_port( - self.local.port, - self.inner.iface().nic_id(), - self.inner.handle(), - ); + self.binding.release(); let socket = self.inner.into_socket(); let socket = match socket { smoltcp::socket::Socket::Tcp(s) => s, @@ -545,7 +749,7 @@ impl Connecting { // existing iface notification registration. Callers that are closing // the socket have already unregistered it before reaching here. let registration = self.registration.retain(); - Established::new_with_connecting_registration(self.inner, true, registration) + Established::new_with_connecting_registration(self.inner, Some(self.binding), registration) } /// Converts a Connecting socket after its iface registration was removed @@ -553,7 +757,7 @@ impl Connecting { /// back any registration racing with close. pub unsafe fn into_established_after_unbind(self) -> Established { self.registration.cancel(); - Established::new(self.inner, true) + Established::new(self.inner, Some(self.binding)) } /// Returns `true` when `conn_result` becomes ready, which indicates that the caller should @@ -762,61 +966,52 @@ pub struct Listening { pub inners: Vec, connect: AtomicUsize, listen_addr: smoltcp::wire::IpListenEndpoint, - reuseport: bool, + pub binding: TcpBinding, + listener_id: Option, } impl Listening { pub fn accept(&mut self) -> Result<(Established, smoltcp::wire::IpEndpoint), SystemError> { - let connected: &mut socket::inet::BoundInner = self + let connected = self .inners .get_mut(self.connect.load(core::sync::atomic::Ordering::Relaxed)) - .unwrap(); - - if connected.with::(|socket| !socket.is_active()) { - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); - } - - let remote_endpoint = connected.with::(|socket| { - socket - .remote_endpoint() - .expect("A Connected Tcp With No Remote Endpoint") - }); - - // log::debug!("local at {:?}", local_endpoint); - - // Create a replacement listen socket on the *same* interface as the one - // that just accepted a connection. This is critical for INADDR_ANY listeners - // where each interface has its own listen socket in the smoltcp SocketSet. - let mut new_listen = if self.listen_addr.addr.is_none() { - socket::inet::BoundInner::bind_on_iface( - new_listen_smoltcp_socket(self.listen_addr, self.reuseport)?, - connected.iface().clone(), - connected.netns(), - )? - } else { - socket::inet::BoundInner::bind( - new_listen_smoltcp_socket(self.listen_addr, self.reuseport)?, - self.listen_addr - .addr - .as_ref() - .unwrap_or(&smoltcp::wire::IpAddress::from( - smoltcp::wire::Ipv4Address::UNSPECIFIED, - )), - connected.netns(), - )? - }; - - // swap the connected socket with the new_listen socket - // TODO is smoltcp socket swappable? - core::mem::swap(&mut new_listen, connected); - - return Ok((Established::new(new_listen, false), remote_endpoint)); + .ok_or(SystemError::EAGAIN_OR_EWOULDBLOCK)?; + + // Allocate the replacement before locking SocketSet, but publish it only + // after a final state/tuple check under the same lock as the handoff. + let replacement = new_listen_smoltcp_socket(self.listen_addr, self.listener_id)?; + let (accepted, local, remote) = connected.accept_tcp(replacement)?; + // Use the endpoint snapshot from the handoff: an RST after unlocking may + // already have cleared the transport tuple before this object is built. + Ok(( + Established { + inner: accepted, + local, + peer: remote, + binding: None, + connecting_registration: None, + }, + remote, + )) } pub fn update_io_events(&self, pollee: &AtomicUsize) { - // log::info!("Listening::update_io_events"); + // A failed pending connection frees its slot. Re-arm it with the same + // logical identity; a Closed transport cannot represent a full listener. + for inner in &self.inners { + inner.with_mut::(|socket| { + if socket.state() == tcp::State::Closed && socket.listen(self.listen_addr).is_ok() { + socket.set_listener_id(self.listener_id); + } + }); + } let position = self.inners.iter().position(|inner| { - inner.with::(|socket| socket.is_active()) + inner.with::(|socket| { + matches!( + socket.state(), + tcp::State::Established | tcp::State::CloseWait + ) + }) }); if let Some(position) = position { @@ -835,34 +1030,17 @@ impl Listening { } pub fn get_name(&self) -> smoltcp::wire::IpEndpoint { - smoltcp::wire::IpEndpoint::new( - self.listen_addr - .addr - .unwrap_or(smoltcp::wire::IpAddress::from( - smoltcp::wire::Ipv4Address::UNSPECIFIED, - )), - self.listen_addr.port, - ) + self.binding.local } pub fn close(&self) { - // log::debug!("Close Listening Socket"); - let port = self.get_name().port; - for inner in self.inners.iter() { - inner.with_mut::(|socket| socket.close()); + for inner in &self.inners { + inner.with_mut::(|socket| { + socket.set_listener_id(None); + socket.abort(); + }); } - // The original port-owning socket is always the *last* element in `inners` - // (pushed last during listen() construction). We must unbind from its - // port_manager, not inners[0] which may belong to a different iface for - // INADDR_ANY listeners. - let owner = self - .inners - .last() - .expect("Listening socket must have at least one inner"); - owner - .iface() - .port_manager() - .unbind_tcp_port(port, owner.iface().nic_id(), owner.handle()); + self.binding.release(); } pub fn release(&mut self) { @@ -878,12 +1056,12 @@ pub struct Established { inner: socket::inet::BoundInner, local: smoltcp::wire::IpEndpoint, peer: smoltcp::wire::IpEndpoint, - owns_port: bool, + binding: Option, connecting_registration: Option, } impl Established { - pub fn new(inner: socket::inet::BoundInner, owns_port: bool) -> Self { + pub fn new(inner: socket::inet::BoundInner, binding: Option) -> Self { let local = inner .with::(|socket| socket.local_endpoint()) .unwrap_or(smoltcp::wire::IpEndpoint::new( @@ -900,17 +1078,17 @@ impl Established { inner, local, peer, - owns_port, + binding, connecting_registration: None, } } fn new_with_connecting_registration( inner: socket::inet::BoundInner, - owns_port: bool, + binding: Option, registration: ConnectingRegistrationLease, ) -> Self { - let mut established = Self::new(inner, owns_port); + let mut established = Self::new(inner, binding); established.connecting_registration = Some(registration); established } @@ -940,8 +1118,10 @@ impl Established { self.inner.handle() } - pub fn owns_port(&self) -> bool { - self.owns_port + pub fn release_binding(&self) { + if let Some(binding) = &self.binding { + binding.release(); + } } pub fn get_name(&self) -> smoltcp::wire::IpEndpoint { @@ -1081,6 +1261,7 @@ impl Established { /// receive queue, and driving readiness/EOF based on shutdown state. #[derive(Debug)] pub struct SelfConnected { + binding: TcpBinding, inner: socket::inet::BoundInner, local: smoltcp::wire::IpEndpoint, state: Mutex, @@ -1100,14 +1281,11 @@ struct SelfConnectedState { } impl SelfConnected { - pub fn new( - inner: socket::inet::BoundInner, - local: smoltcp::wire::IpEndpoint, - rx_cap: usize, - ) -> Self { + pub fn new(bound: Bound, rx_cap: usize) -> Self { Self { - inner, - local, + inner: bound.inner, + local: bound.binding.local, + binding: bound.binding, state: Mutex::new(SelfConnectedState { rx_cap, buf: VecDeque::new(), @@ -1312,7 +1490,7 @@ impl Inner { match self { Inner::Init(init) => match init { Init::Unbound((socket, _)) => f(socket), - Init::Bound((inner, _)) => inner.with(f), + Init::Bound(Bound { inner, .. }) => inner.with(f), }, Inner::Connecting(conn) => conn.with(f), Inner::Listening(listen) => listen.inners[0].with(f), @@ -1323,13 +1501,34 @@ impl Inner { panic!("Inner::with_socket called on SelfConnected socket") } Inner::Closed(_) => { - // Closed 状态不应再触达任何 smoltcp socket。 - // 调用者应当在更上层对 Closed 做分支处理。 + // Closed must not touch any smoltcp socket. + // Callers should branch on Closed at a higher layer. panic!("Inner::with_socket called on Closed socket") } } } + pub fn update_reuse_options( + &mut self, + reuseaddr: Option, + reuseport: Option, + ) -> Result<(), SystemError> { + let binding = match self { + Inner::Init(Init::Bound(bound)) => Some(&bound.binding), + Inner::Connecting(conn) => Some(&conn.binding), + Inner::Listening(listen) => Some(&listen.binding), + Inner::Established(est) => est.binding.as_ref(), + Inner::SelfConnected(sc) => Some(&sc.binding), + _ => None, + }; + if let Some(binding) = binding { + binding.update_options(reuseaddr, reuseport)?; + } + // Listener identity remains stable across option changes. The namespace + // chooses active members and refreshes transport eligibility before ingress. + Ok(()) + } + pub fn for_each_socket_mut(&mut self, mut f: F) where F: FnMut(&mut smoltcp::socket::tcp::Socket<'static>), @@ -1337,7 +1536,7 @@ impl Inner { match self { Inner::Init(init) => match init { Init::Unbound((socket, _)) => f(socket), - Init::Bound((inner, _)) => inner.with_mut(f), + Init::Bound(Bound { inner, .. }) => inner.with_mut(f), }, Inner::Connecting(conn) => conn.with_mut(f), Inner::Listening(listen) => { @@ -1369,7 +1568,7 @@ impl Inner { pub fn iface(&self) -> Option<&alloc::sync::Arc> { match self { - Inner::Init(Init::Bound((inner, _))) => Some(inner.iface()), + Inner::Init(Init::Bound(bound)) => Some(bound.inner.iface()), Inner::Init(Init::Unbound(_)) => None, Inner::Connecting(conn) => Some(conn.inner.iface()), Inner::Listening(listen) => Some(listen.inners[0].iface()), @@ -1392,7 +1591,7 @@ impl Inner { 0, ), }, - Init::Bound((_, local)) => *local, + Init::Bound(bound) => bound.binding.local, }, Inner::Connecting(conn) => conn.get_name(), Inner::Listening(listen) => listen.get_name(), diff --git a/kernel/src/net/socket/inet/stream/lifecycle.rs b/kernel/src/net/socket/inet/stream/lifecycle.rs index cbc7f3c2bb..8cb60d681c 100644 --- a/kernel/src/net/socket/inet/stream/lifecycle.rs +++ b/kernel/src/net/socket/inet/stream/lifecycle.rs @@ -149,10 +149,17 @@ impl TcpSocket { let reuseport = self .so_reuseport() .load(core::sync::atomic::Ordering::Relaxed); - match inner.bind(local_endpoint, self.netns(), reuseaddr, reuseport) { + match inner.bind( + local_endpoint, + self.netns(), + reuseaddr, + reuseport, + self.owner_uid, + ) { Ok(bound) => { - if let inner::Init::Bound((ref bound, _)) = bound { + if let inner::Init::Bound(ref bound) = bound { bound + .inner .iface() .common() .bind_socket(self.self_ref.upgrade().unwrap()); @@ -165,7 +172,7 @@ impl TcpSocket { Err(err) } } - }, + } any => { writer.replace(any); log::error!("TcpSocket::do_bind: not Init"); @@ -185,24 +192,17 @@ impl TcpSocket { let reuseport = self .so_reuseport() .load(core::sync::atomic::Ordering::Relaxed); - let listen_result = init.listen(backlog, self.netns(), reuseaddr, reuseport); + let listen_result = + init.listen(backlog, self.netns(), reuseaddr, reuseport, self.owner_uid); match listen_result { Ok(listening) => { - // DragonOS backlog emulation: listener is represented by multiple - // smoltcp TCP sockets. When all LISTEN sockets are consumed, - // Linux commonly drops incoming SYN (no RST). To implement this - // without changing smoltcp semantics, register the active listen port - // in the iface common registry. - // - // For INADDR_ANY listeners, listen sockets span multiple interfaces, - // so register on each unique interface. - let port = listening.get_name().port; + // Register notifications once on every covered interface. let me = self.self_ref.upgrade().unwrap(); let mut registered_ifaces: alloc::vec::Vec = alloc::vec::Vec::new(); for b in &listening.inners { let nic_id = b.iface().nic_id(); if !registered_ifaces.contains(&nic_id) { - b.iface().common().register_tcp_listen_port(port, backlog); + // Upstream bind_socket deduplicates under one lock. b.iface().common().bind_socket(me.clone()); registered_ifaces.push(nic_id); } @@ -224,7 +224,8 @@ impl TcpSocket { } pub fn try_accept(&self) -> Result<(Arc, smoltcp::wire::IpEndpoint), SystemError> { - // 主动推进协议栈:避免依赖后台 poll 线程,保证 accept 在无事件通知场景下也能前进。 + // Actively advance the stack instead of relying on the background poll thread, + // so accept can make progress even when no event notification arrives. // For INADDR_ANY listeners, poll all interfaces that have listen sockets. let ifaces = { let reader = self.inner.read(); @@ -297,21 +298,31 @@ impl TcpSocket { // smoltcp cannot model this with a single TCP socket instance, so we special-case // it into `Inner::SelfConnected`. match init { - inner::Init::Bound((bound, local)) if local == remote_endpoint => { + inner::Init::Bound(bound) + if !bound.binding.is_released() + && bound.binding.local == remote_endpoint => + { // Capture an effective queue capacity from the underlying socket's recv buffer. let rx_cap = bound + .inner .with::(|s| s.recv_capacity()) .clamp(1 << 20, super::constants::MAX_SOCKET_BUFFER); ( - inner::Inner::SelfConnected(inner::SelfConnected::new( - bound, local, rx_cap, - )), + inner::Inner::SelfConnected(inner::SelfConnected::new(bound, rx_cap)), Ok(()), ) } other => { - let conn_result = - other.connect(remote_endpoint, self.netns(), self.self_ref.clone()); + let conn_result = other.connect( + remote_endpoint, + self.netns(), + self.self_ref.clone(), + self.owner_uid, + self.so_reuseaddr() + .load(core::sync::atomic::Ordering::Relaxed), + self.so_reuseport() + .load(core::sync::atomic::Ordering::Relaxed), + ); match conn_result { Ok(connecting) => ( inner::Inner::Connecting(connecting), @@ -359,10 +370,11 @@ impl TcpSocket { inner::Inner::Closed(_) => (inner, Err(SystemError::ENOTCONN)), }; - // 先落状态再做 iface 侧绑定,避免与 poll 路径形成锁顺序反转死锁: + // Store the new state before binding on the iface side to avoid lock-order deadlocks + // against the poll path: // - poll: bounds.read -> socket.notify -> socket.inner.read/write - // - connect: socket.inner.write -> bounds.write (会与上面互锁) - // SelfConnected 不依赖协议栈推进,不应触发 iface.poll() + // - connect: socket.inner.write -> bounds.write + // SelfConnected does not depend on protocol-stack progress and must not trigger iface.poll(). let need_poll_progress = matches!(init, inner::Inner::Connecting(_)); let registration_publisher = match &init { inner::Inner::Connecting(connecting) => Some(connecting.registration_publisher()), @@ -372,7 +384,8 @@ impl TcpSocket { writer.replace(init); drop(writer); - // 关键语义:connect(2) 进入 Connecting 状态后,socket 必须能被网络轮询推进。 + // Key semantic requirement: after connect(2) enters Connecting, the socket must + // be visible to network polling so the handshake can progress. if need_poll_progress && matches!(result, Ok(()) | Err(SystemError::EINPROGRESS)) { if let Some(iface) = maybe_iface { // log::debug!( @@ -387,7 +400,7 @@ impl TcpSocket { if let Some(netns) = iface.common().net_namespace() { netns.wakeup_poll_thread(); } - // 主动 poll 一次以尽快发出 SYN / 处理握手。 + // Poll once proactively to send SYN or process the handshake sooner. iface.poll(); } } @@ -439,8 +452,8 @@ impl TcpSocket { let mut post_poll_rounds = 0usize; let mut post_notify_bound_sockets = false; - // Linux/gVisor 语义:TIME_WAIT/Closed 的 stream socket 上 shutdown 应返回 ENOTCONN。 - // 但 Listening 和 Connecting 状态下的 shutdown 是允许的。 + // Linux/gVisor semantics: shutdown on a stream socket in TIME_WAIT/Closed + // should return ENOTCONN, but shutdown is allowed in Listening and Connecting. let mut writer = self.inner.write(); let inner = writer.take().expect("Tcp inner::Inner is None"); @@ -485,10 +498,9 @@ impl TcpSocket { inner::Inner::Listening(mut listening) => { if how.contains(ShutdownBit::SHUT_RD) { let original_listen_sockets = listening.inners.len(); - let local = listening.get_name(); - let port = local.port; + listening.binding.shutdown_listener(); - // Unregister listen port and unbind socket from all unique interfaces. + // Remove socket notifications from all unique interfaces. // For INADDR_ANY listeners, listen sockets span multiple interfaces. { let me = self.self_ref.upgrade().unwrap(); @@ -496,7 +508,6 @@ impl TcpSocket { for b in &listening.inners { let nic_id = b.iface().nic_id(); if !unregistered.contains(&nic_id) { - b.iface().common().unregister_tcp_listen_port(port); b.iface().common().unbind_socket(me.clone()); unregistered.push(nic_id); } @@ -528,7 +539,10 @@ impl TcpSocket { // Do not record SHUT_RD bit here because recv() on an unconnected // stream socket should not become EOF just due to this operation. ( - inner::Inner::Init(inner::Init::Bound((keep, local))), + inner::Inner::Init(inner::Init::Bound(inner::Bound { + inner: keep, + binding: listening.binding, + })), ShutdownBit::from_bits_truncate(0), ) } else { @@ -616,7 +630,7 @@ impl TcpSocket { writer.replace(replace); drop(writer); - // 唤醒等待者(含 poll/epoll),让状态变化可见。 + // Wake waiters, including poll/epoll users, so the state change becomes visible. if let Some(iface) = post_poll_iface { Self::kick_iface_after_tcp_state_change( &iface, @@ -686,11 +700,7 @@ impl TcpSocket { let me: alloc::sync::Weak = self.self_ref.clone(); conn.with_mut(|socket| socket.abort()); let initial_state = conn.with(|socket| socket.state()); - if conn.owns_port() { - iface - .port_manager() - .unbind_tcp_port(local_port, iface.nic_id(), handle); - } + conn.release_binding(); iface.common().defer_tcp_close(DeferredTcpCloseRequest { handle, local_port, @@ -718,11 +728,7 @@ impl TcpSocket { let close_action = self.decide_established_close(&es); es.with_mut(|socket| Self::apply_close_action(socket, close_action)); let initial_state = es.with(|socket| socket.state()); - if es.owns_port() { - iface - .port_manager() - .unbind_tcp_port(local_port, iface.nic_id(), es.handle()); - } + es.release_binding(); iface.common().defer_tcp_close(DeferredTcpCloseRequest { handle, local_port, @@ -743,13 +749,7 @@ impl TcpSocket { smoltcp::wire::IpAddress::Ipv6(_) => smoltcp::wire::IpVersion::Ipv6, _ => smoltcp::wire::IpVersion::Ipv4, }; - let port = sc.get_name().port; - let iface = sc.iface().clone(); - let handle = sc.handle(); sc.release(); - iface - .port_manager() - .unbind_tcp_port(port, iface.nic_id(), handle); writer.replace(inner::Inner::Closed(inner::Closed::new(ver))); } inner::Inner::Listening(mut ls) => { @@ -762,9 +762,8 @@ impl TcpSocket { } // close(listen_fd) should stop listening on the port. let original_listen_sockets = ls.inners.len(); - let port = ls.get_name().port; let post_close_iface = ls.inners.first().map(|b| b.iface().clone()); - // Unregister listen port and unbind socket from all unique interfaces. + // Remove socket notifications from all unique interfaces. // For INADDR_ANY listeners, listen sockets span multiple interfaces, // so we must clean up each one. { @@ -773,7 +772,6 @@ impl TcpSocket { for b in &ls.inners { let nic_id = b.iface().nic_id(); if !cleaned.contains(&nic_id) { - b.iface().common().unregister_tcp_listen_port(port); b.iface().common().unbind_socket(me.clone()); cleaned.push(nic_id); } @@ -781,13 +779,15 @@ impl TcpSocket { } ls.close(); // IMPORTANT: - // `ls.release()` 会把 Listening::inners 里的 handle 从 SocketSet 中 remove。 - // 由于 poll 路径可能已经快照了该 TcpSocket 的 Arc,并在 close_socket() 之后仍调用一次 notify, - // 如果我们仍把 inner 维持在 Listening 状态,则 update_events() 会遍历 inners 并访问已失效 handle, - // 导致 smoltcp panic: "handle does not refer to a valid socket"。 + // `ls.release()` removes handles from Listening::inners out of SocketSet. + // The poll path may already have snapshotted this TcpSocket Arc and can still + // call notify once after close_socket(). If inner remains in Listening, then + // update_events() will iterate inners and access stale handles, causing smoltcp + // to panic with "handle does not refer to a valid socket". // - // 因此这里必须在 release 后把状态切到显式 Closed,确保后续 update_events 不再触达 SocketSet, - // 同时语义上也更“优雅”。 + // Therefore the state must switch to explicit Closed after release, ensuring + // later update_events calls never touch SocketSet. This also matches the + // socket lifecycle semantics more cleanly. ls.release(); for bound in &ls.inners { bound.iface().common().finish_routed_socket_publication(); diff --git a/kernel/src/net/socket/inet/stream/option.rs b/kernel/src/net/socket/inet/stream/option.rs index bf55fe4ffc..ed9111f57b 100644 --- a/kernel/src/net/socket/inet/stream/option.rs +++ b/kernel/src/net/socket/inet/stream/option.rs @@ -310,8 +310,25 @@ impl super::TcpSocket { self.apply_keepalive(interval); Ok(()) }), - PSO::REUSEADDR => Self::set_bool_option(self.so_reuseaddr(), val, |_| Ok(())), - PSO::REUSEPORT => Self::set_bool_option(self.so_reuseport(), val, |_| Ok(())), + PSO::REUSEADDR | PSO::REUSEPORT => { + let enabled = byte_parser::read_bool_flag(val)?; + // Serialize the port record and visible option with bind/listen/close. + let mut guard = self.inner.write(); + let is_port = matches!(opt, PSO::REUSEPORT); + if let Some(inner) = guard.as_mut() { + inner.update_reuse_options( + if is_port { None } else { Some(enabled) }, + if is_port { Some(enabled) } else { None }, + )?; + } + let option = if is_port { + self.so_reuseport() + } else { + self.so_reuseaddr() + }; + option.store(enabled, core::sync::atomic::Ordering::Relaxed); + Ok(()) + } PSO::BROADCAST => Self::set_bool_option(self.so_broadcast(), val, |_| Ok(())), PSO::PASSCRED => Self::set_bool_option(self.so_passcred(), val, |_| Ok(())), PSO::NO_CHECK => Self::set_bool_option(self.so_no_check(), val, |_| Ok(())), diff --git a/kernel/src/net/socket/inet/stream/stream_core.rs b/kernel/src/net/socket/inet/stream/stream_core.rs index a136ec6d52..9c32db2b6f 100644 --- a/kernel/src/net/socket/inet/stream/stream_core.rs +++ b/kernel/src/net/socket/inet/stream/stream_core.rs @@ -150,6 +150,8 @@ pub struct TcpSocket { pub(crate) self_ref: Weak, pub(crate) pollee: AtomicUsize, pub(crate) netns: Arc, + /// Socket credentials are captured at creation, independent of the bind caller. + pub(crate) owner_uid: u32, pub(crate) epoll_items: EPollItems, pub(crate) fasync_items: FAsyncItems, pub(crate) options: TcpSocketOptions, @@ -184,6 +186,7 @@ impl TcpSocket { self_ref: me.clone(), pollee: AtomicUsize::new(pollee_bits), netns, + owner_uid: ProcessManager::current_pcb().cred().euid.data() as u32, epoll_items: EPollItems::default(), fasync_items: FAsyncItems::default(), options: TcpSocketOptions::new(), diff --git a/kernel/src/net/tcp_listener_backlog.rs b/kernel/src/net/tcp_listener_backlog.rs deleted file mode 100644 index bcd5b6407a..0000000000 --- a/kernel/src/net/tcp_listener_backlog.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! TCP listen/backlog 语义辅助:在不修改 smoltcp 的前提下尽量贴近 Linux 行为。 -//! -//! 背景: -//! - DragonOS 的 inet stream(TCP) 通过“多个 smoltcp LISTEN socket”来模拟 accept 队列槽位; -//! - smoltcp 在“没有 socket 可以处理该端口”的情况下,可能会发送 RST(尤其是 lo/IPv6 同一轮 poll 内, -//! 第一个 SYN 消耗掉 LISTEN socket,第二个 SYN 立即触发 RST)。 -//! - Linux 6.6 在 accept 队列满时通常是静默丢 SYN(参考 `tcp_conn_request()->goto drop`), -//! backlog==0 时因为 `sk_acceptq_is_full()` 使用 `>`,仍会允许 1 个 pending。 -//! -//! 因此我们需要一个“在进入 smoltcp 前可选丢包”的策略组件,但它必须: -//! - 不在设备收包路径里再次加锁 `SocketSet`(避免 poll 持锁时死锁); -//! - 能在 `IfaceCommon::poll()` 持有 SocketSet 锁时刷新缓存; -//! - 仅对 backlog==0 这种需要 Linux-like timeout 行为的端口启用(当前策略)。 - -use alloc::vec::Vec; - -use crate::libs::rwsem::RwSem; -use smoltcp::wire::{ - EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, Ipv6Packet, TcpPacket, -}; - -#[derive(Debug, Clone, Copy)] -struct TcpListenPortInfo { - port: u16, - /// backlog==0 时启用:当"本轮 poll 里 LISTEN socket 被消耗完"后,后续纯 SYN 直接丢弃(不让 smoltcp 回 RST)。 - drop_syn_when_full: bool, - /// 缓存:当前是否存在至少一个处于 LISTEN 状态的 smoltcp socket(同端口)。 - /// - /// 注意:必须在 `IfaceCommon::poll()` 持有 SocketSet 锁时刷新,且丢包判断路径不得再锁 SocketSet。 - listen_socket_present: bool, -} - -/// 每个 Iface 维护一份 listener/backlog 状态,用于收包入口的"是否丢 SYN"决策。 -#[derive(Debug)] -pub struct TcpListenerBacklog { - ports: RwSem>, -} - -impl TcpListenerBacklog { - pub fn new() -> Self { - Self { - ports: RwSem::new(Vec::new()), - } - } - - pub fn register_tcp_listen_port(&self, port: u16, backlog: usize) { - let mut guard = self.ports.write(); - // gVisor 期望 listen(backlog=0) 只允许 1 个 pending,额外 SYN 应 timeout(丢包)。 - // backlog>0 时维持 smoltcp 默认行为(当前策略),避免把本应尽快暴露的错误(RST)变成超时。 - let drop_syn_when_full = backlog == 0; - if let Some(e) = guard.iter_mut().find(|e| e.port == port) { - e.drop_syn_when_full = drop_syn_when_full; - // 保守:假设 present,等待下一次 poll 刷新。 - e.listen_socket_present = true; - } else { - guard.push(TcpListenPortInfo { - port, - drop_syn_when_full, - listen_socket_present: true, - }); - } - } - - pub fn unregister_tcp_listen_port(&self, port: u16) { - let mut guard = self.ports.write(); - if let Some(i) = guard.iter().position(|e| e.port == port) { - guard.swap_remove(i); - } - } - - /// 在持有 smoltcp SocketSet 锁的前提下刷新缓存。 - /// - /// IMPORTANT: 不要在这里做分配/collect,避免与全局分配器锁产生复杂死锁。 - pub fn refresh_listen_socket_present(&self, sockets: &smoltcp::iface::SocketSet<'static>) { - let mut guard = self.ports.write(); - for entry in guard.iter_mut() { - let mut present = false; - for item in sockets.items() { - if let smoltcp::socket::Socket::Tcp(tcp) = &item.socket { - if tcp.state() == smoltcp::socket::tcp::State::Listen - && tcp.listen_endpoint().port == entry.port - { - present = true; - break; - } - } - } - entry.listen_socket_present = present; - } - } - - #[inline] - fn any_drop_syn_ports(&self) -> bool { - self.ports.read().iter().any(|e| e.drop_syn_when_full) - } - - /// 判定是否应当丢弃一个“纯 SYN”TCP 包(用于 backlog==0 的 Linux-like 行为)。 - /// - /// - 该函数 **不得** 访问/加锁 SocketSet; - /// - 仅当目的端口注册为 backlog==0 且本轮 poll 已经“消耗掉 LISTEN socket”时,才会丢弃。 - pub fn should_drop_backlog_full_tcp_syn_ip(&self, ip_packet: &[u8]) -> bool { - let mut dst_port: Option = None; - let mut is_pure_syn = false; - - if !self.any_drop_syn_ports() { - return false; - } - - // 有些路径可能给原始 IP(Medium::Ip)或以太网帧(Medium::Ethernet)。 - // 先尝试直接按 IP 解析,失败再尝试 Ethernet->IP。 - let mut maybe_ip: &[u8] = ip_packet; - if Ipv4Packet::new_checked(maybe_ip).is_err() && Ipv6Packet::new_checked(maybe_ip).is_err() - { - if let Ok(eth) = EthernetFrame::new_checked(maybe_ip) { - match eth.ethertype() { - EthernetProtocol::Ipv4 | EthernetProtocol::Ipv6 => { - maybe_ip = eth.payload(); - } - _ => {} - } - } - } - - // 先检查 IP 版本,再解析,避免 smoltcp::Ipv4Packet::new_checked 不校验版本字段 - // 导致 IPv6 包误走 IPv4 解析路径。 - let version = maybe_ip.first().map(|b| b >> 4).unwrap_or(0); - - if version == 4 { - if let Ok(pkt4) = Ipv4Packet::new_checked(maybe_ip) { - if pkt4.next_header() != IpProtocol::Tcp { - return false; - } - // A non-initial fragment does not contain a TCP header. Its - // payload is arbitrary stream data and must never be parsed - // as SYN flags or ports by the pre-stack backlog policy. - if pkt4.frag_offset() != 0 { - return false; - } - if let Ok(tcp) = TcpPacket::new_checked(pkt4.payload()) { - dst_port = Some(tcp.dst_port()); - is_pure_syn = tcp.syn() && !tcp.ack(); - } - } - } else if version == 6 { - // IPv6 可能包含扩展头,这里做一个保守跳过:能到 TCP 则解析,否则不丢。 - let data = maybe_ip; - if data.len() < 40 { - return false; - } - let mut next = data[6]; - let mut off = 40usize; - loop { - if next == IpProtocol::Tcp.into() { - if off >= data.len() { - return false; - } - if let Ok(tcp) = TcpPacket::new_checked(&data[off..]) { - dst_port = Some(tcp.dst_port()); - is_pure_syn = tcp.syn() && !tcp.ack(); - } - break; - } - match next { - // Hop-by-Hop / Routing / Destination Options: [next][hdr_ext_len]... - 0 | 43 | 60 => { - if off + 2 > data.len() { - return false; - } - let nh = data[off]; - let hdr_ext_len = data[off + 1] as usize; - let hdr_len = (hdr_ext_len + 1) * 8; - if off + hdr_len > data.len() { - return false; - } - next = nh; - off += hdr_len; - } - // Fragment header: fixed 8 bytes, [next] at first byte. - 44 => { - if off + 8 > data.len() { - return false; - } - let fragment_offset_and_flags = - u16::from_be_bytes([data[off + 2], data[off + 3]]); - if fragment_offset_and_flags & 0xfff8 != 0 { - return false; - } - let nh = data[off]; - next = nh; - off += 8; - } - _ => { - // 未知扩展头:不丢 - return false; - } - } - } - } else { - return false; - } - - let port = match dst_port { - Some(p) => p, - None => return false, - }; - if !is_pure_syn { - return false; - } - - // backlog==0 策略:同一轮 poll 内只允许第一个 SYN 通过,其余纯 SYN 丢弃以避免 RST。 - let mut guard = self.ports.write(); - let Some(entry) = guard.iter_mut().find(|e| e.port == port) else { - return false; - }; - if !entry.drop_syn_when_full { - return false; - } - - if entry.listen_socket_present { - // 允许第一个 SYN 通过,然后在本轮剩余时间里视作“无 LISTEN socket”,让后续 SYN 被丢弃。 - entry.listen_socket_present = false; - false - } else { - true - } - } -} diff --git a/kernel/src/process/namespace/net_namespace.rs b/kernel/src/process/namespace/net_namespace.rs index 65b2e4f5e6..2b4e6c7f0a 100644 --- a/kernel/src/process/namespace/net_namespace.rs +++ b/kernel/src/process/namespace/net_namespace.rs @@ -193,6 +193,8 @@ pub struct NetNamespace { unix_abstract_table: Arc, /// Per-netns IPv4 ephemeral port range (ip_local_port_range) local_port_range: AtomicU32, + /// TCP port ownership spans all interfaces in this network namespace. + tcp_port_manager: crate::net::socket::inet::common::PortManager, /// 当前网络命名空间的 loopback 网卡。 loopback_iface: RcuOptionArcSlot, /// 当前网络命名空间的默认网卡。 @@ -545,6 +547,10 @@ impl InnerNetNamespace { } impl NetNamespace { + pub fn tcp_port_manager(&self) -> &crate::net::socket::inet::common::PortManager { + &self.tcp_port_manager + } + pub fn new_root() -> Arc { let inner = InnerNetNamespace { router: Router::new("root_netns_router".to_string()), @@ -570,6 +576,7 @@ impl NetNamespace { netlink_socket_table: NetlinkSocketTable::default(), netlink_kernel_socket: RwSem::new(generate_supported_netlink_kernel_sockets()), unix_abstract_table: unix_abstract_table.clone(), + tcp_port_manager: crate::net::socket::inet::common::PortManager::default(), local_port_range: AtomicU32::new( crate::net::socket::inet::common::port::DEFAULT_LOCAL_PORT_RANGE, ), @@ -611,6 +618,7 @@ impl NetNamespace { netlink_socket_table: NetlinkSocketTable::default(), netlink_kernel_socket: RwSem::new(generate_supported_netlink_kernel_sockets()), unix_abstract_table: unix_abstract_table.clone(), + tcp_port_manager: crate::net::socket::inet::common::PortManager::default(), local_port_range: AtomicU32::new( crate::net::socket::inet::common::port::DEFAULT_LOCAL_PORT_RANGE, ), diff --git a/kernel/submodules/smoltcp b/kernel/submodules/smoltcp index 3933571d21..3e0cfb4b4d 160000 --- a/kernel/submodules/smoltcp +++ b/kernel/submodules/smoltcp @@ -1 +1 @@ -Subproject commit 3933571d212b5c10cca7222c2e9a20fb2050870b +Subproject commit 3e0cfb4b4d94b3092ecf93a33e4335f63608ba30 From fc23cb75c2eb21b09d00a7fc9efbcf2b842a03e6 Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 12 Sep 2026 18:03:44 +0800 Subject: [PATCH 07/10] fix(net): clear stale events when TCP sockets start listening Rebuild readiness from completed accept slots and refresh it before listen returns, so prior HUP, writable and error events cannot leak into the listener state. --- kernel/src/net/socket/inet/stream/inner.rs | 18 ++++++++---------- kernel/src/net/socket/inet/stream/lifecycle.rs | 7 +++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/kernel/src/net/socket/inet/stream/inner.rs b/kernel/src/net/socket/inet/stream/inner.rs index 4a84460568..b5341290b7 100644 --- a/kernel/src/net/socket/inet/stream/inner.rs +++ b/kernel/src/net/socket/inet/stream/inner.rs @@ -1014,19 +1014,17 @@ impl Listening { }) }); - if let Some(position) = position { + let events = if let Some(position) = position { self.connect .store(position, core::sync::atomic::Ordering::Relaxed); - pollee.fetch_or( - EPollEventType::EPOLL_LISTEN_CAN_ACCEPT.bits() as usize, - core::sync::atomic::Ordering::Relaxed, - ); + EPollEventType::EPOLL_LISTEN_CAN_ACCEPT.bits() as usize } else { - pollee.fetch_and( - !EPollEventType::EPOLL_LISTEN_CAN_ACCEPT.bits() as usize, - core::sync::atomic::Ordering::Relaxed, - ); - } + 0 + }; + // Listening readiness is determined solely by completed accept slots. + // In particular, do not inherit Init's HUP or a previous connection's + // writable/error flags across bind/listen and shutdown/listen. + pollee.store(events, core::sync::atomic::Ordering::Relaxed); } pub fn get_name(&self) -> smoltcp::wire::IpEndpoint { diff --git a/kernel/src/net/socket/inet/stream/lifecycle.rs b/kernel/src/net/socket/inet/stream/lifecycle.rs index 8cb60d681c..d7ae411bb5 100644 --- a/kernel/src/net/socket/inet/stream/lifecycle.rs +++ b/kernel/src/net/socket/inet/stream/lifecycle.rs @@ -215,6 +215,13 @@ impl TcpSocket { _ => (inner, Some(SystemError::EINVAL)), }; writer.replace(listening); + if err.is_none() { + if let Some(inner::Inner::Listening(listening)) = writer.as_ref() { + // Publish the new state's readiness before listen returns; a + // caller may read pollee before the next interface notification. + listening.update_io_events(&self.pollee); + } + } drop(writer); if let Some(err) = err { From 19fa252fa05bdc68bf2d79a61101b2e67a975adb Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 12 Sep 2026 18:03:44 +0800 Subject: [PATCH 08/10] fix(net): report mapped IPv6 addresses for dual-stack TCP Convert IPv4 transport endpoints to IPv4-mapped IPv6 addresses at the AF_INET6 socket ABI boundary for local address, peer address and accept results. --- kernel/src/net/socket/inet/stream/mod.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/kernel/src/net/socket/inet/stream/mod.rs b/kernel/src/net/socket/inet/stream/mod.rs index e8c8d4b08f..db6da30875 100644 --- a/kernel/src/net/socket/inet/stream/mod.rs +++ b/kernel/src/net/socket/inet/stream/mod.rs @@ -33,6 +33,19 @@ mod stream_core; pub use stream_core::TcpSocket; +impl TcpSocket { + /// Keep the transport tuple in its native family, but report addresses in + /// the socket's ABI family (including dual-stack IPv4 connections). + fn user_endpoint(&self, mut endpoint: smoltcp::wire::IpEndpoint) -> Endpoint { + if self.ip_version == smoltcp::wire::IpVersion::Ipv6 { + if let smoltcp::wire::IpAddress::Ipv4(addr) = endpoint.addr { + endpoint.addr = smoltcp::wire::IpAddress::Ipv6(addr.to_ipv6_mapped()); + } + } + Endpoint::Ip(endpoint) + } +} + impl Socket for TcpSocket { fn netns(&self) -> Arc { TcpSocket::netns(self) @@ -64,7 +77,7 @@ impl Socket for TcpSocket { fn local_endpoint(&self) -> Result { let inner = self.inner.read(); let inner = inner.as_ref().ok_or(SystemError::ENOTCONN)?; - Ok(Endpoint::Ip(inner.local_endpoint())) + Ok(self.user_endpoint(inner.local_endpoint())) } fn remote_endpoint(&self) -> Result { @@ -72,7 +85,7 @@ impl Socket for TcpSocket { let inner = inner.as_ref().ok_or(SystemError::ENOTCONN)?; inner .remote_endpoint() - .map(Endpoint::Ip) + .map(|endpoint| self.user_endpoint(endpoint)) .ok_or(SystemError::ENOTCONN) } @@ -172,7 +185,10 @@ impl Socket for TcpSocket { } } } - .map(|(sock, ep)| (sock as Arc, Endpoint::Ip(ep))) + .map(|(sock, ep)| { + let endpoint = sock.user_endpoint(ep); + (sock as Arc, endpoint) + }) } fn recv(&self, buffer: &mut [u8], flags: PMSG) -> Result { From 17fe12445ee8c5bbe18ecbd4bc76a31d2bebe8be Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 12 Sep 2026 18:03:44 +0800 Subject: [PATCH 09/10] test(net): cover TCP reuseport admission and lifecycle Add a host harness for the actual TCP port manager and guest regressions for reuseport admission, distribution, address families, readiness and listener lifecycle. --- tools/test_tcp_port_manager.py | 67 +++ user/apps/c_unitest/test_tcp_reuseport.c | 726 +++++++++++++++++++++++ 2 files changed, 793 insertions(+) create mode 100644 tools/test_tcp_port_manager.py create mode 100644 user/apps/c_unitest/test_tcp_reuseport.c diff --git a/tools/test_tcp_port_manager.py b/tools/test_tcp_port_manager.py new file mode 100644 index 0000000000..7fa776d863 --- /dev/null +++ b/tools/test_tcp_port_manager.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Run the real TCP port-table unit tests on the host. + +Only kernel infrastructure is adapted: Mutex uses std, errno is an enum, +and unused process/random helpers have deterministic stand-ins. smoltcp and +hashbrown are the real dependencies. This checks admission/state transitions, +not kernel scheduling, IRQ safety, or syscall integration; use guest tests for +those. The generated Cargo project stays in a temporary directory. +""" + +import json +from pathlib import Path +import subprocess +import sys +import tempfile + + +def main(): + root = Path(__file__).resolve().parents[1] + port = root / "kernel/src/net/socket/inet/common/port.rs" + smoltcp = root / "kernel/submodules/smoltcp" + with tempfile.TemporaryDirectory(prefix="dragonos-tcp-port-") as temp: + project = Path(temp) + (project / "src").mkdir() + (project / "Cargo.toml").write_text( + '[package]\nname = "tcp-port-host-tests"\nversion = "0.1.0"\n' + 'edition = "2021"\n[dependencies]\nhashbrown = "=0.13.2"\n' + f'smoltcp = {{ path = {json.dumps(str(smoltcp))}, ' + 'default-features = false, features = ["std", "medium-ip", ' + '"proto-ipv4", "proto-ipv6", "socket-tcp"] }\n' + ) + (project / "src/lib.rs").write_text( + '''extern crate alloc; +extern crate self as system_error; +#[allow(non_camel_case_types)] +#[derive(Debug, PartialEq, Eq)] +pub enum SystemError { EADDRINUSE, EINVAL } +pub mod arch { pub mod rand { pub fn rand() -> usize { 19 } } } +pub mod libs { pub mod mutex { + #[derive(Debug)] + pub struct Mutex(std::sync::Mutex); + impl Mutex { + pub fn new(value: T) -> Self { Self(std::sync::Mutex::new(value)) } + pub fn lock(&self) -> std::sync::MutexGuard<'_, T> { self.0.lock().unwrap() } + } +} } +pub mod process { + pub struct ProcessManager; + impl ProcessManager { + pub fn current_netns() -> Self { Self } + pub fn local_port_range(&self) -> (u16, u16) { (32768, 60999) } + pub fn set_local_port_range(&self, _: u16, _: u16) -> Result<(), crate::SystemError> { Ok(()) } + } +} +#[derive(Debug)] +pub enum Types { Tcp, Udp, Other } +''' + + f'#[path = {json.dumps(str(port))}]\npub mod port;\n' + ) + return subprocess.run( + ["cargo", "test", "--manifest-path", str(project / "Cargo.toml"), *sys.argv[1:]], + cwd=root, + ).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/user/apps/c_unitest/test_tcp_reuseport.c b/user/apps/c_unitest/test_tcp_reuseport.c new file mode 100644 index 0000000000..f916cadd0c --- /dev/null +++ b/user/apps/c_unitest/test_tcp_reuseport.c @@ -0,0 +1,726 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef SO_REUSEPORT +#define SO_REUSEPORT 15 +#endif + +#define IO_TIMEOUT_MS 1500 +#define TOTAL_TIMEOUT_SEC 30 +#define DISTRIBUTION_SAMPLES 80 + +static int passed; +static int failed; +static int skipped; + +static void fail_errno(const char *what) +{ + fprintf(stderr, " %s: %s\n", what, strerror(errno)); +} + +static void result(const char *name, int ok) +{ + if (ok) { + printf("[PASS] %s\n", name); + passed++; + } else { + printf("[FAIL] %s\n", name); + failed++; + } +} + +static void skip(const char *name, const char *reason) +{ + printf("[SKIP] %s: %s\n", name, reason); + skipped++; +} + +static int set_bool_opt(int fd, int option, int enabled) +{ + return setsockopt(fd, SOL_SOCKET, option, &enabled, sizeof(enabled)); +} + +static void close_fd(int *fd) +{ + if (*fd >= 0) { + close(*fd); + *fd = -1; + } +} + +static int bind_addr(int fd, int family, bool wildcard, uint16_t port) +{ + if (family == AF_INET) { + struct sockaddr_in addr; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(wildcard ? INADDR_ANY : INADDR_LOOPBACK); + return bind(fd, (struct sockaddr *)&addr, sizeof(addr)); + } + + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(port); + addr6.sin6_addr = wildcard ? in6addr_any : in6addr_loopback; + return bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); +} + +static int socket_port(int fd, int family, uint16_t *port) +{ + if (family == AF_INET) { + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + + if (getsockname(fd, (struct sockaddr *)&addr, &len) < 0) + return -1; + *port = ntohs(addr.sin_port); + return 0; + } + + struct sockaddr_in6 addr6; + socklen_t len = sizeof(addr6); + + if (getsockname(fd, (struct sockaddr *)&addr6, &len) < 0) + return -1; + *port = ntohs(addr6.sin6_port); + return 0; +} + +static int make_bound(int family, bool wildcard, uint16_t port, + int reuseaddr, int reuseport, uint16_t *bound_port) +{ + int fd = socket(family, SOCK_STREAM, 0); + + if (fd < 0) + return -1; + if ((reuseaddr && set_bool_opt(fd, SO_REUSEADDR, 1) < 0) || + (reuseport && set_bool_opt(fd, SO_REUSEPORT, 1) < 0) || + bind_addr(fd, family, wildcard, port) < 0 || + (bound_port && socket_port(fd, family, bound_port) < 0)) { + int saved = errno; + close(fd); + errno = saved; + return -1; + } + return fd; +} + +static int connect_loopback(int family, uint16_t port) +{ + int fd = socket(family, SOCK_STREAM, 0); + int flags; + int rc; + + if (fd < 0) + return -1; + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + goto error; + if (bind_addr(fd, family, false, 0) < 0) { + int saved = errno; + close(fd); + errno = saved; + return -1; + } + + if (family == AF_INET) { + struct sockaddr_in addr; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (rc < 0 && errno != EINPROGRESS) + goto error; + } else { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(port); + addr6.sin6_addr = in6addr_loopback; + rc = connect(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + if (rc < 0 && errno != EINPROGRESS) + goto error; + } + if (rc < 0) { + struct pollfd pfd; + socklen_t error_len; + int socket_error = 0; + + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + do { + rc = poll(&pfd, 1, IO_TIMEOUT_MS); + } while (rc < 0 && errno == EINTR); + if (rc == 0) { + errno = ETIMEDOUT; + goto error; + } + if (rc < 0) + goto error; + error_len = sizeof(socket_error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &socket_error, &error_len) < 0) + goto error; + if (socket_error != 0) { + errno = socket_error; + goto error; + } + } + if (fcntl(fd, F_SETFL, flags) < 0) + goto error; + return fd; + +error: { + int saved = errno; + close(fd); + errno = saved; + return -1; + } +} + +/* Returns the listener index, or -1 after a bounded wait. */ +static int accept_selected(const int *listeners, int count, int *accepted) +{ + struct pollfd pfds[4]; + int i; + int ready; + + if (count > (int)(sizeof(pfds) / sizeof(pfds[0]))) { + errno = EINVAL; + return -1; + } + for (i = 0; i < count; i++) { + pfds[i].fd = listeners[i]; + pfds[i].events = POLLIN; + pfds[i].revents = 0; + } + + do { + ready = poll(pfds, count, IO_TIMEOUT_MS); + } while (ready < 0 && errno == EINTR); + if (ready <= 0) { + if (ready == 0) + errno = ETIMEDOUT; + return -1; + } + for (i = 0; i < count; i++) { + if (pfds[i].revents & POLLIN) { + *accepted = accept(listeners[i], NULL, NULL); + return *accepted < 0 ? -1 : i; + } + } + for (i = 0; i < count; i++) + fprintf(stderr, " listener[%d] poll revents=0x%x\n", i, + pfds[i].revents); + errno = EIO; + return -1; +} + +static int one_connection(const int *listeners, int count, int family, + uint16_t port) +{ + int client = -1; + int accepted = -1; + int selected = -1; + + client = connect_loopback(family, port); + if (client < 0) + goto out; + selected = accept_selected(listeners, count, &accepted); +out: + close_fd(&accepted); + close_fd(&client); + return selected; +} + +static int make_reuseport_pair(int family, bool wildcard, int backlog0, + int backlog1, int listeners[2], uint16_t *port) +{ + listeners[0] = make_bound(family, wildcard, 0, 0, 1, port); + if (listeners[0] < 0) + return -1; + listeners[1] = make_bound(family, wildcard, *port, 0, 1, NULL); + if (listeners[1] < 0) + goto error; + if (listen(listeners[0], backlog0) < 0 || listen(listeners[1], backlog1) < 0) + goto error; + return 0; + +error: + close_fd(&listeners[0]); + close_fd(&listeners[1]); + return -1; +} + +static void test_poll_hup_cleared_by_listen(void) +{ + const char *name = "bind/listen clears pre-bind POLLHUP state"; + struct pollfd pfd; + int listener = -1; + uint16_t port = 0; + int rc; + int ok = 0; + + listener = socket(AF_INET, SOCK_STREAM, 0); + if (listener < 0) + goto out; + pfd.fd = listener; + pfd.events = POLLIN; + pfd.revents = 0; + rc = poll(&pfd, 1, 0); + if (rc < 0) + goto out; + printf(" pre-bind poll: rc=%d revents=0x%x\n", rc, pfd.revents); + + if (set_bool_opt(listener, SO_REUSEPORT, 1) < 0 || + bind_addr(listener, AF_INET, false, 0) < 0 || + socket_port(listener, AF_INET, &port) < 0 || listen(listener, 4) < 0) + goto out; + pfd.revents = 0; + rc = poll(&pfd, 1, 0); + printf(" empty-listener poll: rc=%d revents=0x%x\n", rc, pfd.revents); + ok = rc == 0 && pfd.revents == 0; +out: + if (!ok) + fail_errno("poll event transition"); + result(name, ok); + close_fd(&listener); +} + +static bool is_v4_mapped_loopback(const struct sockaddr_storage *storage) +{ + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)storage; + uint32_t v4; + + if (storage->ss_family != AF_INET6 || !IN6_IS_ADDR_V4MAPPED(&addr6->sin6_addr)) + return false; + memcpy(&v4, &addr6->sin6_addr.s6_addr[12], sizeof(v4)); + return v4 == htonl(INADDR_LOOPBACK); +} + +static void test_dualstack_accept_addresses(void) +{ + const char *name = "IPv6 wildcard dual-stack accept returns mapped IPv6 endpoints"; + struct sockaddr_storage accepted_peer = {0}; + struct sockaddr_storage accepted_local = {0}; + struct sockaddr_storage queried_peer = {0}; + struct sockaddr_in client_local = {0}; + socklen_t accepted_peer_len = sizeof(accepted_peer); + socklen_t accepted_local_len = sizeof(accepted_local); + socklen_t queried_peer_len = sizeof(queried_peer); + socklen_t client_local_len = sizeof(client_local); + struct pollfd pfd; + int listener = -1; + int client = -1; + int accepted = -1; + uint16_t port = 0; + int ok = 0; + + listener = make_bound(AF_INET6, true, 0, 0, 1, &port); + if (listener < 0 || listen(listener, 4) < 0) + goto out; + client = connect_loopback(AF_INET, port); + if (client < 0) + goto out; + pfd.fd = listener; + pfd.events = POLLIN; + pfd.revents = 0; + if (poll(&pfd, 1, IO_TIMEOUT_MS) != 1 || !(pfd.revents & POLLIN)) + goto out; + accepted = accept(listener, (struct sockaddr *)&accepted_peer, + &accepted_peer_len); + if (accepted < 0 || + getsockname(accepted, (struct sockaddr *)&accepted_local, + &accepted_local_len) < 0 || + getpeername(accepted, (struct sockaddr *)&queried_peer, + &queried_peer_len) < 0 || + getsockname(client, (struct sockaddr *)&client_local, + &client_local_len) < 0) + goto out; + + ok = accepted_peer_len == sizeof(struct sockaddr_in6) && + accepted_local_len == sizeof(struct sockaddr_in6) && + queried_peer_len == sizeof(struct sockaddr_in6) && + is_v4_mapped_loopback(&accepted_peer) && + is_v4_mapped_loopback(&accepted_local) && + is_v4_mapped_loopback(&queried_peer) && + ((struct sockaddr_in6 *)&accepted_peer)->sin6_port == + client_local.sin_port && + ((struct sockaddr_in6 *)&queried_peer)->sin6_port == + client_local.sin_port && + ((struct sockaddr_in6 *)&accepted_local)->sin6_port == htons(port); +out: + if (!ok) { + fprintf(stderr, + " accepted_len=%u local_len=%u peer_len=%u families=%d/%d/%d\n", + (unsigned)accepted_peer_len, (unsigned)accepted_local_len, + (unsigned)queried_peer_len, accepted_peer.ss_family, + accepted_local.ss_family, queried_peer.ss_family); + fail_errno("dual-stack accepted endpoints"); + } + result(name, ok); + close_fd(&accepted); + close_fd(&client); + close_fd(&listener); +} + +static void test_shared_accept(int family, const char *name) +{ + int listeners[2] = {-1, -1}; + uint16_t port = 0; + int seen[2] = {0, 0}; + int i; + + if (make_reuseport_pair(family, false, 4, 4, listeners, &port) < 0) { + if (family == AF_INET6 && + (errno == EAFNOSUPPORT || errno == EADDRNOTAVAIL || errno == ENOPROTOOPT)) { + skip(name, "IPv6 or SO_REUSEPORT is unavailable"); + return; + } + fail_errno("create reuseport listeners"); + result(name, 0); + return; + } + for (i = 0; i < 48 && (!seen[0] || !seen[1]); i++) { + int selected = one_connection(listeners, 2, family, port); + + if (selected < 0) { + fail_errno("connect/accept"); + break; + } + seen[selected]++; + } + result(name, seen[0] && seen[1]); + close_fd(&listeners[0]); + close_fd(&listeners[1]); +} + +static void test_backlog_not_weight(void) +{ + const char *name = "backlog 1:8 does not weight reuseport distribution"; + int listeners[2] = {-1, -1}; + int count[2] = {0, 0}; + uint16_t port = 0; + int i; + + if (make_reuseport_pair(AF_INET, false, 1, 8, listeners, &port) < 0) { + fail_errno("create backlog listeners"); + result(name, 0); + return; + } + for (i = 0; i < DISTRIBUTION_SAMPLES; i++) { + int selected = one_connection(listeners, 2, AF_INET, port); + + if (selected < 0) { + fail_errno("distribution connect/accept"); + break; + } + count[selected]++; + } + printf(" distribution: backlog=1 %d, backlog=8 %d\n", count[0], count[1]); + result(name, i == DISTRIBUTION_SAMPLES && count[0] >= 20 && count[1] >= 20); + close_fd(&listeners[0]); + close_fd(&listeners[1]); +} + +static void test_reuseaddr_listen_conflict(void) +{ + const char *name = "SO_REUSEADDR permits double bind but rejects second listen"; + int first = -1; + int second = -1; + uint16_t port = 0; + int ok = 0; + + first = make_bound(AF_INET, false, 0, 1, 0, &port); + if (first < 0) + goto out; + second = make_bound(AF_INET, false, port, 1, 0, NULL); + if (second < 0) + goto out; + if (listen(first, 4) < 0) + goto out; + errno = 0; + ok = listen(second, 4) < 0 && errno == EADDRINUSE; +out: + if (!ok) + fail_errno("reuseaddr bind/listen matrix"); + result(name, ok); + close_fd(&first); + close_fd(&second); +} + +static void test_exact_over_wildcard(void) +{ + const char *name = "exact loopback listener wins over wildcard"; + int listeners[2] = {-1, -1}; + uint16_t port = 0; + int i; + int ok = 1; + + listeners[0] = make_bound(AF_INET, true, 0, 0, 1, &port); + if (listeners[0] < 0) + ok = 0; + if (ok) + listeners[1] = make_bound(AF_INET, false, port, 0, 1, NULL); + if (listeners[1] < 0) + ok = 0; + if (ok && (listen(listeners[0], 4) < 0 || listen(listeners[1], 4) < 0)) + ok = 0; + for (i = 0; ok && i < 12; i++) { + if (one_connection(listeners, 2, AF_INET, port) != 1) + ok = 0; + } + if (!ok) + fail_errno("exact/wildcard selection"); + result(name, ok); + close_fd(&listeners[0]); + close_fd(&listeners[1]); +} + +static void test_close_listeners_then_rebind(void) +{ + const char *name = "accepted connection survives listener close and port can rebind"; + int listeners[2] = {-1, -1}; + int client = -1; + int accepted = -1; + int rebound = -1; + uint16_t port = 0; + int selected; + int ok = 0; + + if (make_reuseport_pair(AF_INET, false, 4, 4, listeners, &port) < 0) + goto out; + client = connect_loopback(AF_INET, port); + if (client < 0) + goto out; + selected = accept_selected(listeners, 2, &accepted); + if (selected < 0) + goto out; + close_fd(&listeners[0]); + close_fd(&listeners[1]); + + /* Keep the established pair open, so TIME_WAIT cannot explain a failure. */ + rebound = make_bound(AF_INET, false, port, 0, 1, NULL); + if (rebound < 0 || listen(rebound, 4) < 0) + goto out; + ok = 1; +out: + if (!ok) + fail_errno("close listeners/rebind"); + result(name, ok); + close_fd(&rebound); + close_fd(&accepted); + close_fd(&client); + close_fd(&listeners[0]); + close_fd(&listeners[1]); +} + +static void test_close_one_member(void) +{ + const char *name = "remaining reuseport member accepts after peer close"; + int listeners[2] = {-1, -1}; + uint16_t port = 0; + int ok = 0; + + if (make_reuseport_pair(AF_INET, false, 4, 4, listeners, &port) < 0) + goto out; + close_fd(&listeners[0]); + ok = one_connection(&listeners[1], 1, AF_INET, port) == 0; +out: + if (!ok) + fail_errno("remaining member accept"); + result(name, ok); + close_fd(&listeners[0]); + close_fd(&listeners[1]); +} + +static void test_shutdown_relisten(void) +{ + const char *name = "shutdown releases old port and re-listen uses a new port"; + int listener = -1; + int rebound = -1; + int old_client = -1; + int new_client = -1; + int accepted = -1; + uint16_t old_port = 0; + uint16_t new_port = 0; + int ok = 0; + + listener = make_bound(AF_INET, false, 0, 0, 1, &old_port); + if (listener < 0 || listen(listener, 4) < 0) + goto out; + if (shutdown(listener, SHUT_RDWR) < 0 || listen(listener, 4) < 0) + goto out; + if (socket_port(listener, AF_INET, &new_port) < 0 || + new_port == 0 || new_port == old_port) + goto out; + + errno = 0; + old_client = connect_loopback(AF_INET, old_port); + if (old_client >= 0 || errno != ECONNREFUSED) + goto out; + new_client = connect_loopback(AF_INET, new_port); + if (new_client < 0 || accept_selected(&listener, 1, &accepted) != 0) + goto out; + + rebound = make_bound(AF_INET, false, old_port, 0, 0, NULL); + if (rebound < 0 || listen(rebound, 4) < 0) + goto out; + ok = 1; +out: + if (!ok) + fail_errno("shutdown/re-listen"); + result(name, ok); + close_fd(&rebound); + close_fd(&accepted); + close_fd(&new_client); + close_fd(&old_client); + close_fd(&listener); +} + +static int dynamic_option_case(int before, int after, bool expect_peer) +{ + int first = -1; + int second = -1; + uint16_t port = 0; + int ok = 0; + + first = make_bound(AF_INET, false, 0, 0, before, &port); + if (first < 0) + goto out; + if (set_bool_opt(first, SO_REUSEPORT, after) < 0) + goto out; + second = make_bound(AF_INET, false, port, 0, 1, NULL); + if (second < 0) { + ok = !expect_peer && errno == EADDRINUSE; + goto out; + } + if (listen(first, 4) < 0) { + ok = !expect_peer && errno == EADDRINUSE; + goto out; + } + errno = 0; + if (listen(second, 4) == 0) + ok = expect_peer; + else + ok = !expect_peer && errno == EADDRINUSE; +out: + if (!ok) + fail_errno("dynamic SO_REUSEPORT admission"); + close_fd(&first); + close_fd(&second); + return ok; +} + +static void test_dynamic_options(void) +{ + result("post-bind SO_REUSEPORT 0->1 admits a reuseport peer", + dynamic_option_case(0, 1, true)); + result("post-bind SO_REUSEPORT 1->0 rejects peer at bind/listen", + dynamic_option_case(1, 0, false)); +} + +static void test_different_uid(void) +{ + const char *name = "different effective UID cannot join reuseport group"; + int listener = -1; + uint16_t port = 0; + pid_t child; + int status; + + if (geteuid() != 0) { + skip(name, "requires root to create a child with a different effective UID"); + return; + } + listener = make_bound(AF_INET, false, 0, 0, 1, &port); + if (listener < 0 || listen(listener, 4) < 0) { + fail_errno("create parent listener"); + result(name, 0); + close_fd(&listener); + return; + } + child = fork(); + if (child < 0) { + fail_errno("fork"); + result(name, 0); + close_fd(&listener); + return; + } + if (child == 0) { + int peer; + + if (setuid(65534) < 0) + _exit(2); + peer = make_bound(AF_INET, false, port, 0, 1, NULL); + if (peer < 0) + _exit(errno == EADDRINUSE || errno == EACCES ? 0 : 3); + if (listen(peer, 4) < 0) + _exit(errno == EADDRINUSE || errno == EACCES ? 0 : 4); + close(peer); + _exit(1); + } + while (waitpid(child, &status, 0) < 0 && errno == EINTR) + ; + if (WIFEXITED(status) && WEXITSTATUS(status) == 2) + skip(name, "setuid(65534) is unavailable in this environment"); + else + result(name, WIFEXITED(status) && WEXITSTATUS(status) == 0); + close_fd(&listener); +} + +static void timeout_handler(int signo) +{ + (void)signo; + static const char message[] = "[FAIL] global test timeout\n"; + ssize_t ignored; + + ignored = write(STDERR_FILENO, message, sizeof(message) - 1); + (void)ignored; + _exit(124); +} + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("[START] TCP SO_REUSEPORT syscall regression\n"); + signal(SIGALRM, timeout_handler); + alarm(TOTAL_TIMEOUT_SEC); + + test_poll_hup_cleared_by_listen(); + test_dualstack_accept_addresses(); + test_shared_accept(AF_INET, "IPv4 reuseport members both accept"); + test_shared_accept(AF_INET6, "IPv6 reuseport members both accept"); + test_backlog_not_weight(); + test_reuseaddr_listen_conflict(); + test_exact_over_wildcard(); + test_close_listeners_then_rebind(); + test_close_one_member(); + test_shutdown_relisten(); + test_dynamic_options(); + test_different_uid(); + + alarm(0); + printf("Summary: PASS=%d FAIL=%d SKIP=%d\n", passed, failed, skipped); + return failed ? EXIT_FAILURE : EXIT_SUCCESS; +} From d6658cd02e9c1eca2c78f274ce2f055814f08d9e Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 12 Sep 2026 18:04:38 +0800 Subject: [PATCH 10/10] docs(net): document TCP reuseport design and validation Record the logical listener design, implementation plan, pre- and post-rebase validation, and paired atomic commits with their delivery boundaries. --- docs/development/net-reuseport-1848-design.md | 127 ++++++++++++++ docs/development/net-reuseport-1848-plan.md | 58 +++++++ .../net-reuseport-1848-validation.md | 160 ++++++++++++++++++ 3 files changed, 345 insertions(+) create mode 100644 docs/development/net-reuseport-1848-design.md create mode 100644 docs/development/net-reuseport-1848-plan.md create mode 100644 docs/development/net-reuseport-1848-validation.md diff --git a/docs/development/net-reuseport-1848-design.md b/docs/development/net-reuseport-1848-design.md new file mode 100644 index 0000000000..6d8f824436 --- /dev/null +++ b/docs/development/net-reuseport-1848-design.md @@ -0,0 +1,127 @@ +# TCP SO_REUSEPORT 续作设计 + +状态:用户于 2026-09-12 确认,实现与本轮验收完成,已拆分为本地语义提交,未推送。 +接续 Claude 会话 `#1848 SO_REUSEPORT`。结果见 [验证记录](net-reuseport-1848-validation.md)。 + +## 续作开始时的基线 + +- DragonOS HEAD:`f6a98466`,分支 `feat/net-reuseport-1848`。 +- smoltcp 已作为 `kernel/submodules/smoltcp` 子模块引入,当时提交为 + `3933571d212b5c10cca7222c2e9a20fb2050870b`。 +- 本次实际执行 `make kernel`,失败于 `stream/inner.rs:61,363`: + `E0599: no method named set_reuseport`。完整日志在 + `/tmp/reuseport-1848-baseline-build.log`,该临时文件不属于交付物。 +- 续作前的三个未提交文件是 `common/port.rs`、`stream/inner.rs`、 + `stream/lifecycle.rs`;其现有注释与格式修改应保留。 +- 上次最后结论是 opaque logical listener ID 候选方案;独立架构评审中断, + 旧计划尚未按该方案更新。不能把旧计划直接当成已经验证的实现规格。 + +## 推荐边界 + +DragonOS 管理 Linux socket 语义:端口所有权、socket owner UID、地址及协议族、 +bind/listen admission、选项变化、监听组成员和关闭过程。 +smoltcp 管理包匹配与处理:已连接四元组优先、地址匹配层级、逻辑 listener 选择、 +选中 listener 内的空闲 slot,以及队列满时的静默丢弃。 + +不采用只放宽 bind 的方案,因为当前 smoltcp TCP ingress 按 SocketSet 遍历顺序 +命中第一个 socket。也不把 Linux UID、SO_REUSEPORT 标志或进程身份带入 smoltcp。 + +### 1. 稳定端口身份 + +为每个成功 binding 分配不随 smoltcp handle 变化的 `TcpBindId`。 +将 `Init::Bound` 的 tuple 改为具名绑定状态,让端口所有权独立于 backlog slot。 +`accept()` 替换 slot、跨接口展开均保留同一身份。shutdown 回退 Bound 时, +显式端口保留绑定;自动分配端口释放绑定,再 listen/connect 时取得新身份与端口。 +每个 binding 的端口记录只释放一次。 + +PortManager 分开记录 Bound 与 Listening 状态。在同一锁域内检查冲突并保留 +listen admission;slot 构建失败必须回滚保留状态并释放新增 handle。 +跨层锁顺序应沿用现有 poll 约束:不在持有端口表锁时再获取 SocketSet 锁。 + +### 2. 通用 listener 选择 + +一个用户 listener 的全部 slots 共享 opaque `listen_id`;新增 slot 不增加选路权重。 +ID 在 pending connection 阶段保留,用于判断选中 listener 是否已经没有空闲 slot; +accept 交付连接时清除其 listener 身份,replacement slot 继承原 ID。 +显式关闭和失败回滚清理该身份;内部 pending RST/超时需要保留逻辑监听意图并及时补槽, +否则同一批收包中的下一个 SYN 会看到监听器短暂消失。 + +fresh SYN 的分发顺序: + +1. 先搜索已有连接/半连接的完整四元组,保证重传回到原 socket。 +2. 查找匹配协议族、目的地址和端口的 listener;优先级为 exact address、 + 指定协议族的 wildcard、未指定协议族的 wildcard。DragonOS 默认 IPv6 `::` + 监听使用最后一种形式保持双栈;IPv4 `0.0.0.0` 使用 IPv4 wildcard。 +3. 在该地址层按逻辑 ID 做带种子的 rendezvous hash;相同 ID 的重复 slots + 得到相同分数,不需要为去重分配 Vec。平分按 ID 决定,不能依赖遍历顺序。 +4. 只在选中的逻辑 listener 内找空闲 LISTEN slot。 +5. 已匹配但没有空闲 slot 时静默丢 SYN;没有 listener 时保持现有 RST 行为。 + +公共入口当前返回 `Option`,已经可以通过直接返回 `None` 表达静默处理; +实现时可在内部使用选择结果枚举,区分 no-match 与 full,避免扩大公共返回类型。 +未配置逻辑 ID 的普通 smoltcp 使用者需要保留原有处理能力;不能要求所有下游 +调用者先接入 DragonOS 的 listener 模型。混合配置的精确规则须用包级测试固定。 + +选中 listener 满时不转投其他成员。种子属于通用分发机制,由调用方配置; +不能以每个 slot 的独立随机数代替逻辑成员身份。 + +实施中增补通用 `listener_enabled` 标记:DragonOS 在持有 SocketSet 锁、进入 poll 前 +刷新当前可选成员,smoltcp 保留 ID 并只对 fresh SYN 应用标记。它不影响已有四元组。 +这使 DragonOS 能区分当前 REUSEPORT 值与既有组成员:关闭选项并不总是退出已有组。 +IPv4 hash 链头插入与 IPv6 reuseport 链尾插入由 DragonOS 记录;smoltcp 不解释这些策略。 + +### 3. 移除端口级丢 SYN 旁路 + +在 smoltcp 的 full/no-match 测试通过后,移除 `tcp_listener_backlog.rs` 以及 +接口注册、刷新和收包检查的调用链。当前旁路只按端口计数,不能表示独立成员 +或 exact/wildcard 层级;仅加引用计数仍不足以支撑选中成员队列满的语义。 + +## 对旧计划的语义修正 + +旧计划将 option 永久冻结为 bind-time snapshot,并断言 bind 后启用 reuseport +仍必然使后续 bind 失败。此断言已被本机 Linux 行为反驳,不能据此写回归测试。 + +本次在 `6.6.87.2-microsoft-standard-WSL2` 的 IPv4 loopback 上运行探针: +第一个 socket 不设置 REUSEADDR,以 bind(0) 获取端口;分别在 Bound 或 Listening +阶段将 REUSEPORT 从 0 改为 1、从 1 改为 0;第二个启用 REUSEPORT 的 socket +均成功 bind 和 listen。只验证了这组调用序列,未验证这些组合的连接分流。 + +Linux 6.6 的动态字段、端口桶 fastreuseport 缓存与实际监听组是不同状态: +不能由该探针推导为“只同步一个 bool 即兼容 Linux”。实施 admission 前应补齐 +显式端口、选项修改时机、成员加入/退出、UID 和连接分发的矩阵,分别对应源码路径。 + +2026-09-12 的补充探针与实现约束: + +- IPv4 已有 A、B 同组,停用 A 后两者仍分流;停用较晚 listen 的 B 后仅 B 接收。 + IPv6 reuseport 链尾插入使这组顺序相反。 +- strict fastreuse 缓存仅允许缓存地址一侧通配;不能因新绑定为 wildcard 跳过其他独占绑定。 +- bind(0) 的 listener shutdown 后 getsockname 暂保留原端口,再 listen 分配新端口; + 显式绑定非零端口则保留绑定。二者在 IPv4/IPv6、exact/wildcard 探针中均已验证。 +- accept 在同一 SocketSet 锁内复核连接状态、快照两端地址、清除旧 slot 身份并 + 发布 replacement,避免 peer RST 与交付竞争;仅 Established/CloseWait 可交付。 +- listen 成功后立即重建 readiness;监听状态按队列状态写入完整事件掩码, + 避免继承未连接状态的 HUP,导致空监听队列的 poll 提前返回。 +- 用户可见端点按 socket 协议族输出:AF_INET6 socket 接收 IPv4 连接时, + accept/getsockname/getpeername 将地址映射为 `::ffff:` 形式,传输四元组不变。 + +参考: + +- [Linux 6.6 inet_connection_sock.c](https://raw.githubusercontent.com/torvalds/linux/v6.6/net/ipv4/inet_connection_sock.c): + `inet_bind_conflict`、`sk_reuseport_match`、`inet_csk_update_fastreuse`。 +- [Linux 6.6 inet_hashtables.c](https://raw.githubusercontent.com/torvalds/linux/v6.6/net/ipv4/inet_hashtables.c): + `inet_reuseport_add_sock`、`__inet_hash`、`inet_unhash`。 + +## 实施与验收顺序 + +1. 用 Linux 行为探针校正 admission 与动态 option 的预期,不复用旧计划中的错误断言。 +2. smoltcp:增加逻辑 listener API 和包级测试,覆盖 IPv4/IPv6、四元组优先、 + 地址优先、slot 数量不影响权重、full 静默丢弃和未监听 RST。 +3. DragonOS:稳定 binding 身份、admission 与回滚、listener slots 生命周期, + 替换当前不存在的 `set_reuseport()` 调用。 +4. 接入新 lookup 后删除端口级旁路;验证 accept 后关闭、成员关闭、shutdown 后 + 再 listen、动态 option、不同 UID、跨接口 wildcard 和失败回滚。 +5. 在 `user/apps/c_unitest` 增加系统调用回归,先 Linux 对照,再 DragonOS guest; + 执行 `make kernel`、相关 gVisor 测例和 QEMU 启动检查。 + +子模块改动与 DragonOS consumer 必须成对交付。未经实际构建及 guest 验证, +不把设计、smoltcp 单元测试或历史运行结果视为 SO_REUSEPORT 已完成。 diff --git a/docs/development/net-reuseport-1848-plan.md b/docs/development/net-reuseport-1848-plan.md new file mode 100644 index 0000000000..375bd689d6 --- /dev/null +++ b/docs/development/net-reuseport-1848-plan.md @@ -0,0 +1,58 @@ +# TCP SO_REUSEPORT Implementation Plan + +> **For agentic workers:** 使用 superpowers:subagent-driven-development,按文件所有权拆分实现并集成审查。 + +**Goal:** 实现 TCP 逻辑 listener 分发和稳定绑定生命周期。 +**Architecture:** DragonOS 管理 Linux admission,smoltcp 管理 opaque listener ID 与包匹配。 +**Tech Stack:** Rust、smoltcp 0.12、C syscall tests、Linux 6.6、QEMU。 +**Spec:** [已确认设计](net-reuseport-1848-design.md)。 + +## Global Constraints + +- 保留已有未提交改动;不推送、不重写历史。 +- 不按 backlog slot 数量加权;full 不 fallback;exact tuple/address 优先。 +- 动态 option 预期以 Linux 6.6 探针和源码校正。 +- 子模块及 consumer 同时验证,不以 host tests 替代 guest 验收。 + +## Task 1: smoltcp listener lookup + +Files: `kernel/submodules/smoltcp/src/socket/tcp.rs`、`src/iface/interface/tcp.rs` 及包级测试。 +Interface: `tcp::Socket::set_listener_id(Option)` / `listener_id() -> Option`。 +调用方在 `listen()` 成功后设置 ID,在 accept 交付前清除;pending 保留。 +hash 使用 Interface 的稳定随机种子,重复 ID 不增加权重。 + +- [x] 写包级失败测试:四元组/地址优先、等权、full 丢弃、RST、IPv4/IPv6。 +- [x] 执行测试确认当前 first-match 行为不能满足测试。 +- [x] 实现三阶段匹配与 metadata 生命周期,保留无 ID 调用方。 +- [x] 执行 smoltcp 单元测试,审查实现及 consumer 接口。 + +## Task 2: DragonOS binding 与 listener 生命周期 + +Files: `kernel/src/net/socket/inet/common/port.rs`、`common/mod.rs`、`stream/*.rs`。 +Interface: 不随 slot 替换变化的 binding ID;端口记录分开保存当前选项、owner UID 和监听状态。 + +- [x] 扩展 Linux 对照矩阵,确认动态选项与不同 UID 的预期。 +- [x] 增加 admission/生命周期测试,暴露现有 record 与 handle 绑定的问题。 +- [x] 实现稳定 binding 身份、Bound/Listening admission、选项同步、失败回滚。 +- [x] 集成 Task 1 API,accept 清除旧 ID,replacement 继承 ID。 +- [x] 删除 `tcp_listener_backlog.rs` 及所有注册、刷新、收包旁路。 +- [x] `make kernel` 验证所有调用路径。 + +## Task 3: syscall 回归与集成验收 + +Files: `user/apps/c_unitest/test_tcp_reuseport.c`。 + +- [x] 编写可在 Linux 与 DragonOS 运行的有超时回归:共享/拒绝矩阵、分发、地址优先、 + accept 后关闭重绑、成员关闭、shutdown、IPv4/IPv6、动态 option。 +- [x] Linux 上编译运行作为预期校验。 +- [x] 执行 DragonOS guest 测试、相关 gVisor 与启动检查:C 13 项、目标 gVisor 6 项通过。 +- [x] 对子模块和主仓库做最终审查,记录通过项与实际环境阻塞。 + +## 执行记录 + +- 2026-09-12:现有隔离工作树验证完成;接口与文件边界无冲突。 +- Task 1 只写 smoltcp 子模块;Task 2 只写 DragonOS 内核;Task 3 只写 C 测试。 +- 2026-09-12:实现和本轮验收完成;详见 [验证记录](net-reuseport-1848-validation.md)。 + smoltcp 614 项、端口表 16 项通过,内核编译成功。该阶段尚未提交或推送。 +- 2026-09-12:按用户要求将 smoltcp 拆为 2 个、主仓库拆为 5 个语义提交; + 包含实现、独立修复、回归与文档,保留 stash,未推送。提交明细见验证记录。 diff --git a/docs/development/net-reuseport-1848-validation.md b/docs/development/net-reuseport-1848-validation.md new file mode 100644 index 0000000000..e672c794d4 --- /dev/null +++ b/docs/development/net-reuseport-1848-validation.md @@ -0,0 +1,160 @@ +# TCP SO_REUSEPORT 验证记录 + +## 原子提交整理 + +2026-09-12:以主仓库 `7edf3e48` 和 smoltcp `ebeaec6` 为基线拆分本地提交。 + +| 仓库 | 提交 | 语义 | +| --- | --- | --- | +| smoltcp | `97ba313` | 逻辑监听器身份、可选状态及生命周期 | +| smoltcp | `3e0cfb4` | 地址优先级、按流选择逻辑监听器及包级回归 | +| DragonOS | `8d32371a` | 网络命名空间 TCP 绑定、reuseport 监听组、accept 切换及配套子模块引用 | +| DragonOS | `fc23cb75` | 进入监听状态时清理旧就绪事件 | +| DragonOS | `19fa252f` | 双栈 TCP 的 IPv4-mapped IPv6 地址返回 | +| DragonOS | `17fe1244` | 端口管理器主机测试和 TCP reuseport 用户态回归 | + +设计、计划和本验证记录作为最后一个独立文档提交。 +子模块提交保存在本地分支 `feat/tcp-logical-listeners-1848`,主仓库功能提交引用 `3e0cfb4`。 +主仓库与子模块的原 stash 均保留,未推送。 + +整理时逐文件 SHA256 对比初始快照,全部实现与测试文件内容保持一致; +仅文档补充提交状态。因此此前的内核构建及 `make test-syscall` 129/129 结果仍对应 +同一份实现,本轮未重复启动 QEMU。 + +- smoltcp 第一个提交的独立暂存快照:635 项通过。 +- smoltcp 最终提交使用独立 Cargo target 目录重新构建:645 项通过。 +- 日志:`/tmp/reuseport-smol-stage1-test.log`、 + `/tmp/reuseport-atomic-smoltcp-final-test.log`。 + +## Rebase 后的 stash 恢复验证 + +2026-09-12:在 `7edf3e48`(已 rebase 到 `master`)上恢复 stash 并解决冲突。 +保留上游 UDP 独立绑定表、网络驱动模块拆分、路由发布租约和 Closed 清理; +将 reuseport 的选择刷新迁移到新 poll/NAPI 路径,删除旧 backlog 旁路。 + +smoltcp 更新到上游内核依赖的 `ebeaec612ccdd36239588e4d8987fc14d623a52b`, +并恢复原有 reuseport 修改。子模块恢复备份 stash `5d8f1c9` 保留,主仓库 +原始 stash 也保留。 + +- `make kernel`:通过;内核 SHA256 + `48358ffdef8f33f0ac6a28acfa6d68cd5ece6af60aabd2e34b2983e25a8d7673`。 +- 端口表:16 项通过;smoltcp:645 项通过,no_std 检查通过。 +- 编译日志 `/tmp/reuseport-post-rebase-kernel.log`;端口测试日志 + `/tmp/reuseport-post-rebase-port.log`;smoltcp 日志 + `/tmp/reuseport-smoltcp-ebeaec6-full.log`。 +- 按用户要求使用仓库标准入口 `make test-syscall`,通过 `qemu-nographic`、 + KVM、Ubuntu rootfs 执行默认白名单:**129/129 个测试程序通过**,命令退出码 0, + 监控报告耗时 1033 秒。完整日志 `/tmp/reuseport-make-test-syscall.log`。 + 该流程重新构建的实际测试内核 SHA256 为 + `280b0644f03e35cb02cedcd52f44d8da6ea9cdeecc443ca0f05da58a8686338d`。 +- 标准监控脚本已终止 QEMU,`config/app-blocklist.toml` 已恢复。 + 所有 stash 冲突已标记解决,`git diff --cached --check` 通过;该阶段尚未提交或推送。 +- 默认白名单不包含 `socket_inet_loopback_test`。本轮未重跑前述专项 C13/10k; + 两次手动启动均未执行到 C13,不计为测试结果。下文为 rebase 前历史验收。 + +## Rebase 前的实现验收 + +日期:2026-09-12。主仓库基线 `f6a98466`,smoltcp 基线 +`3933571d212b5c10cca7222c2e9a20fb2050870b`,当时均包含本次未提交修改。 + +## 主机测试 + +```sh +python3 tools/test_tcp_port_manager.py --offline +cargo test --manifest-path kernel/submodules/smoltcp/Cargo.toml --lib +cargo check --manifest-path kernel/submodules/smoltcp/Cargo.toml \ + --no-default-features --features alloc,medium-ip,socket-tcp,proto-ipv4,proto-ipv6 +make kernel +``` + +- 实际 `port.rs` 的主机适配测试:16 通过,覆盖 UID、地址冲突、动态选项、 + fastreuse 缓存、监听组顺序与生命周期。适配层不验证内核锁/IRQ/调度。 +- smoltcp:614 通过,其中逻辑 listener 回归 11 项;no_std 配置检查通过。 + 覆盖四元组优先、三层地址优先级、重复 slot 不加权、full 不 fallback、 + disabled 成员、pending RST/timeout 恢复及显式关闭不复活。 +- `make kernel`:通过;基线缺失 `set_reuseport()` 的编译错误已消除。 +- C 回归在 Linux `6.6.87.2-microsoft-standard-WSL2` 上:12 通过、0 失败、 + 1 跳过;唯一跳过是非 root 环境下的不同 UID 用例。 + +```sh +gcc -std=c11 -Wall -Wextra -Werror -O2 \ + user/apps/c_unitest/test_tcp_reuseport.c -o /tmp/test_tcp_reuseport +timeout 40s /tmp/test_tcp_reuseport +``` + +## Guest 执行方式 + +在 QEMU TCG 中启动构建出的内核,使用已有 FAT32 rootfs 的临时副本及 +`-snapshot`。C 程序使用项目的 musl 交叉编译器静态编译,置于 +`/opt/tests/test_tcp_reuseport`。直接指定该程序为 init,避免交互串口丢字符。 +程序成功退出后内核会记录 PID 1 `group_exit code 0`,这是此运行方式的退出日志。 + +镜像内外二进制 SHA256 已核对一致:C 程序为 +`482320ef1d2e19bf0ec06139c3a1abfddc4c3b41a61c14f76caf7b56451a84e1`; +gVisor 为 `4f8d3402d4d1e1b93e73a0d9aa94906d6e1f8fd0823e9eb6ea27374ed3032ae9`。 +本记录不将本地 gVisor 源码提交等同于预置二进制的构建版本。 + +gVisor 的 init 参数必须放在内核命令行分隔符 `--` 后,例如: + +```text +rw init=/opt/tests/gvisor/tests/socket_inet_loopback_test console=/dev/hvc0 -- --gtest_filter=*TcpPortReuseMultiThread* +``` + +使用 musl 是本次已有 rootfs 的运行要求:静态 glibc 版本在启动阶段的 brk/TLS +路径异常退出,未执行任何 socket 测试,不能计为 reuseport 测试结果。 + +## 集成结果 + +内核 SHA256 `95e35830cb113a839c0ee56fe7367fa9574ee0e6f9ad093ffe34d6e997b08f5c`: +C 回归 **12 通过、0 失败、0 跳过**,包括不同有效 UID 的隔离。 +backlog 1:8 的 80 次连接分布为 34:46。 + +先前 guest 明确复现空 listener 的 poll 返回 `POLLHUP (0x10)`;修复后空队列 +`poll` 返回 0、事件位为 0,shutdown/relisten 也通过。完整临时日志: +`/tmp/reuseport-poll2-serial.log`(RED)、`/tmp/reuseport-final-c-serial.log`(GREEN)。 + +加入 IPv4-mapped 用户可见端点修复后,最终内核 SHA256 +`3f11d9313bd9c1f19f9da3de9abad92a2bc52d0032b543a5c7d095c01564266a`: +C 回归 **13 通过、0 失败、0 跳过**,分布 40:40。新增用例验证 IPv6 `::` +接收 IPv4 连接时,accept/getsockname/getpeername 返回 AF_INET6、正确的 +sockaddr_in6 长度、mapped loopback 地址及两端端口。 +日志:`/tmp/reuseport-final3-c-serial.log`。 + +gVisor 短回归 **5 通过、0 失败**(同一最终内核,guest 64 ms): + +```text +AllFamilies/SocketMultiProtocolInetLoopbackTest.DualStackV6Any*/TCP:AllFamilies/SocketMultiProtocolInetLoopbackTest.NoReusePortFollowingReusePort/TCP +``` + +包括 IPv6 `::` 的独占预留、REUSEADDR 下的绑定/监听差异,以及非 REUSEPORT +后续成员拒绝;完整日志 `/tmp/reuseport-short-gvisor-serial.log`。 + +多线程 gVisor **1 通过、0 失败**,guest 测试耗时 650892 ms: + +```text +All/SocketInetReusePortTest.TcpPortReuseMultiThread/ListenV4Any_ConnectV4Loopback +``` + +10,000 次连接全部完成,三个 listener 的分流比例断言和 shutdown 后线程退出 +均通过;日志 `/tmp/reuseport-long-gvisor-serial.log`。本次未运行其他四种地址 +参数的 10,000 连接用例;IPv6 与默认双栈另由 C 回归及上述短 gVisor 覆盖。 + +预置二进制在主机 Linux 上执行单个 +`*TcpPortReuseMultiThread/ListenV4Any_ConnectV4Loopback` 已通过(3308 ms)。 +该用例有 3 个 backlog=40 的 listener 和 10,000 次串行连接,无中间进度输出; +首次 guest 运行 60 秒后停止,只能记为未完成,不能据此判断死锁。 +随后在同一最终内核中持续运行,通过 GDB 读取已有 `TcpBindId::new::NEXT_ID` +观察到 1250、1555、2501、4131、5751、6804、8322、9304 的连续增长, +且 CPU 位于网络处理/通知路径,最终完整通过。未为取证加入高频日志或修改内核。 + +## 最终检查 + +- 主仓库和 smoltcp 子模块 `git diff --check` 均通过。 +- 最终定向审查提出的 accept 地址 family 问题已修复并通过 guest 回归。 +- 运行验收对应上述最终内核 SHA256;最后一次构建后只有测试/文档修改。 + +## 交付边界 + +主仓库 consumer 与 smoltcp 子模块修改必须成对交付。现已按下述语义边界创建本地提交,未推送。 +上述测试不能证明完整 Linux 网络语义兼容;未扩展 UDP reuseport 或 TCP IPV6_V6ONLY。 +临时日志用于本次现场取证,复现以代码中的回归测试和以上命令为准。