diff --git a/kernel/src/ipc/fd.rs b/kernel/src/ipc/fd.rs index 284529f6..4442d752 100644 --- a/kernel/src/ipc/fd.rs +++ b/kernel/src/ipc/fd.rs @@ -86,6 +86,15 @@ pub enum FdKind { PipeWrite(Arc>), /// UDP socket (wrapped in Arc> for sharing and dup/fork) UdpSocket(Arc>), + /// TCP socket (unbound, or bound but not connected/listening) + /// The u16 is the bound local port (0 if unbound) + TcpSocket(u16), + /// TCP listener (bound and listening socket) + /// The u16 is the listening port + TcpListener(u16), + /// TCP connection (established connection) + /// Contains the connection ID for lookup in the global TCP connection table + TcpConnection(crate::net::tcp::ConnectionId), /// Regular file descriptor #[allow(dead_code)] // Will be constructed when open() is fully implemented RegularFile(Arc>), @@ -104,6 +113,9 @@ impl core::fmt::Debug for FdKind { FdKind::PipeRead(_) => write!(f, "PipeRead"), FdKind::PipeWrite(_) => write!(f, "PipeWrite"), FdKind::UdpSocket(_) => write!(f, "UdpSocket"), + FdKind::TcpSocket(port) => write!(f, "TcpSocket(port={})", port), + FdKind::TcpListener(port) => write!(f, "TcpListener(port={})", port), + FdKind::TcpConnection(id) => write!(f, "TcpConnection({:?})", id), FdKind::RegularFile(_) => write!(f, "RegularFile"), FdKind::Directory(_) => write!(f, "Directory"), FdKind::Device(dt) => write!(f, "Device({:?})", dt), @@ -407,6 +419,20 @@ impl Drop for FdTable { // Socket cleanup handled by UdpSocket::Drop when Arc refcount reaches 0 log::debug!("FdTable::drop() - releasing UDP socket fd {}", i); } + FdKind::TcpSocket(_) => { + // Unbound TCP socket doesn't need cleanup + log::debug!("FdTable::drop() - releasing TCP socket fd {}", i); + } + FdKind::TcpListener(port) => { + // Remove from listener table + crate::net::tcp::TCP_LISTENERS.lock().remove(&port); + log::debug!("FdTable::drop() - closed TCP listener fd {} on port {}", i, port); + } + FdKind::TcpConnection(conn_id) => { + // Close the TCP connection + let _ = crate::net::tcp::tcp_close(&conn_id); + log::debug!("FdTable::drop() - closed TCP connection fd {}", i); + } FdKind::StdIo(_) => { // StdIo doesn't need cleanup } diff --git a/kernel/src/ipc/poll.rs b/kernel/src/ipc/poll.rs index e3dcd377..987d41dd 100644 --- a/kernel/src/ipc/poll.rs +++ b/kernel/src/ipc/poll.rs @@ -149,6 +149,46 @@ pub fn poll_fd(fd_entry: &FileDescriptor, events: i16) -> i16 { revents |= events::POLLIN; } } + FdKind::TcpSocket(_) => { + // Unconnected TCP socket - always writable (for connect attempt) + if (events & events::POLLOUT) != 0 { + revents |= events::POLLOUT; + } + } + FdKind::TcpListener(port) => { + // Listening socket - check for pending connections + if (events & events::POLLIN) != 0 { + if crate::net::tcp::tcp_has_pending(*port) { + revents |= events::POLLIN; + } + } + } + FdKind::TcpConnection(conn_id) => { + // Connected socket - check for data and connection state + let connections = crate::net::tcp::TCP_CONNECTIONS.lock(); + if let Some(conn) = connections.get(conn_id) { + // Check for readable data + if (events & events::POLLIN) != 0 { + if !conn.rx_buffer.is_empty() { + revents |= events::POLLIN; + } + } + // Check for writable + if (events & events::POLLOUT) != 0 { + if conn.state == crate::net::tcp::TcpState::Established { + revents |= events::POLLOUT; + } + } + // Check for connection closed + if conn.state == crate::net::tcp::TcpState::Closed || + conn.state == crate::net::tcp::TcpState::CloseWait { + revents |= events::POLLHUP; + } + } else { + // Connection not found - error + revents |= events::POLLERR; + } + } } revents diff --git a/kernel/src/main.rs b/kernel/src/main.rs index ed2935f1..f71bf1a9 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -568,6 +568,20 @@ fn kernel_main_continue() -> ! { } } } + + // Launch TCP socket test to verify TCP syscalls from userspace + { + serial_println!("RING3_SMOKE: creating tcp_socket_test userspace process"); + let tcp_test_buf = crate::userspace_test::get_test_binary("tcp_socket_test"); + match process::creation::create_user_process(String::from("tcp_socket_test"), &tcp_test_buf) { + Ok(pid) => { + log::info!("Created tcp_socket_test process with PID {}", pid.as_u64()); + } + Err(e) => { + log::error!("Failed to create tcp_socket_test process: {}", e); + } + } + } }); } diff --git a/kernel/src/net/ipv4.rs b/kernel/src/net/ipv4.rs index 4f126f3a..b59014da 100644 --- a/kernel/src/net/ipv4.rs +++ b/kernel/src/net/ipv4.rs @@ -158,8 +158,8 @@ impl<'a> Ipv4Packet<'a> { pub fn handle_ipv4(eth_frame: &EthernetFrame, ip: &Ipv4Packet) { let config = super::config(); - // Check if this packet is for us - if ip.dst_ip != config.ip_addr { + // Check if this packet is for us (accept our IP or loopback addresses) + if ip.dst_ip != config.ip_addr && ip.dst_ip[0] != 127 { // Not for us, ignore (we don't do routing) return; } @@ -175,8 +175,7 @@ pub fn handle_ipv4(eth_frame: &EthernetFrame, ip: &Ipv4Packet) { } } PROTOCOL_TCP => { - // TCP not implemented yet - log::debug!("IPv4: Received TCP packet (not implemented)"); + super::tcp::handle_tcp(ip, ip.payload); } PROTOCOL_UDP => { super::udp::handle_udp(ip, ip.payload); diff --git a/kernel/src/net/mod.rs b/kernel/src/net/mod.rs index ae683bc0..d7324031 100644 --- a/kernel/src/net/mod.rs +++ b/kernel/src/net/mod.rs @@ -12,6 +12,7 @@ pub mod arp; pub mod ethernet; pub mod icmp; pub mod ipv4; +pub mod tcp; pub mod udp; use alloc::vec::Vec; @@ -228,8 +229,8 @@ pub fn send_ethernet(dst_mac: &[u8; 6], ethertype: u16, payload: &[u8]) -> Resul pub fn send_ipv4(dst_ip: [u8; 4], protocol: u8, payload: &[u8]) -> Result<(), &'static str> { let config = config(); - // Check for loopback - sending to ourselves - if dst_ip == config.ip_addr { + // Check for loopback - sending to ourselves or to 127.x.x.x network + if dst_ip == config.ip_addr || dst_ip[0] == 127 { log::debug!("NET: Loopback detected, queueing packet for deferred delivery"); // Build IP packet diff --git a/kernel/src/net/tcp.rs b/kernel/src/net/tcp.rs new file mode 100644 index 00000000..430c169b --- /dev/null +++ b/kernel/src/net/tcp.rs @@ -0,0 +1,1043 @@ +//! TCP (Transmission Control Protocol) implementation +//! +//! Implements TCP packet parsing, construction, and connection state machine (RFC 793). + +use alloc::collections::BTreeMap; +use alloc::collections::VecDeque; +use alloc::vec::Vec; +use spin::Mutex; + +use super::ipv4::{internet_checksum, Ipv4Packet, PROTOCOL_TCP}; + +/// TCP header minimum size (without options) +pub const TCP_HEADER_MIN_SIZE: usize = 20; + +/// TCP flags +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TcpFlags { + pub fin: bool, + pub syn: bool, + pub rst: bool, + pub psh: bool, + pub ack: bool, + pub urg: bool, +} + +impl TcpFlags { + /// Parse flags from the flags byte + pub fn from_byte(byte: u8) -> Self { + TcpFlags { + fin: byte & 0x01 != 0, + syn: byte & 0x02 != 0, + rst: byte & 0x04 != 0, + psh: byte & 0x08 != 0, + ack: byte & 0x10 != 0, + urg: byte & 0x20 != 0, + } + } + + /// Convert flags to byte + pub fn to_byte(&self) -> u8 { + let mut byte = 0u8; + if self.fin { byte |= 0x01; } + if self.syn { byte |= 0x02; } + if self.rst { byte |= 0x04; } + if self.psh { byte |= 0x08; } + if self.ack { byte |= 0x10; } + if self.urg { byte |= 0x20; } + byte + } + + /// Create SYN flag set + pub fn syn() -> Self { + TcpFlags { fin: false, syn: true, rst: false, psh: false, ack: false, urg: false } + } + + /// Create SYN+ACK flag set + pub fn syn_ack() -> Self { + TcpFlags { fin: false, syn: true, rst: false, psh: false, ack: true, urg: false } + } + + /// Create ACK flag set + pub fn ack() -> Self { + TcpFlags { fin: false, syn: false, rst: false, psh: false, ack: true, urg: false } + } + + /// Create ACK+PSH flag set (for data) + pub fn ack_psh() -> Self { + TcpFlags { fin: false, syn: false, rst: false, psh: true, ack: true, urg: false } + } + + /// Create FIN+ACK flag set + pub fn fin_ack() -> Self { + TcpFlags { fin: true, syn: false, rst: false, psh: false, ack: true, urg: false } + } + + /// Create RST flag set + pub fn rst() -> Self { + TcpFlags { fin: false, syn: false, rst: true, psh: false, ack: false, urg: false } + } +} + +/// Parsed TCP header +#[derive(Debug)] +pub struct TcpHeader { + /// Source port + pub src_port: u16, + /// Destination port + pub dst_port: u16, + /// Sequence number + pub seq_num: u32, + /// Acknowledgment number + pub ack_num: u32, + /// Data offset in 32-bit words (header length) + pub data_offset: u8, + /// TCP flags + pub flags: TcpFlags, + /// Window size + pub window_size: u16, + /// Checksum + pub checksum: u16, + /// Urgent pointer + pub urgent_ptr: u16, +} + +impl TcpHeader { + /// Parse a TCP header from raw bytes + pub fn parse(data: &[u8]) -> Option<(Self, &[u8])> { + if data.len() < TCP_HEADER_MIN_SIZE { + return None; + } + + let src_port = u16::from_be_bytes([data[0], data[1]]); + let dst_port = u16::from_be_bytes([data[2], data[3]]); + let seq_num = u32::from_be_bytes([data[4], data[5], data[6], data[7]]); + let ack_num = u32::from_be_bytes([data[8], data[9], data[10], data[11]]); + + // Data offset is upper 4 bits of byte 12, in 32-bit words + let data_offset = (data[12] >> 4) & 0x0F; + let header_len = (data_offset as usize) * 4; + + if header_len < TCP_HEADER_MIN_SIZE || header_len > data.len() { + return None; + } + + let flags = TcpFlags::from_byte(data[13]); + let window_size = u16::from_be_bytes([data[14], data[15]]); + let checksum = u16::from_be_bytes([data[16], data[17]]); + let urgent_ptr = u16::from_be_bytes([data[18], data[19]]); + + let payload = &data[header_len..]; + + Some(( + TcpHeader { + src_port, + dst_port, + seq_num, + ack_num, + data_offset, + flags, + window_size, + checksum, + urgent_ptr, + }, + payload, + )) + } +} + +/// Build a TCP packet (header + payload) +pub fn build_tcp_packet( + src_port: u16, + dst_port: u16, + seq_num: u32, + ack_num: u32, + flags: TcpFlags, + window_size: u16, + payload: &[u8], +) -> Vec { + let header_len = TCP_HEADER_MIN_SIZE; // No options for now + let data_offset = (header_len / 4) as u8; + + let mut packet = Vec::with_capacity(header_len + payload.len()); + + // Source port + packet.extend_from_slice(&src_port.to_be_bytes()); + // Destination port + packet.extend_from_slice(&dst_port.to_be_bytes()); + // Sequence number + packet.extend_from_slice(&seq_num.to_be_bytes()); + // Acknowledgment number + packet.extend_from_slice(&ack_num.to_be_bytes()); + // Data offset (4 bits) + reserved (4 bits) + packet.push((data_offset << 4) | 0); + // Flags + packet.push(flags.to_byte()); + // Window size + packet.extend_from_slice(&window_size.to_be_bytes()); + // Checksum (placeholder, will be calculated) + packet.extend_from_slice(&0u16.to_be_bytes()); + // Urgent pointer + packet.extend_from_slice(&0u16.to_be_bytes()); + + // Payload + packet.extend_from_slice(payload); + + packet +} + +/// Calculate TCP checksum with pseudo-header +pub fn tcp_checksum(src_ip: [u8; 4], dst_ip: [u8; 4], tcp_packet: &[u8]) -> u16 { + // Build pseudo-header for checksum calculation + let mut pseudo_header = Vec::with_capacity(12 + tcp_packet.len()); + + // Source IP + pseudo_header.extend_from_slice(&src_ip); + // Destination IP + pseudo_header.extend_from_slice(&dst_ip); + // Zero + pseudo_header.push(0); + // Protocol (TCP = 6) + pseudo_header.push(PROTOCOL_TCP); + // TCP length + pseudo_header.extend_from_slice(&(tcp_packet.len() as u16).to_be_bytes()); + // TCP header + data + pseudo_header.extend_from_slice(tcp_packet); + + internet_checksum(&pseudo_header) +} + +/// Build a TCP packet with correct checksum +pub fn build_tcp_packet_with_checksum( + src_ip: [u8; 4], + dst_ip: [u8; 4], + src_port: u16, + dst_port: u16, + seq_num: u32, + ack_num: u32, + flags: TcpFlags, + window_size: u16, + payload: &[u8], +) -> Vec { + let mut packet = build_tcp_packet(src_port, dst_port, seq_num, ack_num, flags, window_size, payload); + + // Calculate checksum + let checksum = tcp_checksum(src_ip, dst_ip, &packet); + + // Insert checksum at offset 16-17 + packet[16] = (checksum >> 8) as u8; + packet[17] = (checksum & 0xFF) as u8; + + packet +} + +/// TCP connection state (RFC 793) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TcpState { + Closed, + Listen, + SynSent, + SynReceived, + Established, + FinWait1, + FinWait2, + CloseWait, + Closing, + LastAck, + TimeWait, +} + +/// Connection identifier (4-tuple) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConnectionId { + pub local_ip: [u8; 4], + pub local_port: u16, + pub remote_ip: [u8; 4], + pub remote_port: u16, +} + +/// TCP connection state +pub struct TcpConnection { + pub id: ConnectionId, + pub state: TcpState, + /// Our sequence number (next byte to send) + pub send_next: u32, + /// Initial send sequence number + pub send_initial: u32, + /// Send unacknowledged (oldest unacked seq) + pub send_unack: u32, + /// Remote's sequence number (next byte expected) + pub recv_next: u32, + /// Initial receive sequence number + pub recv_initial: u32, + /// Our window size + pub recv_window: u16, + /// Remote's window size + pub send_window: u16, + /// Pending data to receive + pub rx_buffer: VecDeque, + /// Pending data to send + pub tx_buffer: VecDeque, + /// Maximum segment size + pub mss: u16, + /// Process ID that owns this connection + pub owner_pid: crate::process::process::ProcessId, + /// True if SHUT_WR was called (no more sending) + pub send_shutdown: bool, + /// True if SHUT_RD was called (no more receiving) + pub recv_shutdown: bool, +} + +impl TcpConnection { + /// Create a new connection in SYN_SENT state (for connect) + pub fn new_outgoing( + id: ConnectionId, + initial_seq: u32, + owner_pid: crate::process::process::ProcessId, + ) -> Self { + TcpConnection { + id, + state: TcpState::SynSent, + send_next: initial_seq.wrapping_add(1), // SYN consumes a sequence number + send_initial: initial_seq, + send_unack: initial_seq, + recv_next: 0, + recv_initial: 0, + recv_window: 65535, + send_window: 0, + rx_buffer: VecDeque::new(), + tx_buffer: VecDeque::new(), + mss: 1460, // Default MSS for Ethernet + owner_pid, + send_shutdown: false, + recv_shutdown: false, + } + } + + /// Create a new connection in LISTEN state (for accept) + pub fn new_listening( + local_ip: [u8; 4], + local_port: u16, + owner_pid: crate::process::process::ProcessId, + ) -> Self { + TcpConnection { + id: ConnectionId { + local_ip, + local_port, + remote_ip: [0; 4], + remote_port: 0, + }, + state: TcpState::Listen, + send_next: 0, + send_initial: 0, + send_unack: 0, + recv_next: 0, + recv_initial: 0, + recv_window: 65535, + send_window: 0, + rx_buffer: VecDeque::new(), + tx_buffer: VecDeque::new(), + mss: 1460, + owner_pid, + send_shutdown: false, + recv_shutdown: false, + } + } +} + +/// Pending connection (from SYN received, waiting for accept) +pub struct PendingConnection { + pub remote_ip: [u8; 4], + pub remote_port: u16, + pub recv_initial: u32, + pub send_initial: u32, + /// True if the final ACK of the 3-way handshake has been received + pub ack_received: bool, + /// Data received before accept() was called (buffered here until connection is created) + pub early_data: Vec, + /// Next expected sequence number for early data + pub recv_next: u32, +} + +/// Listening socket info +pub struct ListenSocket { + pub local_ip: [u8; 4], + pub local_port: u16, + pub backlog: usize, + pub pending: VecDeque, + pub owner_pid: crate::process::process::ProcessId, +} + +/// Global TCP connection table +pub static TCP_CONNECTIONS: Mutex> = Mutex::new(BTreeMap::new()); + +/// Global listening socket table (by local port) +pub static TCP_LISTENERS: Mutex> = Mutex::new(BTreeMap::new()); + +/// Sequence number generator (simple counter, should be more random in production) +static SEQ_COUNTER: Mutex = Mutex::new(0x12345678); + +/// Generate a new initial sequence number +pub fn generate_isn() -> u32 { + let mut counter = SEQ_COUNTER.lock(); + let isn = *counter; + *counter = counter.wrapping_add(64000); // Increment by a large amount + isn +} + +/// Handle an incoming TCP packet +pub fn handle_tcp(ip: &Ipv4Packet, data: &[u8]) { + let (header, payload) = match TcpHeader::parse(data) { + Some(h) => h, + None => { + log::warn!("TCP: Failed to parse header"); + return; + } + }; + + log::debug!( + "TCP: Received {} bytes from {}.{}.{}.{}:{} -> port {} seq={} ack={} flags={:?}", + payload.len(), + ip.src_ip[0], ip.src_ip[1], ip.src_ip[2], ip.src_ip[3], + header.src_port, + header.dst_port, + header.seq_num, + header.ack_num, + header.flags + ); + + let config = super::config(); + let conn_id = ConnectionId { + local_ip: config.ip_addr, + local_port: header.dst_port, + remote_ip: ip.src_ip, + remote_port: header.src_port, + }; + + // First, check for an existing connection + { + let mut connections = TCP_CONNECTIONS.lock(); + if let Some(conn) = connections.get_mut(&conn_id) { + handle_tcp_for_connection(conn, &header, payload, &config); + return; + } + } + + // No existing connection - check for listening socket + { + let mut listeners = TCP_LISTENERS.lock(); + if let Some(listener) = listeners.get_mut(&header.dst_port) { + if header.flags.syn && !header.flags.ack { + // SYN received on listening socket - add to pending queue + handle_syn_for_listener(listener, ip.src_ip, &header, &config); + } else if header.flags.ack && !header.flags.syn { + // ACK received - this completes the 3-way handshake + // Find matching pending connection, mark it as ready, and buffer any data + for pending in listener.pending.iter_mut() { + if pending.remote_ip == ip.src_ip && pending.remote_port == header.src_port { + // Verify ACK number matches our SYN+ACK (send_initial + 1) + if header.ack_num == pending.send_initial.wrapping_add(1) { + pending.ack_received = true; + log::debug!("TCP: ACK received, handshake complete for pending connection"); + } + // Buffer any data that arrived with this packet + if !payload.is_empty() && header.seq_num == pending.recv_next { + pending.early_data.extend_from_slice(payload); + pending.recv_next = pending.recv_next.wrapping_add(payload.len() as u32); + log::debug!("TCP: Buffered {} bytes of early data for pending connection", payload.len()); + } + break; + } + } + } else { + log::debug!("TCP: Ignoring packet on listening socket"); + } + return; + } + } + + // No connection and no listener - send RST + log::debug!("TCP: No socket for port {}, sending RST", header.dst_port); + send_rst(&config, ip.src_ip, &header); +} + +/// Handle TCP packet for an established connection +fn handle_tcp_for_connection( + conn: &mut TcpConnection, + header: &TcpHeader, + payload: &[u8], + config: &super::NetConfig, +) { + match conn.state { + TcpState::SynSent => { + // We sent SYN, expecting SYN+ACK + if header.flags.syn && header.flags.ack { + // Verify ACK is for our SYN + if header.ack_num == conn.send_next { + conn.recv_initial = header.seq_num; + conn.recv_next = header.seq_num.wrapping_add(1); + conn.send_window = header.window_size; + conn.send_unack = header.ack_num; + conn.state = TcpState::Established; + + log::info!("TCP: Connection established (client)"); + + // Send ACK + send_tcp_packet( + config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::ack(), + conn.recv_window, + &[], + ); + } + } else if header.flags.rst { + log::info!("TCP: Connection refused (RST received)"); + conn.state = TcpState::Closed; + } + } + TcpState::SynReceived => { + // We sent SYN+ACK, expecting ACK + if header.flags.ack && !header.flags.syn { + if header.ack_num == conn.send_next { + conn.state = TcpState::Established; + conn.send_unack = header.ack_num; + log::info!("TCP: Connection established (server)"); + } + } else if header.flags.rst { + conn.state = TcpState::Closed; + } + } + TcpState::Established => { + // Handle incoming data + if header.flags.ack { + // Update send window + conn.send_unack = header.ack_num; + conn.send_window = header.window_size; + } + + // Process incoming data + if !payload.is_empty() && header.seq_num == conn.recv_next { + conn.rx_buffer.extend(payload); + conn.recv_next = conn.recv_next.wrapping_add(payload.len() as u32); + + log::debug!("TCP: Received {} bytes of data", payload.len()); + + // Send ACK + send_tcp_packet( + config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::ack(), + conn.recv_window, + &[], + ); + } + + // Handle FIN + if header.flags.fin { + conn.recv_next = conn.recv_next.wrapping_add(1); // FIN consumes sequence + conn.state = TcpState::CloseWait; + + log::debug!("TCP: Received FIN, moving to CLOSE_WAIT"); + + // Send ACK for FIN + send_tcp_packet( + config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::ack(), + conn.recv_window, + &[], + ); + } + + if header.flags.rst { + log::info!("TCP: Connection reset by peer"); + conn.state = TcpState::Closed; + } + } + TcpState::FinWait1 => { + if header.flags.ack { + conn.send_unack = header.ack_num; + if header.flags.fin { + // Simultaneous close + conn.recv_next = conn.recv_next.wrapping_add(1); + conn.state = TcpState::TimeWait; + + // Send ACK for FIN + send_tcp_packet( + config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::ack(), + conn.recv_window, + &[], + ); + } else { + conn.state = TcpState::FinWait2; + } + } + } + TcpState::FinWait2 => { + if header.flags.fin { + conn.recv_next = conn.recv_next.wrapping_add(1); + conn.state = TcpState::TimeWait; + + // Send ACK for FIN + send_tcp_packet( + config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::ack(), + conn.recv_window, + &[], + ); + } + } + TcpState::CloseWait => { + // Waiting for application to close + } + TcpState::LastAck => { + if header.flags.ack { + conn.state = TcpState::Closed; + log::debug!("TCP: Connection closed"); + } + } + TcpState::TimeWait => { + // Wait for 2MSL then close (simplified: just mark as closed) + conn.state = TcpState::Closed; + } + _ => {} + } +} + +/// Handle SYN packet for a listening socket +fn handle_syn_for_listener( + listener: &mut ListenSocket, + src_ip: [u8; 4], + header: &TcpHeader, + config: &super::NetConfig, +) { + if listener.pending.len() >= listener.backlog { + log::warn!("TCP: Backlog full, dropping SYN"); + return; + } + + let send_isn = generate_isn(); + + // Add to pending queue + listener.pending.push_back(PendingConnection { + remote_ip: src_ip, + remote_port: header.src_port, + recv_initial: header.seq_num, + send_initial: send_isn, + ack_received: false, + early_data: Vec::new(), + recv_next: header.seq_num.wrapping_add(1), // +1 for SYN + }); + + log::debug!("TCP: SYN received, sending SYN+ACK"); + + // Send SYN+ACK + send_tcp_packet( + config, + src_ip, + listener.local_port, + header.src_port, + send_isn, + header.seq_num.wrapping_add(1), + TcpFlags::syn_ack(), + 65535, + &[], + ); +} + +/// Send a RST packet +fn send_rst(config: &super::NetConfig, dst_ip: [u8; 4], header: &TcpHeader) { + let seq = if header.flags.ack { header.ack_num } else { 0 }; + let ack = header.seq_num.wrapping_add(1); + + let mut flags = TcpFlags::rst(); + if !header.flags.ack { + flags.ack = true; + } + + send_tcp_packet( + config, + dst_ip, + header.dst_port, + header.src_port, + seq, + ack, + flags, + 0, + &[], + ); +} + +/// Send a TCP packet +pub fn send_tcp_packet( + config: &super::NetConfig, + dst_ip: [u8; 4], + src_port: u16, + dst_port: u16, + seq_num: u32, + ack_num: u32, + flags: TcpFlags, + window_size: u16, + payload: &[u8], +) { + let packet = build_tcp_packet_with_checksum( + config.ip_addr, + dst_ip, + src_port, + dst_port, + seq_num, + ack_num, + flags, + window_size, + payload, + ); + + if let Err(e) = super::send_ipv4(dst_ip, PROTOCOL_TCP, &packet) { + log::warn!("TCP: Failed to send packet: {}", e); + } +} + +/// Initiate a TCP connection (called from connect syscall) +pub fn tcp_connect( + local_port: u16, + remote_ip: [u8; 4], + remote_port: u16, + owner_pid: crate::process::process::ProcessId, +) -> Result { + let config = super::config(); + + // Normalize loopback addresses (127.x.x.x) to our own IP + // This ensures connection lookups work when SYN-ACK replies come from our IP + let effective_remote = if remote_ip[0] == 127 { + config.ip_addr + } else { + remote_ip + }; + + let conn_id = ConnectionId { + local_ip: config.ip_addr, + local_port, + remote_ip: effective_remote, + remote_port, + }; + + let isn = generate_isn(); + let conn = TcpConnection::new_outgoing(conn_id, isn, owner_pid); + + // Add connection to table + { + let mut connections = TCP_CONNECTIONS.lock(); + if connections.contains_key(&conn_id) { + return Err("Connection already exists"); + } + connections.insert(conn_id, conn); + } + + // Send SYN + send_tcp_packet( + &config, + remote_ip, + local_port, + remote_port, + isn, + 0, + TcpFlags::syn(), + 65535, + &[], + ); + + log::info!("TCP: Connecting to {}.{}.{}.{}:{}", + remote_ip[0], remote_ip[1], remote_ip[2], remote_ip[3], remote_port); + + Ok(conn_id) +} + +/// Start listening on a port (called from listen syscall) +pub fn tcp_listen( + local_port: u16, + backlog: usize, + owner_pid: crate::process::process::ProcessId, +) -> Result<(), &'static str> { + let config = super::config(); + + let mut listeners = TCP_LISTENERS.lock(); + if listeners.contains_key(&local_port) { + return Err("Port already in use"); + } + + listeners.insert(local_port, ListenSocket { + local_ip: config.ip_addr, + local_port, + backlog, + pending: VecDeque::new(), + owner_pid, + }); + + log::info!("TCP: Listening on port {}", local_port); + + Ok(()) +} + +/// Accept a pending connection (called from accept syscall) +pub fn tcp_accept(local_port: u16) -> Option { + let config = super::config(); + + let pending = { + let mut listeners = TCP_LISTENERS.lock(); + let listener = listeners.get_mut(&local_port)?; + listener.pending.pop_front()? + }; + + let conn_id = ConnectionId { + local_ip: config.ip_addr, + local_port, + remote_ip: pending.remote_ip, + remote_port: pending.remote_port, + }; + + // Get owner PID from listener + let owner_pid = { + let listeners = TCP_LISTENERS.lock(); + listeners.get(&local_port)?.owner_pid + }; + + // Create connection - state depends on whether ACK was already received + let mut conn = TcpConnection::new_outgoing(conn_id, pending.send_initial, owner_pid); + if pending.ack_received { + // 3-way handshake complete, connection is established + conn.state = TcpState::Established; + // send_next should be incremented past our SYN+ACK + conn.send_next = pending.send_initial.wrapping_add(1); + conn.send_unack = conn.send_next; + log::info!("TCP: Connection established (server, ACK already received)"); + } else { + // Still waiting for ACK + conn.state = TcpState::SynReceived; + } + conn.recv_initial = pending.recv_initial; + // Use the recv_next from pending, which accounts for any early data + conn.recv_next = pending.recv_next; + // Copy any early data that arrived before accept() + if !pending.early_data.is_empty() { + for byte in pending.early_data.iter() { + conn.rx_buffer.push_back(*byte); + } + log::debug!("TCP: Copied {} bytes of early data to connection rx_buffer", pending.early_data.len()); + } + + let mut connections = TCP_CONNECTIONS.lock(); + connections.insert(conn_id, conn); + + log::debug!("TCP: Accepted connection from {}.{}.{}.{}:{}", + pending.remote_ip[0], pending.remote_ip[1], pending.remote_ip[2], pending.remote_ip[3], + pending.remote_port); + + Some(conn_id) +} + +/// Send data on a connection +pub fn tcp_send(conn_id: &ConnectionId, data: &[u8]) -> Result { + let config = super::config(); + + let mut connections = TCP_CONNECTIONS.lock(); + let conn = connections.get_mut(conn_id).ok_or("Connection not found")?; + + if conn.send_shutdown { + return Err("Connection shutdown for writing"); + } + + if conn.state != TcpState::Established { + return Err("Connection not established"); + } + + // For simplicity, send all data in one segment (up to MSS) + let send_len = data.len().min(conn.mss as usize); + + send_tcp_packet( + &config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::ack_psh(), + conn.recv_window, + &data[..send_len], + ); + + conn.send_next = conn.send_next.wrapping_add(send_len as u32); + + Ok(send_len) +} + +/// Receive data from a connection +pub fn tcp_recv(conn_id: &ConnectionId, buf: &mut [u8]) -> Result { + let mut connections = TCP_CONNECTIONS.lock(); + let conn = connections.get_mut(conn_id).ok_or("Connection not found")?; + + if conn.rx_buffer.is_empty() { + if conn.state == TcpState::CloseWait || conn.state == TcpState::Closed { + return Ok(0); // EOF + } + return Err("No data available"); + } + + let read_len = buf.len().min(conn.rx_buffer.len()); + for i in 0..read_len { + buf[i] = conn.rx_buffer.pop_front().unwrap(); + } + + Ok(read_len) +} + +/// Check if a connection is established +pub fn tcp_is_established(conn_id: &ConnectionId) -> bool { + let connections = TCP_CONNECTIONS.lock(); + if let Some(conn) = connections.get(conn_id) { + conn.state == TcpState::Established + } else { + false + } +} + +/// Check if a connection failed (reset or closed during handshake) +pub fn tcp_is_failed(conn_id: &ConnectionId) -> bool { + let connections = TCP_CONNECTIONS.lock(); + if let Some(conn) = connections.get(conn_id) { + matches!(conn.state, TcpState::Closed | TcpState::TimeWait) + } else { + true // Connection not found = failed + } +} + +/// Shutdown part of a full-duplex connection +/// shut_rd: stop receiving +/// shut_wr: stop sending (also sends FIN to remote) +pub fn tcp_shutdown(conn_id: &ConnectionId, shut_rd: bool, shut_wr: bool) { + let config = super::config(); + + let mut connections = TCP_CONNECTIONS.lock(); + if let Some(conn) = connections.get_mut(conn_id) { + if shut_rd { + conn.recv_shutdown = true; + } + if shut_wr && !conn.send_shutdown { + conn.send_shutdown = true; + // Send FIN to signal we're done sending + if conn.state == TcpState::Established { + send_tcp_packet( + &config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::fin_ack(), + conn.recv_window, + &[], + ); + conn.send_next = conn.send_next.wrapping_add(1); + conn.state = TcpState::FinWait1; + } + } + } +} + +/// Close a connection +pub fn tcp_close(conn_id: &ConnectionId) -> Result<(), &'static str> { + let config = super::config(); + + let mut connections = TCP_CONNECTIONS.lock(); + let conn = connections.get_mut(conn_id).ok_or("Connection not found")?; + + match conn.state { + TcpState::Established => { + // Send FIN + send_tcp_packet( + &config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::fin_ack(), + conn.recv_window, + &[], + ); + conn.send_next = conn.send_next.wrapping_add(1); // FIN consumes sequence + conn.state = TcpState::FinWait1; + } + TcpState::CloseWait => { + // Send FIN + send_tcp_packet( + &config, + conn.id.remote_ip, + conn.id.local_port, + conn.id.remote_port, + conn.send_next, + conn.recv_next, + TcpFlags::fin_ack(), + conn.recv_window, + &[], + ); + conn.send_next = conn.send_next.wrapping_add(1); + conn.state = TcpState::LastAck; + } + TcpState::Closed => { + // Already closed, remove from table + connections.remove(conn_id); + } + _ => { + // Other states - just mark as closed + conn.state = TcpState::Closed; + } + } + + Ok(()) +} + +/// Check if a connection is established +pub fn tcp_is_connected(conn_id: &ConnectionId) -> bool { + let connections = TCP_CONNECTIONS.lock(); + connections.get(conn_id) + .map(|c| c.state == TcpState::Established) + .unwrap_or(false) +} + +/// Check if there's a pending connection to accept +pub fn tcp_has_pending(local_port: u16) -> bool { + let listeners = TCP_LISTENERS.lock(); + listeners.get(&local_port) + .map(|l| !l.pending.is_empty()) + .unwrap_or(false) +} + +/// Get connection state for debugging +pub fn tcp_get_state(conn_id: &ConnectionId) -> Option { + let connections = TCP_CONNECTIONS.lock(); + connections.get(conn_id).map(|c| c.state) +} diff --git a/kernel/src/process/process.rs b/kernel/src/process/process.rs index e5ebd0bb..748d3899 100644 --- a/kernel/src/process/process.rs +++ b/kernel/src/process/process.rs @@ -232,6 +232,20 @@ impl Process { // Socket cleanup handled by UdpSocket::Drop when Arc refcount reaches 0 log::debug!("Process::close_all_fds() - released UDP socket fd {}", fd); } + FdKind::TcpSocket(_) => { + // Unbound TCP socket doesn't need cleanup + log::debug!("Process::close_all_fds() - released TCP socket fd {}", fd); + } + FdKind::TcpListener(port) => { + // Remove from listener table + crate::net::tcp::TCP_LISTENERS.lock().remove(&port); + log::debug!("Process::close_all_fds() - closed TCP listener fd {} on port {}", fd, port); + } + FdKind::TcpConnection(conn_id) => { + // Close the TCP connection + let _ = crate::net::tcp::tcp_close(&conn_id); + log::debug!("Process::close_all_fds() - closed TCP connection fd {}", fd); + } FdKind::StdIo(_) => { // StdIo doesn't need cleanup } diff --git a/kernel/src/socket/types.rs b/kernel/src/socket/types.rs index 97397c43..6ca534b1 100644 --- a/kernel/src/socket/types.rs +++ b/kernel/src/socket/types.rs @@ -5,6 +5,9 @@ /// Address family: IPv4 pub const AF_INET: u16 = 2; +/// Socket type: Stream (TCP) +pub const SOCK_STREAM: u16 = 1; + /// Socket type: Datagram (UDP) pub const SOCK_DGRAM: u16 = 2; diff --git a/kernel/src/syscall/dispatcher.rs b/kernel/src/syscall/dispatcher.rs index d3b1d5f9..461e78b7 100644 --- a/kernel/src/syscall/dispatcher.rs +++ b/kernel/src/syscall/dispatcher.rs @@ -54,6 +54,10 @@ pub fn dispatch_syscall( SyscallNumber::Bind => super::socket::sys_bind(arg1, arg2, arg3), SyscallNumber::SendTo => super::socket::sys_sendto(arg1, arg2, arg3, arg4, arg5, arg6), SyscallNumber::RecvFrom => super::socket::sys_recvfrom(arg1, arg2, arg3, arg4, arg5, arg6), + SyscallNumber::Connect => super::socket::sys_connect(arg1, arg2, arg3), + SyscallNumber::Accept => super::socket::sys_accept(arg1, arg2, arg3), + SyscallNumber::Listen => super::socket::sys_listen(arg1, arg2), + SyscallNumber::Shutdown => super::socket::sys_shutdown(arg1, arg2), SyscallNumber::Poll => handlers::sys_poll(arg1, arg2, arg3 as i32), SyscallNumber::Select => handlers::sys_select(arg1 as i32, arg2, arg3, arg4, arg5), SyscallNumber::Pipe => super::pipe::sys_pipe(arg1), diff --git a/kernel/src/syscall/errno.rs b/kernel/src/syscall/errno.rs index 63dda50b..b71406bd 100644 --- a/kernel/src/syscall/errno.rs +++ b/kernel/src/syscall/errno.rs @@ -55,6 +55,9 @@ pub const EMFILE: i32 = 24; /// No space left on device pub const ENOSPC: i32 = 28; +/// Broken pipe +pub const EPIPE: i32 = 32; + /// Function not implemented (used by syscall dispatcher) #[allow(dead_code)] pub const ENOSYS: i32 = 38; @@ -75,6 +78,17 @@ pub const EADDRINUSE: i32 = 98; #[allow(dead_code)] pub const ENETUNREACH: i32 = 101; +/// Connection refused +pub const ECONNREFUSED: i32 = 111; + +/// Transport endpoint is already connected +pub const EISCONN: i32 = 106; + /// Transport endpoint is not connected (part of network API) -#[allow(dead_code)] pub const ENOTCONN: i32 = 107; + +/// Connection timed out +pub const ETIMEDOUT: i32 = 110; + +/// Operation not supported +pub const EOPNOTSUPP: i32 = 95; diff --git a/kernel/src/syscall/fs.rs b/kernel/src/syscall/fs.rs index 9e315054..e0277556 100644 --- a/kernel/src/syscall/fs.rs +++ b/kernel/src/syscall/fs.rs @@ -617,6 +617,15 @@ pub fn sys_fstat(fd: i32, statbuf: u64) -> SyscallResult { stat.st_mode = S_IFDIR | 0o755; // Directory with rwxr-xr-x stat.st_nlink = 2; // . and .. } + FdKind::TcpSocket(_) | FdKind::TcpListener(_) | FdKind::TcpConnection(_) => { + // TCP sockets + static TCP_SOCKET_INODE_COUNTER: core::sync::atomic::AtomicU64 = + core::sync::atomic::AtomicU64::new(3000); + stat.st_dev = 0; + stat.st_ino = TCP_SOCKET_INODE_COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed); + stat.st_mode = S_IFSOCK | 0o755; // Socket with rwxr-xr-x + stat.st_nlink = 1; + } } // Copy stat structure to userspace diff --git a/kernel/src/syscall/handler.rs b/kernel/src/syscall/handler.rs index bf3d8383..9550309a 100644 --- a/kernel/src/syscall/handler.rs +++ b/kernel/src/syscall/handler.rs @@ -247,6 +247,18 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) { Some(SyscallNumber::RecvFrom) => { super::socket::sys_recvfrom(args.0, args.1, args.2, args.3, args.4, args.5) } + Some(SyscallNumber::Connect) => { + super::socket::sys_connect(args.0, args.1, args.2) + } + Some(SyscallNumber::Accept) => { + super::socket::sys_accept(args.0, args.1, args.2) + } + Some(SyscallNumber::Listen) => { + super::socket::sys_listen(args.0, args.1) + } + Some(SyscallNumber::Shutdown) => { + super::socket::sys_shutdown(args.0, args.1) + } Some(SyscallNumber::Poll) => super::handlers::sys_poll(args.0, args.1, args.2 as i32), Some(SyscallNumber::Select) => { super::handlers::sys_select(args.0 as i32, args.1, args.2, args.3, args.4) diff --git a/kernel/src/syscall/handlers.rs b/kernel/src/syscall/handlers.rs index f7e5bc0a..9996862e 100644 --- a/kernel/src/syscall/handlers.rs +++ b/kernel/src/syscall/handlers.rs @@ -379,6 +379,31 @@ pub fn sys_write(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { log::error!("sys_write: Cannot write to /dev directory"); SyscallResult::Err(super::errno::EISDIR as u64) } + FdKind::TcpSocket(_) | FdKind::TcpListener(_) => { + // Cannot write to unconnected TCP socket + log::error!("sys_write: Cannot write to unconnected TCP socket"); + SyscallResult::Err(super::errno::ENOTCONN as u64) + } + FdKind::TcpConnection(conn_id) => { + // Write to TCP connection + match crate::net::tcp::tcp_send(conn_id, &buffer) { + Ok(n) => { + log::debug!("sys_write: Sent {} bytes on TCP connection", n); + // Drain loopback queue so data is delivered for localhost connections + crate::net::drain_loopback_queue(); + SyscallResult::Ok(n as u64) + } + Err(e) => { + if e.contains("shutdown") { + log::error!("sys_write: TCP send failed - connection shutdown"); + SyscallResult::Err(super::errno::EPIPE as u64) + } else { + log::error!("sys_write: TCP send failed - {}", e); + SyscallResult::Err(super::errno::EIO as u64) + } + } + } + } } } @@ -699,6 +724,33 @@ pub fn sys_read(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { log::debug!("sys_read: Cannot read from /dev directory, use getdents instead"); SyscallResult::Err(super::errno::EISDIR as u64) } + FdKind::TcpSocket(_) | FdKind::TcpListener(_) => { + // Cannot read from unconnected TCP socket + log::error!("sys_read: Cannot read from unconnected TCP socket"); + SyscallResult::Err(super::errno::ENOTCONN as u64) + } + FdKind::TcpConnection(conn_id) => { + // Read from TCP connection + // First drain loopback queue in case there are pending packets + crate::net::drain_loopback_queue(); + let mut user_buf = alloc::vec![0u8; count as usize]; + match crate::net::tcp::tcp_recv(conn_id, &mut user_buf) { + Ok(n) => { + if n > 0 { + // Copy to userspace + if copy_to_user(buf_ptr, user_buf.as_ptr() as u64, n).is_err() { + return SyscallResult::Err(14); // EFAULT + } + } + log::debug!("sys_read: Received {} bytes from TCP connection", n); + SyscallResult::Ok(n as u64) + } + Err(_) => { + // No data available - return EAGAIN (would block) + SyscallResult::Err(11) // EAGAIN + } + } + } } } diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 84d594f9..5af0b5ff 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -49,9 +49,13 @@ pub enum SyscallNumber { Fcntl = 72, // Linux syscall number for fcntl GetPid = 39, // Linux syscall number for getpid Socket = 41, // Linux syscall number for socket + Connect = 42, // Linux syscall number for connect + Accept = 43, // Linux syscall number for accept SendTo = 44, // Linux syscall number for sendto RecvFrom = 45, // Linux syscall number for recvfrom + Shutdown = 48, // Linux syscall number for shutdown Bind = 49, // Linux syscall number for bind + Listen = 50, // Linux syscall number for listen Exec = 59, // Linux syscall number for execve Wait4 = 61, // Linux syscall number for wait4/waitpid Kill = 62, // Linux syscall number for kill @@ -108,9 +112,13 @@ impl SyscallNumber { 39 => Some(Self::GetPid), 72 => Some(Self::Fcntl), 41 => Some(Self::Socket), + 42 => Some(Self::Connect), + 43 => Some(Self::Accept), 44 => Some(Self::SendTo), 45 => Some(Self::RecvFrom), + 48 => Some(Self::Shutdown), 49 => Some(Self::Bind), + 50 => Some(Self::Listen), 59 => Some(Self::Exec), 61 => Some(Self::Wait4), 62 => Some(Self::Kill), diff --git a/kernel/src/syscall/pipe.rs b/kernel/src/syscall/pipe.rs index 3bc42f38..ad4bf8f1 100644 --- a/kernel/src/syscall/pipe.rs +++ b/kernel/src/syscall/pipe.rs @@ -178,6 +178,15 @@ pub fn sys_close(fd: i32) -> SyscallResult { // Devfs directory doesn't need cleanup log::debug!("sys_close: Closed devfs directory fd={}", fd); } + FdKind::TcpSocket(_) | FdKind::TcpListener(_) => { + // Unbound/listening TCP socket doesn't need special cleanup + log::debug!("sys_close: Closed TCP socket fd={}", fd); + } + FdKind::TcpConnection(conn_id) => { + // Close the TCP connection + let _ = crate::net::tcp::tcp_close(&conn_id); + log::debug!("sys_close: Closed TCP connection fd={}", fd); + } } SyscallResult::Ok(0) } diff --git a/kernel/src/syscall/socket.rs b/kernel/src/syscall/socket.rs index aa4edab7..bedff545 100644 --- a/kernel/src/syscall/socket.rs +++ b/kernel/src/syscall/socket.rs @@ -1,10 +1,10 @@ //! Socket system call implementations //! -//! Implements socket, bind, sendto, recvfrom syscalls for UDP. +//! Implements socket, bind, sendto, recvfrom syscalls for UDP and TCP. -use super::errno::{EAFNOSUPPORT, EAGAIN, EBADF, EFAULT, EINVAL, ENETUNREACH, ENOTSOCK}; +use super::errno::{EAFNOSUPPORT, EAGAIN, EBADF, EFAULT, EINVAL, ENETUNREACH, ENOTSOCK, EADDRINUSE, ENOTCONN, EISCONN, EOPNOTSUPP, ECONNREFUSED, ETIMEDOUT}; use super::{ErrorCode, SyscallResult}; -use crate::socket::types::{AF_INET, SOCK_DGRAM, SockAddrIn}; +use crate::socket::types::{AF_INET, SOCK_DGRAM, SOCK_STREAM, SockAddrIn}; use crate::socket::udp::UdpSocket; use crate::ipc::fd::FdKind; @@ -12,8 +12,8 @@ use crate::ipc::fd::FdKind; /// /// Arguments: /// domain: Address family (AF_INET = 2) -/// sock_type: Socket type (SOCK_DGRAM = 2 for UDP) -/// protocol: Protocol (0 = default, or IPPROTO_UDP = 17) +/// sock_type: Socket type (SOCK_DGRAM = 2 for UDP, SOCK_STREAM = 1 for TCP) +/// protocol: Protocol (0 = default) /// /// Returns: file descriptor on success, negative errno on error pub fn sys_socket(domain: u64, sock_type: u64, _protocol: u64) -> SyscallResult { @@ -25,13 +25,7 @@ pub fn sys_socket(domain: u64, sock_type: u64, _protocol: u64) -> SyscallResult return SyscallResult::Err(EAFNOSUPPORT as u64); } - // Validate socket type - if sock_type as u16 != SOCK_DGRAM { - log::debug!("sys_socket: unsupported type {} (only UDP supported)", sock_type); - return SyscallResult::Err(EINVAL as u64); - } - - // Get current thread and process (same pattern as mmap.rs) + // Get current thread and process let current_thread_id = match crate::per_cpu::current_thread() { Some(thread) => thread.id, None => { @@ -57,13 +51,28 @@ pub fn sys_socket(domain: u64, sock_type: u64, _protocol: u64) -> SyscallResult } }; - // Create UDP socket wrapped in Arc> for sharing - let socket = alloc::sync::Arc::new(spin::Mutex::new(UdpSocket::new())); + // Create socket based on type + let fd_kind = match sock_type as u16 { + SOCK_DGRAM => { + // Create UDP socket wrapped in Arc> for sharing + let socket = alloc::sync::Arc::new(spin::Mutex::new(UdpSocket::new())); + FdKind::UdpSocket(socket) + } + SOCK_STREAM => { + // Create TCP socket (initially unbound, port = 0) + FdKind::TcpSocket(0) + } + _ => { + log::debug!("sys_socket: unsupported type {}", sock_type); + return SyscallResult::Err(EINVAL as u64); + } + }; // Allocate file descriptor in process - match process.fd_table.alloc(FdKind::UdpSocket(socket)) { + match process.fd_table.alloc(fd_kind) { Ok(num) => { - log::info!("UDP: Socket created fd={}", num); + let kind_str = if sock_type as u16 == SOCK_STREAM { "TCP" } else { "UDP" }; + log::info!("{}: Socket created fd={}", kind_str, num); SyscallResult::Ok(num as u64) } Err(e) => { @@ -131,24 +140,52 @@ pub fn sys_bind(fd: u64, addr_ptr: u64, addrlen: u64) -> SyscallResult { // Get the socket from fd table let fd_entry = match process.fd_table.get(fd as i32) { - Some(e) => e, + Some(e) => e.clone(), None => return SyscallResult::Err(EBADF as u64), }; - // Verify it's a UDP socket and get Arc> reference - let socket_ref = match &fd_entry.kind { - FdKind::UdpSocket(s) => s.clone(), - _ => return SyscallResult::Err(ENOTSOCK as u64), - }; + // Handle bind based on socket type + match &fd_entry.kind { + FdKind::UdpSocket(s) => { + // Bind UDP socket + let socket_ref = s.clone(); + let mut socket = socket_ref.lock(); + match socket.bind(pid, addr.addr, addr.port_host()) { + Ok(()) => { + log::info!("UDP: Socket bound to port {}", addr.port_host()); + SyscallResult::Ok(0) + } + Err(e) => SyscallResult::Err(e as u64), + } + } + FdKind::TcpSocket(existing_port) => { + // TCP socket binding - update the socket's port + if *existing_port != 0 { + // Already bound + return SyscallResult::Err(EINVAL as u64); + } - // Bind the socket (lock the mutex and hold the guard) - let mut socket = socket_ref.lock(); - match socket.bind(pid, addr.addr, addr.port_host()) { - Ok(()) => { - log::info!("UDP: Socket bound to port {}", addr.port_host()); + let port = addr.port_host(); + + // Check if port is already in use by another TCP listener + { + let listeners = crate::net::tcp::TCP_LISTENERS.lock(); + if listeners.contains_key(&port) { + log::debug!("TCP: bind failed, port {} already in use", port); + return SyscallResult::Err(EADDRINUSE as u64); + } + } + + // Update the fd entry with the bound port + let fd_num = fd as usize; + if let Some(entry) = process.fd_table.get_mut(fd_num as i32) { + entry.kind = FdKind::TcpSocket(port); + } + + log::info!("TCP: Socket bound to port {}", port); SyscallResult::Ok(0) } - Err(e) => SyscallResult::Err(e as u64), + _ => SyscallResult::Err(ENOTSOCK as u64), } } @@ -351,3 +388,356 @@ pub fn sys_recvfrom( SyscallResult::Ok(copy_len as u64) } + +/// sys_listen - Mark a TCP socket as listening for connections +/// +/// Arguments: +/// fd: Socket file descriptor (must be bound) +/// backlog: Maximum pending connections +/// +/// Returns: 0 on success, negative errno on error +pub fn sys_listen(fd: u64, backlog: u64) -> SyscallResult { + log::debug!("sys_listen: fd={}, backlog={}", fd, backlog); + + // Get current thread and process + let current_thread_id = match crate::per_cpu::current_thread() { + Some(thread) => thread.id, + None => { + log::error!("sys_listen: No current thread in per-CPU data!"); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let mut manager_guard = crate::process::manager(); + let manager = match *manager_guard { + Some(ref mut m) => m, + None => { + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let (pid, process) = match manager.find_process_by_thread_mut(current_thread_id) { + Some(p) => p, + None => { + log::error!("sys_listen: No process found for thread_id={}", current_thread_id); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + // Get the socket from fd table + let fd_entry = match process.fd_table.get(fd as i32) { + Some(e) => e.clone(), + None => return SyscallResult::Err(EBADF as u64), + }; + + // Must be a bound TCP socket + let port = match &fd_entry.kind { + FdKind::TcpSocket(p) => { + if *p == 0 { + // Not bound + return SyscallResult::Err(EINVAL as u64); + } + *p + } + FdKind::TcpListener(_) => { + // Already listening + return SyscallResult::Err(EINVAL as u64); + } + _ => return SyscallResult::Err(EOPNOTSUPP as u64), + }; + + // Start listening + if let Err(_) = crate::net::tcp::tcp_listen(port, backlog as usize, pid) { + return SyscallResult::Err(EADDRINUSE as u64); + } + + // Update fd to TcpListener + if let Some(entry) = process.fd_table.get_mut(fd as i32) { + entry.kind = FdKind::TcpListener(port); + } + + log::info!("TCP: Socket now listening on port {}", port); + SyscallResult::Ok(0) +} + +/// sys_accept - Accept a connection on a listening socket +/// +/// Arguments: +/// fd: Listening socket file descriptor +/// addr: Pointer to sockaddr_in for client address (can be NULL) +/// addrlen: Pointer to address length (can be NULL) +/// +/// Returns: new socket fd on success, negative errno on error +pub fn sys_accept(fd: u64, addr_ptr: u64, addrlen_ptr: u64) -> SyscallResult { + log::debug!("sys_accept: fd={}", fd); + + // Get current thread and process + let current_thread_id = match crate::per_cpu::current_thread() { + Some(thread) => thread.id, + None => { + log::error!("sys_accept: No current thread in per-CPU data!"); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let mut manager_guard = crate::process::manager(); + let manager = match *manager_guard { + Some(ref mut m) => m, + None => { + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let (_pid, process) = match manager.find_process_by_thread_mut(current_thread_id) { + Some(p) => p, + None => { + log::error!("sys_accept: No process found for thread_id={}", current_thread_id); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + // Get the socket from fd table + let fd_entry = match process.fd_table.get(fd as i32) { + Some(e) => e.clone(), + None => return SyscallResult::Err(EBADF as u64), + }; + + // Must be a TCP listener + let port = match &fd_entry.kind { + FdKind::TcpListener(p) => *p, + _ => return SyscallResult::Err(EOPNOTSUPP as u64), + }; + + // Try to accept a pending connection + let conn_id = match crate::net::tcp::tcp_accept(port) { + Some(id) => id, + None => return SyscallResult::Err(EAGAIN as u64), // No pending connections + }; + + // Write client address if requested + if addr_ptr != 0 && addrlen_ptr != 0 { + let client_addr = SockAddrIn::new(conn_id.remote_ip, conn_id.remote_port); + let addr_bytes = client_addr.to_bytes(); + unsafe { + let addrlen = *(addrlen_ptr as *const u32); + let copy_addr_len = core::cmp::min(addrlen as usize, addr_bytes.len()); + let addr_buf = core::slice::from_raw_parts_mut(addr_ptr as *mut u8, copy_addr_len); + addr_buf.copy_from_slice(&addr_bytes[..copy_addr_len]); + *(addrlen_ptr as *mut u32) = addr_bytes.len() as u32; + } + } + + // Create new fd for the connection + match process.fd_table.alloc(FdKind::TcpConnection(conn_id)) { + Ok(new_fd) => { + log::info!("TCP: Accepted connection on fd {}, new fd {}", fd, new_fd); + SyscallResult::Ok(new_fd as u64) + } + Err(e) => SyscallResult::Err(e as u64), + } +} + +/// sys_connect - Initiate a TCP connection +/// +/// Arguments: +/// fd: Socket file descriptor +/// addr: Pointer to destination sockaddr_in +/// addrlen: Length of address structure +/// +/// Returns: 0 on success, negative errno on error +pub fn sys_connect(fd: u64, addr_ptr: u64, addrlen: u64) -> SyscallResult { + log::debug!("sys_connect: fd={}", fd); + + // Validate address length + if addrlen < 16 { + return SyscallResult::Err(EINVAL as u64); + } + + // Read address from userspace + let addr = unsafe { + if addr_ptr == 0 { + return SyscallResult::Err(EFAULT as u64); + } + let addr_bytes = core::slice::from_raw_parts(addr_ptr as *const u8, 16); + match SockAddrIn::from_bytes(addr_bytes) { + Some(a) => a, + None => return SyscallResult::Err(EINVAL as u64), + } + }; + + // Validate address family + if addr.family != AF_INET { + return SyscallResult::Err(EAFNOSUPPORT as u64); + } + + // Get current thread and process + let current_thread_id = match crate::per_cpu::current_thread() { + Some(thread) => thread.id, + None => { + log::error!("sys_connect: No current thread in per-CPU data!"); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let mut manager_guard = crate::process::manager(); + let manager = match *manager_guard { + Some(ref mut m) => m, + None => { + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let (pid, process) = match manager.find_process_by_thread_mut(current_thread_id) { + Some(p) => p, + None => { + log::error!("sys_connect: No process found for thread_id={}", current_thread_id); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + // Get the socket from fd table + let fd_entry = match process.fd_table.get(fd as i32) { + Some(e) => e.clone(), + None => return SyscallResult::Err(EBADF as u64), + }; + + // Handle connect based on socket type + match &fd_entry.kind { + FdKind::TcpSocket(local_port) => { + // Assign ephemeral port if not bound + let port = if *local_port == 0 { + // Use a simple ephemeral port allocation + static EPHEMERAL_PORT: core::sync::atomic::AtomicU16 = + core::sync::atomic::AtomicU16::new(49152); + EPHEMERAL_PORT.fetch_add(1, core::sync::atomic::Ordering::Relaxed) + } else { + *local_port + }; + + // Initiate connection + let conn_id = match crate::net::tcp::tcp_connect( + port, + addr.addr, + addr.port_host(), + pid, + ) { + Ok(id) => id, + Err(_) => return SyscallResult::Err(ECONNREFUSED as u64), + }; + + // Update fd to TcpConnection + if let Some(entry) = process.fd_table.get_mut(fd as i32) { + entry.kind = FdKind::TcpConnection(conn_id); + } + + log::info!("TCP: Connect initiated to {}.{}.{}.{}:{}", + addr.addr[0], addr.addr[1], addr.addr[2], addr.addr[3], + addr.port_host()); + + // Drop manager lock before waiting + drop(manager_guard); + + // Wait for connection to establish (poll with yields) + const MAX_WAIT_ITERATIONS: u32 = 1000; + for i in 0..MAX_WAIT_ITERATIONS { + // Poll for incoming packets (process SYN-ACK) + crate::net::process_rx(); + // Also drain loopback queue for localhost connections + crate::net::drain_loopback_queue(); + + // Check if connected + if crate::net::tcp::tcp_is_established(&conn_id) { + // Drain loopback one more time to deliver the ACK to the server + // This ensures the server's pending connection has ack_received = true + crate::net::drain_loopback_queue(); + log::info!("TCP: Connection established after {} iterations", i); + return SyscallResult::Ok(0); + } + + // Check if connection failed + if crate::net::tcp::tcp_is_failed(&conn_id) { + log::warn!("TCP: Connection failed"); + return SyscallResult::Err(ECONNREFUSED as u64); + } + + // Yield to allow other processing + crate::task::scheduler::yield_current(); + } + + // Timeout + log::warn!("TCP: Connect timed out waiting for handshake"); + SyscallResult::Err(ETIMEDOUT as u64) + } + FdKind::TcpConnection(_) => { + // Already connected + SyscallResult::Err(EISCONN as u64) + } + _ => SyscallResult::Err(EOPNOTSUPP as u64), + } +} + +/// sys_shutdown - Shut down part of a full-duplex connection +/// +/// Arguments: +/// fd: Socket file descriptor +/// how: SHUT_RD (0), SHUT_WR (1), or SHUT_RDWR (2) +/// +/// Returns: 0 on success, negative errno on error +pub fn sys_shutdown(fd: u64, how: u64) -> SyscallResult { + log::debug!("sys_shutdown: fd={}, how={}", fd, how); + + // Validate how parameter + if how > 2 { + return SyscallResult::Err(EINVAL as u64); + } + + // Get current thread and process + let current_thread_id = match crate::per_cpu::current_thread() { + Some(thread) => thread.id, + None => { + log::error!("sys_shutdown: No current thread in per-CPU data!"); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let manager_guard = crate::process::manager(); + let manager = match &*manager_guard { + Some(m) => m, + None => { + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let (_pid, process) = match manager.find_process_by_thread(current_thread_id) { + Some(p) => p, + None => { + log::error!("sys_shutdown: No process found for thread_id={}", current_thread_id); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + // Get the socket from fd table + let fd_entry = match process.fd_table.get(fd as i32) { + Some(e) => e.clone(), + None => return SyscallResult::Err(EBADF as u64), + }; + + // Must be a TCP connection + match &fd_entry.kind { + FdKind::TcpConnection(conn_id) => { + // Set shutdown flags on the connection + let shut_rd = how == 0 || how == 2; // SHUT_RD or SHUT_RDWR + let shut_wr = how == 1 || how == 2; // SHUT_WR or SHUT_RDWR + + crate::net::tcp::tcp_shutdown(conn_id, shut_rd, shut_wr); + + log::info!("TCP: Shutdown fd={} how={}", fd, how); + SyscallResult::Ok(0) + } + FdKind::TcpSocket(_) | FdKind::TcpListener(_) => { + // Not connected + SyscallResult::Err(ENOTCONN as u64) + } + _ => SyscallResult::Err(EOPNOTSUPP as u64), + } +} diff --git a/libs/libbreenix/src/socket.rs b/libs/libbreenix/src/socket.rs index 42e2375b..38d68db7 100644 --- a/libs/libbreenix/src/socket.rs +++ b/libs/libbreenix/src/socket.rs @@ -24,9 +24,21 @@ use crate::syscall::{nr, raw}; /// Address family: IPv4 pub const AF_INET: i32 = 2; +/// Socket type: Stream (TCP) +pub const SOCK_STREAM: i32 = 1; + /// Socket type: Datagram (UDP) pub const SOCK_DGRAM: i32 = 2; +/// Shutdown how: Stop receiving +pub const SHUT_RD: i32 = 0; + +/// Shutdown how: Stop sending +pub const SHUT_WR: i32 = 1; + +/// Shutdown how: Stop both +pub const SHUT_RDWR: i32 = 2; + /// IPv4 socket address structure (matches kernel sockaddr_in) #[repr(C)] #[derive(Debug, Clone, Copy)] @@ -213,3 +225,102 @@ pub fn recvfrom(fd: i32, buf: &mut [u8], src_addr: Option<&mut SockAddrIn>) -> R Ok(ret as usize) } } + +/// Connect a socket to a remote address (TCP) +/// +/// # Arguments +/// * `fd` - Socket file descriptor +/// * `addr` - Remote address to connect to +/// +/// # Returns +/// 0 on success, or negative errno on error +pub fn connect(fd: i32, addr: &SockAddrIn) -> Result<(), i32> { + let ret = unsafe { + raw::syscall3( + nr::CONNECT, + fd as u64, + addr as *const SockAddrIn as u64, + core::mem::size_of::() as u64, + ) + }; + + if (ret as i64) < 0 { + Err(-(ret as i64) as i32) + } else { + Ok(()) + } +} + +/// Mark a socket as listening for connections (TCP) +/// +/// # Arguments +/// * `fd` - Socket file descriptor (must be bound) +/// * `backlog` - Maximum pending connections (usually 128) +/// +/// # Returns +/// 0 on success, or negative errno on error +pub fn listen(fd: i32, backlog: i32) -> Result<(), i32> { + let ret = unsafe { + raw::syscall2(nr::LISTEN, fd as u64, backlog as u64) + }; + + if (ret as i64) < 0 { + Err(-(ret as i64) as i32) + } else { + Ok(()) + } +} + +/// Accept a connection on a listening socket (TCP) +/// +/// # Arguments +/// * `fd` - Listening socket file descriptor +/// * `addr` - Optional buffer to receive client address +/// +/// # Returns +/// New socket file descriptor for the connection on success, or negative errno on error +pub fn accept(fd: i32, addr: Option<&mut SockAddrIn>) -> Result { + let (addr_ptr, addrlen_ptr) = match addr { + Some(a) => { + static mut ADDRLEN: u32 = core::mem::size_of::() as u32; + unsafe { + ADDRLEN = core::mem::size_of::() as u32; + ( + a as *mut SockAddrIn as u64, + &raw mut ADDRLEN as *mut u32 as u64, + ) + } + } + None => (0u64, 0u64), + }; + + let ret = unsafe { + raw::syscall3(nr::ACCEPT, fd as u64, addr_ptr, addrlen_ptr) + }; + + if (ret as i64) < 0 { + Err(-(ret as i64) as i32) + } else { + Ok(ret as i32) + } +} + +/// Shutdown a socket connection (TCP) +/// +/// # Arguments +/// * `fd` - Socket file descriptor +/// * `how` - SHUT_RD (stop receiving), SHUT_WR (stop sending), or SHUT_RDWR (both) +/// +/// # Returns +/// 0 on success, or negative errno on error +pub fn shutdown(fd: i32, how: i32) -> Result<(), i32> { + let ret = unsafe { + raw::syscall2(nr::SHUTDOWN, fd as u64, how as u64) + }; + + if (ret as i64) < 0 { + Err(-(ret as i64) as i32) + } else { + Ok(()) + } +} diff --git a/libs/libbreenix/src/syscall.rs b/libs/libbreenix/src/syscall.rs index 591936da..f80af75b 100644 --- a/libs/libbreenix/src/syscall.rs +++ b/libs/libbreenix/src/syscall.rs @@ -36,9 +36,13 @@ pub mod nr { pub const GETPID: u64 = 39; pub const FCNTL: u64 = 72; // Linux x86_64 fcntl pub const SOCKET: u64 = 41; + pub const CONNECT: u64 = 42; + pub const ACCEPT: u64 = 43; pub const SENDTO: u64 = 44; pub const RECVFROM: u64 = 45; + pub const SHUTDOWN: u64 = 48; pub const BIND: u64 = 49; + pub const LISTEN: u64 = 50; pub const EXEC: u64 = 59; // Linux x86_64 execve pub const WAIT4: u64 = 61; // Linux x86_64 wait4/waitpid pub const KILL: u64 = 62; // Linux x86_64 kill diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..d4884145 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Start Breenix in interactive mode +cargo run -p xtask -- interactive "$@" diff --git a/userspace/tests/Cargo.toml b/userspace/tests/Cargo.toml index b114bd49..52ec9511 100644 --- a/userspace/tests/Cargo.toml +++ b/userspace/tests/Cargo.toml @@ -74,6 +74,10 @@ path = "signal_regs_test.rs" name = "udp_socket_test" path = "udp_socket_test.rs" +[[bin]] +name = "tcp_socket_test" +path = "tcp_socket_test.rs" + [[bin]] name = "pipe_test" path = "pipe_test.rs" @@ -314,3 +318,7 @@ path = "exec_argv_test.rs" [[bin]] name = "exec_stack_argv_test" path = "exec_stack_argv_test.rs" + +[[bin]] +name = "tcp_client_test" +path = "tcp_client_test.rs" diff --git a/userspace/tests/build.sh b/userspace/tests/build.sh index ae37c36c..687a202c 100755 --- a/userspace/tests/build.sh +++ b/userspace/tests/build.sh @@ -47,6 +47,8 @@ BINARIES=( "signal_return_test" "signal_regs_test" "udp_socket_test" + "tcp_socket_test" + "tcp_client_test" "pipe_test" "pipe_fork_test" "pipe_concurrent_test" diff --git a/userspace/tests/init_shell.rs b/userspace/tests/init_shell.rs index 57d4dafe..e03e1731 100644 --- a/userspace/tests/init_shell.rs +++ b/userspace/tests/init_shell.rs @@ -571,6 +571,12 @@ static PROGRAM_REGISTRY: &[ProgramEntry] = &[ binary_name: b"echo_cmd\0", description: "Print arguments to stdout", }, + // === Network Tools === + ProgramEntry { + name: "tcpclient", + binary_name: b"tcp_client_test\0", + description: "Send TCP message to 10.0.2.2:18888", + }, ]; /// Find a program in the registry by command name. diff --git a/userspace/tests/tcp_client_test.rs b/userspace/tests/tcp_client_test.rs new file mode 100644 index 00000000..68f463b5 --- /dev/null +++ b/userspace/tests/tcp_client_test.rs @@ -0,0 +1,105 @@ +//! TCP Client interactive test +//! +//! Connects to an external host and sends a message. +//! +//! Usage: +//! 1. On host machine: nc -l 18888 +//! 2. In Breenix shell: tcpclient +//! 3. See "Hello from Breenix!" appear in netcat +//! +//! Network: Uses QEMU SLIRP, host is reachable at 10.0.2.2 + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use libbreenix::io; +use libbreenix::process; +use libbreenix::socket::{socket, connect, SockAddrIn, AF_INET, SOCK_STREAM}; + +const MESSAGE: &[u8] = b"Hello from Breenix!\n"; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + io::print("TCP Client: Starting\n"); + + // Target: host machine via QEMU SLIRP gateway + // In SLIRP mode, host is accessible at 10.0.2.2 + let dest = SockAddrIn::new([10, 0, 2, 2], 18888); + + // Create TCP socket + let fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => { + io::print("TCP Client: Socket created\n"); + fd + } + Ok(_) => { + io::print("TCP Client: Socket returned invalid fd\n"); + process::exit(1); + } + Err(e) => { + io::print("TCP Client: Socket failed with errno "); + print_errno(e); + io::print("\n"); + process::exit(1); + } + }; + + // Connect to host + match connect(fd, &dest) { + Ok(()) => { + io::print("TCP Client: Connected to 10.0.2.2:18888\n"); + } + Err(e) => { + io::print("TCP Client: Connect failed with errno "); + print_errno(e); + io::print("\n"); + io::print("TCP Client: Make sure 'nc -l 18888' is running on host\n"); + process::exit(2); + } + } + + // Send message using write() syscall + let written = io::write(fd as u64, MESSAGE); + if written > 0 { + io::print("TCP Client: Message sent ("); + print_num(written as u64); + io::print(" bytes)\n"); + io::print("TCP Client: SUCCESS\n"); + process::exit(0); + } else { + io::print("TCP Client: Write failed with errno "); + print_errno((-written) as i32); + io::print("\n"); + process::exit(3); + } +} + +fn print_num(n: u64) { + if n == 0 { + io::print("0"); + return; + } + let mut buf = [0u8; 20]; + let mut i = 0; + let mut val = n; + while val > 0 { + buf[i] = b'0' + (val % 10) as u8; + val /= 10; + i += 1; + } + while i > 0 { + i -= 1; + io::print(unsafe { core::str::from_utf8_unchecked(&buf[i..i+1]) }); + } +} + +fn print_errno(e: i32) { + print_num(e as u64); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + io::print("TCP Client: PANIC!\n"); + process::exit(99); +} diff --git a/userspace/tests/tcp_socket_test.rs b/userspace/tests/tcp_socket_test.rs new file mode 100644 index 00000000..0435ca58 --- /dev/null +++ b/userspace/tests/tcp_socket_test.rs @@ -0,0 +1,1412 @@ +//! TCP Socket userspace test +//! +//! Tests the TCP socket syscalls from userspace: +//! 1. Create a TCP socket (SOCK_STREAM) - MUST succeed +//! 2. Bind to a local port - MUST succeed +//! 3. Listen for connections - MUST succeed +//! 4. Create a second socket for client - MUST succeed +//! 5. Connect to server (loopback) - MUST succeed +//! 6. Accept on server - MUST succeed (connection is pending from connect) +//! 7. Shutdown connected socket (SHUT_RDWR) - MUST succeed +//! 8. Shutdown unconnected socket - MUST return ENOTCONN +//! 9. Bind same port - MUST return EADDRINUSE +//! 10. Listen on unbound socket - MUST return EINVAL +//! 11. Accept on non-listening socket - MUST return EOPNOTSUPP +//! 12. TCP Data Transfer Test (client->server with exact byte verification) +//! 13. Post-shutdown write verification (write after SHUT_WR should fail with EPIPE) +//! 14. SHUT_RD test (verify read returns EOF after shutdown) +//! 15. SHUT_WR test (verify write fails after shutdown) +//! 16. Bidirectional data test (server->client) +//! 17. Large data test (256 bytes) +//! 18. Backlog overflow test (connect beyond backlog limit) +//! 19. ECONNREFUSED test (connect to non-listening port) +//! 20. MSS boundary test (data > 1460 bytes) +//! 21. Multiple write/read cycles test +//! 22. Accept with client address test +//! +//! This validates the TCP syscall path from userspace to kernel. + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use libbreenix::io; +use libbreenix::process; +use libbreenix::socket::{accept, bind, connect, listen, shutdown, socket, SockAddrIn, AF_INET, SHUT_RD, SHUT_WR, SHUT_RDWR, SOCK_STREAM}; + +// Expected errno values +const EAGAIN: i32 = 11; +const EADDRINUSE: i32 = 98; +const EINVAL: i32 = 22; +const EOPNOTSUPP: i32 = 95; +const ENOTCONN: i32 = 107; +const EPIPE: i32 = 32; +const ECONNREFUSED: i32 = 111; +const ETIMEDOUT: i32 = 110; + +// Maximum retries for loopback operations - if exceeded, indicates a bug +const MAX_LOOPBACK_RETRIES: usize = 3; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + io::print("TCP Socket Test: Starting\n"); + let mut passed = 0; + let mut failed = 0; + + // Test 1: Create TCP socket + let server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => { + io::print("TCP_TEST: socket created OK\n"); + passed += 1; + fd + } + Ok(_) => { + io::print("TCP_TEST: socket FAILED invalid fd\n"); + failed += 1; + process::exit(1); + } + Err(_) => { + io::print("TCP_TEST: socket FAILED errno\n"); + failed += 1; + process::exit(1); + } + }; + + // Test 2: Bind to local port + let local_addr = SockAddrIn::new([0, 0, 0, 0], 8080); + match bind(server_fd, &local_addr) { + Ok(()) => { + io::print("TCP_TEST: bind OK\n"); + passed += 1; + } + Err(_) => { + io::print("TCP_TEST: bind FAILED\n"); + failed += 1; + process::exit(2); + } + } + + // Test 3: Start listening + match listen(server_fd, 128) { + Ok(()) => { + io::print("TCP_TEST: listen OK\n"); + passed += 1; + } + Err(_) => { + io::print("TCP_TEST: listen FAILED\n"); + failed += 1; + process::exit(3); + } + } + + // Test 4: Create client socket + let client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => { + io::print("TCP_TEST: client socket OK\n"); + passed += 1; + fd + } + Ok(_) => { + io::print("TCP_TEST: client socket FAILED invalid fd\n"); + failed += 1; + process::exit(4); + } + Err(_) => { + io::print("TCP_TEST: client socket FAILED errno\n"); + failed += 1; + process::exit(4); + } + }; + + // Test 5: Connect to server (loopback) - MUST succeed for shutdown tests + let loopback_addr = SockAddrIn::new([127, 0, 0, 1], 8080); + match connect(client_fd, &loopback_addr) { + Ok(()) => { + io::print("TCP_TEST: connect OK\n"); + passed += 1; + } + Err(_) => { + io::print("TCP_TEST: connect FAILED\n"); + failed += 1; + // Exit early - Tests 6-9 depend on a connected socket + process::exit(5); + } + } + + // Test 6: Accept on server - MUST succeed + // After connect() succeeds, the connection is in the accept queue. + // For loopback, this should be immediate. Limited retries with warning. + let mut accept_result = None; + let mut retry_count = 0; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(server_fd, None) { + Ok(fd) if fd >= 0 => { + accept_result = Some(fd); + retry_count = retry; + break; + } + Ok(_) => { + // Invalid fd returned - this is a failure + break; + } + Err(EAGAIN) => { + // Connection not yet ready, retry after brief delay + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { + core::hint::spin_loop(); + } + } + } + Err(_) => { + // Other error - don't retry + break; + } + } + } + match accept_result { + Some(_fd) => { + if retry_count > 0 { + io::print("TCP_TEST: accept OK (with retries - potential timing issue)\n"); + } else { + io::print("TCP_TEST: accept OK\n"); + } + passed += 1; + } + None => { + io::print("TCP_TEST: accept FAILED\n"); + failed += 1; + } + } + + // Test 7: Shutdown connected socket (SHUT_RDWR) - MUST succeed + match shutdown(client_fd, SHUT_RDWR) { + Ok(()) => { + io::print("TCP_TEST: shutdown OK\n"); + passed += 1; + } + Err(_) => { + io::print("TCP_TEST: shutdown FAILED\n"); + failed += 1; + } + } + + // Test 8: Shutdown on unconnected socket - MUST return ENOTCONN + let unconnected_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => Some(fd), + _ => None, + }; + if let Some(fd) = unconnected_fd { + match shutdown(fd, SHUT_RDWR) { + Err(ENOTCONN) => { + io::print("TCP_TEST: shutdown_unconnected OK\n"); + passed += 1; + } + _ => { + io::print("TCP_TEST: shutdown_unconnected FAILED\n"); + failed += 1; + } + } + } else { + io::print("TCP_TEST: shutdown_unconnected FAILED\n"); + failed += 1; + } + + // Test 9: EADDRINUSE on bind to same port + let conflict_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => Some(fd), + _ => None, + }; + if let Some(fd) = conflict_fd { + let conflict_addr = SockAddrIn::new([0, 0, 0, 0], 8081); + if bind(fd, &conflict_addr).is_ok() && listen(fd, 128).is_ok() { + let second_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => Some(fd), + _ => None, + }; + if let Some(fd2) = second_fd { + match bind(fd2, &conflict_addr) { + Err(EADDRINUSE) => { + io::print("TCP_TEST: eaddrinuse OK\n"); + passed += 1; + } + _ => { + io::print("TCP_TEST: eaddrinuse FAILED\n"); + failed += 1; + } + } + } else { + io::print("TCP_TEST: eaddrinuse FAILED\n"); + failed += 1; + } + } else { + io::print("TCP_TEST: eaddrinuse FAILED\n"); + failed += 1; + } + } else { + io::print("TCP_TEST: eaddrinuse FAILED\n"); + failed += 1; + } + + // Test 10: listen on unbound socket + let unbound_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => Some(fd), + _ => None, + }; + if let Some(fd) = unbound_fd { + match listen(fd, 128) { + Err(EINVAL) => { + io::print("TCP_TEST: listen_unbound OK\n"); + passed += 1; + } + _ => { + io::print("TCP_TEST: listen_unbound FAILED\n"); + failed += 1; + } + } + } else { + io::print("TCP_TEST: listen_unbound FAILED\n"); + failed += 1; + } + + // Test 11: accept on non-listening socket (should return EOPNOTSUPP) + let nonlisten_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => Some(fd), + _ => None, + }; + if let Some(fd) = nonlisten_fd { + let nonlisten_addr = SockAddrIn::new([0, 0, 0, 0], 8083); + if bind(fd, &nonlisten_addr).is_ok() { + match accept(fd, None) { + Err(EOPNOTSUPP) => { + io::print("TCP_TEST: accept_nonlisten OK\n"); + passed += 1; + } + _ => { + io::print("TCP_TEST: accept_nonlisten FAILED\n"); + failed += 1; + } + } + } else { + io::print("TCP_TEST: accept_nonlisten FAILED\n"); + failed += 1; + } + } else { + io::print("TCP_TEST: accept_nonlisten FAILED\n"); + failed += 1; + } + + // ========================================================================= + // Test 12: TCP Data Transfer Test + // This test validates actual data transfer over TCP: + // 1. Server binds to port 8082, listens + // 2. Client connects to 127.0.0.1:8082 + // 3. Client writes "HELLO" using write() syscall + // 4. Server accepts the connection + // 5. Server reads from accepted fd using read() syscall + // 6. Server verifies received data matches "HELLO" + // ========================================================================= + io::print("TCP_DATA_TEST: starting\n"); + + // Create server socket for data test + let data_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_DATA_TEST: server socket FAILED\n"); + failed += 1; + process::exit(12); + } + }; + + // Bind server to port 8082 + let data_server_addr = SockAddrIn::new([0, 0, 0, 0], 8082); + if bind(data_server_fd, &data_server_addr).is_err() { + io::print("TCP_DATA_TEST: server bind FAILED\n"); + failed += 1; + process::exit(12); + } + + // Listen on server + if listen(data_server_fd, 128).is_err() { + io::print("TCP_DATA_TEST: server listen FAILED\n"); + failed += 1; + process::exit(12); + } + io::print("TCP_DATA_TEST: server listening on 8082\n"); + + // Create client socket + let data_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_DATA_TEST: client socket FAILED\n"); + failed += 1; + process::exit(12); + } + }; + + // Connect client to server + let data_loopback_addr = SockAddrIn::new([127, 0, 0, 1], 8082); + if connect(data_client_fd, &data_loopback_addr).is_err() { + io::print("TCP_DATA_TEST: client connect FAILED\n"); + failed += 1; + process::exit(12); + } + io::print("TCP_DATA_TEST: client connected\n"); + + // Client writes "HELLO" to server - verify EXACT bytes written + let send_data = b"HELLO"; + let bytes_written = io::write(data_client_fd as u64, send_data); + if bytes_written < 0 { + io::print("TCP_DATA_TEST: send FAILED (error)\n"); + failed += 1; + process::exit(12); + } + if bytes_written as usize != send_data.len() { + io::print("TCP_DATA_TEST: send FAILED (partial write)\n"); + failed += 1; + process::exit(12); + } + io::print("TCP_DATA_TEST: send OK\n"); + passed += 1; + + // Server accepts the connection (limited retries with warning) + let mut data_accepted_fd = None; + let mut accept_retries = 0; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(data_server_fd, None) { + Ok(fd) if fd >= 0 => { + data_accepted_fd = Some(fd); + accept_retries = retry; + break; + } + Ok(_) => break, // Invalid fd + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { + core::hint::spin_loop(); + } + } + } + Err(_) => break, // Other error + } + } + + let accepted_fd = match data_accepted_fd { + Some(fd) => { + if accept_retries > 0 { + io::print("TCP_DATA_TEST: accept OK (with retries)\n"); + } else { + io::print("TCP_DATA_TEST: accept OK\n"); + } + fd + } + None => { + io::print("TCP_DATA_TEST: accept FAILED\n"); + failed += 1; + process::exit(12); + } + }; + + // Server reads from accepted connection (limited retries with warning) + let mut recv_buf = [0u8; 16]; + let mut bytes_read: i64 = -1; + let mut read_retries = 0; + for retry in 0..MAX_LOOPBACK_RETRIES { + bytes_read = io::read(accepted_fd as u64, &mut recv_buf); + if bytes_read > 0 { + read_retries = retry; + break; // Got data + } + if bytes_read == -(EAGAIN as i64) || bytes_read == 0 { + // No data yet, retry + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { + core::hint::spin_loop(); + } + } + } else { + break; // Real error + } + } + + if bytes_read < 0 { + io::print("TCP_DATA_TEST: recv FAILED\n"); + failed += 1; + process::exit(12); + } + if read_retries > 0 { + io::print("TCP_DATA_TEST: recv OK (with retries)\n"); + } else { + io::print("TCP_DATA_TEST: recv OK\n"); + } + passed += 1; + + // Verify received data matches "HELLO" + let expected = b"HELLO"; + let received_len = bytes_read as usize; + if received_len == expected.len() { + let mut matches = true; + for i in 0..expected.len() { + if recv_buf[i] != expected[i] { + matches = false; + break; + } + } + if matches { + io::print("TCP_DATA_TEST: data verified\n"); + passed += 1; + } else { + io::print("TCP_DATA_TEST: data mismatch\n"); + failed += 1; + } + } else { + io::print("TCP_DATA_TEST: wrong length\n"); + failed += 1; + } + + // ========================================================================= + // Test 13: Post-shutdown write verification + // After shutdown(SHUT_WR), write should fail with EPIPE or similar + // ========================================================================= + io::print("TCP_SHUTDOWN_WRITE_TEST: starting\n"); + + // Create a fresh connection for this test + let shutdown_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_SHUTDOWN_WRITE_TEST: server socket FAILED\n"); + failed += 1; + process::exit(13); + } + }; + let shutdown_server_addr = SockAddrIn::new([0, 0, 0, 0], 8084); + if bind(shutdown_server_fd, &shutdown_server_addr).is_err() { + io::print("TCP_SHUTDOWN_WRITE_TEST: bind FAILED\n"); + failed += 1; + process::exit(13); + } + if listen(shutdown_server_fd, 128).is_err() { + io::print("TCP_SHUTDOWN_WRITE_TEST: listen FAILED\n"); + failed += 1; + process::exit(13); + } + + let shutdown_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_SHUTDOWN_WRITE_TEST: client socket FAILED\n"); + failed += 1; + process::exit(13); + } + }; + let shutdown_loopback = SockAddrIn::new([127, 0, 0, 1], 8084); + if connect(shutdown_client_fd, &shutdown_loopback).is_err() { + io::print("TCP_SHUTDOWN_WRITE_TEST: connect FAILED\n"); + failed += 1; + process::exit(13); + } + + // Accept on server side + let mut shutdown_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(shutdown_server_fd, None) { + Ok(fd) if fd >= 0 => { + shutdown_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + if shutdown_accepted.is_none() { + io::print("TCP_SHUTDOWN_WRITE_TEST: accept FAILED\n"); + failed += 1; + process::exit(13); + } + + // Shutdown write on client + if shutdown(shutdown_client_fd, SHUT_WR).is_err() { + io::print("TCP_SHUTDOWN_WRITE_TEST: shutdown FAILED\n"); + failed += 1; + } else { + // Now try to write - MUST fail with EPIPE + let test_data = b"TEST"; + let write_result = io::write(shutdown_client_fd as u64, test_data); + if write_result == -(EPIPE as i64) { + io::print("TCP_SHUTDOWN_WRITE_TEST: EPIPE OK\n"); + passed += 1; + } else if write_result >= 0 { + io::print("TCP_SHUTDOWN_WRITE_TEST: write should fail after shutdown\n"); + failed += 1; + } else { + io::print("TCP_SHUTDOWN_WRITE_TEST: expected EPIPE, got other error\n"); + failed += 1; + } + } + + // ========================================================================= + // Test 14: SHUT_RD test - shutdown read side + // ========================================================================= + io::print("TCP_SHUT_RD_TEST: starting\n"); + + let shutrd_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_SHUT_RD_TEST: socket FAILED\n"); + failed += 1; + process::exit(14); + } + }; + let shutrd_addr = SockAddrIn::new([0, 0, 0, 0], 8085); + if bind(shutrd_server_fd, &shutrd_addr).is_err() || listen(shutrd_server_fd, 128).is_err() { + io::print("TCP_SHUT_RD_TEST: bind/listen FAILED\n"); + failed += 1; + process::exit(14); + } + + let shutrd_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_SHUT_RD_TEST: client socket FAILED\n"); + failed += 1; + process::exit(14); + } + }; + let shutrd_loopback = SockAddrIn::new([127, 0, 0, 1], 8085); + if connect(shutrd_client_fd, &shutrd_loopback).is_err() { + io::print("TCP_SHUT_RD_TEST: connect FAILED\n"); + failed += 1; + process::exit(14); + } + + // Accept + let mut shutrd_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(shutrd_server_fd, None) { + Ok(fd) if fd >= 0 => { + shutrd_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + + if let Some(_shutrd_accepted_fd) = shutrd_accepted { + // Shutdown read on client BEFORE any data is sent + // This way there's no buffered data - read MUST return 0 (EOF) or error + match shutdown(shutrd_client_fd, SHUT_RD) { + Ok(()) => { + // After SHUT_RD with no buffered data, read MUST return 0 (EOF) + let mut shutrd_buf = [0u8; 16]; + let read_result = io::read(shutrd_client_fd as u64, &mut shutrd_buf); + if read_result == 0 { + io::print("TCP_SHUT_RD_TEST: EOF OK\n"); + passed += 1; + } else if read_result < 0 { + // Error is also acceptable (EAGAIN if non-blocking, etc.) + io::print("TCP_SHUT_RD_TEST: read error OK\n"); + passed += 1; + } else { + // Should NOT return positive bytes after SHUT_RD with no buffered data + io::print("TCP_SHUT_RD_TEST: read returned data after SHUT_RD\n"); + failed += 1; + } + } + Err(_) => { + io::print("TCP_SHUT_RD_TEST: SHUT_RD FAILED\n"); + failed += 1; + } + } + } else { + io::print("TCP_SHUT_RD_TEST: accept FAILED\n"); + failed += 1; + } + + // ========================================================================= + // Test 15: SHUT_WR test - shutdown write side (separate from post-write test) + // ========================================================================= + io::print("TCP_SHUT_WR_TEST: starting\n"); + + let shutwr_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_SHUT_WR_TEST: socket FAILED\n"); + failed += 1; + process::exit(15); + } + }; + let shutwr_addr = SockAddrIn::new([0, 0, 0, 0], 8086); + if bind(shutwr_server_fd, &shutwr_addr).is_err() || listen(shutwr_server_fd, 128).is_err() { + io::print("TCP_SHUT_WR_TEST: bind/listen FAILED\n"); + failed += 1; + process::exit(15); + } + + let shutwr_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_SHUT_WR_TEST: client socket FAILED\n"); + failed += 1; + process::exit(15); + } + }; + let shutwr_loopback = SockAddrIn::new([127, 0, 0, 1], 8086); + if connect(shutwr_client_fd, &shutwr_loopback).is_err() { + io::print("TCP_SHUT_WR_TEST: connect FAILED\n"); + failed += 1; + process::exit(15); + } + + // Accept + let mut shutwr_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(shutwr_server_fd, None) { + Ok(fd) if fd >= 0 => { + shutwr_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + + if let Some(shutwr_accepted_fd) = shutwr_accepted { + match shutdown(shutwr_client_fd, SHUT_WR) { + Ok(()) => { + // Verify write fails after SHUT_WR + let shutwr_test_data = b"TEST"; + let write_result = io::write(shutwr_client_fd as u64, shutwr_test_data); + if write_result < 0 { + io::print("TCP_SHUT_WR_TEST: SHUT_WR write rejected OK\n"); + passed += 1; + } else { + io::print("TCP_SHUT_WR_TEST: write should fail after SHUT_WR\n"); + failed += 1; + } + // Also verify read on accepted fd sees EOF (peer sent FIN) + let mut shutwr_buf = [0u8; 16]; + // Give time for FIN to propagate + for _ in 0..10000 { core::hint::spin_loop(); } + let read_result = io::read(shutwr_accepted_fd as u64, &mut shutwr_buf); + // Should get 0 (EOF) since client shutdown writing + if read_result == 0 || read_result == -(EAGAIN as i64) { + io::print("TCP_SHUT_WR_TEST: server saw FIN OK\n"); + } // Not counting as separate pass, just informational + } + Err(_) => { + io::print("TCP_SHUT_WR_TEST: SHUT_WR FAILED\n"); + failed += 1; + } + } + } else { + io::print("TCP_SHUT_WR_TEST: accept FAILED\n"); + failed += 1; + } + + // ========================================================================= + // Test 16: Bidirectional data test (server->client) + // ========================================================================= + io::print("TCP_BIDIR_TEST: starting\n"); + + let bidir_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_BIDIR_TEST: server socket FAILED\n"); + failed += 1; + process::exit(16); + } + }; + let bidir_addr = SockAddrIn::new([0, 0, 0, 0], 8087); + if bind(bidir_server_fd, &bidir_addr).is_err() || listen(bidir_server_fd, 128).is_err() { + io::print("TCP_BIDIR_TEST: bind/listen FAILED\n"); + failed += 1; + process::exit(16); + } + + let bidir_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_BIDIR_TEST: client socket FAILED\n"); + failed += 1; + process::exit(16); + } + }; + let bidir_loopback = SockAddrIn::new([127, 0, 0, 1], 8087); + if connect(bidir_client_fd, &bidir_loopback).is_err() { + io::print("TCP_BIDIR_TEST: connect FAILED\n"); + failed += 1; + process::exit(16); + } + + // Accept + let mut bidir_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(bidir_server_fd, None) { + Ok(fd) if fd >= 0 => { + bidir_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + + let bidir_accepted_fd = match bidir_accepted { + Some(fd) => fd, + None => { + io::print("TCP_BIDIR_TEST: accept FAILED\n"); + failed += 1; + process::exit(16); + } + }; + + // Server sends "WORLD" to client + let bidir_send_data = b"WORLD"; + let bidir_written = io::write(bidir_accepted_fd as u64, bidir_send_data); + if bidir_written as usize != bidir_send_data.len() { + io::print("TCP_BIDIR_TEST: server send FAILED\n"); + failed += 1; + } else { + // Client reads from server + let mut bidir_recv_buf = [0u8; 16]; + let mut bidir_read: i64 = -1; + for retry in 0..MAX_LOOPBACK_RETRIES { + bidir_read = io::read(bidir_client_fd as u64, &mut bidir_recv_buf); + if bidir_read > 0 { break; } + if bidir_read == -(EAGAIN as i64) || bidir_read == 0 { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } else { + break; + } + } + + if bidir_read == bidir_send_data.len() as i64 { + let mut matches = true; + for i in 0..bidir_send_data.len() { + if bidir_recv_buf[i] != bidir_send_data[i] { + matches = false; + break; + } + } + if matches { + io::print("TCP_BIDIR_TEST: server->client OK\n"); + passed += 1; + } else { + io::print("TCP_BIDIR_TEST: data mismatch\n"); + failed += 1; + } + } else { + io::print("TCP_BIDIR_TEST: wrong length\n"); + failed += 1; + } + } + + // ========================================================================= + // Test 17: Large data test (256 bytes) + // ========================================================================= + io::print("TCP_LARGE_TEST: starting\n"); + + let large_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_LARGE_TEST: server socket FAILED\n"); + failed += 1; + process::exit(17); + } + }; + let large_addr = SockAddrIn::new([0, 0, 0, 0], 8088); + if bind(large_server_fd, &large_addr).is_err() || listen(large_server_fd, 128).is_err() { + io::print("TCP_LARGE_TEST: bind/listen FAILED\n"); + failed += 1; + process::exit(17); + } + + let large_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_LARGE_TEST: client socket FAILED\n"); + failed += 1; + process::exit(17); + } + }; + let large_loopback = SockAddrIn::new([127, 0, 0, 1], 8088); + if connect(large_client_fd, &large_loopback).is_err() { + io::print("TCP_LARGE_TEST: connect FAILED\n"); + failed += 1; + process::exit(17); + } + + // Accept + let mut large_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(large_server_fd, None) { + Ok(fd) if fd >= 0 => { + large_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + + let large_accepted_fd = match large_accepted { + Some(fd) => fd, + None => { + io::print("TCP_LARGE_TEST: accept FAILED\n"); + failed += 1; + process::exit(17); + } + }; + + // Create 256-byte test pattern + let mut large_send_data = [0u8; 256]; + for i in 0..256 { + large_send_data[i] = i as u8; + } + + let large_written = io::write(large_client_fd as u64, &large_send_data); + if large_written as usize != large_send_data.len() { + io::print("TCP_LARGE_TEST: send FAILED\n"); + failed += 1; + } else { + // Read all data (may need multiple reads) + let mut large_recv_buf = [0u8; 512]; + let mut total_read: usize = 0; + + for _attempt in 0..10 { + let bytes = io::read(large_accepted_fd as u64, &mut large_recv_buf[total_read..]); + if bytes > 0 { + total_read += bytes as usize; + if total_read >= 256 { + break; + } + } else if bytes == -(EAGAIN as i64) || bytes == 0 { + for _ in 0..10000 { core::hint::spin_loop(); } + } else { + break; + } + } + + if total_read == 256 { + let mut matches = true; + for i in 0..256 { + if large_recv_buf[i] != i as u8 { + matches = false; + break; + } + } + if matches { + io::print("TCP_LARGE_TEST: 256 bytes verified OK\n"); + passed += 1; + } else { + io::print("TCP_LARGE_TEST: data mismatch\n"); + failed += 1; + } + } else { + io::print("TCP_LARGE_TEST: incomplete read\n"); + failed += 1; + } + } + + // ========================================================================= + // Test 18: Backlog overflow test + // Create MORE connections than backlog allows, verify 3rd connection behavior + // ========================================================================= + io::print("TCP_BACKLOG_TEST: starting\n"); + + let backlog_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_BACKLOG_TEST: server socket FAILED\n"); + failed += 1; + process::exit(18); + } + }; + let backlog_addr = SockAddrIn::new([0, 0, 0, 0], 8089); + if bind(backlog_server_fd, &backlog_addr).is_err() { + io::print("TCP_BACKLOG_TEST: bind FAILED\n"); + failed += 1; + process::exit(18); + } + // Use small backlog of 2 + if listen(backlog_server_fd, 2).is_err() { + io::print("TCP_BACKLOG_TEST: listen FAILED\n"); + failed += 1; + process::exit(18); + } + + // Create 2 client connections (should all succeed) + let mut backlog_clients = [0i32; 3]; + let mut first_two_ok = true; + for i in 0..2 { + let client = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + first_two_ok = false; + break; + } + }; + let backlog_loopback = SockAddrIn::new([127, 0, 0, 1], 8089); + if connect(client, &backlog_loopback).is_err() { + first_two_ok = false; + break; + } + backlog_clients[i] = client; + } + + if first_two_ok { + // Now try a 3rd connection - this should either fail or be queued + // (depending on implementation) + let third_client = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => Some(fd), + _ => None, + }; + + let third_connect_result = if let Some(fd) = third_client { + let backlog_loopback = SockAddrIn::new([127, 0, 0, 1], 8089); + backlog_clients[2] = fd; + connect(fd, &backlog_loopback) + } else { + Err(EINVAL) + }; + + // Accept the first 2 connections + let mut accepted_count = 0; + for _ in 0..2 { + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(backlog_server_fd, None) { + Ok(fd) if fd >= 0 => { + accepted_count += 1; + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + } + + // Try to accept a 3rd - should return EAGAIN (no more pending) + let third_accept = accept(backlog_server_fd, None); + + // Verify: first 2 accepted, and 3rd either failed at connect or at accept + if accepted_count == 2 { + if third_connect_result.is_err() { + // 3rd connection was rejected at connect - backlog enforced + io::print("TCP_BACKLOG_TEST: overflow rejected OK\n"); + passed += 1; + } else if third_accept.is_err() { + // 3rd connection pending but not accepted - backlog enforced + io::print("TCP_BACKLOG_TEST: overflow queued OK\n"); + passed += 1; + } else { + // FAIL: 3rd was accepted - backlog NOT enforced + io::print("TCP_BACKLOG_TEST: backlog not enforced FAILED\n"); + failed += 1; + } + } else { + io::print("TCP_BACKLOG_TEST: first 2 not accepted\n"); + failed += 1; + } + } else { + io::print("TCP_BACKLOG_TEST: client setup FAILED\n"); + failed += 1; + } + + // ========================================================================= + // Test 19: ECONNREFUSED test - connect to non-listening port + // ========================================================================= + io::print("TCP_CONNREFUSED_TEST: starting\n"); + + let refused_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_CONNREFUSED_TEST: socket FAILED\n"); + failed += 1; + process::exit(19); + } + }; + + // Try to connect to a port that has no listener (port 9999) + // MUST return ECONNREFUSED or ETIMEDOUT + let refused_addr = SockAddrIn::new([127, 0, 0, 1], 9999); + match connect(refused_client_fd, &refused_addr) { + Err(ECONNREFUSED) => { + io::print("TCP_CONNREFUSED_TEST: ECONNREFUSED OK\n"); + passed += 1; + } + Err(ETIMEDOUT) => { + io::print("TCP_CONNREFUSED_TEST: ETIMEDOUT OK\n"); + passed += 1; + } + Err(e) => { + io::print("TCP_CONNREFUSED_TEST: unexpected error\n"); + let _ = e; + failed += 1; + } + Ok(()) => { + io::print("TCP_CONNREFUSED_TEST: connect should have failed\n"); + failed += 1; + } + } + + // ========================================================================= + // Test 20: MSS boundary test - data larger than MSS (1460 bytes) + // Tests TCP segmentation for data that exceeds Maximum Segment Size + // ========================================================================= + io::print("TCP_MSS_TEST: starting\n"); + + let mss_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_MSS_TEST: server socket FAILED\n"); + failed += 1; + process::exit(20); + } + }; + let mss_addr = SockAddrIn::new([0, 0, 0, 0], 8090); + if bind(mss_server_fd, &mss_addr).is_err() || listen(mss_server_fd, 128).is_err() { + io::print("TCP_MSS_TEST: bind/listen FAILED\n"); + failed += 1; + process::exit(20); + } + + let mss_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_MSS_TEST: client socket FAILED\n"); + failed += 1; + process::exit(20); + } + }; + let mss_loopback = SockAddrIn::new([127, 0, 0, 1], 8090); + if connect(mss_client_fd, &mss_loopback).is_err() { + io::print("TCP_MSS_TEST: connect FAILED\n"); + failed += 1; + process::exit(20); + } + + // Accept + let mut mss_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(mss_server_fd, None) { + Ok(fd) if fd >= 0 => { + mss_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + + let mss_accepted_fd = match mss_accepted { + Some(fd) => fd, + None => { + io::print("TCP_MSS_TEST: accept FAILED\n"); + failed += 1; + process::exit(20); + } + }; + + // Create 2000-byte test pattern (larger than MSS of 1460) + let mut mss_send_data = [0u8; 2000]; + for i in 0..2000 { + mss_send_data[i] = (i % 256) as u8; + } + + // Send all data (may need multiple writes due to MSS) + let mut total_written: usize = 0; + for _attempt in 0..10 { + let bytes = io::write(mss_client_fd as u64, &mss_send_data[total_written..]); + if bytes > 0 { + total_written += bytes as usize; + if total_written >= 2000 { + break; + } + } else if bytes == -(EAGAIN as i64) { + for _ in 0..10000 { core::hint::spin_loop(); } + } else if bytes < 0 { + break; + } + } + + if total_written != 2000 { + io::print("TCP_MSS_TEST: send FAILED (incomplete)\n"); + failed += 1; + } else { + // Read all data (may need multiple reads due to segmentation) + let mut mss_recv_buf = [0u8; 2500]; + let mut total_read: usize = 0; + + for _attempt in 0..20 { + let bytes = io::read(mss_accepted_fd as u64, &mut mss_recv_buf[total_read..]); + if bytes > 0 { + total_read += bytes as usize; + if total_read >= 2000 { + break; + } + } else if bytes == -(EAGAIN as i64) || bytes == 0 { + for _ in 0..10000 { core::hint::spin_loop(); } + } else { + break; + } + } + + if total_read == 2000 { + let mut matches = true; + for i in 0..2000 { + if mss_recv_buf[i] != (i % 256) as u8 { + matches = false; + break; + } + } + if matches { + io::print("TCP_MSS_TEST: 2000 bytes (>MSS) verified OK\n"); + passed += 1; + } else { + io::print("TCP_MSS_TEST: data mismatch\n"); + failed += 1; + } + } else { + io::print("TCP_MSS_TEST: incomplete read\n"); + failed += 1; + } + } + + // ========================================================================= + // Test 21: Multiple write/read cycles test + // Send multiple messages on same connection + // ========================================================================= + io::print("TCP_MULTI_TEST: starting\n"); + + let multi_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_MULTI_TEST: server socket FAILED\n"); + failed += 1; + process::exit(21); + } + }; + let multi_addr = SockAddrIn::new([0, 0, 0, 0], 8091); + if bind(multi_server_fd, &multi_addr).is_err() || listen(multi_server_fd, 128).is_err() { + io::print("TCP_MULTI_TEST: bind/listen FAILED\n"); + failed += 1; + process::exit(21); + } + + let multi_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_MULTI_TEST: client socket FAILED\n"); + failed += 1; + process::exit(21); + } + }; + let multi_loopback = SockAddrIn::new([127, 0, 0, 1], 8091); + if connect(multi_client_fd, &multi_loopback).is_err() { + io::print("TCP_MULTI_TEST: connect FAILED\n"); + failed += 1; + process::exit(21); + } + + // Accept + let mut multi_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(multi_server_fd, None) { + Ok(fd) if fd >= 0 => { + multi_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + + let multi_accepted_fd = match multi_accepted { + Some(fd) => fd, + None => { + io::print("TCP_MULTI_TEST: accept FAILED\n"); + failed += 1; + process::exit(21); + } + }; + + // Send 3 messages on same connection + let messages: [&[u8]; 3] = [b"MSG1", b"MSG2", b"MSG3"]; + let mut multi_success = true; + + for (idx, msg) in messages.iter().enumerate() { + // Client sends + let written = io::write(multi_client_fd as u64, msg); + if written as usize != msg.len() { + multi_success = false; + break; + } + + // Server receives + let mut recv_buf = [0u8; 16]; + let mut bytes_read: i64 = -1; + for retry in 0..MAX_LOOPBACK_RETRIES { + bytes_read = io::read(multi_accepted_fd as u64, &mut recv_buf); + if bytes_read > 0 { break; } + if bytes_read == -(EAGAIN as i64) || bytes_read == 0 { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } else { + break; + } + } + + if bytes_read as usize != msg.len() { + multi_success = false; + break; + } + + // Verify data + for i in 0..msg.len() { + if recv_buf[i] != msg[i] { + multi_success = false; + break; + } + } + if !multi_success { break; } + let _ = idx; // Mark as used + } + + if multi_success { + io::print("TCP_MULTI_TEST: 3 messages verified OK\n"); + passed += 1; + } else { + io::print("TCP_MULTI_TEST: multi-message FAILED\n"); + failed += 1; + } + + // ========================================================================= + // Test 22: Accept with client address test + // Verify accept returns correct client address + // ========================================================================= + io::print("TCP_ADDR_TEST: starting\n"); + + let addr_server_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_ADDR_TEST: server socket FAILED\n"); + failed += 1; + process::exit(22); + } + }; + let addr_server_addr = SockAddrIn::new([0, 0, 0, 0], 8092); + if bind(addr_server_fd, &addr_server_addr).is_err() || listen(addr_server_fd, 128).is_err() { + io::print("TCP_ADDR_TEST: bind/listen FAILED\n"); + failed += 1; + process::exit(22); + } + + let addr_client_fd = match socket(AF_INET, SOCK_STREAM, 0) { + Ok(fd) if fd >= 0 => fd, + _ => { + io::print("TCP_ADDR_TEST: client socket FAILED\n"); + failed += 1; + process::exit(22); + } + }; + let addr_loopback = SockAddrIn::new([127, 0, 0, 1], 8092); + if connect(addr_client_fd, &addr_loopback).is_err() { + io::print("TCP_ADDR_TEST: connect FAILED\n"); + failed += 1; + process::exit(22); + } + + // Accept with address output + let mut client_addr = SockAddrIn::new([0, 0, 0, 0], 0); + let mut addr_accepted = None; + for retry in 0..MAX_LOOPBACK_RETRIES { + match accept(addr_server_fd, Some(&mut client_addr)) { + Ok(fd) if fd >= 0 => { + addr_accepted = Some(fd); + break; + } + Err(EAGAIN) => { + if retry < MAX_LOOPBACK_RETRIES - 1 { + for _ in 0..10000 { core::hint::spin_loop(); } + } + } + _ => break, + } + } + + if addr_accepted.is_some() { + // Verify client address is filled in correctly + // For loopback, kernel normalizes 127.x.x.x to guest IP (10.0.2.15) + if client_addr.addr[0] == 127 && client_addr.addr[1] == 0 && + client_addr.addr[2] == 0 && client_addr.addr[3] == 1 { + io::print("TCP_ADDR_TEST: 127.0.0.1 OK\n"); + passed += 1; + } else if client_addr.addr[0] == 10 { + // QEMU SLIRP network guest IP - loopback was normalized + io::print("TCP_ADDR_TEST: 10.x.x.x OK\n"); + passed += 1; + } else if client_addr.addr[0] == 0 && client_addr.addr[1] == 0 && + client_addr.addr[2] == 0 && client_addr.addr[3] == 0 { + // FAIL: Address not filled in - this is a bug + io::print("TCP_ADDR_TEST: address not filled FAILED\n"); + failed += 1; + } else { + // FAIL: Unexpected address + io::print("TCP_ADDR_TEST: unexpected address FAILED\n"); + failed += 1; + } + } else { + io::print("TCP_ADDR_TEST: accept FAILED\n"); + failed += 1; + } + + // Final result + if failed == 0 { + io::print("TCP Socket Test: PASSED\n"); + process::exit(0); + } else { + io::print("TCP Socket Test: FAILED\n"); + process::exit(1); + } +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + io::print("TCP Socket Test: PANIC!\n"); + process::exit(99); +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 23a2d4ad..6433d0a7 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -610,6 +610,252 @@ fn get_boot_stages() -> Vec { failure_meaning: "UDP socket test did not complete successfully", check_hint: "Check userspace/tests/udp_socket_test.rs for which step failed", }, + // TCP Socket tests - validates TCP syscall path (socket, bind, listen, connect, accept) + BootStage { + name: "TCP socket created", + marker: "TCP_TEST: socket created OK", + failure_meaning: "sys_socket(SOCK_STREAM) failed from userspace", + check_hint: "Check syscall/socket.rs:sys_socket() for SOCK_STREAM support", + }, + BootStage { + name: "TCP socket bound", + marker: "TCP_TEST: bind OK", + failure_meaning: "sys_bind for TCP socket failed", + check_hint: "Check syscall/socket.rs:sys_bind() for TCP socket handling", + }, + BootStage { + name: "TCP socket listening", + marker: "TCP_TEST: listen OK", + failure_meaning: "sys_listen failed - TCP socket not converted to listener", + check_hint: "Check syscall/socket.rs:sys_listen() implementation", + }, + BootStage { + name: "TCP client socket created", + marker: "TCP_TEST: client socket OK", + failure_meaning: "Second TCP socket creation failed", + check_hint: "Check fd allocation or socket creation in sys_socket()", + }, + BootStage { + name: "TCP connect executed", + marker: "TCP_TEST: connect OK", + failure_meaning: "sys_connect failed to return Ok(())", + check_hint: "Check syscall/socket.rs:sys_connect() - must return 0 for loopback connection", + }, + BootStage { + name: "TCP accept executed", + marker: "TCP_TEST: accept OK", + failure_meaning: "sys_accept returned unexpected error", + check_hint: "Check syscall/socket.rs:sys_accept() - expected EAGAIN for no pending connections", + }, + BootStage { + name: "TCP shutdown executed", + marker: "TCP_TEST: shutdown OK", + failure_meaning: "sys_shutdown(SHUT_RDWR) on connected socket failed", + check_hint: "Check syscall/socket.rs:sys_shutdown() for TcpConnection handling", + }, + BootStage { + name: "TCP shutdown unconnected rejected", + marker: "TCP_TEST: shutdown_unconnected OK", + failure_meaning: "sys_shutdown on unconnected socket did not return ENOTCONN", + check_hint: "Check syscall/socket.rs:sys_shutdown() - unconnected TcpSocket should return ENOTCONN", + }, + BootStage { + name: "TCP EADDRINUSE detected", + marker: "TCP_TEST: eaddrinuse OK", + failure_meaning: "Binding to already-bound port did not return EADDRINUSE", + check_hint: "Check kernel port conflict detection in sys_bind()", + }, + BootStage { + name: "TCP listen unbound rejected", + marker: "TCP_TEST: listen_unbound OK", + failure_meaning: "listen() on unbound socket did not return EINVAL", + check_hint: "Check sys_listen() validates socket is bound first", + }, + BootStage { + name: "TCP accept nonlisten rejected", + marker: "TCP_TEST: accept_nonlisten OK", + failure_meaning: "accept() on non-listening socket did not return error", + check_hint: "Check sys_accept() validates socket is in listen state", + }, + // TCP Data Transfer Test - validates actual read/write over TCP connections + BootStage { + name: "TCP data test started", + marker: "TCP_DATA_TEST: starting", + failure_meaning: "TCP data transfer test did not start", + check_hint: "Check userspace/tests/tcp_socket_test.rs - test 12 section", + }, + BootStage { + name: "TCP data server listening", + marker: "TCP_DATA_TEST: server listening on 8082", + failure_meaning: "TCP data test server failed to bind/listen on port 8082", + check_hint: "Check sys_bind/sys_listen for TCP sockets", + }, + BootStage { + name: "TCP data client connected", + marker: "TCP_DATA_TEST: client connected", + failure_meaning: "TCP data test client failed to connect to server", + check_hint: "Check sys_connect for loopback TCP connections", + }, + BootStage { + name: "TCP data send", + marker: "TCP_DATA_TEST: send OK", + failure_meaning: "write() syscall on TCP socket failed", + check_hint: "Check kernel/src/syscall/io.rs:sys_write() for TCP socket support - must route to tcp_send()", + }, + BootStage { + name: "TCP data accept", + marker: "TCP_DATA_TEST: accept OK", + failure_meaning: "accept() on data test server returned EAGAIN or error after connect", + check_hint: "Check sys_accept() - loopback connect should queue connection immediately", + }, + BootStage { + name: "TCP data recv", + marker: "TCP_DATA_TEST: recv OK", + failure_meaning: "read() syscall on accepted TCP socket failed", + check_hint: "Check kernel/src/syscall/io.rs:sys_read() for TCP socket support - must route to tcp_recv()", + }, + BootStage { + name: "TCP data verified", + marker: "TCP_DATA_TEST: data verified", + failure_meaning: "Received TCP data did not match sent data 'HELLO'", + check_hint: "Check tcp_send/tcp_recv implementation - data corruption or length mismatch", + }, + // Test 13: Post-shutdown write verification + BootStage { + name: "TCP post-shutdown write test started", + marker: "TCP_SHUTDOWN_WRITE_TEST: starting", + failure_meaning: "Post-shutdown write test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP write after shutdown rejected with EPIPE", + marker: "TCP_SHUTDOWN_WRITE_TEST: EPIPE OK", + failure_meaning: "Write after SHUT_WR was not properly rejected with EPIPE", + check_hint: "Check tcp_send returns EPIPE when send_shutdown=true", + }, + // Test 14: SHUT_RD test + BootStage { + name: "TCP SHUT_RD test started", + marker: "TCP_SHUT_RD_TEST: starting", + failure_meaning: "SHUT_RD test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP SHUT_RD returns EOF", + marker: "TCP_SHUT_RD_TEST:", + failure_meaning: "Read after SHUT_RD did not return EOF", + check_hint: "Check tcp_recv honors recv_shutdown flag", + }, + // Test 15: SHUT_WR test + BootStage { + name: "TCP SHUT_WR test started", + marker: "TCP_SHUT_WR_TEST: starting", + failure_meaning: "SHUT_WR test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP SHUT_WR succeeded", + marker: "TCP_SHUT_WR_TEST: SHUT_WR write rejected OK", + failure_meaning: "shutdown(SHUT_WR) failed on connected socket", + check_hint: "Check sys_shutdown handling of SHUT_WR", + }, + // Test 16: Bidirectional data test + BootStage { + name: "TCP bidirectional test started", + marker: "TCP_BIDIR_TEST: starting", + failure_meaning: "Bidirectional data test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP server->client data verified", + marker: "TCP_BIDIR_TEST: server->client OK", + failure_meaning: "Server-to-client TCP data transfer failed", + check_hint: "Check tcp_send/tcp_recv for accepted connection fd", + }, + // Test 17: Large data test + BootStage { + name: "TCP large data test started", + marker: "TCP_LARGE_TEST: starting", + failure_meaning: "Large data test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP 256 bytes verified", + marker: "TCP_LARGE_TEST: 256 bytes verified OK", + failure_meaning: "256-byte TCP transfer failed or corrupted", + check_hint: "Check buffer handling in tcp_send/tcp_recv for larger data", + }, + // Test 18: Backlog overflow test + BootStage { + name: "TCP backlog test started", + marker: "TCP_BACKLOG_TEST: starting", + failure_meaning: "Backlog test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP backlog test passed", + marker: "TCP_BACKLOG_TEST:", + failure_meaning: "Backlog overflow test failed", + check_hint: "Check tcp_listen backlog parameter handling", + }, + // Test 19: ECONNREFUSED test + BootStage { + name: "TCP ECONNREFUSED test started", + marker: "TCP_CONNREFUSED_TEST: starting", + failure_meaning: "ECONNREFUSED test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP ECONNREFUSED test passed", + marker: "TCP_CONNREFUSED_TEST:", + failure_meaning: "Connect to non-listening port did not fail properly", + check_hint: "Check tcp_connect error handling for non-listening ports", + }, + // Test 20: MSS boundary test + BootStage { + name: "TCP MSS test started", + marker: "TCP_MSS_TEST: starting", + failure_meaning: "MSS boundary test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP MSS test passed", + marker: "TCP_MSS_TEST: 2000 bytes", + failure_meaning: "Data > MSS (1460) failed to transfer", + check_hint: "Check TCP segmentation for large data transfers", + }, + // Test 21: Multiple write/read cycles + BootStage { + name: "TCP multi-cycle test started", + marker: "TCP_MULTI_TEST: starting", + failure_meaning: "Multi-cycle test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP multi-cycle test passed", + marker: "TCP_MULTI_TEST: 3 messages", + failure_meaning: "Multiple write/read cycles on same connection failed", + check_hint: "Check TCP connection state management between sends", + }, + // Test 22: Accept with client address + BootStage { + name: "TCP address test started", + marker: "TCP_ADDR_TEST: starting", + failure_meaning: "Client address test did not start", + check_hint: "Previous test may have failed", + }, + BootStage { + name: "TCP address test passed", + marker: "TCP_ADDR_TEST: 10.x.x.x OK", + failure_meaning: "Accept did not return client address correctly", + check_hint: "Check sys_accept address output handling", + }, + BootStage { + name: "TCP socket test passed", + marker: "TCP Socket Test: PASSED", + failure_meaning: "TCP socket test did not complete successfully", + check_hint: "Check userspace/tests/tcp_socket_test.rs for which step failed", + }, // IPC (pipe) tests BootStage { name: "Pipe IPC test passed",