From 81c0eeba32a4815ed30bafb49ef3fc487ab5bf8e Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Fri, 4 Sep 2026 15:45:03 -0700 Subject: [PATCH 01/20] refactor(net): time to fight the slop and use my own brain! removed the network stack ahead of its redesign --- kernel/boot/boot.cpp | 5 - kernel/drivers/net/bcm_genet.cpp | 155 +-- kernel/drivers/net/bcm_genet.h | 11 +- kernel/drivers/net/rtl8168.cpp | 123 +- kernel/drivers/net/rtl8168.h | 9 +- kernel/drivers/net/virtio_net.cpp | 112 +- kernel/drivers/net/virtio_net.h | 18 +- kernel/net/arp.cpp | 247 ---- kernel/net/arp.h | 54 - kernel/net/byteorder.h | 15 - kernel/net/checksum.h | 98 -- kernel/net/dhcp.cpp | 651 --------- kernel/net/dhcp.h | 175 --- kernel/net/ethernet.cpp | 62 - kernel/net/ethernet.h | 41 - kernel/net/icmp.cpp | 115 -- kernel/net/icmp.h | 45 - kernel/net/inet_socket.cpp | 572 -------- kernel/net/inet_socket.h | 47 - kernel/net/ipv4.cpp | 178 --- kernel/net/ipv4.h | 65 - kernel/net/loopback.cpp | 78 -- kernel/net/loopback.h | 27 - kernel/net/net.cpp | 361 ----- kernel/net/net.h | 171 --- kernel/net/netinfo.h | 75 - kernel/net/route.cpp | 178 --- kernel/net/route.h | 106 -- kernel/net/tcp.cpp | 1592 ---------------------- kernel/net/tcp.h | 137 -- kernel/net/udp.cpp | 164 --- kernel/net/udp.h | 63 - kernel/syscall/handlers/sys_shutdown.cpp | 8 +- kernel/syscall/handlers/sys_sockaddr.cpp | 112 +- kernel/syscall/handlers/sys_socket.cpp | 17 - kernel/tests/net/dhcp.test.cpp | 665 --------- kernel/tests/net/genet.test.cpp | 292 ---- kernel/tests/net/inet_bind.test.cpp | 351 ----- kernel/tests/net/loopback.test.cpp | 530 ------- kernel/tests/net/route.test.cpp | 573 -------- kernel/tests/net/rtl8168.test.cpp | 265 ---- 41 files changed, 143 insertions(+), 8420 deletions(-) delete mode 100644 kernel/net/arp.cpp delete mode 100644 kernel/net/arp.h delete mode 100644 kernel/net/byteorder.h delete mode 100644 kernel/net/checksum.h delete mode 100644 kernel/net/dhcp.cpp delete mode 100644 kernel/net/dhcp.h delete mode 100644 kernel/net/ethernet.cpp delete mode 100644 kernel/net/ethernet.h delete mode 100644 kernel/net/icmp.cpp delete mode 100644 kernel/net/icmp.h delete mode 100644 kernel/net/inet_socket.cpp delete mode 100644 kernel/net/inet_socket.h delete mode 100644 kernel/net/ipv4.cpp delete mode 100644 kernel/net/ipv4.h delete mode 100644 kernel/net/loopback.cpp delete mode 100644 kernel/net/loopback.h delete mode 100644 kernel/net/net.cpp delete mode 100644 kernel/net/net.h delete mode 100644 kernel/net/netinfo.h delete mode 100644 kernel/net/route.cpp delete mode 100644 kernel/net/route.h delete mode 100644 kernel/net/tcp.cpp delete mode 100644 kernel/net/tcp.h delete mode 100644 kernel/net/udp.cpp delete mode 100644 kernel/net/udp.h delete mode 100644 kernel/tests/net/dhcp.test.cpp delete mode 100644 kernel/tests/net/genet.test.cpp delete mode 100644 kernel/tests/net/inet_bind.test.cpp delete mode 100644 kernel/tests/net/loopback.test.cpp delete mode 100644 kernel/tests/net/route.test.cpp delete mode 100644 kernel/tests/net/rtl8168.test.cpp diff --git a/kernel/boot/boot.cpp b/kernel/boot/boot.cpp index 1ee512c1..8d00705a 100644 --- a/kernel/boot/boot.cpp +++ b/kernel/boot/boot.cpp @@ -26,7 +26,6 @@ #include "drivers/platform_driver.h" #include "drivers/graphics/gfxfb.h" #include "drivers/input/input.h" -#include "net/net.h" #include "random/random.h" #include "sysstat/sysstat.h" #include "sync/futex.h" @@ -154,10 +153,6 @@ extern "C" __PRIVILEGED_CODE void stlx_init() { log::warn("smp::init failed, continuing with single CPU"); } - if (net::init() != net::OK) { - log::warn("net::init failed, networking unavailable"); - } - #ifdef STLX_UNIT_TESTS_ENABLED stlx_test::run_all(); while (true) { diff --git a/kernel/drivers/net/bcm_genet.cpp b/kernel/drivers/net/bcm_genet.cpp index 741e095d..fbce9413 100644 --- a/kernel/drivers/net/bcm_genet.cpp +++ b/kernel/drivers/net/bcm_genet.cpp @@ -9,9 +9,6 @@ #include "common/string.h" #include "sched/sched.h" #include "dynpriv/dynpriv.h" -#include "net/net.h" -#include "net/ipv4.h" -#include "net/dhcp.h" #if defined(__aarch64__) #include "irq/irq_arch.h" @@ -42,8 +39,7 @@ bcm_genet_driver::bcm_genet_driver(uint64_t reg_phys, uint64_t reg_size, , m_tx_queued(0) , m_has_irq(false) { m_lock = sync::SPINLOCK_INIT; - uint8_t* p = reinterpret_cast(&m_netif); - for (size_t i = 0; i < sizeof(m_netif); i++) p[i] = 0; + string::memset(m_mac, 0, sizeof(m_mac)); } bcm_genet_driver* create_bcm_genet(uint64_t reg_phys, uint64_t reg_size, @@ -333,33 +329,33 @@ void bcm_genet_driver::read_mac_address() { uint32_t mac0 = reg_read(UMAC_MAC0); uint32_t mac1 = reg_read(UMAC_MAC1); - m_netif.mac[0] = static_cast((mac0 >> 24) & 0xFF); - m_netif.mac[1] = static_cast((mac0 >> 16) & 0xFF); - m_netif.mac[2] = static_cast((mac0 >> 8) & 0xFF); - m_netif.mac[3] = static_cast(mac0 & 0xFF); - m_netif.mac[4] = static_cast((mac1 >> 8) & 0xFF); - m_netif.mac[5] = static_cast(mac1 & 0xFF); + m_mac[0] = static_cast((mac0 >> 24) & 0xFF); + m_mac[1] = static_cast((mac0 >> 16) & 0xFF); + m_mac[2] = static_cast((mac0 >> 8) & 0xFF); + m_mac[3] = static_cast(mac0 & 0xFF); + m_mac[4] = static_cast((mac1 >> 8) & 0xFF); + m_mac[5] = static_cast(mac1 & 0xFF); bool all_zero = true, all_ff = true; for (int i = 0; i < 6; i++) { - if (m_netif.mac[i] != 0x00) all_zero = false; - if (m_netif.mac[i] != 0xFF) all_ff = false; + if (m_mac[i] != 0x00) all_zero = false; + if (m_mac[i] != 0xFF) all_ff = false; } if (all_zero || all_ff) { // Fixed fallback address with the Raspberry Pi vendor prefix. - m_netif.mac[0] = 0xDC; m_netif.mac[1] = 0xA6; m_netif.mac[2] = 0x32; - m_netif.mac[3] = 0x01; m_netif.mac[4] = 0x02; m_netif.mac[5] = 0x03; + m_mac[0] = 0xDC; m_mac[1] = 0xA6; m_mac[2] = 0x32; + m_mac[3] = 0x01; m_mac[4] = 0x02; m_mac[5] = 0x03; log::warn("genet: firmware MAC invalid, using fallback"); } } void bcm_genet_driver::write_mac_address() { - uint32_t mac0 = (static_cast(m_netif.mac[0]) << 24) | - (static_cast(m_netif.mac[1]) << 16) | - (static_cast(m_netif.mac[2]) << 8) | - static_cast(m_netif.mac[3]); - uint32_t mac1 = (static_cast(m_netif.mac[4]) << 8) | - static_cast(m_netif.mac[5]); + uint32_t mac0 = (static_cast(m_mac[0]) << 24) | + (static_cast(m_mac[1]) << 16) | + (static_cast(m_mac[2]) << 8) | + static_cast(m_mac[3]); + uint32_t mac1 = (static_cast(m_mac[4]) << 8) | + static_cast(m_mac[5]); reg_write(UMAC_MAC0, mac0); reg_write(UMAC_MAC1, mac1); } @@ -504,40 +500,39 @@ void bcm_genet_driver::dma_disable_tx_rx() { // TX path -int32_t bcm_genet_driver::tx_callback(net::netif* iface, const uint8_t* frame, size_t len) { - if (!iface || !frame || len == 0) return -1; - - auto* drv = static_cast(iface->driver_data); - if (!drv) return -1; +int32_t bcm_genet_driver::transmit(const uint8_t* frame, size_t len) { + if (!frame || len == 0) { + return -1; + } int32_t result = -1; RUN_ELEVATED({ - sync::irq_lock_guard guard(drv->m_lock); - drv->process_tx_completions(); + sync::irq_lock_guard guard(m_lock); + process_tx_completions(); - if (drv->m_tx_queued >= DMA_DESC_COUNT || len > MAX_PACKET_SIZE) { + if (m_tx_queued >= DMA_DESC_COUNT || len > MAX_PACKET_SIZE) { result = -1; } else { - uint16_t idx = drv->m_tx_prod_index % DMA_DESC_COUNT; + uint16_t idx = m_tx_prod_index % DMA_DESC_COUNT; - uintptr_t buf_va = drv->m_tx_buf_vaddr + + uintptr_t buf_va = m_tx_buf_vaddr + static_cast(idx) * MAX_PACKET_SIZE; string::memcpy(reinterpret_cast(buf_va), frame, len); - uint64_t buf_phys = drv->m_tx_buf_phys + + uint64_t buf_phys = m_tx_buf_phys + static_cast(idx) * MAX_PACKET_SIZE; uint32_t status = TX_DESC_SOP | TX_DESC_EOP | TX_DESC_CRC | TX_DESC_QTAG_MASK | (static_cast(len) << TX_DESC_BUFLEN_SHIFT); - drv->reg_write(TX_DESC_ADDR_LO(idx), static_cast(buf_phys & 0xFFFFFFFF)); - drv->reg_write(TX_DESC_ADDR_HI(idx), static_cast(buf_phys >> 32)); - drv->reg_write(TX_DESC_STATUS(idx), status); + reg_write(TX_DESC_ADDR_LO(idx), static_cast(buf_phys & 0xFFFFFFFF)); + reg_write(TX_DESC_ADDR_HI(idx), static_cast(buf_phys >> 32)); + reg_write(TX_DESC_STATUS(idx), status); - drv->m_tx_prod_index = (drv->m_tx_prod_index + 1) & DMA_INDEX_MASK; - drv->reg_write(TX_DMA_PROD_INDEX(DMA_DEFAULT_QUEUE), drv->m_tx_prod_index); - drv->m_tx_queued++; + m_tx_prod_index = (m_tx_prod_index + 1) & DMA_INDEX_MASK; + reg_write(TX_DMA_PROD_INDEX(DMA_DEFAULT_QUEUE), m_tx_prod_index); + m_tx_queued++; result = 0; } }); @@ -579,13 +574,8 @@ void bcm_genet_driver::process_rx() { if (buf_len > MAX_PACKET_SIZE || buf_len <= 2) goto recycle; - { - uintptr_t buf_va = m_rx_buf_vaddr + - static_cast(idx) * MAX_PACKET_SIZE; - // Skip 2-byte RBUF alignment padding - const uint8_t* frame = reinterpret_cast(buf_va + 2); - net::rx_frame(&m_netif, frame, buf_len - 2); - } + // Frames stop here until a protocol stack attaches. The payload + // starts 2 bytes in, after the RBUF alignment padding. recycle: rx_remap_descriptor(idx); @@ -677,29 +667,6 @@ void bcm_genet_driver::disable_interrupts() { reg_write(INTRL2_CPU_CLEAR, 0xFFFFFFFF); } -// Net interface callbacks - -bool bcm_genet_driver::link_callback(net::netif* iface) { - if (!iface) return false; - auto* drv = static_cast(iface->driver_data); - return drv ? drv->m_link_up : false; -} - -void bcm_genet_driver::poll_callback(net::netif* iface) { - if (!iface) return; - - auto* drv = static_cast(iface->driver_data); - if (!drv) return; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(drv->m_lock); - drv->process_rx(); - drv->process_tx_completions(); - }); - - RUN_ELEVATED(net::drain_deferred_tx()); -} - // MAC filter void bcm_genet_driver::set_promisc(bool enable) { @@ -712,13 +679,13 @@ void bcm_genet_driver::set_promisc(bool enable) { void bcm_genet_driver::setup_rx_filter() { // Slot 0: unicast (our MAC) reg_write(UMAC_MDF_ADDR0(0), - static_cast(m_netif.mac[1]) | - (static_cast(m_netif.mac[0]) << 8)); + static_cast(m_mac[1]) | + (static_cast(m_mac[0]) << 8)); reg_write(UMAC_MDF_ADDR1(0), - static_cast(m_netif.mac[5]) | - (static_cast(m_netif.mac[4]) << 8) | - (static_cast(m_netif.mac[3]) << 16) | - (static_cast(m_netif.mac[2]) << 24)); + static_cast(m_mac[5]) | + (static_cast(m_mac[4]) << 8) | + (static_cast(m_mac[3]) << 16) | + (static_cast(m_mac[2]) << 24)); // Slot 1: broadcast reg_write(UMAC_MDF_ADDR0(1), 0xFFFF); @@ -793,8 +760,8 @@ int32_t bcm_genet_driver::attach() { // Read MAC before reset (in case reset clears the firmware-programmed value) read_mac_address(); log::info("genet: MAC %02x:%02x:%02x:%02x:%02x:%02x", - m_netif.mac[0], m_netif.mac[1], m_netif.mac[2], - m_netif.mac[3], m_netif.mac[4], m_netif.mac[5]); + m_mac[0], m_mac[1], m_mac[2], + m_mac[3], m_mac[4], m_mac[5]); // Reset controller and stop any DMA left running by firmware genet_reset(); @@ -827,39 +794,6 @@ int32_t bcm_genet_driver::attach() { if (m_has_irq) enable_interrupts(); - string::memcpy(m_netif.name, "eth0", 5); - m_netif.transmit = tx_callback; - m_netif.link_up = link_callback; - m_netif.poll = poll_callback; - m_netif.driver_data = this; - - net::register_netif(&m_netif); - - // Wait for PHY link before DHCP, auto-negotiation takes time. - bool got_link = false; - for (int i = 0; i < 50; i++) { - if (m_link_up) { - got_link = true; - break; - } - - RUN_ELEVATED(sched::sleep_ms(100)); - phy_update_link(); - } - - if (!got_link) { - log::warn("genet: link not up after 5 seconds, proceeding anyway"); - } - - int32_t dhcp_rc = net::dhcp_configure(&m_netif); - if (dhcp_rc != net::OK) { - log::warn("genet: DHCP failed (%d), using static fallback", dhcp_rc); - net::configure(&m_netif, - net::ipv4_addr(10, 0, 0, 200), - net::ipv4_addr(255, 255, 255, 0), - net::ipv4_addr(10, 0, 0, 1)); - } - log::info("genet: attached successfully"); dump_state(); return 0; @@ -871,7 +805,6 @@ int32_t bcm_genet_driver::detach() { dma_disable_tx_rx(); disable_interrupts(); teardown_interrupts(); - net::unregister_netif(&m_netif); dma_free(); return 0; @@ -902,8 +835,6 @@ void bcm_genet_driver::run() { process_tx_completions(); }); - RUN_ELEVATED(net::drain_deferred_tx()); - if (++link_poll_counter >= LINK_POLL_INTERVAL) { link_poll_counter = 0; RUN_ELEVATED(phy_update_link()); diff --git a/kernel/drivers/net/bcm_genet.h b/kernel/drivers/net/bcm_genet.h index 2ad6a51f..a1377465 100644 --- a/kernel/drivers/net/bcm_genet.h +++ b/kernel/drivers/net/bcm_genet.h @@ -4,7 +4,6 @@ #include "drivers/platform_driver.h" #include "drivers/net/bcm_genet_regs.h" #include "drivers/net/phy_regs.h" -#include "net/net.h" #include "sync/spinlock.h" namespace drivers { @@ -55,7 +54,7 @@ class bcm_genet_driver : public platform_driver { void dma_disable_tx_rx(); // TX path - static int32_t tx_callback(net::netif* iface, const uint8_t* frame, size_t len); + int32_t transmit(const uint8_t* frame, size_t len); void process_tx_completions(); // RX path @@ -69,10 +68,6 @@ class bcm_genet_driver : public platform_driver { void enable_interrupts(); void disable_interrupts(); - // Net interface callbacks - static bool link_callback(net::netif* iface); - static void poll_callback(net::netif* iface); - // MAC filter void set_promisc(bool enable); void setup_rx_filter(); @@ -103,8 +98,8 @@ class bcm_genet_driver : public platform_driver { // Lock protecting DMA state and register access sync::spinlock m_lock; - // Network interface - net::netif m_netif; + // Hardware address, from the firmware or a fixed fallback + uint8_t m_mac[6]; // Whether interrupts were successfully set up bool m_has_irq; diff --git a/kernel/drivers/net/rtl8168.cpp b/kernel/drivers/net/rtl8168.cpp index 3d24e10d..df15d81b 100644 --- a/kernel/drivers/net/rtl8168.cpp +++ b/kernel/drivers/net/rtl8168.cpp @@ -9,13 +9,14 @@ #include "common/string.h" #include "dynpriv/dynpriv.h" #include "sched/sched.h" -#include "net/net.h" -#include "net/dhcp.h" namespace drivers { using namespace rtl8168; +// Largest frame the rings carry, a 1500 byte payload plus the Ethernet header +constexpr size_t ETH_FRAME_MAX = 1514; + rtl8168_driver::rtl8168_driver(pci::device* dev) : pci_driver("rtl8168", dev) , m_mmio_va(0) @@ -38,7 +39,7 @@ rtl8168_driver::rtl8168_driver(pci::device* dev) , m_has_msi(false) , m_imr(INT_MASK_DEFAULT) { m_lock = sync::SPINLOCK_INIT; - string::memset(&m_netif, 0, sizeof(m_netif)); + string::memset(m_mac, 0, sizeof(m_mac)); } uint8_t rtl8168_driver::reg_read8(uint16_t offset) { @@ -132,21 +133,21 @@ void rtl8168_driver::read_mac_address() { uint32_t idr0 = reg_read32(REG_IDR0); uint32_t idr4 = reg_read32(REG_IDR4); - m_netif.mac[0] = static_cast(idr0 & 0xFF); - m_netif.mac[1] = static_cast((idr0 >> 8) & 0xFF); - m_netif.mac[2] = static_cast((idr0 >> 16) & 0xFF); - m_netif.mac[3] = static_cast((idr0 >> 24) & 0xFF); - m_netif.mac[4] = static_cast(idr4 & 0xFF); - m_netif.mac[5] = static_cast((idr4 >> 8) & 0xFF); + m_mac[0] = static_cast(idr0 & 0xFF); + m_mac[1] = static_cast((idr0 >> 8) & 0xFF); + m_mac[2] = static_cast((idr0 >> 16) & 0xFF); + m_mac[3] = static_cast((idr0 >> 24) & 0xFF); + m_mac[4] = static_cast(idr4 & 0xFF); + m_mac[5] = static_cast((idr4 >> 8) & 0xFF); bool all_zero = true, all_ff = true; for (int i = 0; i < 6; i++) { - if (m_netif.mac[i] != 0x00) all_zero = false; - if (m_netif.mac[i] != 0xFF) all_ff = false; + if (m_mac[i] != 0x00) all_zero = false; + if (m_mac[i] != 0xFF) all_ff = false; } if (all_zero || all_ff) { - m_netif.mac[0] = 0x52; m_netif.mac[1] = 0x54; m_netif.mac[2] = 0x00; - m_netif.mac[3] = 0x12; m_netif.mac[4] = 0x34; m_netif.mac[5] = 0x56; + m_mac[0] = 0x52; m_mac[1] = 0x54; m_mac[2] = 0x00; + m_mac[3] = 0x12; m_mac[4] = 0x34; m_mac[5] = 0x56; log::warn("rtl8168: EEPROM MAC invalid, using fallback"); } } @@ -295,7 +296,7 @@ int32_t rtl8168_driver::alloc_rings() { m_tx_ring = reinterpret_cast(tx_ring_va); m_tx_ring_phys = tx_ring_pa; - size_t tx_buf_total = static_cast(TX_DESC_COUNT) * net::ETH_FRAME_MAX; + size_t tx_buf_total = static_cast(TX_DESC_COUNT) * ETH_FRAME_MAX; size_t tx_buf_pages = (tx_buf_total + 0xFFF) / 0x1000; RUN_ELEVATED( rc = vmm::alloc_contiguous(tx_buf_pages, pmm::ZONE_DMA32, DMA_FLAGS, @@ -419,35 +420,32 @@ void rtl8168_driver::set_descriptor_addresses() { reg_write32(REG_RDSAR + 4, static_cast(m_rx_ring_phys >> 32)); } -int32_t rtl8168_driver::tx_callback( - net::netif* iface, const uint8_t* frame, size_t len -) { - if (!iface || !frame || len == 0) return -1; - - auto* drv = static_cast(iface->driver_data); - if (!drv) return -1; +int32_t rtl8168_driver::transmit(const uint8_t* frame, size_t len) { + if (!frame || len == 0) { + return -1; + } int32_t result = -1; RUN_ELEVATED({ - sync::irq_lock_guard guard(drv->m_lock); + sync::irq_lock_guard guard(m_lock); - drv->process_tx_completions(); + process_tx_completions(); - if (drv->m_tx_queued >= TX_DESC_COUNT || len > net::ETH_FRAME_MAX) { + if (m_tx_queued >= TX_DESC_COUNT || len > ETH_FRAME_MAX) { result = -1; } else { - uint32_t idx = drv->m_tx_prod; + uint32_t idx = m_tx_prod; - uintptr_t buf_va = drv->m_tx_buf_vaddr + - static_cast(idx) * net::ETH_FRAME_MAX; + uintptr_t buf_va = m_tx_buf_vaddr + + static_cast(idx) * ETH_FRAME_MAX; string::memcpy(reinterpret_cast(buf_va), frame, len); - uint64_t buf_phys = drv->m_tx_buf_phys + - static_cast(idx) * net::ETH_FRAME_MAX; + uint64_t buf_phys = m_tx_buf_phys + + static_cast(idx) * ETH_FRAME_MAX; - drv->m_tx_ring[idx].addr_lo = static_cast(buf_phys & 0xFFFFFFFF); - drv->m_tx_ring[idx].addr_hi = static_cast(buf_phys >> 32); - drv->m_tx_ring[idx].opts2 = 0; + m_tx_ring[idx].addr_lo = static_cast(buf_phys & 0xFFFFFFFF); + m_tx_ring[idx].addr_hi = static_cast(buf_phys >> 32); + m_tx_ring[idx].opts2 = 0; uint32_t opts1 = TX_OWN | TX_FS | TX_LS | (static_cast(len) & TX_LEN_MASK); @@ -456,12 +454,12 @@ int32_t rtl8168_driver::tx_callback( // Release fence: NIC must see addr/opts2 before OWN is set. sync::atomic_fence_release(); - drv->m_tx_ring[idx].opts1 = opts1; + m_tx_ring[idx].opts1 = opts1; - drv->m_tx_prod = (idx + 1) % TX_DESC_COUNT; - drv->m_tx_queued++; + m_tx_prod = (idx + 1) % TX_DESC_COUNT; + m_tx_queued++; - drv->reg_write8(REG_TPPOLL, TPPOLL_NPQ); + reg_write8(REG_TPPOLL, TPPOLL_NPQ); result = 0; } @@ -520,14 +518,10 @@ void rtl8168_driver::process_rx() { else goto recycle; - if (frame_len > net::ETH_FRAME_MAX) + if (frame_len > ETH_FRAME_MAX) goto recycle; - uintptr_t buf_va = m_rx_buf_vaddr + - static_cast(idx) * RX_BUF_SIZE; - const uint8_t* frame = reinterpret_cast(buf_va); - - net::rx_frame(&m_netif, frame, frame_len); + // Frames stop here until a protocol stack attaches } recycle: @@ -575,27 +569,6 @@ void rtl8168_driver::disable_interrupts() { reg_write16(REG_ISR, isr); } -bool rtl8168_driver::link_callback(net::netif* iface) { - if (!iface) return false; - auto* drv = static_cast(iface->driver_data); - return drv ? drv->m_link_up : false; -} - -void rtl8168_driver::poll_callback(net::netif* iface) { - if (!iface) return; - - auto* drv = static_cast(iface->driver_data); - if (!drv) return; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(drv->m_lock); - drv->process_rx(); - drv->process_tx_completions(); - }); - - RUN_ELEVATED(net::drain_deferred_tx()); -} - /** * Start sequence per datasheet section 7: configure C+CR, CMD, TCR, RCR. * TX+RX engines are enabled before writing TCR/RCR (datasheet requirement). @@ -668,8 +641,8 @@ void rtl8168_driver::dump_state() { reg_read32(REG_MISC), (reg_read32(REG_MISC) & MISC_RXDV_GATED) ? "ON" : "off"); log::debug("rtl8168: MAC %02x:%02x:%02x:%02x:%02x:%02x", - m_netif.mac[0], m_netif.mac[1], m_netif.mac[2], - m_netif.mac[3], m_netif.mac[4], m_netif.mac[5]); + m_mac[0], m_mac[1], m_mac[2], + m_mac[3], m_mac[4], m_mac[5]); log::debug("rtl8168: chip=0x%03x link=%s speed=%u duplex=%s", static_cast(m_chip_version), m_link_up ? "up" : "down", @@ -717,8 +690,8 @@ int32_t rtl8168_driver::attach() { read_mac_address(); log::info("rtl8168: MAC %02x:%02x:%02x:%02x:%02x:%02x", - m_netif.mac[0], m_netif.mac[1], m_netif.mac[2], - m_netif.mac[3], m_netif.mac[4], m_netif.mac[5]); + m_mac[0], m_mac[1], m_mac[2], + m_mac[3], m_mac[4], m_mac[5]); rc = phy_reset(); if (rc != 0) { @@ -749,14 +722,6 @@ int32_t rtl8168_driver::attach() { hw_start(); - string::memcpy(m_netif.name, "eth0", 5); - m_netif.transmit = tx_callback; - m_netif.link_up = link_callback; - m_netif.poll = poll_callback; - m_netif.driver_data = this; - - net::register_netif(&m_netif); - log::info("rtl8168: attached successfully (%s)", m_has_msi ? "MSI" : "polling"); dump_state(); @@ -767,7 +732,6 @@ int32_t rtl8168_driver::detach() { log::info("rtl8168: detaching"); hw_stop(); - net::unregister_netif(&m_netif); free_rings(); return pci_driver::detach(); @@ -793,11 +757,6 @@ void rtl8168_driver::run() { log::warn("rtl8168: link not up after 5 seconds, proceeding anyway"); } - int32_t dhcp_rc = net::dhcp_configure(&m_netif); - if (dhcp_rc != net::OK) { - log::warn("rtl8168: DHCP failed (%d), interface left unconfigured", dhcp_rc); - } - uint32_t link_poll_counter = 0; constexpr uint32_t LINK_POLL_INTERVAL = 100; @@ -814,8 +773,6 @@ void rtl8168_driver::run() { process_tx_completions(); }); - RUN_ELEVATED(net::drain_deferred_tx()); - if (++link_poll_counter >= LINK_POLL_INTERVAL) { link_poll_counter = 0; phy_update_link(); diff --git a/kernel/drivers/net/rtl8168.h b/kernel/drivers/net/rtl8168.h index f8c06eb5..642defdc 100644 --- a/kernel/drivers/net/rtl8168.h +++ b/kernel/drivers/net/rtl8168.h @@ -3,7 +3,6 @@ #include "drivers/pci_driver.h" #include "drivers/net/rtl8168_regs.h" -#include "net/net.h" #include "sync/spinlock.h" namespace drivers { @@ -50,14 +49,10 @@ class rtl8168_driver : public pci_driver { int32_t fill_rx_ring(); void set_descriptor_addresses(); - static int32_t tx_callback(net::netif* iface, - const uint8_t* frame, size_t len); + int32_t transmit(const uint8_t* frame, size_t len); void process_tx_completions(); void process_rx(); - static bool link_callback(net::netif* iface); - static void poll_callback(net::netif* iface); - void hw_start(); void hw_stop(); void enable_interrupts(); @@ -86,7 +81,7 @@ class rtl8168_driver : public pci_driver { uint64_t m_rx_buf_phys; sync::spinlock m_lock; - net::netif m_netif; + uint8_t m_mac[6]; bool m_has_msi; uint16_t m_imr; }; diff --git a/kernel/drivers/net/virtio_net.cpp b/kernel/drivers/net/virtio_net.cpp index 33e5e8e3..bef81756 100644 --- a/kernel/drivers/net/virtio_net.cpp +++ b/kernel/drivers/net/virtio_net.cpp @@ -7,9 +7,6 @@ #include "common/string.h" #include "dynpriv/dynpriv.h" #include "sched/sched.h" -#include "net/net.h" -#include "net/ipv4.h" -#include "net/dhcp.h" namespace drivers { @@ -217,22 +214,22 @@ int32_t virtio_net_driver::negotiate_features() { int32_t virtio_net_driver::read_mac() { if (!m_device_cfg || !m_has_mac) { // Device provides no MAC, use a fixed locally administered address. - m_netif.mac[0] = 0x52; - m_netif.mac[1] = 0x54; - m_netif.mac[2] = 0x00; - m_netif.mac[3] = 0x12; - m_netif.mac[4] = 0x34; - m_netif.mac[5] = 0x56; + m_mac[0] = 0x52; + m_mac[1] = 0x54; + m_mac[2] = 0x00; + m_mac[3] = 0x12; + m_mac[4] = 0x34; + m_mac[5] = 0x56; return 0; } for (int i = 0; i < 6; i++) { - m_netif.mac[i] = m_device_cfg->mac[i]; + m_mac[i] = m_device_cfg->mac[i]; } log::info("virtio-net: MAC %02x:%02x:%02x:%02x:%02x:%02x", - m_netif.mac[0], m_netif.mac[1], m_netif.mac[2], - m_netif.mac[3], m_netif.mac[4], m_netif.mac[5]); + m_mac[0], m_mac[1], m_mac[2], + m_mac[3], m_mac[4], m_mac[5]); return 0; } @@ -444,18 +441,6 @@ int32_t virtio_net_driver::attach() { log::info("virtio-net: DRIVER_OK, device is live"); - // Register with network stack - string::memcpy(m_netif.name, "eth0", 5); - m_netif.transmit = tx_callback; - m_netif.link_up = link_callback; - m_netif.poll = poll_callback; - m_netif.driver_data = this; - - net::register_netif(&m_netif); - - // IP configuration is deferred to run(), where DHCP runs in a - // proper kernel task context with sched::sleep_ms() available. - return 0; } @@ -514,12 +499,10 @@ void virtio_net_driver::drain_rx_locked(rx_batch& batch) { } } +// Frames stop here until a protocol stack attaches, so this only hands the +// buffers back. It runs without m_vq_lock so delivery may later transmit. void virtio_net_driver::deliver_rx_batch(rx_batch& batch) { - // Called without m_vq_lock held so protocol processing can - // call back into tx_callback (e.g. ARP replies) without deadlock. for (uint16_t i = 0; i < batch.count; i++) { - net::rx_frame(&m_netif, batch.entries[i].data, batch.entries[i].len); - // Clear delivering flag so replenish_rx can re-post this buffer m_rx_bufs[batch.entries[i].buf_idx].delivering = false; } } @@ -567,18 +550,6 @@ void virtio_net_driver::process_tx_completions() { void virtio_net_driver::run() { log::info("virtio-net: driver task running"); - // DHCP runs here rather than in attach() because its timeouts need - // sched::sleep_ms(), and attach() runs before the driver task exists. - int32_t dhcp_rc = net::dhcp_configure(&m_netif); - if (dhcp_rc != net::OK) { - log::warn("virtio-net: DHCP failed (%d), using static fallback", dhcp_rc); - // QEMU user-mode networking defaults: IP, netmask, gateway. - net::configure(&m_netif, - net::ipv4_addr(10, 0, 2, 15), - net::ipv4_addr(255, 255, 255, 0), - net::ipv4_addr(10, 0, 2, 2)); - } - // Check MSI mode once at start (elevated because m_dev is in privileged memory) bool has_msi = false; RUN_ELEVATED(has_msi = m_dev->get_msi_state().mode != pci::MSI_MODE_NONE); @@ -604,27 +575,22 @@ void virtio_net_driver::run() { sync::irq_lock_guard guard(m_vq_lock); replenish_rx(); }); - - // Flush protocol responses queued during RX delivery. At top level - // ipv4_send and ARP resolution cannot recurse into deliver_rx_batch. - RUN_ELEVATED(net::drain_deferred_tx()); } } -int32_t virtio_net_driver::tx_callback(net::netif* iface, const uint8_t* frame, size_t len) { - if (!iface || !frame || len == 0) return -1; - - auto* drv = static_cast(iface->driver_data); - if (!drv) return -1; +int32_t virtio_net_driver::transmit(const uint8_t* frame, size_t len) { + if (!frame || len == 0) { + return -1; + } int32_t result = -1; RUN_ELEVATED({ - sync::irq_lock_guard guard(drv->m_vq_lock); + sync::irq_lock_guard guard(m_vq_lock); // Find a free TX buffer int32_t buf_idx = -1; for (uint16_t i = 0; i < TX_BUF_COUNT; i++) { - if (!drv->m_tx_bufs[i].in_use) { + if (!m_tx_bufs[i].in_use) { buf_idx = static_cast(i); break; } @@ -632,9 +598,9 @@ int32_t virtio_net_driver::tx_callback(net::netif* iface, const uint8_t* frame, if (buf_idx < 0) { // Process completions and try again - drv->process_tx_completions(); + process_tx_completions(); for (uint16_t i = 0; i < TX_BUF_COUNT; i++) { - if (!drv->m_tx_bufs[i].in_use) { + if (!m_tx_bufs[i].in_use) { buf_idx = static_cast(i); break; } @@ -642,9 +608,9 @@ int32_t virtio_net_driver::tx_callback(net::netif* iface, const uint8_t* frame, } if (buf_idx >= 0) { - auto& buf = drv->m_tx_bufs[buf_idx]; + auto& buf = m_tx_bufs[buf_idx]; - size_t hdr_size = drv->m_net_hdr_size; + size_t hdr_size = m_net_hdr_size; if (hdr_size + len <= TX_BUF_SIZE) { auto* nethdr = reinterpret_cast(buf.vaddr); string::memset(nethdr, 0, hdr_size); @@ -652,11 +618,11 @@ int32_t virtio_net_driver::tx_callback(net::netif* iface, const uint8_t* frame, string::memcpy(reinterpret_cast(buf.vaddr + hdr_size), frame, len); - int32_t rc = drv->m_txq.add_buf(buf.phys, static_cast(hdr_size + len), 0); + int32_t rc = m_txq.add_buf(buf.phys, static_cast(hdr_size + len), 0); if (rc >= 0) { buf.in_use = true; buf.desc_id = static_cast(rc); - drv->m_txq.kick(drv->m_tx_notify_addr); + m_txq.kick(m_tx_notify_addr); result = 0; } } @@ -666,36 +632,10 @@ int32_t virtio_net_driver::tx_callback(net::netif* iface, const uint8_t* frame, return result; } -void virtio_net_driver::poll_callback(net::netif* iface) { - if (!iface) return; - - auto* drv = static_cast(iface->driver_data); - if (!drv) return; - - rx_batch batch; - RUN_ELEVATED({ - sync::irq_lock_guard guard(drv->m_vq_lock); - drv->drain_rx_locked(batch); - drv->process_tx_completions(); - }); - RUN_ELEVATED(drv->deliver_rx_batch(batch)); - RUN_ELEVATED({ - sync::irq_lock_guard guard(drv->m_vq_lock); - drv->replenish_rx(); - }); - - RUN_ELEVATED(net::drain_deferred_tx()); -} - -bool virtio_net_driver::link_callback(net::netif* iface) { - if (!iface) return false; - - auto* drv = static_cast(iface->driver_data); - if (!drv) return false; - +bool virtio_net_driver::link_up() { bool up = true; - if (drv->m_has_status && drv->m_device_cfg) { - up = (drv->m_device_cfg->status & 1) != 0; + if (m_has_status && m_device_cfg) { + up = (m_device_cfg->status & 1) != 0; } return up; diff --git a/kernel/drivers/net/virtio_net.h b/kernel/drivers/net/virtio_net.h index 3a883e37..036be82b 100644 --- a/kernel/drivers/net/virtio_net.h +++ b/kernel/drivers/net/virtio_net.h @@ -4,7 +4,6 @@ #include "drivers/pci_driver.h" #include "drivers/net/virtio_pci.h" #include "drivers/net/virtio_queue.h" -#include "net/net.h" #include "common/string.h" #include "sync/spinlock.h" @@ -21,9 +20,7 @@ class virtio_net_driver : public pci_driver { , m_device_cfg(nullptr) , m_rx_notify_addr(0) , m_tx_notify_addr(0) { - // Zero netif including any padding - uint8_t* p = reinterpret_cast(&m_netif); - for (size_t i = 0; i < sizeof(m_netif); i++) p[i] = 0; + string::memset(m_mac, 0, sizeof(m_mac)); m_vq_lock = sync::SPINLOCK_INIT; } @@ -44,7 +41,7 @@ class virtio_net_driver : public pci_driver { int32_t read_mac(); // Batch of received frames drained from the virtqueue under lock, - // then delivered to the protocol stack without the lock held. + // then released without the lock held. static constexpr uint16_t RX_BATCH_MAX = 16; struct rx_batch_entry { const uint8_t* data; @@ -57,9 +54,8 @@ class virtio_net_driver : public pci_driver { }; // Packet I/O - static int32_t tx_callback(net::netif* iface, const uint8_t* frame, size_t len); - static bool link_callback(net::netif* iface); - static void poll_callback(net::netif* iface); + int32_t transmit(const uint8_t* frame, size_t len); + bool link_up(); void drain_rx_locked(rx_batch& batch); // requires m_vq_lock void deliver_rx_batch(rx_batch& batch); // called without m_vq_lock void process_tx_completions(); // under lock @@ -110,11 +106,11 @@ class virtio_net_driver : public pci_driver { tx_buf_info m_tx_bufs[TX_BUF_COUNT]; // Protects all virtqueue and buffer pool state (m_rxq, m_txq, m_rx_bufs, - // m_tx_bufs), held by run(), poll_callback, and tx_callback. + // m_tx_bufs), held by run() and transmit(). sync::spinlock m_vq_lock; - // Network interface - net::netif m_netif; + // Hardware address, from the device or a fixed fallback + uint8_t m_mac[6]; // Feature flags bool m_has_mac = false; diff --git a/kernel/net/arp.cpp b/kernel/net/arp.cpp deleted file mode 100644 index 44068a1e..00000000 --- a/kernel/net/arp.cpp +++ /dev/null @@ -1,247 +0,0 @@ -#include "net/arp.h" -#include "net/net.h" -#include "net/netinfo.h" -#include "net/ethernet.h" -#include "net/byteorder.h" -#include "common/logging.h" -#include "common/string.h" -#include "sync/spinlock.h" -#include "clock/clock.h" -#include "sched/sched.h" -#include "dynpriv/dynpriv.h" - -namespace net { - -constexpr uint64_t ARP_ENTRY_TTL_NS = 60ULL * 1000000000ULL; // 60 seconds - -namespace { - -struct arp_entry { - uint32_t ip; // host byte order - uint8_t mac[MAC_ADDR_LEN]; - bool valid; - uint64_t last_updated_ns; -}; - -} // anonymous namespace - -static arp_entry g_arp_table[ARP_TABLE_SIZE] = {}; -static sync::spinlock g_arp_lock = sync::SPINLOCK_INIT; - -void arp_init() { - for (uint32_t i = 0; i < ARP_TABLE_SIZE; i++) { - g_arp_table[i].valid = false; - } -} - -static void arp_table_update(uint32_t ip, const uint8_t* mac) { - RUN_ELEVATED({ - sync::irq_state irq = sync::spin_lock_irqsave(g_arp_lock); - uint64_t now = clock::now_ns(); - - bool updated = false; - for (uint32_t i = 0; i < ARP_TABLE_SIZE; i++) { - if (g_arp_table[i].valid && g_arp_table[i].ip == ip) { - string::memcpy(g_arp_table[i].mac, mac, MAC_ADDR_LEN); - g_arp_table[i].last_updated_ns = now; - updated = true; - break; - } - } - - if (!updated) { - for (uint32_t i = 0; i < ARP_TABLE_SIZE; i++) { - if (!g_arp_table[i].valid) { - g_arp_table[i].ip = ip; - string::memcpy(g_arp_table[i].mac, mac, MAC_ADDR_LEN); - g_arp_table[i].valid = true; - g_arp_table[i].last_updated_ns = now; - updated = true; - break; - } - } - } - - // Table full: evict the oldest entry - if (!updated) { - uint32_t oldest = 0; - for (uint32_t i = 1; i < ARP_TABLE_SIZE; i++) { - if (g_arp_table[i].last_updated_ns < g_arp_table[oldest].last_updated_ns) { - oldest = i; - } - } - - g_arp_table[oldest].ip = ip; - string::memcpy(g_arp_table[oldest].mac, mac, MAC_ADDR_LEN); - g_arp_table[oldest].valid = true; - g_arp_table[oldest].last_updated_ns = now; - } - - sync::spin_unlock_irqrestore(g_arp_lock, irq); - }); -} - -static bool arp_table_lookup(uint32_t ip, uint8_t* out_mac) { - bool found = false; - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_arp_lock); - uint64_t now = clock::now_ns(); - - for (uint32_t i = 0; i < ARP_TABLE_SIZE; i++) { - if (g_arp_table[i].valid && g_arp_table[i].ip == ip) { - if (now - g_arp_table[i].last_updated_ns > ARP_ENTRY_TTL_NS) { - break; // stale, force re-resolution - } - - string::memcpy(out_mac, g_arp_table[i].mac, MAC_ADDR_LEN); - found = true; - break; - } - } - }); - - return found; -} - -void arp_recv(netif* iface, const uint8_t* data, size_t len) { - if (!iface || !data || len < sizeof(arp_header)) { - return; - } - - const auto* arp = reinterpret_cast(data); - - if (ntohs(arp->hw_type) != ARP_HW_ETHERNET) { - return; - } - - if (ntohs(arp->proto_type) != ETH_TYPE_IPV4) { - return; - } - - if (arp->hw_len != MAC_ADDR_LEN) { - return; - } - - if (arp->proto_len != 4) { - return; - } - - uint32_t sender_ip = ntohl(arp->sender_ip); - uint32_t target_ip = ntohl(arp->target_ip); - uint16_t opcode = ntohs(arp->opcode); - - // Always learn from incoming ARP packets - arp_table_update(sender_ip, arp->sender_mac); - - if (opcode == ARP_OP_REQUEST && iface->configured && target_ip == iface->ipv4_addr) { - // Queue ARP reply for deferred transmission (same principle as - // ICMP echo replies, no inline TX from RX processing context). - arp_header reply = {}; - reply.hw_type = htons(ARP_HW_ETHERNET); - reply.proto_type = htons(ETH_TYPE_IPV4); - reply.hw_len = MAC_ADDR_LEN; - reply.proto_len = 4; - reply.opcode = htons(ARP_OP_REPLY); - - string::memcpy(reply.sender_mac, iface->mac, MAC_ADDR_LEN); - reply.sender_ip = htonl(iface->ipv4_addr); - string::memcpy(reply.target_mac, arp->sender_mac, MAC_ADDR_LEN); - reply.target_ip = arp->sender_ip; - - queue_deferred_eth_tx(iface, arp->sender_mac, ETH_TYPE_ARP, - reinterpret_cast(&reply), sizeof(reply)); - } else if (opcode == ARP_OP_REPLY) { - log::debug("arp: got reply from %u.%u.%u.%u", - (sender_ip >> 24) & 0xFF, (sender_ip >> 16) & 0xFF, - (sender_ip >> 8) & 0xFF, sender_ip & 0xFF); - } -} - -void arp_send_request(netif* iface, uint32_t target_ip) { - if (!iface || !iface->configured) return; - - arp_header req = {}; - req.hw_type = htons(ARP_HW_ETHERNET); - req.proto_type = htons(ETH_TYPE_IPV4); - req.hw_len = MAC_ADDR_LEN; - req.proto_len = 4; - req.opcode = htons(ARP_OP_REQUEST); - string::memcpy(req.sender_mac, iface->mac, MAC_ADDR_LEN); - req.sender_ip = htonl(iface->ipv4_addr); - string::memset(req.target_mac, 0, MAC_ADDR_LEN); - req.target_ip = htonl(target_ip); - - eth_send(iface, ETH_BROADCAST, ETH_TYPE_ARP, - reinterpret_cast(&req), sizeof(req)); -} - -int32_t arp_resolve(netif* iface, uint32_t target_ip, uint8_t* out_mac) { - if (!iface || !out_mac) return ERR_INVAL; - - // Broadcast destinations map directly to the Ethernet broadcast MAC - if (target_ip == 0xFFFFFFFF || - target_ip == (iface->ipv4_addr | ~iface->ipv4_netmask)) { - string::memcpy(out_mac, ETH_BROADCAST, MAC_ADDR_LEN); - return OK; - } - - // Check cache first - if (arp_table_lookup(target_ip, out_mac)) { - return OK; - } - - // Sleep between polls so the scheduler can run other tasks and interrupts can fire - constexpr uint32_t POLLS_PER_ATTEMPT = 100; - constexpr uint64_t POLL_SLEEP_MS = 10; - - for (uint32_t attempt = 0; attempt < ARP_RETRY_COUNT; attempt++) { - arp_send_request(iface, target_ip); - - for (uint32_t poll = 0; poll < POLLS_PER_ATTEMPT; poll++) { - RUN_ELEVATED({ - sched::sleep_ms(POLL_SLEEP_MS); - if (iface->poll) { - iface->poll(iface); - } - }); - - if (arp_table_lookup(target_ip, out_mac)) { - return OK; - } - } - } - - log::warn("arp: failed to resolve %u.%u.%u.%u", - (target_ip >> 24) & 0xFF, (target_ip >> 16) & 0xFF, - (target_ip >> 8) & 0xFF, target_ip & 0xFF); - return ERR_NOARP; -} - -__PRIVILEGED_CODE int32_t query_arp_table(arp_table_status* out) { - if (!out) return ERR_INVAL; - - string::memset(out, 0, sizeof(arp_table_status)); - uint32_t count = 0; - - { - sync::irq_lock_guard guard(g_arp_lock); - uint64_t now = clock::now_ns(); - - for (uint32_t i = 0; i < ARP_TABLE_SIZE && count < ARP_QUERY_MAX; i++) { - if (!g_arp_table[i].valid) continue; - - auto& e = out->entries[count]; - e.ipv4_addr = g_arp_table[i].ip; - string::memcpy(e.mac, g_arp_table[i].mac, MAC_ADDR_LEN); - uint64_t age_ns = now - g_arp_table[i].last_updated_ns; - e.age_ms = static_cast(age_ns / 1000000ULL); - e.flags = 0; - count++; - } - } - - out->entry_count = count; - return OK; -} - -} // namespace net diff --git a/kernel/net/arp.h b/kernel/net/arp.h deleted file mode 100644 index ffdc1566..00000000 --- a/kernel/net/arp.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef STELLUX_NET_ARP_H -#define STELLUX_NET_ARP_H - -#include "common/types.h" -#include "net/net.h" - -namespace net { - -constexpr uint16_t ARP_HW_ETHERNET = 1; -constexpr uint16_t ARP_OP_REQUEST = 1; -constexpr uint16_t ARP_OP_REPLY = 2; - -constexpr uint32_t ARP_TABLE_SIZE = 32; -constexpr uint32_t ARP_RETRY_COUNT = 3; - -struct arp_header { - uint16_t hw_type; // network byte order - uint16_t proto_type; // network byte order - uint8_t hw_len; - uint8_t proto_len; - uint16_t opcode; // network byte order - uint8_t sender_mac[MAC_ADDR_LEN]; - uint32_t sender_ip; // network byte order - uint8_t target_mac[MAC_ADDR_LEN]; - uint32_t target_ip; // network byte order -} __attribute__((packed)); - -static_assert(sizeof(arp_header) == 28, "arp_header must be 28 bytes"); - -/** - * Initialize the ARP table. - */ -void arp_init(); - -/** - * Process a received ARP packet (after Ethernet header is stripped). - */ -void arp_recv(netif* iface, const uint8_t* data, size_t len); - -/** - * Resolve an IPv4 address (host byte order) to a MAC address. - * May block briefly if an ARP request needs to be sent. - * @return 0 on success (out_mac filled), negative on failure. - */ -int32_t arp_resolve(netif* iface, uint32_t target_ip, uint8_t* out_mac); - -/** - * Send an ARP request for the given IP (host byte order). - */ -void arp_send_request(netif* iface, uint32_t target_ip); - -} // namespace net - -#endif // STELLUX_NET_ARP_H diff --git a/kernel/net/byteorder.h b/kernel/net/byteorder.h deleted file mode 100644 index c2d98302..00000000 --- a/kernel/net/byteorder.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef STELLUX_NET_BYTEORDER_H -#define STELLUX_NET_BYTEORDER_H - -#include "common/types.h" - -namespace net { - -inline uint16_t htons(uint16_t v) { return __builtin_bswap16(v); } -inline uint32_t htonl(uint32_t v) { return __builtin_bswap32(v); } -inline uint16_t ntohs(uint16_t v) { return __builtin_bswap16(v); } -inline uint32_t ntohl(uint32_t v) { return __builtin_bswap32(v); } - -} // namespace net - -#endif // STELLUX_NET_BYTEORDER_H diff --git a/kernel/net/checksum.h b/kernel/net/checksum.h deleted file mode 100644 index e21c7b84..00000000 --- a/kernel/net/checksum.h +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef STELLUX_NET_CHECKSUM_H -#define STELLUX_NET_CHECKSUM_H - -#include "common/types.h" - -namespace net { - -/** - * Compute RFC 1071 internet checksum (one's complement sum). - * Used by IPv4 and ICMP headers. - */ -inline uint16_t inet_checksum(const void* data, size_t len) { - const auto* ptr = static_cast(data); - uint32_t sum = 0; - - // Sum 16-bit words - while (len > 1) { - uint16_t word = static_cast(ptr[0]) | - (static_cast(ptr[1]) << 8); - sum += word; - ptr += 2; - len -= 2; - } - - // Add odd byte if present - if (len == 1) { - sum += static_cast(ptr[0]); - } - - // Fold 32-bit sum into 16 bits - while (sum >> 16) { - sum = (sum & 0xFFFF) + (sum >> 16); - } - - return static_cast(~sum); -} - -/** - * Compute transport-layer checksum over pseudo-header + payload. - * Used by both UDP (protocol=17) and TCP (protocol=6). - * IP addresses must be in network byte order. - * - * TX usage: if result is 0, transmit 0xFFFF. - * RX usage: pass full segment including checksum field; result == 0 means valid. - */ -inline uint16_t transport_checksum(uint32_t src_ip_net, uint32_t dst_ip_net, - uint8_t protocol, - const uint8_t* payload, size_t payload_len) { - uint32_t sum = 0; - - const auto* s = reinterpret_cast(&src_ip_net); - const auto* d = reinterpret_cast(&dst_ip_net); - sum += static_cast(s[0]) | (static_cast(s[1]) << 8); - sum += static_cast(s[2]) | (static_cast(s[3]) << 8); - sum += static_cast(d[0]) | (static_cast(d[1]) << 8); - sum += static_cast(d[2]) | (static_cast(d[3]) << 8); - - // Zero + protocol as LE word: network bytes [0x00, proto] - sum += static_cast(protocol) << 8; - - // Payload length in network byte order as LE word - uint16_t pl = static_cast(payload_len); - sum += static_cast((pl >> 8) | ((pl & 0xFF) << 8)); - - const uint8_t* ptr = payload; - size_t remaining = payload_len; - while (remaining > 1) { - uint16_t word = static_cast(ptr[0]) | - (static_cast(ptr[1]) << 8); - sum += word; - ptr += 2; - remaining -= 2; - } - - if (remaining == 1) { - sum += static_cast(ptr[0]); - } - - while (sum >> 16) { - sum = (sum & 0xFFFF) + (sum >> 16); - } - - return static_cast(~sum); -} - -inline uint16_t udp_checksum(uint32_t src_ip_net, uint32_t dst_ip_net, - const uint8_t* udp_packet, size_t udp_len) { - return transport_checksum(src_ip_net, dst_ip_net, 17, udp_packet, udp_len); -} - -inline uint16_t tcp_checksum(uint32_t src_ip_net, uint32_t dst_ip_net, - const uint8_t* tcp_segment, size_t tcp_len) { - return transport_checksum(src_ip_net, dst_ip_net, 6, tcp_segment, tcp_len); -} - -} // namespace net - -#endif // STELLUX_NET_CHECKSUM_H diff --git a/kernel/net/dhcp.cpp b/kernel/net/dhcp.cpp deleted file mode 100644 index 480feed4..00000000 --- a/kernel/net/dhcp.cpp +++ /dev/null @@ -1,651 +0,0 @@ -#include "net/dhcp.h" -#include "net/ethernet.h" -#include "net/ipv4.h" -#include "net/udp.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "common/logging.h" -#include "common/string.h" -#include "mm/heap.h" -#include "sched/sched.h" -#include "clock/clock.h" -#include "dynpriv/dynpriv.h" -#include "sync/atomic.h" - -namespace net { - -// The DHCP client runs at Ring 3 and cannot use __PRIVILEGED_CODE APIs, so -// udp_recv() at Ring 0 hands packets over through a static receive context. - -namespace { - -struct dhcp_rx_context { - uint8_t buffer[DHCP_PACKET_MAX]; - size_t length; - sync::atomic ready; // set by udp_recv hook, cleared by DHCP poll - sync::atomic active; // true while DHCP is waiting for packets -}; - -} // anonymous namespace - -// Single static context, only one DHCP exchange at a time. Deliberately not -// __PRIVILEGED_DATA so both Ring 0 delivery and the Ring 3 client can access it. -static dhcp_rx_context g_dhcp_rx = {}; - -// Not __PRIVILEGED_CODE, the receive context lives in regular .bss so any -// delivery context, interrupt-driven or elevated poll, can call this. -void dhcp_rx_hook(const uint8_t* data, size_t len) { - if (!g_dhcp_rx.active.load_acquire()) return; - - if (g_dhcp_rx.ready.load_acquire()) return; - - size_t copy_len = len < DHCP_PACKET_MAX ? len : DHCP_PACKET_MAX; - string::memcpy(g_dhcp_rx.buffer, data, copy_len); - g_dhcp_rx.length = copy_len; - - // Write barrier: ensure buffer/length are visible before ready flag - g_dhcp_rx.ready.store_release(true); -} - -// Internal helpers - -/** - * Initialize the common fields of a DHCP packet header. - */ -static void init_dhcp_header(dhcp_packet* pkt, const uint8_t* mac, uint32_t xid) { - string::memset(pkt, 0, sizeof(dhcp_packet)); - pkt->op = DHCP_OP_BOOTREQUEST; - pkt->htype = DHCP_HTYPE_ETHERNET; - pkt->hlen = DHCP_HLEN_ETHERNET; - pkt->hops = 0; - pkt->xid = xid; // already in network byte order - pkt->secs = 0; - pkt->flags = htons(DHCP_FLAG_BROADCAST); - // ciaddr, yiaddr, siaddr, giaddr all zero - string::memcpy(pkt->chaddr, mac, MAC_ADDR_LEN); - // sname and file are zero-filled by memset - pkt->magic = htonl(DHCP_MAGIC_COOKIE); -} - -/** - * Append a DHCP option to the options buffer. - * @return Number of bytes written, or 0 if buffer is too small. - */ -static size_t append_option(uint8_t* opts, size_t offset, size_t max_len, - uint8_t code, const uint8_t* data, uint8_t data_len) { - if (code == DHCP_OPT_END) { - if (offset >= max_len) return 0; - opts[offset] = DHCP_OPT_END; - return 1; - } - - size_t needed = 2 + static_cast(data_len); - if (offset + needed > max_len) return 0; - - opts[offset] = code; - opts[offset + 1] = data_len; - if (data_len > 0 && data) { - string::memcpy(opts + offset + 2, data, data_len); - } - - return needed; -} - -/** - * Append a single-byte DHCP option. - */ -static size_t append_option_u8(uint8_t* opts, size_t offset, size_t max_len, - uint8_t code, uint8_t value) { - return append_option(opts, offset, max_len, code, &value, 1); -} - -/** - * Append a 4-byte (uint32_t, network byte order) DHCP option. - */ -static size_t append_option_u32(uint8_t* opts, size_t offset, size_t max_len, - uint8_t code, uint32_t value_net) { - return append_option(opts, offset, max_len, code, - reinterpret_cast(&value_net), 4); -} - -/** - * Generate a pseudo-random transaction ID from the clock and MAC address. - * Returns the xid in network byte order. - */ -static uint32_t generate_xid(const uint8_t* mac) { - uint64_t ns = clock::now_ns(); - uint32_t seed = static_cast(ns ^ (ns >> 32)); - - // Mix in MAC bytes for additional entropy - seed ^= (static_cast(mac[2]) << 24) | - (static_cast(mac[3]) << 16) | - (static_cast(mac[4]) << 8) | - static_cast(mac[5]); - - return htonl(seed); -} - -/** - * Build a full Ethernet+IPv4+UDP broadcast frame around a DHCP message and - * hand it straight to the driver's transmit callback. The normal send path - * is bypassed, it needs a configured interface and privileged allocations, - * neither of which holds for the Ring 3 DHCP client. - */ -static int32_t send_dhcp_broadcast(netif* iface, const uint8_t* payload, - size_t payload_len) { - if (!iface || !iface->transmit || !payload || payload_len == 0) { - return ERR_INVAL; - } - - size_t udp_total = sizeof(udp_header) + payload_len; - size_t ip_total = sizeof(ipv4_header) + udp_total; - size_t frame_len = sizeof(eth_header) + ip_total; - - if (frame_len > ETH_FRAME_MAX) { - return ERR_INVAL; - } - - // Use unprivileged heap, callable from Ring 3 (auto-elevates internally) - auto* frame = static_cast(heap::uzalloc(frame_len)); - if (!frame) { - return ERR_NOMEM; - } - - // Ethernet header - auto* eth = reinterpret_cast(frame); - string::memcpy(eth->dst, ETH_BROADCAST, MAC_ADDR_LEN); - string::memcpy(eth->src, iface->mac, MAC_ADDR_LEN); - eth->ethertype = htons(ETH_TYPE_IPV4); - - // IPv4 header - auto* ip = reinterpret_cast(frame + sizeof(eth_header)); - ip->ver_ihl = (4 << 4) | 5; // IPv4, IHL=5 (20 bytes) - ip->tos = 0; - ip->total_len = htons(static_cast(ip_total)); - ip->id = 0; - ip->flags_frag = 0; // no DF, no fragmentation - ip->ttl = 64; - ip->protocol = IPV4_PROTO_UDP; - ip->checksum = 0; - ip->src_ip = 0; // 0.0.0.0 - ip->dst_ip = htonl(0xFFFFFFFF); // 255.255.255.255 - ip->checksum = inet_checksum(ip, sizeof(ipv4_header)); - - // UDP header - auto* udp = reinterpret_cast(frame + sizeof(eth_header) + sizeof(ipv4_header)); - udp->src_port = htons(DHCP_CLIENT_PORT); - udp->dst_port = htons(DHCP_SERVER_PORT); - udp->length = htons(static_cast(udp_total)); - udp->checksum = 0; - - // DHCP payload - string::memcpy(frame + sizeof(eth_header) + sizeof(ipv4_header) + sizeof(udp_header), - payload, payload_len); - - // Compute UDP checksum over pseudo-header + UDP segment. - uint16_t csum = udp_checksum(ip->src_ip, ip->dst_ip, - frame + sizeof(eth_header) + sizeof(ipv4_header), - udp_total); - udp->checksum = (csum == 0) ? static_cast(0xFFFF) : csum; - - // The driver's transmit callback elevates internally for its lock/DMA access - int32_t rc = iface->transmit(iface, frame, frame_len); - heap::ufree(frame); - return rc; -} - -/** - * Check if a DHCP response is available in the receive hook buffer. - * Non-blocking: returns false if no data is available. - */ -static bool try_read_dhcp_response(uint8_t* out_buf, size_t buf_size, - size_t* out_len) { - if (!out_buf || !out_len) return false; - - if (!g_dhcp_rx.ready.load_acquire()) { - return false; - } - - size_t copy_len = g_dhcp_rx.length < buf_size ? g_dhcp_rx.length : buf_size; - string::memcpy(out_buf, g_dhcp_rx.buffer, copy_len); - *out_len = copy_len; - - // Mark as consumed - g_dhcp_rx.ready.store_release(false); - - return true; -} - -/** - * Activate the DHCP receive hook. udp_recv() will start copying - * port-68 packets to the static buffer. - */ -static void activate_rx_hook() { - g_dhcp_rx.length = 0; - g_dhcp_rx.ready.store_release(false); - g_dhcp_rx.active.store_release(true); -} - -/** - * Deactivate the DHCP receive hook. - */ -static void deactivate_rx_hook() { - g_dhcp_rx.active.store_release(false); - g_dhcp_rx.ready.store_release(false); -} - -// Packet Build Functions - -size_t dhcp_build_discover(uint8_t* out, size_t out_size, - const uint8_t* mac, uint32_t xid) { - if (!out || !mac || out_size < sizeof(dhcp_packet) + 16) { - return 0; - } - - auto* pkt = reinterpret_cast(out); - init_dhcp_header(pkt, mac, xid); - - uint8_t* opts = out + sizeof(dhcp_packet); - size_t opts_max = out_size - sizeof(dhcp_packet); - size_t pos = 0; - size_t n; - - // Option 53: DHCP Message Type = DISCOVER - n = append_option_u8(opts, pos, opts_max, DHCP_OPT_MSG_TYPE, DHCP_MSG_DISCOVER); - if (n == 0) return 0; - - pos += n; - - // Option 12: Hostname - static const char hostname[] = "stellux"; - n = append_option(opts, pos, opts_max, DHCP_OPT_HOSTNAME, - reinterpret_cast(hostname), sizeof(hostname) - 1); - if (n == 0) return 0; - - pos += n; - - // Option 55: Parameter Request List - uint8_t param_list[] = { - DHCP_OPT_SUBNET_MASK, - DHCP_OPT_ROUTER, - DHCP_OPT_DNS, - DHCP_OPT_LEASE_TIME, - }; - n = append_option(opts, pos, opts_max, DHCP_OPT_PARAM_LIST, - param_list, sizeof(param_list)); - if (n == 0) return 0; - - pos += n; - - // Option 255: End - n = append_option(opts, pos, opts_max, DHCP_OPT_END, nullptr, 0); - if (n == 0) return 0; - - pos += n; - - return sizeof(dhcp_packet) + pos; -} - -size_t dhcp_build_request(uint8_t* out, size_t out_size, - const uint8_t* mac, uint32_t xid, - uint32_t offered_ip, uint32_t server_id) { - if (!out || !mac || out_size < sizeof(dhcp_packet) + 32) { - return 0; - } - - auto* pkt = reinterpret_cast(out); - init_dhcp_header(pkt, mac, xid); - - uint8_t* opts = out + sizeof(dhcp_packet); - size_t opts_max = out_size - sizeof(dhcp_packet); - size_t pos = 0; - size_t n; - - // Option 53: DHCP Message Type = REQUEST - n = append_option_u8(opts, pos, opts_max, DHCP_OPT_MSG_TYPE, DHCP_MSG_REQUEST); - if (n == 0) return 0; - - pos += n; - - // Option 12: Hostname - static const char hostname[] = "stellux"; - n = append_option(opts, pos, opts_max, DHCP_OPT_HOSTNAME, - reinterpret_cast(hostname), sizeof(hostname) - 1); - if (n == 0) return 0; - - pos += n; - - // Option 50: Requested IP Address (network byte order) - uint32_t req_ip_net = htonl(offered_ip); - n = append_option_u32(opts, pos, opts_max, DHCP_OPT_REQUESTED_IP, req_ip_net); - if (n == 0) return 0; - - pos += n; - - // Option 54: Server Identifier (network byte order) - uint32_t srv_id_net = htonl(server_id); - n = append_option_u32(opts, pos, opts_max, DHCP_OPT_SERVER_ID, srv_id_net); - if (n == 0) return 0; - - pos += n; - - // Option 55: Parameter Request List - uint8_t param_list[] = { - DHCP_OPT_SUBNET_MASK, - DHCP_OPT_ROUTER, - DHCP_OPT_DNS, - DHCP_OPT_LEASE_TIME, - }; - n = append_option(opts, pos, opts_max, DHCP_OPT_PARAM_LIST, - param_list, sizeof(param_list)); - if (n == 0) return 0; - - pos += n; - - // Option 255: End - n = append_option(opts, pos, opts_max, DHCP_OPT_END, nullptr, 0); - if (n == 0) return 0; - - pos += n; - - return sizeof(dhcp_packet) + pos; -} - -// Packet Parse Function - -bool dhcp_parse_response(const dhcp_packet* pkt, size_t pkt_len, - dhcp_config* out) { - if (!pkt || !out || pkt_len < sizeof(dhcp_packet)) { - return false; - } - - string::memset(out, 0, sizeof(dhcp_config)); - out->valid = false; - - if (pkt->op != DHCP_OP_BOOTREPLY) return false; - - if (ntohl(pkt->magic) != DHCP_MAGIC_COOKIE) return false; - - out->offered_ip = ntohl(pkt->yiaddr); - - const uint8_t* opts = reinterpret_cast(pkt) + sizeof(dhcp_packet); - size_t opts_len = pkt_len - sizeof(dhcp_packet); - size_t pos = 0; - - while (pos < opts_len) { - uint8_t code = opts[pos]; - - if (code == DHCP_OPT_END) break; - - if (code == DHCP_OPT_PAD) { - pos++; - continue; - } - - if (pos + 1 >= opts_len) break; - - uint8_t opt_len = opts[pos + 1]; - if (pos + 2 + opt_len > opts_len) break; - - const uint8_t* opt_data = opts + pos + 2; - - switch (code) { - case DHCP_OPT_MSG_TYPE: - if (opt_len >= 1) out->msg_type = opt_data[0]; - break; - case DHCP_OPT_SUBNET_MASK: - if (opt_len >= 4) { - uint32_t val; - string::memcpy(&val, opt_data, 4); - out->subnet_mask = ntohl(val); - } - break; - case DHCP_OPT_ROUTER: - if (opt_len >= 4) { - uint32_t val; - string::memcpy(&val, opt_data, 4); - out->gateway = ntohl(val); - } - break; - case DHCP_OPT_DNS: - if (opt_len >= 4) { - uint32_t val; - string::memcpy(&val, opt_data, 4); - out->dns_server = ntohl(val); - } - break; - case DHCP_OPT_LEASE_TIME: - if (opt_len >= 4) { - uint32_t val; - string::memcpy(&val, opt_data, 4); - out->lease_time = ntohl(val); - } - break; - case DHCP_OPT_SERVER_ID: - if (opt_len >= 4) { - uint32_t val; - string::memcpy(&val, opt_data, 4); - out->server_id = ntohl(val); - } - break; - default: - break; - } - - pos += 2 + opt_len; - } - - if (out->msg_type == 0) return false; - - out->valid = true; - return true; -} - -// DHCP Client State Machine - -int32_t dhcp_configure(netif* iface) { - if (!iface || !iface->transmit) { - return ERR_INVAL; - } - - log::info("dhcp: %s starting DHCP configuration", iface->name); - - // Activate the receive hook so udp_recv() copies port-68 packets - // into the static buffer for us to poll. - activate_rx_hook(); - - uint32_t xid = generate_xid(iface->mac); - - // Allocate buffers using unprivileged heap (auto-elevates, Ring 3 safe) - auto* tx_buf = static_cast(heap::uzalloc(DHCP_PACKET_MAX)); - auto* rx_buf = static_cast(heap::uzalloc(DHCP_PACKET_MAX)); - if (!tx_buf || !rx_buf) { - if (tx_buf) heap::ufree(tx_buf); - if (rx_buf) heap::ufree(rx_buf); - deactivate_rx_hook(); - log::error("dhcp: failed to allocate buffers"); - return ERR_NOMEM; - } - - int32_t result = ERR_TIMEOUT; - - for (uint32_t attempt = 0; attempt < DHCP_ATTEMPTS; attempt++) { - if (attempt > 0) { - log::info("dhcp: %s attempt %u/%u", iface->name, - attempt + 1, DHCP_ATTEMPTS); - } - - // Broadcast a DISCOVER to solicit lease offers - size_t discover_len = dhcp_build_discover(tx_buf, DHCP_PACKET_MAX, - iface->mac, xid); - if (discover_len == 0) { - log::error("dhcp: failed to build DISCOVER"); - result = ERR_INVAL; - break; - } - - log::info("dhcp: %s sending DISCOVER (xid=0x%08x)", - iface->name, ntohl(xid)); - - int32_t tx_rc = send_dhcp_broadcast(iface, tx_buf, discover_len); - if (tx_rc != OK) { - log::error("dhcp: %s DISCOVER send failed: %d", iface->name, tx_rc); - continue; - } - - // Poll for an OFFER matching our transaction id - dhcp_config offer = {}; - bool got_offer = false; - uint64_t deadline = clock::now_ns() + - static_cast(DHCP_TIMEOUT_MS) * 1000000ULL; - - while (clock::now_ns() < deadline) { - // Poll the NIC to process incoming packets - RUN_ELEVATED({ - if (iface->poll) { - iface->poll(iface); - } - }); - - // Check if a DHCP response arrived via the hook - size_t rx_len = 0; - if (try_read_dhcp_response(rx_buf, DHCP_PACKET_MAX, &rx_len)) { - if (rx_len >= sizeof(dhcp_packet)) { - auto* resp = reinterpret_cast(rx_buf); - - if (resp->xid == xid) { - if (dhcp_parse_response(resp, rx_len, &offer) && - offer.msg_type == DHCP_MSG_OFFER) { - got_offer = true; - break; - } - } - } - } - - // Sleep briefly between polls - RUN_ELEVATED(sched::sleep_ms(DHCP_POLL_INTERVAL_MS)); - } - - if (!got_offer) { - log::warn("dhcp: %s no OFFER received (attempt %u)", - iface->name, attempt + 1); - continue; - } - - log::info("dhcp: %s received OFFER: %u.%u.%u.%u from server %u.%u.%u.%u", - iface->name, - (offer.offered_ip >> 24) & 0xFF, - (offer.offered_ip >> 16) & 0xFF, - (offer.offered_ip >> 8) & 0xFF, - offer.offered_ip & 0xFF, - (offer.server_id >> 24) & 0xFF, - (offer.server_id >> 16) & 0xFF, - (offer.server_id >> 8) & 0xFF, - offer.server_id & 0xFF); - - // REQUEST the offered lease from the offering server - size_t request_len = dhcp_build_request(tx_buf, DHCP_PACKET_MAX, - iface->mac, xid, - offer.offered_ip, - offer.server_id); - if (request_len == 0) { - log::error("dhcp: failed to build REQUEST"); - result = ERR_INVAL; - break; - } - - log::info("dhcp: %s sending REQUEST for %u.%u.%u.%u", - iface->name, - (offer.offered_ip >> 24) & 0xFF, - (offer.offered_ip >> 16) & 0xFF, - (offer.offered_ip >> 8) & 0xFF, - offer.offered_ip & 0xFF); - - tx_rc = send_dhcp_broadcast(iface, tx_buf, request_len); - if (tx_rc != OK) { - log::error("dhcp: %s REQUEST send failed: %d", iface->name, tx_rc); - continue; - } - - // Poll for the ACK that commits the lease, a NAK abandons the attempt - dhcp_config ack = {}; - bool got_ack = false; - deadline = clock::now_ns() + - static_cast(DHCP_TIMEOUT_MS) * 1000000ULL; - - while (clock::now_ns() < deadline) { - RUN_ELEVATED({ - if (iface->poll) { - iface->poll(iface); - } - }); - - size_t rx_len = 0; - if (try_read_dhcp_response(rx_buf, DHCP_PACKET_MAX, &rx_len)) { - if (rx_len >= sizeof(dhcp_packet)) { - auto* resp = reinterpret_cast(rx_buf); - - if (resp->xid == xid) { - if (dhcp_parse_response(resp, rx_len, &ack)) { - if (ack.msg_type == DHCP_MSG_ACK) { - got_ack = true; - break; - } else if (ack.msg_type == DHCP_MSG_NAK) { - log::warn("dhcp: %s received NAK", iface->name); - break; - } - } - } - } - } - - RUN_ELEVATED(sched::sleep_ms(DHCP_POLL_INTERVAL_MS)); - } - - if (!got_ack) { - log::warn("dhcp: %s no ACK received (attempt %u)", - iface->name, attempt + 1); - continue; - } - - // ACK fields win, an ACK may omit options already given in the OFFER - uint32_t ip = ack.offered_ip ? ack.offered_ip : offer.offered_ip; - uint32_t mask = ack.subnet_mask ? ack.subnet_mask : offer.subnet_mask; - uint32_t gw = ack.gateway ? ack.gateway : offer.gateway; - uint32_t dns = ack.dns_server ? ack.dns_server : offer.dns_server; - - log::info("dhcp: %s received ACK: %u.%u.%u.%u/%u.%u.%u.%u gw %u.%u.%u.%u dns %u.%u.%u.%u lease %us", - iface->name, - (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, - (ip >> 8) & 0xFF, ip & 0xFF, - (mask >> 24) & 0xFF, (mask >> 16) & 0xFF, - (mask >> 8) & 0xFF, mask & 0xFF, - (gw >> 24) & 0xFF, (gw >> 16) & 0xFF, - (gw >> 8) & 0xFF, gw & 0xFF, - (dns >> 24) & 0xFF, (dns >> 16) & 0xFF, - (dns >> 8) & 0xFF, dns & 0xFF, - ack.lease_time ? ack.lease_time : offer.lease_time); - - iface->ipv4_dns = dns; - configure(iface, ip, mask, gw); - - result = OK; - break; - } - - // Cleanup - deactivate_rx_hook(); - heap::ufree(tx_buf); - heap::ufree(rx_buf); - - if (result == OK) { - log::info("dhcp: %s configuration complete", iface->name); - } else { - log::warn("dhcp: %s configuration failed", iface->name); - } - - return result; -} - -} // namespace net diff --git a/kernel/net/dhcp.h b/kernel/net/dhcp.h deleted file mode 100644 index 396b49e1..00000000 --- a/kernel/net/dhcp.h +++ /dev/null @@ -1,175 +0,0 @@ -#ifndef STELLUX_NET_DHCP_H -#define STELLUX_NET_DHCP_H - -#include "common/types.h" -#include "net/net.h" - -namespace net { - -// DHCP Constants - -constexpr uint16_t DHCP_SERVER_PORT = 67; -constexpr uint16_t DHCP_CLIENT_PORT = 68; -constexpr uint32_t DHCP_MAGIC_COOKIE = 0x63825363; - -// BOOTP opcodes -constexpr uint8_t DHCP_OP_BOOTREQUEST = 1; -constexpr uint8_t DHCP_OP_BOOTREPLY = 2; - -// Hardware types -constexpr uint8_t DHCP_HTYPE_ETHERNET = 1; -constexpr uint8_t DHCP_HLEN_ETHERNET = 6; - -// DHCP message types (Option 53 values) -constexpr uint8_t DHCP_MSG_DISCOVER = 1; -constexpr uint8_t DHCP_MSG_OFFER = 2; -constexpr uint8_t DHCP_MSG_REQUEST = 3; -constexpr uint8_t DHCP_MSG_DECLINE = 4; -constexpr uint8_t DHCP_MSG_ACK = 5; -constexpr uint8_t DHCP_MSG_NAK = 6; -constexpr uint8_t DHCP_MSG_RELEASE = 7; - -// DHCP option codes -constexpr uint8_t DHCP_OPT_PAD = 0; -constexpr uint8_t DHCP_OPT_SUBNET_MASK = 1; -constexpr uint8_t DHCP_OPT_ROUTER = 3; -constexpr uint8_t DHCP_OPT_DNS = 6; -constexpr uint8_t DHCP_OPT_HOSTNAME = 12; -constexpr uint8_t DHCP_OPT_REQUESTED_IP = 50; -constexpr uint8_t DHCP_OPT_LEASE_TIME = 51; -constexpr uint8_t DHCP_OPT_MSG_TYPE = 53; -constexpr uint8_t DHCP_OPT_SERVER_ID = 54; -constexpr uint8_t DHCP_OPT_PARAM_LIST = 55; -constexpr uint8_t DHCP_OPT_END = 255; - -// DHCP flags -constexpr uint16_t DHCP_FLAG_BROADCAST = 0x8000; - -// Timing -constexpr uint32_t DHCP_ATTEMPTS = 3; -constexpr uint32_t DHCP_POLL_INTERVAL_MS = 50; -constexpr uint32_t DHCP_TIMEOUT_MS = 5000; - -// DHCP Packet Structure - -/** - * Fixed-size BOOTP/DHCP header (236 bytes). - * DHCP options follow immediately after this header in the packet. - */ -struct dhcp_packet { - uint8_t op; // message op: 1=BOOTREQUEST, 2=BOOTREPLY - uint8_t htype; // hardware type: 1=Ethernet - uint8_t hlen; // hardware address length: 6 for Ethernet - uint8_t hops; // relay hops, client sets to 0 - uint32_t xid; // transaction ID (network byte order) - uint16_t secs; // seconds since client began (network byte order) - uint16_t flags; // flags (network byte order), 0x8000=broadcast - uint32_t ciaddr; // client IP (network byte order), 0 during discover - uint32_t yiaddr; // "your" (offered) IP (network byte order) - uint32_t siaddr; // next server IP (network byte order) - uint32_t giaddr; // relay agent IP (network byte order) - uint8_t chaddr[16]; // client hardware address (MAC in first 6 bytes) - uint8_t sname[64]; // server host name (optional, zero-filled) - uint8_t file[128]; // boot file name (optional, zero-filled) - uint32_t magic; // DHCP magic cookie: 0x63825363 (network byte order) -} __attribute__((packed)); - -static_assert(sizeof(dhcp_packet) == 240, "dhcp_packet must be 240 bytes"); - -// Maximum DHCP options region size (after the fixed header) -constexpr size_t DHCP_OPTIONS_MAX = 312; - -// Maximum total DHCP message size (header + options) -constexpr size_t DHCP_PACKET_MAX = sizeof(dhcp_packet) + DHCP_OPTIONS_MAX; - -// Parsed DHCP Configuration - -/** - * Holds parsed configuration from a DHCP OFFER or ACK response. - * All IP addresses are in HOST byte order. - */ -struct dhcp_config { - uint32_t offered_ip; // "your" IP address - uint32_t subnet_mask; // subnet mask - uint32_t gateway; // default gateway (router) - uint32_t dns_server; // primary DNS server - uint32_t server_id; // DHCP server identifier - uint32_t lease_time; // lease duration in seconds - uint8_t msg_type; // DHCP message type (OFFER, ACK, NAK) - bool valid; // true if parsing succeeded -}; - -// Packet Build/Parse Functions (unit-testable) - -/** - * Build a DHCP DISCOVER packet. - * @param out Buffer to write the DHCP message (header + options). - * @param out_size Size of the output buffer. - * @param mac Client MAC address (6 bytes). - * @param xid Transaction ID in network byte order. - * @return Number of bytes written, or 0 on failure. - */ -size_t dhcp_build_discover(uint8_t* out, size_t out_size, - const uint8_t* mac, uint32_t xid); - -/** - * Build a DHCP REQUEST packet. - * @param out Buffer to write the DHCP message (header + options). - * @param out_size Size of the output buffer. - * @param mac Client MAC address (6 bytes). - * @param xid Transaction ID in network byte order. - * @param offered_ip Requested IP in HOST byte order (from OFFER). - * @param server_id Server identifier in HOST byte order (from OFFER). - * @return Number of bytes written, or 0 on failure. - */ -size_t dhcp_build_request(uint8_t* out, size_t out_size, - const uint8_t* mac, uint32_t xid, - uint32_t offered_ip, uint32_t server_id); - -/** - * Parse DHCP options from a received DHCP packet. - * @param pkt Pointer to the DHCP packet (header + options). - * @param pkt_len Total length of the DHCP packet. - * @param out Parsed configuration output. - * @return true if parsing succeeded and a valid message type was found. - */ -bool dhcp_parse_response(const dhcp_packet* pkt, size_t pkt_len, - dhcp_config* out); - -// DHCP Receive Hook (called by udp_recv, runs at Ring 0) - -/** - * Called by udp_recv() when a UDP packet arrives on port 68 (DHCP client). - * Copies the UDP payload (DHCP message) into an internal static buffer - * for the DHCP client to poll. Uses unprivileged data so it is callable - * from any privilege level. - * - * @param data UDP payload (DHCP packet, after UDP header is stripped). - * @param len Length of the DHCP payload. - */ -void dhcp_rx_hook(const uint8_t* data, size_t len); - -// DHCP Client API - -/** - * Run a full DHCP exchange on the given interface. - * - * Sends DISCOVER, waits for OFFER, sends REQUEST, waits for ACK. - * On success, calls net::configure() to set the interface's IP configuration - * and stores the DNS server address in iface->ipv4_dns. - * - * This function blocks and should be called from a kernel task context - * (e.g. a driver's run() method) where sched::sleep_ms() is safe. - * - * The interface must be registered (via register_netif()) and the NIC - * must be ready to transmit/receive, but need NOT be configured yet. - * - * @param iface The network interface to configure via DHCP. - * @return net::OK on success, net::ERR_TIMEOUT if no response, - * or another negative error code on failure. - */ -int32_t dhcp_configure(netif* iface); - -} // namespace net - -#endif // STELLUX_NET_DHCP_H diff --git a/kernel/net/ethernet.cpp b/kernel/net/ethernet.cpp deleted file mode 100644 index f9f028a1..00000000 --- a/kernel/net/ethernet.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "net/ethernet.h" -#include "net/arp.h" -#include "net/ipv4.h" -#include "net/byteorder.h" -#include "common/logging.h" -#include "common/string.h" -#include "mm/heap.h" - -namespace net { - -void eth_recv(netif* iface, const uint8_t* data, size_t len) { - if (!iface || !data || len < sizeof(eth_header)) { - return; - } - - const auto* hdr = reinterpret_cast(data); - uint16_t ethertype = ntohs(hdr->ethertype); - - const uint8_t* payload = data + sizeof(eth_header); - size_t payload_len = len - sizeof(eth_header); - - switch (ethertype) { - case ETH_TYPE_ARP: - arp_recv(iface, payload, payload_len); - break; - case ETH_TYPE_IPV4: - ipv4_recv(iface, payload, payload_len); - break; - default: - break; - } -} - -int32_t eth_send(netif* iface, const uint8_t* dst_mac, - uint16_t ethertype, const uint8_t* payload, size_t payload_len) { - if (!iface || !iface->transmit || !dst_mac || !payload) { - return ERR_INVAL; - } - - size_t frame_len = sizeof(eth_header) + payload_len; - if (frame_len > ETH_FRAME_MAX) { - return ERR_INVAL; - } - - auto* frame = static_cast(heap::uzalloc(frame_len)); - if (!frame) { - return ERR_NOMEM; - } - - auto* hdr = reinterpret_cast(frame); - string::memcpy(hdr->dst, dst_mac, MAC_ADDR_LEN); - string::memcpy(hdr->src, iface->mac, MAC_ADDR_LEN); - hdr->ethertype = htons(ethertype); - - string::memcpy(frame + sizeof(eth_header), payload, payload_len); - - int32_t rc = iface->transmit(iface, frame, frame_len); - heap::ufree(frame); - return rc; -} - -} // namespace net diff --git a/kernel/net/ethernet.h b/kernel/net/ethernet.h deleted file mode 100644 index bd183c9a..00000000 --- a/kernel/net/ethernet.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef STELLUX_NET_ETHERNET_H -#define STELLUX_NET_ETHERNET_H - -#include "common/types.h" -#include "net/net.h" - -namespace net { - -constexpr uint16_t ETH_TYPE_ARP = 0x0806; -constexpr uint16_t ETH_TYPE_IPV4 = 0x0800; - -struct eth_header { - uint8_t dst[MAC_ADDR_LEN]; - uint8_t src[MAC_ADDR_LEN]; - uint16_t ethertype; // network byte order -} __attribute__((packed)); - -static_assert(sizeof(eth_header) == 14, "eth_header must be 14 bytes"); - -constexpr uint8_t ETH_BROADCAST[MAC_ADDR_LEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; - -/** - * Process a received Ethernet frame. Dispatches to ARP or IPv4 based on ethertype. - */ -void eth_recv(netif* iface, const uint8_t* data, size_t len); - -/** - * Send an Ethernet frame. Prepends the Ethernet header and calls iface->transmit(). - * @param iface Interface to send on. - * @param dst_mac Destination MAC address (6 bytes). - * @param ethertype Ethertype in HOST byte order. - * @param payload Payload data (after Ethernet header). - * @param payload_len Length of payload. - * @return 0 on success, negative error code on failure. - */ -int32_t eth_send(netif* iface, const uint8_t* dst_mac, - uint16_t ethertype, const uint8_t* payload, size_t payload_len); - -} // namespace net - -#endif // STELLUX_NET_ETHERNET_H diff --git a/kernel/net/icmp.cpp b/kernel/net/icmp.cpp deleted file mode 100644 index 2392aa0b..00000000 --- a/kernel/net/icmp.cpp +++ /dev/null @@ -1,115 +0,0 @@ -#include "net/icmp.h" -#include "net/ipv4.h" -#include "net/inet_socket.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "net/net.h" -#include "common/string.h" -#include "common/ring_buffer.h" -#include "mm/heap.h" -#include "sync/spinlock.h" -#include "dynpriv/dynpriv.h" - -namespace net { - -// Linked list of ICMP sockets registered for packet delivery. -// Protected by g_icmp_sock_lock. -static inet_socket* g_icmp_sock_list = nullptr; -static sync::spinlock g_icmp_sock_lock = sync::SPINLOCK_INIT; - -// Ring buffer entry framing: [src_ip(4)] [payload_len(2)] [data(N)] -constexpr size_t RX_ENTRY_HEADER = 6; - -static void deliver_to_sockets(uint32_t src_ip, const uint8_t* data, size_t len) { - if (!data || len == 0) return; - - uint32_t src_ip_net = htonl(src_ip); - uint16_t payload_len = static_cast(len); - - size_t entry_len = RX_ENTRY_HEADER + len; - auto* entry = static_cast(heap::uzalloc(entry_len)); - if (!entry) return; - - string::memcpy(entry, &src_ip_net, 4); - string::memcpy(entry + 4, &payload_len, 2); - string::memcpy(entry + RX_ENTRY_HEADER, data, len); - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_icmp_sock_lock); - - for (inet_socket* s = g_icmp_sock_list; s; s = s->next) { - if (s->rx_buf) { - (void)ring_buffer_write_all(s->rx_buf, entry, entry_len, true); - } - } - }); - - heap::ufree(entry); -} - -void icmp_recv(netif* iface, uint32_t src_ip, const uint8_t* data, size_t len) { - if (!iface || !data || len < sizeof(icmp_header)) { - return; - } - - const auto* hdr = reinterpret_cast(data); - - // Verify ICMP checksum - uint16_t computed = inet_checksum(data, len); - if (computed != 0) { - return; - } - - if (hdr->type == ICMP_TYPE_ECHO_REQUEST && hdr->code == 0) { - // Queue the echo reply for deferred transmission, sending inline - // from RX context would recurse back into RX processing. - if (len <= ETH_MTU) { - auto* reply = static_cast(heap::uzalloc(len)); - if (reply) { - string::memcpy(reply, data, len); - auto* reply_hdr = reinterpret_cast(reply); - reply_hdr->type = ICMP_TYPE_ECHO_REPLY; - reply_hdr->code = 0; - reply_hdr->checksum = 0; - reply_hdr->checksum = inet_checksum(reply, len); - - queue_deferred_tx(iface, src_ip, IPV4_PROTO_ICMP, reply, len); - heap::ufree(reply); - } - } - } - - // Deliver all ICMP packets to registered userland sockets - deliver_to_sockets(src_ip, data, len); -} - -void icmp_register_socket(inet_socket* sock) { - if (!sock) return; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_icmp_sock_lock); - - sock->next = g_icmp_sock_list; - g_icmp_sock_list = sock; - }); -} - -void icmp_unregister_socket(inet_socket* sock) { - if (!sock) return; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_icmp_sock_lock); - - inet_socket** pp = &g_icmp_sock_list; - while (*pp) { - if (*pp == sock) { - *pp = sock->next; - sock->next = nullptr; - break; - } - pp = &(*pp)->next; - } - }); -} - -} // namespace net diff --git a/kernel/net/icmp.h b/kernel/net/icmp.h deleted file mode 100644 index 2593a6c8..00000000 --- a/kernel/net/icmp.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef STELLUX_NET_ICMP_H -#define STELLUX_NET_ICMP_H - -#include "common/types.h" -#include "net/net.h" - -namespace net { - -constexpr uint8_t ICMP_TYPE_ECHO_REPLY = 0; -constexpr uint8_t ICMP_TYPE_ECHO_REQUEST = 8; - -struct icmp_header { - uint8_t type; - uint8_t code; - uint16_t checksum; // network byte order - uint16_t id; // network byte order - uint16_t sequence; // network byte order -} __attribute__((packed)); - -static_assert(sizeof(icmp_header) == 8, "icmp_header must be 8 bytes"); - -struct inet_socket; - -/** - * Process a received ICMP packet (after IPv4 header is stripped). - * Handles echo requests (kernel replies via deferred TX) and delivers - * all ICMP packets to registered sockets. - */ -void icmp_recv(netif* iface, uint32_t src_ip, const uint8_t* data, size_t len); - -/** - * Register an inet socket to receive ICMP packets. - * Called during ICMP socket creation. - */ -void icmp_register_socket(inet_socket* sock); - -/** - * Unregister an inet socket from ICMP delivery. - * Called during ICMP socket close. - */ -void icmp_unregister_socket(inet_socket* sock); - -} // namespace net - -#endif // STELLUX_NET_ICMP_H diff --git a/kernel/net/inet_socket.cpp b/kernel/net/inet_socket.cpp deleted file mode 100644 index 2ee72300..00000000 --- a/kernel/net/inet_socket.cpp +++ /dev/null @@ -1,572 +0,0 @@ -#include "net/inet_socket.h" -#include "net/net.h" -#include "net/netinfo.h" -#include "net/ipv4.h" -#include "net/icmp.h" -#include "net/udp.h" -#include "net/route.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "common/ring_buffer.h" -#include "common/string.h" -#include "mm/heap.h" -#include "mm/uaccess.h" -#include "sync/spinlock.h" -#include "sync/poll.h" -#include "dynpriv/dynpriv.h" - -namespace net { - -constexpr size_t RX_BUF_CAPACITY = 16384; - -// Ring buffer entry framing (must match icmp.cpp delivery format): -// [4 bytes: src_ip in network byte order] -// [2 bytes: payload length in host byte order] -// [N bytes: payload data] -constexpr size_t RX_ENTRY_HEADER = 6; - -__PRIVILEGED_CODE static ssize_t inet_sendto( - resource::resource_object* obj, const void* ksrc, size_t count, - uint32_t flags, const void* kaddr, size_t addrlen -) { - (void)flags; - if (!obj || !obj->impl || !ksrc) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - if (sock->protocol != IPV4_PROTO_ICMP) { - return resource::ERR_INVAL; - } - - if (!kaddr || addrlen < sizeof(kernel_sockaddr_in)) { - return resource::ERR_INVAL; - } - - const auto* addr = static_cast(kaddr); - if (addr->sin_family != AF_INET_VAL) { - return resource::ERR_INVAL; - } - - uint32_t dst_ip = ntohl(addr->sin_addr); - - netif* iface = get_default_netif(); - if (!iface || !iface->configured) { - return resource::ERR_IO; - } - - // Trigger RX processing so pending incoming packets are delivered - if (iface->poll) { - iface->poll(iface); - } - - int32_t rc = ipv4_send(iface, dst_ip, IPV4_PROTO_ICMP, - static_cast(ksrc), count); - if (rc != OK) { - return resource::ERR_IO; - } - - return static_cast(count); -} - -__PRIVILEGED_CODE static ssize_t inet_recvfrom( - resource::resource_object* obj, void* kdst, size_t count, - uint32_t flags, void* kaddr, size_t* addrlen -) { - if (!obj || !obj->impl || !kdst) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - bool nonblock = (flags & 0x40) != 0; // MSG_DONTWAIT - - // Trigger RX processing to deliver pending packets - netif* iface = get_default_netif(); - if (iface && iface->poll) { - iface->poll(iface); - } - - // Entries are written as one atomic unit, so once the header is read the - // payload is guaranteed to follow, no external locking is needed. - uint8_t hdr[RX_ENTRY_HEADER]; - ssize_t hdr_rc = ring_buffer_read(sock->rx_buf, hdr, RX_ENTRY_HEADER, nonblock); - if (hdr_rc == RB_ERR_AGAIN) { - return resource::ERR_AGAIN; - } - - if (hdr_rc < static_cast(RX_ENTRY_HEADER)) { - return resource::ERR_IO; - } - - uint32_t src_ip_net; - string::memcpy(&src_ip_net, hdr, 4); - uint16_t payload_len; - string::memcpy(&payload_len, hdr + 4, 2); - - size_t to_read = payload_len < count ? payload_len : count; - ssize_t data_rc = ring_buffer_read(sock->rx_buf, static_cast(kdst), - to_read, true); - - // Always drain the rest of the entry so the stream stays framed, even on - // short user buffers or failed payload reads. - size_t consumed = (data_rc > 0) ? static_cast(data_rc) : 0; - if (consumed < payload_len) { - size_t discard = payload_len - consumed; - uint8_t trash[64]; - while (discard > 0) { - size_t chunk = discard < sizeof(trash) ? discard : sizeof(trash); - (void)ring_buffer_read(sock->rx_buf, trash, chunk, true); - discard -= chunk; - } - } - - if (data_rc < 0) { - return resource::ERR_IO; - } - - if (kaddr && addrlen && *addrlen >= sizeof(kernel_sockaddr_in)) { - auto* src = static_cast(kaddr); - string::memset(src, 0, sizeof(*src)); - src->sin_family = AF_INET_VAL; - src->sin_addr = src_ip_net; - *addrlen = sizeof(kernel_sockaddr_in); - } - - return data_rc; -} - -__PRIVILEGED_CODE static int32_t inet_ioctl( - resource::resource_object* obj, uint32_t cmd, uint64_t arg) { - (void)obj; - - if (cmd == STLX_SIOCGNETSTATUS) { - net_status status = {}; - query_status(&status); - int32_t rc = mm::uaccess::copy_to_user( - reinterpret_cast(arg), &status, sizeof(status)); - return (rc == mm::uaccess::OK) ? resource::OK : resource::ERR_INVAL; - } - - if (cmd == STLX_SIOCGARPTABLE) { - arp_table_status table = {}; - query_arp_table(&table); - int32_t rc = mm::uaccess::copy_to_user( - reinterpret_cast(arg), &table, sizeof(table)); - return (rc == mm::uaccess::OK) ? resource::OK : resource::ERR_INVAL; - } - - return resource::ERR_UNSUP; -} - -__PRIVILEGED_CODE static void inet_close(resource::resource_object* obj) { - if (!obj || !obj->impl) { - return; - } - - auto* sock = static_cast(obj->impl); - - if (sock->protocol == IPV4_PROTO_ICMP) { - icmp_unregister_socket(sock); - } else if (sock->protocol == IPV4_PROTO_UDP && sock->bound_port != 0) { - udp_unregister_socket(sock); - } - - if (sock->rx_buf) { - ring_buffer_destroy(sock->rx_buf); - } - heap::kfree_delete(sock); - obj->impl = nullptr; -} - -__PRIVILEGED_CODE static uint32_t inet_poll( - resource::resource_object* obj, sync::poll_table* pt -) { - if (!obj || !obj->impl) return sync::POLL_NVAL; - auto* sock = static_cast(obj->impl); - return ring_buffer_poll_read(sock->rx_buf, pt) | sync::POLL_OUT; -} - -static const resource::resource_ops g_inet_icmp_ops = { - nullptr, - nullptr, - inet_close, - inet_ioctl, - nullptr, - inet_sendto, - inet_recvfrom, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - inet_poll, - nullptr, -}; - -// UDP ring buffer entry framing: -// [4 bytes: src_ip in network byte order] -// [2 bytes: src_port in network byte order] -// [2 bytes: payload length in host byte order] -// [N bytes: payload data] -constexpr size_t UDP_RX_ENTRY_HEADER = 8; - -__PRIVILEGED_CODE static ssize_t inet_udp_sendto( - resource::resource_object* obj, const void* ksrc, size_t count, - uint32_t flags, const void* kaddr, size_t addrlen -) { - (void)flags; - if (!obj || !obj->impl || !ksrc) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - - if (!kaddr || addrlen < sizeof(kernel_sockaddr_in)) { - return resource::ERR_INVAL; - } - - const auto* addr = static_cast(kaddr); - if (addr->sin_family != AF_INET_VAL) { - return resource::ERR_INVAL; - } - - uint32_t dst_ip = ntohl(addr->sin_addr); - uint16_t dst_port = ntohs(addr->sin_port); - - netif* iface = get_default_netif(); - if (!iface || !iface->configured) { - return resource::ERR_IO; - } - - // First send assigns the ephemeral port, double-checked under sock->lock - // so racing sendto calls cannot register the socket twice. - if (sock->bound_port == 0) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (sock->bound_port == 0) { - sock->bound_port = udp_alloc_ephemeral_port(); - udp_register_socket(sock); - } - sync::spin_unlock_irqrestore(sock->lock, irq); - } - - if (iface->poll) { - iface->poll(iface); - } - - // The checksum's source IP must match what ipv4_send stamps in the IP - // header: the bound address if any, otherwise the route-derived source. - uint32_t src_ip = sock->bound_addr; - if (src_ip == 0) { - src_ip = iface->ipv4_addr; - route_result rt; - if (route_lookup(dst_ip, &rt) == OK) { - if (rt.type == route_type::LOCAL) { - src_ip = dst_ip; - } else if (rt.iface && (rt.iface->flags & NETIF_LOOPBACK)) { - src_ip = rt.iface->ipv4_addr; - } - } - } - - // Build UDP packet: header + payload - size_t udp_total = sizeof(udp_header) + count; - auto* udp_pkt = static_cast(heap::uzalloc(udp_total)); - if (!udp_pkt) { - return resource::ERR_NOMEM; - } - - auto* uhdr = reinterpret_cast(udp_pkt); - uhdr->src_port = htons(sock->bound_port); - uhdr->dst_port = htons(dst_port); - uhdr->length = htons(static_cast(udp_total)); - uhdr->checksum = 0; - string::memcpy(udp_pkt + sizeof(udp_header), ksrc, count); - - uint16_t csum = udp_checksum( - htonl(src_ip), htonl(dst_ip), udp_pkt, udp_total); - uhdr->checksum = (csum == 0) ? static_cast(0xFFFF) : csum; - - int32_t rc = ipv4_send(iface, dst_ip, IPV4_PROTO_UDP, udp_pkt, udp_total, - sock->bound_addr); - heap::ufree(udp_pkt); - - if (rc != OK) { - return resource::ERR_IO; - } - - return static_cast(count); -} - -__PRIVILEGED_CODE static ssize_t inet_udp_recvfrom( - resource::resource_object* obj, void* kdst, size_t count, - uint32_t flags, void* kaddr, size_t* addrlen -) { - if (!obj || !obj->impl || !kdst) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - bool nonblock = (flags & 0x40) != 0; // MSG_DONTWAIT - - netif* iface = get_default_netif(); - if (iface && iface->poll) { - iface->poll(iface); - } - - uint8_t hdr[UDP_RX_ENTRY_HEADER]; - ssize_t hdr_rc = ring_buffer_read(sock->rx_buf, hdr, UDP_RX_ENTRY_HEADER, nonblock); - if (hdr_rc == RB_ERR_AGAIN) { - return resource::ERR_AGAIN; - } - - if (hdr_rc < static_cast(UDP_RX_ENTRY_HEADER)) { - return resource::ERR_IO; - } - - uint32_t src_ip_net; - string::memcpy(&src_ip_net, hdr, 4); - uint16_t src_port_net; - string::memcpy(&src_port_net, hdr + 4, 2); - uint16_t payload_len; - string::memcpy(&payload_len, hdr + 6, 2); - - size_t to_read = payload_len < count ? payload_len : count; - ssize_t data_rc = ring_buffer_read(sock->rx_buf, static_cast(kdst), - to_read, true); - - size_t consumed = (data_rc > 0) ? static_cast(data_rc) : 0; - if (consumed < payload_len) { - size_t discard = payload_len - consumed; - uint8_t trash[64]; - while (discard > 0) { - size_t chunk = discard < sizeof(trash) ? discard : sizeof(trash); - (void)ring_buffer_read(sock->rx_buf, trash, chunk, true); - discard -= chunk; - } - } - - if (data_rc < 0) { - return resource::ERR_IO; - } - - if (kaddr && addrlen && *addrlen >= sizeof(kernel_sockaddr_in)) { - auto* src = static_cast(kaddr); - string::memset(src, 0, sizeof(*src)); - src->sin_family = AF_INET_VAL; - src->sin_port = src_port_net; - src->sin_addr = src_ip_net; - *addrlen = sizeof(kernel_sockaddr_in); - } - - return data_rc; -} - -__PRIVILEGED_CODE static int32_t inet_udp_bind( - resource::resource_object* obj, const void* kaddr, size_t addrlen -) { - if (!obj || !obj->impl) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - if (sock->protocol != IPV4_PROTO_UDP) { - return resource::ERR_INVAL; - } - - if (!kaddr || addrlen < sizeof(kernel_sockaddr_in)) { - return resource::ERR_INVAL; - } - - const auto* addr = static_cast(kaddr); - if (addr->sin_family != AF_INET_VAL) { - return resource::ERR_INVAL; - } - - uint32_t bind_addr = ntohl(addr->sin_addr); - uint16_t bind_port = ntohs(addr->sin_port); - - if (bind_addr != 0 && !is_local_ip(bind_addr)) { - return resource::ERR_INVAL; - } - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - if (sock->bound_port != 0) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_INVAL; - } - - if (bind_port == 0) { - bind_port = udp_alloc_ephemeral_port(); - } - - sock->bound_addr = bind_addr; - sock->bound_port = bind_port; - - if (!udp_try_register(sock)) { - sock->bound_addr = 0; - sock->bound_port = 0; - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_ADDRINUSE; - } - - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::OK; -} - -__PRIVILEGED_CODE static int32_t inet_setsockopt( - resource::resource_object* obj, int32_t level, - int32_t optname, const void* optval, size_t optlen -) { - auto* sock = static_cast(obj->impl); - - if (level != SOL_SOCKET) { - return resource::ERR_NOPROTOOPT; - } - - if (optname != SO_REUSEADDR) { - return resource::ERR_NOPROTOOPT; - } - - if (optlen < sizeof(int32_t)) { - return resource::ERR_INVAL; - } - - int32_t val = 0; - string::memcpy(&val, optval, sizeof(val)); - - sync::irq_lock_guard guard(sock->lock); - - if (val) - sock->so_options |= static_cast(SO_REUSEADDR); - else - sock->so_options &= ~static_cast(SO_REUSEADDR); - - return resource::OK; -} - -__PRIVILEGED_CODE static int32_t inet_getsockopt( - resource::resource_object* obj, int32_t level, - int32_t optname, void* optval, size_t* optlen -) { - auto* sock = static_cast(obj->impl); - - if (level != SOL_SOCKET) { - return resource::ERR_NOPROTOOPT; - } - - if (optname != SO_REUSEADDR) { - return resource::ERR_NOPROTOOPT; - } - - if (!optlen || *optlen < sizeof(int32_t)) { - return resource::ERR_INVAL; - } - - sync::irq_lock_guard guard(sock->lock); - - int32_t val = (sock->so_options & static_cast(SO_REUSEADDR)) ? 1 : 0; - string::memcpy(optval, &val, sizeof(val)); - *optlen = sizeof(val); - - return resource::OK; -} - -static const resource::resource_ops g_inet_udp_ops = { - nullptr, - nullptr, - inet_close, - inet_ioctl, - nullptr, - inet_udp_sendto, - inet_udp_recvfrom, - inet_udp_bind, - nullptr, - nullptr, - nullptr, - inet_setsockopt, - inet_getsockopt, - inet_poll, - nullptr, -}; - -int32_t create_inet_icmp_socket(resource::resource_object** out) { - if (!out) { - return resource::ERR_INVAL; - } - - auto* sock = heap::kalloc_new(); - if (!sock) { - return resource::ERR_NOMEM; - } - - sock->protocol = IPV4_PROTO_ICMP; - sock->bound_addr = 0; - sock->bound_port = 0; - sock->so_options = 0; - sock->lock = sync::SPINLOCK_INIT; - sock->next = nullptr; - - sock->rx_buf = ring_buffer_create(RX_BUF_CAPACITY); - if (!sock->rx_buf) { - heap::kfree_delete(sock); - return resource::ERR_NOMEM; - } - - auto* obj = heap::kalloc_new(); - if (!obj) { - ring_buffer_destroy(sock->rx_buf); - heap::kfree_delete(sock); - return resource::ERR_NOMEM; - } - - obj->type = resource::resource_type::SOCKET; - obj->ops = &g_inet_icmp_ops; - obj->impl = sock; - - icmp_register_socket(sock); - - *out = obj; - return resource::OK; -} - -int32_t create_inet_udp_socket(resource::resource_object** out) { - if (!out) { - return resource::ERR_INVAL; - } - - auto* sock = heap::kalloc_new(); - if (!sock) { - return resource::ERR_NOMEM; - } - - sock->protocol = IPV4_PROTO_UDP; - sock->bound_addr = 0; - sock->bound_port = 0; - sock->so_options = 0; - sock->lock = sync::SPINLOCK_INIT; - sock->next = nullptr; - - sock->rx_buf = ring_buffer_create(RX_BUF_CAPACITY); - if (!sock->rx_buf) { - heap::kfree_delete(sock); - return resource::ERR_NOMEM; - } - - auto* obj = heap::kalloc_new(); - if (!obj) { - ring_buffer_destroy(sock->rx_buf); - heap::kfree_delete(sock); - return resource::ERR_NOMEM; - } - - obj->type = resource::resource_type::SOCKET; - obj->ops = &g_inet_udp_ops; - obj->impl = sock; - - *out = obj; - return resource::OK; -} - -} // namespace net diff --git a/kernel/net/inet_socket.h b/kernel/net/inet_socket.h deleted file mode 100644 index 023ba2e1..00000000 --- a/kernel/net/inet_socket.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef STELLUX_NET_INET_SOCKET_H -#define STELLUX_NET_INET_SOCKET_H - -#include "common/types.h" -#include "resource/resource.h" -#include "sync/spinlock.h" - -struct ring_buffer; - -namespace net { - -// Kernel representation of sockaddr_in (matches Linux/musl layout). -// Used by sendto/recvfrom syscall handlers and inet socket ops. -struct kernel_sockaddr_in { - uint16_t sin_family; // AF_INET = 2 - uint16_t sin_port; // network byte order - uint32_t sin_addr; // network byte order - uint8_t sin_zero[8]; -}; - -constexpr uint16_t AF_INET_VAL = 2; - -struct inet_socket { - uint8_t protocol; // e.g. IPPROTO_ICMP (1), IPPROTO_UDP (17) - uint32_t bound_addr; // 0 = any (host byte order) - uint16_t bound_port; // host byte order, 0 = unbound (UDP) - ring_buffer* rx_buf; // incoming packets queued here - uint32_t so_options; // bitmask of socket options - sync::spinlock lock; - inet_socket* next; // linked list for protocol registry -}; - -/** - * Create an AF_INET SOCK_DGRAM IPPROTO_ICMP socket. - * Registers the socket with the ICMP protocol layer for packet delivery. - */ -int32_t create_inet_icmp_socket(resource::resource_object** out); - -/** - * Create an AF_INET SOCK_DGRAM IPPROTO_UDP socket. - * Port registration is deferred until the first sendto assigns an ephemeral port. - */ -int32_t create_inet_udp_socket(resource::resource_object** out); - -} // namespace net - -#endif // STELLUX_NET_INET_SOCKET_H diff --git a/kernel/net/ipv4.cpp b/kernel/net/ipv4.cpp deleted file mode 100644 index a65e9f38..00000000 --- a/kernel/net/ipv4.cpp +++ /dev/null @@ -1,178 +0,0 @@ -#include "net/ipv4.h" -#include "net/ethernet.h" -#include "net/arp.h" -#include "net/icmp.h" -#include "net/udp.h" -#include "net/tcp.h" -#include "net/route.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "common/logging.h" -#include "common/string.h" -#include "mm/heap.h" -#include "sync/atomic.h" - -namespace net { - -static sync::atomic g_ipv4_id_counter{0}; - -static uint16_t next_ipv4_id() { - return static_cast(g_ipv4_id_counter.fetch_add_relaxed(1)); -} - -void ipv4_recv(netif* iface, const uint8_t* data, size_t len) { - if (!iface || !data || len < sizeof(ipv4_header)) { - return; - } - - const auto* hdr = reinterpret_cast(data); - - uint8_t version = (hdr->ver_ihl >> 4) & 0xF; - if (version != 4) return; - - uint8_t ihl = hdr->ver_ihl & 0xF; - if (ihl < 5) return; - - size_t header_len = static_cast(ihl) * 4; - if (header_len > len) return; - - uint16_t total_len = ntohs(hdr->total_len); - if (total_len > len) return; - - if (total_len < header_len) return; - - uint16_t computed = inet_checksum(data, header_len); - if (computed != 0) { - log::debug("ipv4: bad header checksum, dropping"); - return; - } - - uint32_t dst_ip = ntohl(hdr->dst_ip); - - // On the loopback interface, accept: - // - Any address in the loopback subnet (127.0.0.0/8) per RFC 1122 section 3.2.1.3 - // - Any locally-configured IP (delivered via LOCAL routes for self-addressed - // traffic, e.g. sending to our own eth0 IP routes through loopback) - // On other interfaces, only accept our exact IP, broadcast, or subnet broadcast. - bool is_loopback = (iface->flags & NETIF_LOOPBACK) != 0; - - if (iface->configured && dst_ip != iface->ipv4_addr && - dst_ip != 0xFFFFFFFF && - dst_ip != (iface->ipv4_addr | ~iface->ipv4_netmask) && - !(is_loopback && ((dst_ip & iface->ipv4_netmask) == - (iface->ipv4_addr & iface->ipv4_netmask) || - is_local_ip(dst_ip)))) { - return; // not for us - } - - uint32_t src_ip = ntohl(hdr->src_ip); - const uint8_t* payload = data + header_len; - size_t payload_len = total_len - header_len; - - switch (hdr->protocol) { - case IPV4_PROTO_ICMP: - icmp_recv(iface, src_ip, payload, payload_len); - break; - case IPV4_PROTO_TCP: - tcp_recv(iface, src_ip, dst_ip, payload, payload_len); - break; - case IPV4_PROTO_UDP: - udp_recv(iface, src_ip, dst_ip, payload, payload_len); - break; - default: - break; - } -} - -int32_t ipv4_send(netif* iface, uint32_t dst_ip, uint8_t protocol, - const uint8_t* payload, size_t payload_len, - uint32_t src_ip_override) { - if (!payload) { - return ERR_INVAL; - } - - if (payload_len > ETH_MTU - sizeof(ipv4_header)) { - return ERR_INVAL; - } - - route_result rt; - int32_t rt_rc = route_lookup(dst_ip, &rt); - if (rt_rc != OK) { - // Without a route, fall back to direct delivery on the caller's - // interface, which covers interfaces set up without routes. - if (!iface || !iface->configured) { - return ERR_NOIF; - } - - rt.iface = iface; - rt.next_hop = dst_ip; - rt.type = route_type::CONNECTED; - } - - // Loopback-bound routes override the caller's interface so self-addressed - // traffic never leaves loopback, otherwise the caller's choice wins. - bool route_is_loopback = (rt.type == route_type::LOCAL) || - (rt.iface && (rt.iface->flags & NETIF_LOOPBACK)); - netif* out_iface = rt.iface; - if (iface && !route_is_loopback) { - out_iface = iface; - } - - if (!out_iface || !out_iface->configured) { - return ERR_NOIF; - } - - size_t total_len = sizeof(ipv4_header) + payload_len; - auto* packet = static_cast(heap::uzalloc(total_len)); - if (!packet) { - return ERR_NOMEM; - } - - auto* hdr = reinterpret_cast(packet); - hdr->ver_ihl = (4 << 4) | 5; - hdr->tos = 0; - hdr->total_len = htons(static_cast(total_len)); - hdr->id = htons(next_ipv4_id()); - hdr->flags_frag = htons(0x4000); - hdr->ttl = IPV4_DEFAULT_TTL; - hdr->protocol = protocol; - hdr->checksum = 0; - - // A caller override (e.g. a bound socket address) wins, LOCAL routes are - // self-addressed so src equals dst, otherwise use the interface's IP. - if (src_ip_override != 0) { - hdr->src_ip = htonl(src_ip_override); - } else if (rt.type == route_type::LOCAL) { - hdr->src_ip = htonl(dst_ip); - } else { - hdr->src_ip = htonl(out_iface->ipv4_addr); - } - - hdr->dst_ip = htonl(dst_ip); - hdr->checksum = inet_checksum(hdr, sizeof(ipv4_header)); - - string::memcpy(packet + sizeof(ipv4_header), payload, payload_len); - - // Local or loopback delivery, no ARP needed. Use the interface's - // own MAC as destination (all zeros for lo). - bool is_loopback_iface = (out_iface->flags & NETIF_LOOPBACK) != 0; - if (rt.type == route_type::LOCAL || is_loopback_iface) { - int32_t rc = eth_send(out_iface, out_iface->mac, ETH_TYPE_IPV4, - packet, total_len); - heap::ufree(packet); - return rc; - } - - uint8_t dst_mac[MAC_ADDR_LEN]; - int32_t arp_rc = arp_resolve(out_iface, rt.next_hop, dst_mac); - if (arp_rc != OK) { - heap::ufree(packet); - return arp_rc; - } - - int32_t rc = eth_send(out_iface, dst_mac, ETH_TYPE_IPV4, packet, total_len); - heap::ufree(packet); - return rc; -} - -} // namespace net diff --git a/kernel/net/ipv4.h b/kernel/net/ipv4.h deleted file mode 100644 index 784223fe..00000000 --- a/kernel/net/ipv4.h +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef STELLUX_NET_IPV4_H -#define STELLUX_NET_IPV4_H - -#include "common/types.h" -#include "net/net.h" - -namespace net { - -constexpr uint8_t IPV4_PROTO_ICMP = 1; -constexpr uint8_t IPV4_PROTO_TCP = 6; -constexpr uint8_t IPV4_PROTO_UDP = 17; - -constexpr uint8_t IPV4_DEFAULT_TTL = 64; - -struct ipv4_header { - uint8_t ver_ihl; // version(4) + IHL(4) - uint8_t tos; - uint16_t total_len; // network byte order - uint16_t id; // network byte order - uint16_t flags_frag; // network byte order - uint8_t ttl; - uint8_t protocol; - uint16_t checksum; // network byte order - uint32_t src_ip; // network byte order - uint32_t dst_ip; // network byte order -} __attribute__((packed)); - -static_assert(sizeof(ipv4_header) == 20, "ipv4_header must be 20 bytes"); - -/** - * Helper to construct an IPv4 address from 4 octets (host byte order). - */ -inline constexpr uint32_t ipv4_addr(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { - return (static_cast(a) << 24) | - (static_cast(b) << 16) | - (static_cast(c) << 8) | - static_cast(d); -} - -/** - * Process a received IPv4 packet (after Ethernet header is stripped). - * Validates header, checksum, and dispatches to the appropriate protocol handler. - */ -void ipv4_recv(netif* iface, const uint8_t* data, size_t len); - -/** - * Send an IPv4 packet. - * Builds the IP header, resolves the next-hop MAC via ARP, and sends via Ethernet. - * @param iface Interface to send on. - * @param dst_ip Destination IP in HOST byte order. - * @param protocol IP protocol number (e.g., IPV4_PROTO_ICMP). - * @param payload Payload data. - * @param payload_len Length of payload. - * @param src_ip_override Source IP in HOST byte order, or 0 to let - * routing decide. When non-zero, this address is stamped into - * the IPv4 header instead of the route-derived source. - * @return 0 on success, negative error code on failure. - */ -int32_t ipv4_send(netif* iface, uint32_t dst_ip, uint8_t protocol, - const uint8_t* payload, size_t payload_len, - uint32_t src_ip_override = 0); - -} // namespace net - -#endif // STELLUX_NET_IPV4_H diff --git a/kernel/net/loopback.cpp b/kernel/net/loopback.cpp deleted file mode 100644 index 6d79b438..00000000 --- a/kernel/net/loopback.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "net/loopback.h" -#include "net/net.h" -#include "net/ipv4.h" -#include "common/string.h" -#include "common/logging.h" - -namespace net { - -static netif g_lo_netif = {}; -static bool g_lo_initialized = false; - -/** - * Loopback transmit callback. Feeds the frame straight back into rx_frame(). - * Safe from recursion because transmits only happen from top-level send - * paths, never inside RX processing, replies use the deferred TX queue. - */ -static int32_t lo_transmit(netif* iface, const uint8_t* frame, size_t len) { - if (!iface || !frame || len == 0) { - return ERR_INVAL; - } - - // Feed the frame back to the receive path. - rx_frame(iface, frame, len); - - // Hardware NICs drain deferred TX from their driver event loop. Loopback - // has none, so replies queued during this RX are drained right here. - drain_deferred_tx(); - - return OK; -} - -/** - * Loopback link status callback. Loopback is always up. - */ -static bool lo_link_up(netif*) { - return true; -} - -__PRIVILEGED_CODE int32_t loopback_init() { - string::memset(&g_lo_netif, 0, sizeof(g_lo_netif)); - string::memcpy(g_lo_netif.name, "lo", 3); - - // Loopback has no real MAC. Ethernet framing still works because the - // all-zeros MAC is never resolved via ARP. - string::memset(g_lo_netif.mac, 0, MAC_ADDR_LEN); - - g_lo_netif.transmit = lo_transmit; - g_lo_netif.link_up = lo_link_up; - g_lo_netif.poll = nullptr; // no polling needed - g_lo_netif.driver_data = nullptr; - g_lo_netif.flags = NETIF_UP | NETIF_RUNNING | NETIF_LOOPBACK; - - int32_t rc = register_netif(&g_lo_netif); - if (rc != OK) { - log::error("loopback: failed to register interface"); - return rc; - } - - // Configure with 127.0.0.1/8 (no gateway needed for loopback) - rc = configure(&g_lo_netif, - ipv4_addr(127, 0, 0, 1), - ipv4_addr(255, 0, 0, 0), - 0); - if (rc != OK) { - log::error("loopback: failed to configure interface"); - return rc; - } - - g_lo_initialized = true; - log::info("loopback: initialized lo (127.0.0.1/8)"); - return OK; -} - -netif* get_loopback_netif() { - return g_lo_initialized ? &g_lo_netif : nullptr; -} - -} // namespace net diff --git a/kernel/net/loopback.h b/kernel/net/loopback.h deleted file mode 100644 index 32d59412..00000000 --- a/kernel/net/loopback.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef STELLUX_NET_LOOPBACK_H -#define STELLUX_NET_LOOPBACK_H - -#include "net/net.h" - -namespace net { - -/** - * Initialize and register the loopback network interface ("lo"). - * Creates a virtual interface that loops transmitted frames back to the - * receive path. Automatically configured with 127.0.0.1/255.0.0.0. - * - * Called from net::init(), before any hardware drivers register. - * @return net::OK on success. - * @note Privilege: **required** - */ -__PRIVILEGED_CODE int32_t loopback_init(); - -/** - * Get the loopback network interface. - * Returns nullptr if loopback has not been initialized. - */ -netif* get_loopback_netif(); - -} // namespace net - -#endif // STELLUX_NET_LOOPBACK_H diff --git a/kernel/net/net.cpp b/kernel/net/net.cpp deleted file mode 100644 index 327719d7..00000000 --- a/kernel/net/net.cpp +++ /dev/null @@ -1,361 +0,0 @@ -#include "net/net.h" -#include "net/netinfo.h" -#include "net/ethernet.h" -#include "net/ipv4.h" -#include "net/arp.h" -#include "net/loopback.h" -#include "net/route.h" -#include "common/logging.h" -#include "common/string.h" -#include "sync/spinlock.h" -#include "mm/heap.h" -#include "dynpriv/dynpriv.h" - -namespace net { - -// Interface state stays unprivileged: the netif objects it points at are -// owned by drivers and allocated from the unprivileged heap. -static netif* g_iface_list = nullptr; -static netif* g_default_iface = nullptr; -static sync::spinlock g_net_lock = sync::SPINLOCK_INIT; - -// Deferred TX queue: protocol-generated responses (e.g. ICMP echo replies) -// that cannot be sent inline from RX processing context. -constexpr uint32_t DEFERRED_TX_MAX = 8; - -namespace { - -enum class deferred_tx_kind : uint8_t { - ipv4, // send via ipv4_send (dst_ip + protocol + payload) - ethernet, // send via eth_send (dst_mac + ethertype + payload) -}; - -struct deferred_tx_entry { - netif* iface; - deferred_tx_kind kind; - size_t len; - uint8_t data[ETH_MTU]; - - // IPv4-level fields - uint32_t dst_ip; - uint8_t protocol; - - // Ethernet-level fields - uint8_t dst_mac[MAC_ADDR_LEN]; - uint16_t ethertype; -}; - -} // anonymous namespace - -static deferred_tx_entry g_deferred_tx[DEFERRED_TX_MAX] = {}; -static uint32_t g_deferred_tx_count = 0; -static sync::spinlock g_deferred_tx_lock = sync::SPINLOCK_INIT; - -__PRIVILEGED_CODE int32_t init() { - arp_init(); - route_init(); - - int32_t lo_rc = loopback_init(); - if (lo_rc != OK) { - log::warn("net: loopback init failed"); - } - - log::info("net: initialized"); - return OK; -} - -int32_t register_netif(netif* iface) { - if (!iface || !iface->transmit) { - return ERR_INVAL; - } - - iface->configured = false; - iface->next = nullptr; - - // Mark as administratively up on registration (unless already set - // with specific flags, e.g. loopback sets flags before registration). - if (!(iface->flags & NETIF_UP)) { - iface->flags |= NETIF_UP; - } - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_net_lock); - - iface->next = g_iface_list; - g_iface_list = iface; - - // Loopback must never be the default outbound interface, so the - // default stays unset until a real NIC registers. - if (!g_default_iface && !(iface->flags & NETIF_LOOPBACK)) { - g_default_iface = iface; - } - }); - - log::info("net: registered interface %s (%02x:%02x:%02x:%02x:%02x:%02x)", - iface->name, - iface->mac[0], iface->mac[1], iface->mac[2], - iface->mac[3], iface->mac[4], iface->mac[5]); - return OK; -} - -int32_t unregister_netif(netif* iface) { - if (!iface) return ERR_INVAL; - - // Drop this interface's routes before unlinking it. Matching on the owner - // field also removes its LOCAL routes, which point at loopback. - route_del_iface(iface); - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_net_lock); - - netif** pp = &g_iface_list; - while (*pp) { - if (*pp == iface) { - *pp = iface->next; - break; - } - pp = &(*pp)->next; - } - - // Re-evaluate the default interface: pick the first non-loopback - // configured interface, or nullptr if none exists. - if (g_default_iface == iface) { - g_default_iface = nullptr; - for (netif* cur = g_iface_list; cur; cur = cur->next) { - if (!(cur->flags & NETIF_LOOPBACK) && cur->configured) { - g_default_iface = cur; - break; - } - } - } - }); - - iface->next = nullptr; - return OK; -} - -int32_t configure(netif* iface, uint32_t ip, uint32_t netmask, uint32_t gateway) { - if (!iface) return ERR_INVAL; - - // Reconfiguration replaces any routes this interface already owns, matching - // on the owner field also covers its LOCAL routes that point at loopback. - route_del_iface(iface); - - iface->ipv4_addr = ip; - iface->ipv4_netmask = netmask; - iface->ipv4_gateway = gateway; - iface->configured = true; - - route_add_interface_routes(iface); - - log::info("net: %s configured %u.%u.%u.%u/%u.%u.%u.%u gw %u.%u.%u.%u", - iface->name, - (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, - (ip >> 8) & 0xFF, ip & 0xFF, - (netmask >> 24) & 0xFF, (netmask >> 16) & 0xFF, - (netmask >> 8) & 0xFF, netmask & 0xFF, - (gateway >> 24) & 0xFF, (gateway >> 16) & 0xFF, - (gateway >> 8) & 0xFF, gateway & 0xFF); - return OK; -} - -netif* get_default_netif() { - return g_default_iface; -} - -uint32_t get_dns_server() { - netif* iface = g_default_iface; - if (iface && iface->configured) { - return iface->ipv4_dns; - } - - return 0; -} - -netif* find_netif(const char* name) { - if (!name) return nullptr; - - netif* result = nullptr; - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_net_lock); - - for (netif* cur = g_iface_list; cur; cur = cur->next) { - if (string::strcmp(cur->name, name) == 0) { - result = cur; - break; - } - } - }); - - return result; -} - -netif* find_netif_by_ip(uint32_t ip) { - if (ip == 0) return nullptr; - - netif* result = nullptr; - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_net_lock); - - for (netif* cur = g_iface_list; cur; cur = cur->next) { - if (cur->configured && cur->ipv4_addr == ip) { - result = cur; - break; - } - } - }); - - return result; -} - -bool is_local_ip(uint32_t ip) { - return find_netif_by_ip(ip) != nullptr; -} - -void rx_frame(netif* iface, const uint8_t* data, size_t len) { - if (!iface || !data || len < sizeof(eth_header)) { - return; - } - - eth_recv(iface, data, len); -} - -void queue_deferred_tx(netif* iface, uint32_t dst_ip, uint8_t protocol, - const uint8_t* data, size_t len) { - if (!iface || !data || len == 0 || len > ETH_MTU) { - return; - } - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_deferred_tx_lock); - - if (g_deferred_tx_count < DEFERRED_TX_MAX) { - auto& entry = g_deferred_tx[g_deferred_tx_count]; - entry.iface = iface; - entry.kind = deferred_tx_kind::ipv4; - entry.dst_ip = dst_ip; - entry.protocol = protocol; - entry.len = len; - string::memcpy(entry.data, data, len); - - g_deferred_tx_count++; - } - }); -} - -void queue_deferred_eth_tx(netif* iface, const uint8_t* dst_mac, - uint16_t ethertype, const uint8_t* data, size_t len) { - if (!iface || !dst_mac || !data || len == 0 || len > ETH_MTU) { - return; - } - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_deferred_tx_lock); - - if (g_deferred_tx_count < DEFERRED_TX_MAX) { - auto& entry = g_deferred_tx[g_deferred_tx_count]; - entry.iface = iface; - entry.kind = deferred_tx_kind::ethernet; - string::memcpy(entry.dst_mac, dst_mac, MAC_ADDR_LEN); - entry.ethertype = ethertype; - entry.len = len; - string::memcpy(entry.data, data, len); - - g_deferred_tx_count++; - } - }); -} - -void drain_deferred_tx() { - // Snapshot and clear the queue under the lock, then send outside it. - uint32_t count = 0; - - auto* local = static_cast( - heap::kzalloc(DEFERRED_TX_MAX * sizeof(deferred_tx_entry)) - ); - if (!local) { - return; - } - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_deferred_tx_lock); - - count = g_deferred_tx_count; - if (count > 0) { - string::memcpy(local, g_deferred_tx, count * sizeof(deferred_tx_entry)); - g_deferred_tx_count = 0; - } - }); - - for (uint32_t i = 0; i < count; i++) { - if (local[i].kind == deferred_tx_kind::ipv4) { - ipv4_send(local[i].iface, local[i].dst_ip, local[i].protocol, - local[i].data, local[i].len); - } else if (local[i].kind == deferred_tx_kind::ethernet) { - eth_send(local[i].iface, local[i].dst_mac, local[i].ethertype, - local[i].data, local[i].len); - } - } - - heap::kfree(local); -} - -__PRIVILEGED_CODE int32_t query_status(net_status* out) { - if (!out) return ERR_INVAL; - - string::memset(out, 0, sizeof(net_status)); - - // Snapshot each interface under the lock, then query live link status - // outside it, driver callbacks must not run while g_net_lock is held. - struct snapshot_entry { - netif* iface; - netif_link_fn link_fn; - bool is_default; - }; - snapshot_entry snap[MAX_INTERFACES]; - uint32_t count = 0; - - { - sync::irq_lock_guard guard(g_net_lock); - - netif* cur = g_iface_list; - while (cur && count < MAX_INTERFACES) { - auto& e = out->interfaces[count]; - string::memcpy(e.name, cur->name, 16); - string::memcpy(e.mac, cur->mac, MAC_ADDR_LEN); - e.ipv4_addr = cur->ipv4_addr; - e.ipv4_netmask = cur->ipv4_netmask; - e.ipv4_gateway = cur->ipv4_gateway; - e.ipv4_dns = cur->ipv4_dns; - - e.flags = 0; - if (cur->configured) { - e.flags |= IFF_CONFIGURED; - } - if (cur->flags & NETIF_LOOPBACK) { - e.flags |= IFF_LOOPBACK; - } - - snap[count].iface = cur; - snap[count].link_fn = cur->link_up; - snap[count].is_default = (cur == g_default_iface); - count++; - cur = cur->next; - } - } - - // Query live link status outside the lock. - for (uint32_t i = 0; i < count; i++) { - if (snap[i].link_fn && snap[i].link_fn(snap[i].iface)) { - out->interfaces[i].flags |= IFF_UP; - } - if (snap[i].is_default) { - out->interfaces[i].flags |= IFF_DEFAULT; - } - } - - out->if_count = count; - return OK; -} - -} // namespace net diff --git a/kernel/net/net.h b/kernel/net/net.h deleted file mode 100644 index 091358f3..00000000 --- a/kernel/net/net.h +++ /dev/null @@ -1,171 +0,0 @@ -#ifndef STELLUX_NET_NET_H -#define STELLUX_NET_NET_H - -#include "common/types.h" - -namespace net { - -constexpr int32_t OK = 0; -constexpr int32_t ERR_INIT = -1; -constexpr int32_t ERR_NOMEM = -2; -constexpr int32_t ERR_INVAL = -3; -constexpr int32_t ERR_NOIF = -4; -constexpr int32_t ERR_TIMEOUT = -5; -constexpr int32_t ERR_NOARP = -6; -constexpr int32_t ERR_NOREPLY = -7; - -// Maximum Ethernet frame size (without FCS) -constexpr size_t ETH_FRAME_MAX = 1514; -constexpr size_t ETH_MTU = 1500; -constexpr size_t MAC_ADDR_LEN = 6; -constexpr size_t MAX_INTERFACES = 8; - -// Interface flags -constexpr uint32_t NETIF_UP = (1u << 0); // administratively up -constexpr uint32_t NETIF_RUNNING = (1u << 1); // link is up (carrier detected) -constexpr uint32_t NETIF_LOOPBACK = (1u << 2); // virtual loopback device - -struct netif; - -// Driver callback types -using netif_tx_fn = int32_t (*)(netif* iface, const uint8_t* frame, size_t len); -using netif_link_fn = bool (*)(netif* iface); -using netif_poll_fn = void (*)(netif* iface); - -/** - * Network interface descriptor. - * Drivers fill in identity + callbacks, then call register_netif(). - * The protocol stack manages everything else. - */ -struct netif { - // Identity (set by driver before registration) - char name[16]; - uint8_t mac[MAC_ADDR_LEN]; - - // Driver callbacks (set by driver before registration) - netif_tx_fn transmit; // send a raw Ethernet frame - netif_link_fn link_up; // query link status - void* driver_data; // opaque pointer back to driver instance - netif_poll_fn poll; // synchronously process pending RX (optional) - - // Stack-managed state (set by net::configure() or register_netif()) - uint32_t ipv4_addr; // host byte order - uint32_t ipv4_netmask; // host byte order - uint32_t ipv4_gateway; // host byte order - uint32_t ipv4_dns; // DNS server, host byte order (from DHCP) - uint32_t flags; // NETIF_* flags - bool configured; - - // Internal linkage (managed by net subsystem) - netif* next; -}; - -/** - * Initialize the network subsystem. - * Must be called before drivers::init() so interfaces can register. - * @note Privilege: **required** - */ -__PRIVILEGED_CODE int32_t init(); - -/** - * Register a network interface with the stack. - * The driver must have filled in name, mac, transmit, link_up, and driver_data. - * The first interface registered becomes the default. - * @note Safe to call from any kernel context. - */ -int32_t register_netif(netif* iface); - -/** - * Unregister a network interface (for hot-unplug). - * @note Safe to call from any kernel context. - */ -int32_t unregister_netif(netif* iface); - -/** - * Configure IPv4 on a registered interface. - * Addresses are in HOST byte order. - */ -int32_t configure(netif* iface, uint32_t ip, uint32_t netmask, uint32_t gateway); - -/** - * Get the default (first registered) network interface. - * Returns nullptr if no interface is registered. - */ -netif* get_default_netif(); - -/** - * Get the DNS server IP from the default interface. - * Returns the address in HOST byte order, or 0 if none is configured. - */ -uint32_t get_dns_server(); - -/** - * Find a network interface by name. - * @return Pointer to the netif, or nullptr if not found. - */ -netif* find_netif(const char* name); - -/** - * Find a network interface by configured IPv4 address. - * @param ip IPv4 address in host byte order. - * @return Pointer to the netif, or nullptr if not found. - */ -netif* find_netif_by_ip(uint32_t ip); - -/** - * Check if an IPv4 address is configured on any local interface. - * @param ip IPv4 address in host byte order. - * @return true if the address is local. - */ -bool is_local_ip(uint32_t ip); - -/** - * Called by NIC drivers when a complete Ethernet frame is received. - * The frame includes the Ethernet header. FCS should be stripped. - * @param iface The interface that received the frame. - * @param data Pointer to the complete Ethernet frame. - * @param len Length of the frame in bytes. - */ -void rx_frame(netif* iface, const uint8_t* data, size_t len); - -/** - * Queue a protocol-generated response (e.g. ICMP echo reply) for - * deferred transmission. Called from RX processing context where - * inline TX would cause recursion through the ARP/poll path. - * Packets are sent later by drain_deferred_tx(). - * @param iface Interface to send on. - * @param dst_ip Destination IP in host byte order. - * @param protocol IPv4 protocol number. - * @param data Payload data (copied into the queue). - * @param len Payload length. - */ -void queue_deferred_tx(netif* iface, uint32_t dst_ip, uint8_t protocol, - const uint8_t* data, size_t len); - -/** - * Queue a raw Ethernet frame for deferred transmission. - * Used by ARP replies which bypass the IPv4 layer. - */ -void queue_deferred_eth_tx(netif* iface, const uint8_t* dst_mac, - uint16_t ethertype, const uint8_t* data, size_t len); - -/** - * Send all packets queued by queue_deferred_tx(). - * Called from the driver's run() or poll_callback after RX delivery - * is complete. Runs at the top level, outside any RX processing, - * so ipv4_send and ARP resolution are safe. - */ -void drain_deferred_tx(); - -// Socket option constants (matching Linux/musl ABI) -constexpr int32_t SOL_SOCKET = 1; -constexpr int32_t SO_REUSEADDR = 2; - -// Socket shutdown flags -constexpr int32_t SHUT_RD = 0; -constexpr int32_t SHUT_WR = 1; -constexpr int32_t SHUT_RDWR = 2; - -} // namespace net - -#endif // STELLUX_NET_NET_H diff --git a/kernel/net/netinfo.h b/kernel/net/netinfo.h deleted file mode 100644 index 70589931..00000000 --- a/kernel/net/netinfo.h +++ /dev/null @@ -1,75 +0,0 @@ -#ifndef STELLUX_NET_NETINFO_H -#define STELLUX_NET_NETINFO_H - -#include "common/types.h" - -namespace net { - -constexpr uint32_t STLX_SIOCGNETSTATUS = 0x4E01; - -constexpr uint32_t IFF_UP = (1u << 0); -constexpr uint32_t IFF_CONFIGURED = (1u << 1); -constexpr uint32_t IFF_DEFAULT = (1u << 2); -constexpr uint32_t IFF_LOOPBACK = (1u << 3); - -struct net_status_entry { - char name[16]; - uint8_t mac[6]; - uint8_t _pad[2]; - uint32_t ipv4_addr; // host byte order - uint32_t ipv4_netmask; // host byte order - uint32_t ipv4_gateway; // host byte order - uint32_t ipv4_dns; // host byte order - uint32_t flags; // IFF_* bitmask -}; - -static_assert(sizeof(net_status_entry) == 44, "net_status_entry ABI size mismatch"); - -struct net_status { - uint32_t if_count; - uint32_t _reserved; - net_status_entry interfaces[8]; -}; - -static_assert(sizeof(net_status) == 360, "net_status ABI size mismatch"); - -/** - * Query the status of all registered network interfaces. - * Fills out->interfaces[] with identity, IPv4 config, and live link - * status for each registered interface. Sets IFF_DEFAULT on the - * default outbound interface. - * @note Privilege: **required** (calls driver link_up callbacks) - */ -__PRIVILEGED_CODE int32_t query_status(net_status* out); - -constexpr uint32_t STLX_SIOCGARPTABLE = 0x4E02; -constexpr uint32_t ARP_QUERY_MAX = 32; - -struct arp_table_entry { - uint32_t ipv4_addr; // host byte order - uint8_t mac[6]; - uint8_t _pad[2]; - uint32_t age_ms; // ms since last update - uint32_t flags; // reserved -}; - -static_assert(sizeof(arp_table_entry) == 20, "arp_table_entry ABI size mismatch"); - -struct arp_table_status { - uint32_t entry_count; - uint32_t _reserved; - arp_table_entry entries[ARP_QUERY_MAX]; -}; - -static_assert(sizeof(arp_table_status) == 648, "arp_table_status ABI size mismatch"); - -/** - * Snapshot the ARP cache into out->entries[]. - * Computes age_ms from internal timestamps at query time. - * @note Privilege: **required** - */ -__PRIVILEGED_CODE int32_t query_arp_table(arp_table_status* out); - -} // namespace net - -#endif // STELLUX_NET_NETINFO_H diff --git a/kernel/net/route.cpp b/kernel/net/route.cpp deleted file mode 100644 index 578d5293..00000000 --- a/kernel/net/route.cpp +++ /dev/null @@ -1,178 +0,0 @@ -#include "net/route.h" -#include "net/loopback.h" -#include "sync/spinlock.h" -#include "dynpriv/dynpriv.h" - -namespace net { - -static route_entry g_route_table[ROUTE_TABLE_SIZE] = {}; -static sync::spinlock g_route_lock = sync::SPINLOCK_INIT; - -/** - * Count the number of set bits in a 32-bit value. - * Used for comparing prefix lengths (netmask bit count). - */ -static uint32_t popcount32(uint32_t v) { - v = v - ((v >> 1) & 0x55555555); - v = (v & 0x33333333) + ((v >> 2) & 0x33333333); - return (((v + (v >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24; -} - -__PRIVILEGED_CODE void route_init() { - for (uint32_t i = 0; i < ROUTE_TABLE_SIZE; i++) { - g_route_table[i].valid = false; - } -} - -int32_t route_add(uint32_t dest, uint32_t netmask, uint32_t gateway, - netif* iface, route_type type, uint16_t metric, - netif* owner) { - if (!iface) { - return ERR_INVAL; - } - - // Default owner to iface if not specified - if (!owner) { - owner = iface; - } - - int32_t result = ERR_NOMEM; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_route_lock); - - for (uint32_t i = 0; i < ROUTE_TABLE_SIZE; i++) { - if (!g_route_table[i].valid) { - g_route_table[i].dest = dest; - g_route_table[i].netmask = netmask; - g_route_table[i].gateway = gateway; - g_route_table[i].iface = iface; - g_route_table[i].owner = owner; - g_route_table[i].type = type; - g_route_table[i].metric = metric; - g_route_table[i].valid = true; - - result = OK; - break; - } - } - }); - - return result; -} - -void route_del_iface(netif* iface) { - if (!iface) return; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_route_lock); - - // Match on the owner field, LOCAL routes point at loopback but belong - // to the interface whose configure() created them. - for (uint32_t i = 0; i < ROUTE_TABLE_SIZE; i++) { - if (g_route_table[i].valid && g_route_table[i].owner == iface) { - g_route_table[i].valid = false; - } - } - }); -} - -int32_t route_lookup(uint32_t dst_ip, route_result* result) { - if (!result) return ERR_INVAL; - - int32_t rc = ERR_NOIF; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_route_lock); - - bool found = false; - uint32_t best_prefix_len = 0; - uint16_t best_metric = 0xFFFF; - uint32_t best_idx = 0; - - for (uint32_t i = 0; i < ROUTE_TABLE_SIZE; i++) { - if (!g_route_table[i].valid) continue; - - // Check if destination matches this route's network - if ((dst_ip & g_route_table[i].netmask) != g_route_table[i].dest) { - continue; - } - - uint32_t prefix_len = popcount32(g_route_table[i].netmask); - - // Prefer longest prefix match, then lowest metric - if (!found || - prefix_len > best_prefix_len || - (prefix_len == best_prefix_len && - g_route_table[i].metric < best_metric)) { - best_prefix_len = prefix_len; - best_metric = g_route_table[i].metric; - best_idx = i; - found = true; - } - } - - if (found) { - const auto& best = g_route_table[best_idx]; - result->iface = best.iface; - result->type = best.type; - - switch (best.type) { - case route_type::LOCAL: - result->next_hop = dst_ip; - break; - case route_type::CONNECTED: - result->next_hop = dst_ip; - break; - case route_type::GATEWAY: - result->next_hop = best.gateway; - break; - } - - rc = OK; - } - }); - - return rc; -} - -void route_add_interface_routes(netif* iface) { - if (!iface || !iface->configured) return; - - netif* lo = get_loopback_netif(); - - // Self-addressed traffic is delivered via loopback. Ownership keeps - // teardown correct, and lo's own 127.0.0.0/8 route already covers lo. - if (lo && iface != lo) { - route_add(iface->ipv4_addr, 0xFFFFFFFF, 0, - lo, route_type::LOCAL, METRIC_LOCAL, iface); - } - - uint32_t subnet = iface->ipv4_addr & iface->ipv4_netmask; - route_add(subnet, iface->ipv4_netmask, 0, - iface, route_type::CONNECTED, METRIC_CONNECTED); - - // A configured gateway becomes the default route (0.0.0.0/0) - if (iface->ipv4_gateway != 0) { - route_add(0, 0, iface->ipv4_gateway, - iface, route_type::GATEWAY, METRIC_DEFAULT); - } -} - -uint32_t route_count() { - uint32_t count = 0; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_route_lock); - - for (uint32_t i = 0; i < ROUTE_TABLE_SIZE; i++) { - if (g_route_table[i].valid) { - count++; - } - } - }); - - return count; -} - -} // namespace net diff --git a/kernel/net/route.h b/kernel/net/route.h deleted file mode 100644 index a24e37ef..00000000 --- a/kernel/net/route.h +++ /dev/null @@ -1,106 +0,0 @@ -#ifndef STELLUX_NET_ROUTE_H -#define STELLUX_NET_ROUTE_H - -#include "common/types.h" -#include "net/net.h" - -namespace net { - -/** - * Route type, determines how a packet is delivered. - */ -enum class route_type : uint8_t { - LOCAL, // Destination is a local address -> deliver via loopback - CONNECTED, // Destination is directly reachable on this interface - GATEWAY, // Destination is reachable via a next-hop gateway -}; - -/** - * A single entry in the routing table. - */ -struct route_entry { - uint32_t dest; // destination network, host byte order - uint32_t netmask; // network mask, host byte order - uint32_t gateway; // next hop IP (0 for connected/local), host byte order - netif* iface; // outgoing interface - netif* owner; // interface whose configure() created this route - // (differs from iface for LOCAL routes via loopback) - route_type type; - uint8_t _pad[1]; - uint16_t metric; // lower = preferred - bool valid; // slot in use - uint8_t _pad2[3]; -}; - -/** - * Result of a route lookup. - */ -struct route_result { - netif* iface; // outgoing interface - uint32_t next_hop; // IP to ARP resolve (gateway or dst) - route_type type; -}; - -constexpr uint32_t ROUTE_TABLE_SIZE = 32; - -// Metric constants, lower = higher priority -constexpr uint16_t METRIC_LOCAL = 0; // own IPs -> loopback -constexpr uint16_t METRIC_CONNECTED = 100; // directly connected subnets -constexpr uint16_t METRIC_STATIC = 200; // manually added routes -constexpr uint16_t METRIC_DEFAULT = 1024; // default gateway - -/** - * Initialize the routing table. Called from net::init(). - * @note Privilege: **required** - */ -__PRIVILEGED_CODE void route_init(); - -/** - * Add a route to the routing table. - * @param owner The interface whose configure() call created this route. - * For LOCAL routes, owner is the configured interface while - * iface is loopback. For other routes, owner == iface. - * If nullptr, defaults to iface. - * @return net::OK on success, ERR_NOMEM if table is full, ERR_INVAL on bad args. - */ -int32_t route_add(uint32_t dest, uint32_t netmask, uint32_t gateway, - netif* iface, route_type type, uint16_t metric, - netif* owner = nullptr); - -/** - * Remove all routes owned by the given interface. - * Matches on the owner field, not the outgoing interface. This ensures - * that LOCAL routes (which point to loopback) are correctly removed - * when their owning interface is unregistered or reconfigured, without - * accidentally removing LOCAL routes belonging to other interfaces. - */ -void route_del_iface(netif* iface); - -/** - * Lookup a route for a destination IP (host byte order). - * Uses longest prefix match, then lowest metric for ties. - * @param dst_ip Destination IP in host byte order. - * @param result Output: interface, next-hop, route type. - * @return net::OK if a route was found, ERR_NOIF if no route matches. - */ -int32_t route_lookup(uint32_t dst_ip, route_result* result); - -/** - * Auto-populate routes for a newly configured interface. - * Adds: - * - LOCAL host route for the interface's own IP (loopback) - * - CONNECTED subnet route for the interface's subnet - * - GATEWAY default route if gateway != 0 - * - * Called from net::configure() after setting interface IP config. - */ -void route_add_interface_routes(netif* iface); - -/** - * Count the number of valid routes in the table. (For testing/debugging) - */ -uint32_t route_count(); - -} // namespace net - -#endif // STELLUX_NET_ROUTE_H diff --git a/kernel/net/tcp.cpp b/kernel/net/tcp.cpp deleted file mode 100644 index d9ab9802..00000000 --- a/kernel/net/tcp.cpp +++ /dev/null @@ -1,1592 +0,0 @@ -#include "net/tcp.h" -#include "net/inet_socket.h" -#include "net/ipv4.h" -#include "net/route.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "net/net.h" -#include "common/logging.h" -#include "common/string.h" -#include "common/ring_buffer.h" -#include "mm/heap.h" -#include "sync/atomic.h" -#include "sync/spinlock.h" -#include "sync/poll.h" -#include "fs/fstypes.h" -#include "sched/sched.h" -#include "sched/task.h" -#include "signals/signal.h" -#include "dynpriv/dynpriv.h" - -namespace net { - -// Port registry: every TCP socket with a local port assigned, searched by -// tcp_recv to match incoming segments to their socket. -static tcp_socket* g_tcp_sock_list = nullptr; -static sync::spinlock g_tcp_sock_lock = sync::SPINLOCK_INIT; - -// ISN generator. A monotonic counter is functionally correct, though it lacks -// the off-path spoofing protection of a clock-based scheme (RFC 6528). -static sync::atomic g_tcp_isn_counter{1000}; - -constexpr size_t TCP_MSS = ETH_MTU - sizeof(ipv4_header) - sizeof(tcp_header); - -static uint32_t tcp_generate_isn() { - return g_tcp_isn_counter.fetch_add_relaxed(64000); -} - -constexpr uint16_t TCP_DEFAULT_WINDOW = 8192; -constexpr uint32_t TCP_DEFAULT_BACKLOG = 16; -constexpr uint32_t TCP_MAX_BACKLOG = 128; -constexpr size_t TCP_RX_BUF_CAPACITY = 16384; -constexpr uint16_t TCP_PORT_EPHEMERAL_MIN = 49152; -constexpr uint16_t TCP_PORT_EPHEMERAL_MAX = 65535; - -static sync::atomic g_tcp_ephemeral_next{TCP_PORT_EPHEMERAL_MIN}; - -static uint16_t tcp_alloc_ephemeral_port() { - uint32_t port = g_tcp_ephemeral_next.fetch_add_relaxed(1); - uint32_t range = TCP_PORT_EPHEMERAL_MAX - TCP_PORT_EPHEMERAL_MIN + 1; - return static_cast( - TCP_PORT_EPHEMERAL_MIN + (port - TCP_PORT_EPHEMERAL_MIN) % range); -} - -// Build a TCP segment and send it. deferred=true queues it for deferred TX -// (safe from RX/IRQ context), deferred=false sends inline (syscall context only). -__PRIVILEGED_CODE static int32_t tcp_send( - uint32_t src_ip, uint16_t src_port, - uint32_t dst_ip, uint16_t dst_port, - uint32_t seq, uint32_t ack_num, - uint8_t flags, uint16_t window, - const uint8_t* data, size_t data_len, - bool deferred -) { - size_t seg_len = sizeof(tcp_header) + data_len; - auto* buf = static_cast(heap::uzalloc(seg_len)); - if (!buf) { - return ERR_NOMEM; - } - - auto* hdr = reinterpret_cast(buf); - hdr->src_port = htons(src_port); - hdr->dst_port = htons(dst_port); - hdr->seq = htonl(seq); - hdr->ack = htonl(ack_num); - hdr->data_off = (5 << 4); // 5 * 4 = 20 bytes, no options - hdr->flags = flags; - hdr->window = htons(window); - hdr->checksum = 0; - hdr->urgent_ptr = 0; - - if (data && data_len > 0) { - string::memcpy(buf + sizeof(tcp_header), data, data_len); - } - - uint16_t csum = tcp_checksum(htonl(src_ip), htonl(dst_ip), buf, seg_len); - hdr->checksum = (csum == 0) ? static_cast(0xFFFF) : csum; - - netif* iface = get_default_netif(); - int32_t rc; - if (deferred) { - queue_deferred_tx(iface, dst_ip, IPV4_PROTO_TCP, buf, seg_len); - rc = OK; - } else { - rc = ipv4_send(iface, dst_ip, IPV4_PROTO_TCP, buf, seg_len, src_ip); - } - heap::ufree(buf); - return rc; -} - -// Convenience wrappers with clear intent. -__PRIVILEGED_CODE static int32_t tcp_send_segment( - uint32_t src_ip, uint16_t src_port, - uint32_t dst_ip, uint16_t dst_port, - uint32_t seq, uint32_t ack_num, - uint8_t flags, uint16_t window, - const uint8_t* data, size_t data_len -) { - return tcp_send(src_ip, src_port, dst_ip, dst_port, - seq, ack_num, flags, window, data, data_len, true); -} - -__PRIVILEGED_CODE static int32_t tcp_send_data( - uint32_t src_ip, uint16_t src_port, - uint32_t dst_ip, uint16_t dst_port, - uint32_t seq, uint32_t ack_num, - uint8_t flags, uint16_t window, - const uint8_t* data, size_t data_len -) { - return tcp_send(src_ip, src_port, dst_ip, dst_port, - seq, ack_num, flags, window, data, data_len, false); -} - -__PRIVILEGED_CODE static void tcp_sock_release(tcp_socket* sock) { - if (!sock) { - return; - } - - if (sock->release()) { - tcp_socket::ref_destroy(sock); - } -} - -static void tcp_destroy_socket(tcp_socket* sock) { - if (!sock) { - return; - } - - if (sock->local_port != 0) { - tcp_unregister_socket(sock); - } - tcp_sock_release(sock); -} - -__PRIVILEGED_CODE static void tcp_close(resource::resource_object* obj) { - if (!obj || !obj->impl) { - return; - } - - auto* sock = static_cast(obj->impl); - - // Drain accept queue if this was a LISTEN socket - if (sock->state == tcp_state::LISTEN) { - constexpr uint32_t MAX_DRAIN = 256; - tcp_pending_conn* drain_list[MAX_DRAIN]; - uint32_t drain_count = 0; - tcp_socket* destroy_list = nullptr; - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (sock->state != tcp_state::LISTEN) { - sync::spin_unlock_irqrestore(sock->lock, irq); - goto cleanup; - } - - sock->state = tcp_state::CLOSED; - - { - sync::irq_lock_guard guard(g_tcp_sock_lock); - - while (tcp_pending_conn* pc = sock->accept_queue.pop_front()) { - if (drain_count < MAX_DRAIN) { - drain_list[drain_count++] = pc; - } - } - - tcp_socket** pp = &g_tcp_sock_list; - while (*pp) { - tcp_socket* child = *pp; - if (child->parent == sock) { - if (child->state == tcp_state::SYN_RECEIVED) { - *pp = child->next; - child->next = destroy_list; - destroy_list = child; - child->parent = nullptr; - continue; - } - child->parent = nullptr; - } - pp = &(*pp)->next; - } - - // Unlink listener itself from the global list - pp = &g_tcp_sock_list; - while (*pp) { - if (*pp == sock) { - *pp = sock->next; - sock->next = nullptr; - break; - } - pp = &(*pp)->next; - } - } - sync::spin_unlock_irqrestore(sock->lock, irq); - - sync::wake_all(sock->accept_wq); - - for (uint32_t i = 0; i < drain_count; i++) { - if (drain_list[i]->conn_obj) { - resource::resource_release(drain_list[i]->conn_obj); - } - heap::kfree(drain_list[i]); - } - - while (destroy_list) { - tcp_socket* child = destroy_list; - destroy_list = child->next; - child->next = nullptr; - tcp_sock_release(child); - } - - tcp_sock_release(sock); - obj->impl = nullptr; - return; - } - - // For ESTABLISHED or CLOSE_WAIT: send FIN and let the socket linger - // in the port registry until the FIN handshake completes. - if (sock->state == tcp_state::ESTABLISHED - || sock->state == tcp_state::CLOSE_WAIT) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - // Re-check under lock - tcp_state cur = sock->state; - if (cur != tcp_state::ESTABLISHED && cur != tcp_state::CLOSE_WAIT) { - sync::spin_unlock_irqrestore(sock->lock, irq); - goto cleanup; - } - - uint32_t fin_seq = sock->snd_nxt; - sock->snd_nxt++; - - tcp_state next_state = (cur == tcp_state::ESTABLISHED) - ? tcp_state::FIN_WAIT_1 - : tcp_state::LAST_ACK; - sock->state = next_state; - - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t r_nxt = sock->rcv_nxt; - sync::spin_unlock_irqrestore(sock->lock, irq); - - obj->impl = nullptr; - - tcp_send_data(l_addr, l_port, r_addr, r_port, - fin_seq, r_nxt, - TCP_FIN | TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - return; - } - - // FIN handshake still in flight: detach the fd and drop the ref shutdown() - // took. The creation ref keeps the socket alive until tcp_recv tears it down. - if (sock->state == tcp_state::FIN_WAIT_1 || - sock->state == tcp_state::FIN_WAIT_2 || - sock->state == tcp_state::CLOSING || - sock->state == tcp_state::LAST_ACK) { - obj->impl = nullptr; - tcp_sock_release(sock); - return; - } - - // SYN_SENT: wake blocked connect() before cleanup - if (sock->state == tcp_state::SYN_SENT) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - sock->state = tcp_state::CLOSED; - sync::spin_unlock_irqrestore(sock->lock, irq); - sync::wake_one(sock->accept_wq); - } - -cleanup: - // All other states (CLOSED, SYN_RECEIVED, etc.): immediate cleanup - if (sock->local_port != 0) { - tcp_unregister_socket(sock); - } - tcp_sock_release(sock); - obj->impl = nullptr; -} - -__PRIVILEGED_CODE static int32_t tcp_bind( - resource::resource_object* obj, const void* kaddr, size_t addrlen -) { - if (!obj || !obj->impl) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - - if (!kaddr || addrlen < sizeof(kernel_sockaddr_in)) { - return resource::ERR_INVAL; - } - - const auto* addr = static_cast(kaddr); - if (addr->sin_family != AF_INET_VAL) { - return resource::ERR_INVAL; - } - - uint32_t bind_addr = ntohl(addr->sin_addr); - uint16_t bind_port = ntohs(addr->sin_port); - - if (bind_addr != 0 && !is_local_ip(bind_addr)) { - return resource::ERR_INVAL; - } - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - if (sock->local_port != 0) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_INVAL; - } - - if (sock->state != tcp_state::CLOSED) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_INVAL; - } - - sock->local_addr = bind_addr; - sock->local_port = bind_port; - - if (!tcp_try_register(sock)) { - sock->local_addr = 0; - sock->local_port = 0; - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_ADDRINUSE; - } - - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::OK; -} - -__PRIVILEGED_CODE static int32_t tcp_listen( - resource::resource_object* obj, int32_t backlog -) { - if (!obj || !obj->impl) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - if (sock->state != tcp_state::CLOSED || sock->local_port == 0) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_INVAL; - } - - uint32_t bl = (backlog <= 0) ? TCP_DEFAULT_BACKLOG - : static_cast(backlog); - if (bl > TCP_MAX_BACKLOG) { - bl = TCP_MAX_BACKLOG; - } - - sock->backlog = bl; - sock->pending_count = 0; - sock->accept_queue.init(); - sock->accept_wq.init(); - sock->state = tcp_state::LISTEN; - - sync::spin_unlock_irqrestore(sock->lock, irq); - - log::info("tcp: listening on port %u (backlog %u)", sock->local_port, bl); - return resource::OK; -} - -__PRIVILEGED_CODE static int32_t tcp_accept( - resource::resource_object* obj, resource::resource_object** new_obj, - void* kaddr, size_t* addrlen, bool nonblock -) { - if (!obj || !obj->impl || !new_obj) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - if (sock->state != tcp_state::LISTEN) { - return resource::ERR_INVAL; - } - - sched::task* task = sched::current(); - if (!task) { - return resource::ERR_IO; - } - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - if (sock->accept_queue.empty()) { - if (nonblock) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_AGAIN; - } - } - - while (sock->accept_queue.empty() - && sock->state == tcp_state::LISTEN - && !signals::interrupt_pending(task)) { - irq = sync::wait(sock->accept_wq, sock->lock, irq); - } - - if (signals::interrupt_pending(task)) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_INTR; - } - - tcp_pending_conn* pc = nullptr; - { - sync::irq_lock_guard guard(g_tcp_sock_lock); - pc = sock->accept_queue.pop_front(); - } - - if (!pc) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_INVAL; - } - - sock->pending_count--; - sync::spin_unlock_irqrestore(sock->lock, irq); - - *new_obj = pc->conn_obj; - auto* child = static_cast(pc->conn_obj->impl); - heap::kfree(pc); - - // Fill in peer address for the accept() syscall - if (kaddr && addrlen && *addrlen >= sizeof(kernel_sockaddr_in)) { - auto* peer = static_cast(kaddr); - string::memset(peer, 0, sizeof(*peer)); - peer->sin_family = AF_INET_VAL; - peer->sin_port = htons(child->remote_port); - peer->sin_addr = htonl(child->remote_addr); - *addrlen = sizeof(kernel_sockaddr_in); - } - - return resource::OK; -} - -__PRIVILEGED_CODE static ssize_t tcp_read( - resource::resource_object* obj, void* kdst, size_t count, uint32_t flags -) { - if (!obj || !obj->impl || !kdst) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - tcp_state cur = sock->state; - sync::spin_unlock_irqrestore(sock->lock, irq); - - if (cur != tcp_state::ESTABLISHED && cur != tcp_state::CLOSE_WAIT && - cur != tcp_state::FIN_WAIT_1 && cur != tcp_state::FIN_WAIT_2) { - return resource::ERR_NOTCONN; - } - - if (!sock->rx_buf) { - return resource::ERR_IO; - } - - bool nonblock = (flags & fs::O_NONBLOCK) != 0; - return ring_buffer_read(sock->rx_buf, static_cast(kdst), count, nonblock); -} - -__PRIVILEGED_CODE static ssize_t tcp_write( - resource::resource_object* obj, const void* ksrc, size_t count, uint32_t flags -) { - (void)flags; - if (!obj || !obj->impl || !ksrc) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - const auto* src = static_cast(ksrc); - size_t remaining = count; - - while (remaining > 0) { - size_t chunk = remaining < TCP_MSS ? remaining : TCP_MSS; - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (sock->shut_wr) { - sync::spin_unlock_irqrestore(sock->lock, irq); - if (count > remaining) { - return static_cast(count - remaining); - } - - // POSIX: writing a shut-down stream raises SIGPIPE, and write - // has no MSG_NOSIGNAL to suppress it. - signals::send_to_task(sched::current(), signals::SIGPIPE); - return resource::ERR_PIPE; - } - - tcp_state cur = sock->state; - if (cur != tcp_state::ESTABLISHED && cur != tcp_state::CLOSE_WAIT) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return (count > remaining) - ? static_cast(count - remaining) - : resource::ERR_NOTCONN; - } - - // Respect peer's advertised receive window. snd_wnd is always set from - // the handshake before ESTABLISHED, so 0 here is a real zero window. - uint32_t in_flight = sock->snd_nxt - sock->snd_una; - uint32_t wnd = sock->snd_wnd; - if (wnd == 0 || in_flight >= wnd) { - sync::spin_unlock_irqrestore(sock->lock, irq); - if (count > remaining) { - return static_cast(count - remaining); - } - - return resource::ERR_AGAIN; - } - - if (chunk > wnd - in_flight) { - chunk = wnd - in_flight; - } - - uint32_t seq = sock->snd_nxt; - sock->snd_nxt += static_cast(chunk); - uint32_t ack_val = sock->rcv_nxt; - uint32_t src_ip = sock->local_addr; - uint16_t src_port = sock->local_port; - uint32_t dst_ip = sock->remote_addr; - uint16_t dst_port = sock->remote_port; - sync::spin_unlock_irqrestore(sock->lock, irq); - - uint8_t seg_flags = TCP_ACK; - if (chunk == remaining) { - seg_flags |= TCP_PSH; - } - - int32_t rc = tcp_send_data( - src_ip, src_port, dst_ip, dst_port, - seq, ack_val, seg_flags, TCP_DEFAULT_WINDOW, - src, chunk); - - if (rc != OK) { - sync::irq_state irq2 = sync::spin_lock_irqsave(sock->lock); - sock->snd_nxt -= static_cast(chunk); - sync::spin_unlock_irqrestore(sock->lock, irq2); - return (count > remaining) - ? static_cast(count - remaining) - : resource::ERR_IO; - } - - src += chunk; - remaining -= chunk; - } - - return static_cast(count); -} - -__PRIVILEGED_CODE static int32_t tcp_connect( - resource::resource_object* obj, const void* kaddr, size_t addrlen -) { - if (!obj || !obj->impl) { - return resource::ERR_INVAL; - } - - auto* sock = static_cast(obj->impl); - - if (sock->state == tcp_state::ESTABLISHED) { - return resource::ERR_ISCONN; - } - - if (sock->state != tcp_state::CLOSED) { - return resource::ERR_INVAL; - } - - if (!kaddr || addrlen < sizeof(kernel_sockaddr_in)) { - return resource::ERR_INVAL; - } - - const auto* addr = static_cast(kaddr); - if (addr->sin_family != AF_INET_VAL) { - return resource::ERR_INVAL; - } - - uint32_t dst_ip = ntohl(addr->sin_addr); - uint16_t dst_port = ntohs(addr->sin_port); - if (dst_port == 0) { - return resource::ERR_INVAL; - } - - sched::task* task = sched::current(); - if (!task) { - return resource::ERR_IO; - } - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - bool already_registered = (sock->local_port != 0); - - if (!already_registered) { - sock->local_port = tcp_alloc_ephemeral_port(); - } - - sock->remote_addr = dst_ip; - sock->remote_port = dst_port; - - // Resolve source IP before registration so tcp_recv can't - // observe a half-initialized local_addr. - if (sock->local_addr == 0) { - route_result rt; - if (route_lookup(dst_ip, &rt) == OK) { - if (rt.type == route_type::LOCAL) { - sock->local_addr = dst_ip; - } else if (rt.iface && (rt.iface->flags & NETIF_LOOPBACK)) { - sock->local_addr = rt.iface->ipv4_addr; - } else { - netif* def = get_default_netif(); - if (def) { - sock->local_addr = def->ipv4_addr; - } - } - } else { - netif* def = get_default_netif(); - if (def) { - sock->local_addr = def->ipv4_addr; - } - } - } - - uint32_t isn = tcp_generate_isn(); - sock->snd_una = isn; - sock->snd_nxt = isn + 1; - - sock->state = tcp_state::SYN_SENT; - sock->accept_wq.init(); - - if (!already_registered) { - { - sync::irq_lock_guard guard(g_tcp_sock_lock); - - sock->next = g_tcp_sock_list; - g_tcp_sock_list = sock; - } - } - sync::spin_unlock_irqrestore(sock->lock, irq); - - int32_t send_rc = tcp_send_data( - sock->local_addr, sock->local_port, - dst_ip, dst_port, - isn, 0, - TCP_SYN, TCP_DEFAULT_WINDOW, - nullptr, 0); - - if (send_rc != OK) { - tcp_unregister_socket(sock); - sock->state = tcp_state::CLOSED; - sock->remote_addr = 0; - sock->remote_port = 0; - return resource::ERR_IO; - } - - // Block until state changes from SYN_SENT - irq = sync::spin_lock_irqsave(sock->lock); - while (sock->state == tcp_state::SYN_SENT - && !signals::interrupt_pending(task)) { - irq = sync::wait(sock->accept_wq, sock->lock, irq); - } - - tcp_state final_state = sock->state; - sync::spin_unlock_irqrestore(sock->lock, irq); - - if (signals::interrupt_pending(task)) { - return resource::ERR_INTR; - } - - if (final_state == tcp_state::ESTABLISHED) { - return resource::OK; - } - - return resource::ERR_CONNREFUSED; -} - -__PRIVILEGED_CODE static int32_t tcp_setsockopt( - resource::resource_object* obj, int32_t level, - int32_t optname, const void* optval, size_t optlen -) { - auto* sock = static_cast(obj->impl); - - if (level != SOL_SOCKET) { - return resource::ERR_NOPROTOOPT; - } - - if (optname != SO_REUSEADDR) { - return resource::ERR_NOPROTOOPT; - } - - if (optlen < sizeof(int32_t)) { - return resource::ERR_INVAL; - } - - int32_t val = 0; - string::memcpy(&val, optval, sizeof(val)); - - sync::irq_lock_guard guard(sock->lock); - - if (val) - sock->so_options |= static_cast(SO_REUSEADDR); - else - sock->so_options &= ~static_cast(SO_REUSEADDR); - - return resource::OK; -} - -__PRIVILEGED_CODE static int32_t tcp_getsockopt( - resource::resource_object* obj, int32_t level, - int32_t optname, void* optval, size_t* optlen -) { - auto* sock = static_cast(obj->impl); - - if (level != SOL_SOCKET) { - return resource::ERR_NOPROTOOPT; - } - - if (optname != SO_REUSEADDR) { - return resource::ERR_NOPROTOOPT; - } - - if (!optlen || *optlen < sizeof(int32_t)) { - return resource::ERR_INVAL; - } - - sync::irq_lock_guard guard(sock->lock); - - int32_t val = (sock->so_options & static_cast(SO_REUSEADDR)) ? 1 : 0; - string::memcpy(optval, &val, sizeof(val)); - *optlen = sizeof(val); - - return resource::OK; -} - -__PRIVILEGED_CODE static uint32_t tcp_poll( - resource::resource_object* obj, sync::poll_table* pt -) { - if (!obj || !obj->impl) return sync::POLL_NVAL; - - auto* sock = static_cast(obj->impl); - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - tcp_state st = sock->state; - bool wr_shut = sock->shut_wr; - sync::spin_unlock_irqrestore(sock->lock, irq); - - if (st == tcp_state::ESTABLISHED || st == tcp_state::CLOSE_WAIT) { - uint32_t mask = ring_buffer_poll_read(sock->rx_buf, pt); - if (!wr_shut) { - mask |= sync::POLL_OUT; - } - if (st == tcp_state::CLOSE_WAIT) { - mask |= sync::POLL_HUP; - } - return mask; - } - - if (st == tcp_state::FIN_WAIT_1 || st == tcp_state::FIN_WAIT_2) { - uint32_t mask = sock->rx_buf ? ring_buffer_poll_read(sock->rx_buf, pt) : 0; - return mask; - } - - if (st == tcp_state::CLOSING || st == tcp_state::LAST_ACK) { - uint32_t mask = sock->rx_buf ? ring_buffer_poll_read(sock->rx_buf, pt) : 0; - mask |= sync::POLL_HUP; - return mask; - } - - if (st == tcp_state::LISTEN) { - if (pt) { - sync::poll_subscribe(*pt, sock->accept_wq); - } - - irq = sync::spin_lock_irqsave(sock->lock); - uint32_t mask = sock->accept_queue.empty() ? 0 : sync::POLL_IN; - sync::spin_unlock_irqrestore(sock->lock, irq); - return mask; - } - - if (st == tcp_state::SYN_SENT || st == tcp_state::SYN_RECEIVED) { - if (pt) { - sync::poll_subscribe(*pt, sock->accept_wq); - } - return 0; - } - - return sync::POLL_HUP; -} - -__PRIVILEGED_CODE static int32_t tcp_shutdown( - resource::resource_object* obj, int32_t how -) { - if (!obj || !obj->impl) { - return resource::ERR_NOTCONN; - } - - auto* sock = static_cast(obj->impl); - - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - uint32_t l_addr = 0, r_addr = 0, fin_seq = 0, r_nxt = 0; - uint16_t l_port = 0, r_port = 0; - bool send_fin = false; - - if (how == net::SHUT_WR || how == net::SHUT_RDWR) { - if (!sock->shut_wr) { - tcp_state cur = sock->state; - if (cur != tcp_state::ESTABLISHED && cur != tcp_state::CLOSE_WAIT) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return resource::ERR_NOTCONN; - } else { - sock->shut_wr = true; - fin_seq = sock->snd_nxt; - sock->snd_nxt++; - sock->state = (cur == tcp_state::ESTABLISHED) - ? tcp_state::FIN_WAIT_1 - : tcp_state::LAST_ACK; - l_addr = sock->local_addr; - l_port = sock->local_port; - r_addr = sock->remote_addr; - r_port = sock->remote_port; - r_nxt = sock->rcv_nxt; - send_fin = true; - sock->add_ref(); - } - } - } - - if (how == net::SHUT_RD || how == net::SHUT_RDWR) { - tcp_state cur = sock->state; - if (!sock->shut_rd && - (cur == tcp_state::ESTABLISHED || cur == tcp_state::CLOSE_WAIT || - cur == tcp_state::FIN_WAIT_1 || cur == tcp_state::FIN_WAIT_2 || - cur == tcp_state::CLOSING || cur == tcp_state::LAST_ACK)) { - sock->shut_rd = true; - if (sock->rx_buf) { - ring_buffer_close_write(sock->rx_buf); - } - } - } - - sync::spin_unlock_irqrestore(sock->lock, irq); - - if (send_fin) { - tcp_send_data(l_addr, l_port, r_addr, r_port, - fin_seq, r_nxt, - TCP_FIN | TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - } - - return resource::OK; -} - -const resource::resource_ops g_tcp_ops = { - tcp_read, - tcp_write, - tcp_close, - nullptr, - nullptr, - nullptr, - nullptr, - tcp_bind, - tcp_listen, - tcp_accept, - tcp_connect, - tcp_setsockopt, - tcp_getsockopt, - tcp_poll, - tcp_shutdown, -}; - -// Match an incoming segment to a socket: exact 4-tuple first, then a LISTEN -// socket on the local port. Caller holds g_tcp_sock_lock, the result is ref'd. -static tcp_socket* tcp_lookup(uint32_t src_ip, uint16_t src_port, - uint32_t dst_ip, uint16_t dst_port) { - tcp_socket* listen_match = nullptr; - - for (tcp_socket* s = g_tcp_sock_list; s; s = s->next) { - if (s->local_port != dst_port) continue; - - if (s->local_addr != 0 && s->local_addr != dst_ip) continue; - - // Exact 4-tuple match (established connections) takes priority - if (s->remote_port == src_port && s->remote_addr == src_ip) { - if (s->try_add_ref()) { - if (listen_match) tcp_sock_release(listen_match); - return s; - } - - return listen_match; - } - - if (s->state == tcp_state::LISTEN && !listen_match) { - if (s->try_add_ref()) { - listen_match = s; - } - } - } - - return listen_match; -} - -// Handle a SYN arriving on a LISTEN socket (RFC 9293 Section 3.10.7.2) -static void tcp_listen_recv_syn(tcp_socket* listener, - uint32_t src_ip, uint16_t src_port, - uint32_t dst_ip, uint16_t dst_port, - uint32_t their_seq) { - sync::irq_state irq = sync::spin_lock_irqsave(listener->lock); - if (listener->state != tcp_state::LISTEN) { - sync::spin_unlock_irqrestore(listener->lock, irq); - return; - } - - if (listener->pending_count >= listener->backlog) { - sync::spin_unlock_irqrestore(listener->lock, irq); - return; - } - - listener->pending_count++; - sync::spin_unlock_irqrestore(listener->lock, irq); - - // Create child socket for this connection - auto* child = heap::kalloc_new(); - if (!child) { - sync::irq_state irq2 = sync::spin_lock_irqsave(listener->lock); - listener->pending_count--; - sync::spin_unlock_irqrestore(listener->lock, irq2); - return; - } - - uint32_t our_isn = tcp_generate_isn(); - - child->state = tcp_state::SYN_RECEIVED; - child->local_addr = dst_ip; - child->local_port = dst_port; - child->remote_addr = src_ip; - child->remote_port = src_port; - child->rcv_nxt = their_seq + 1; - child->snd_wnd = 0; - child->snd_una = our_isn; - child->snd_nxt = our_isn + 1; - child->rx_buf = nullptr; - child->rx_wq.init(); - child->parent = nullptr; - child->backlog = 0; - child->pending_count = 0; - child->accept_queue.init(); - child->accept_wq.init(); - child->so_options = 0; - child->shut_rd = false; - child->shut_wr = false; - child->lock = sync::SPINLOCK_INIT; - child->next = nullptr; - - bool registered = false; - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_tcp_sock_lock); - - child->parent = listener; - if (listener->state == tcp_state::LISTEN) { - child->next = g_tcp_sock_list; - g_tcp_sock_list = child; - registered = true; - } - }); - - if (!registered) { - sync::irq_state irq2 = sync::spin_lock_irqsave(listener->lock); - listener->pending_count--; - sync::spin_unlock_irqrestore(listener->lock, irq2); - tcp_sock_release(child); - return; - } - - // Send SYN-ACK - tcp_send_segment(dst_ip, dst_port, src_ip, src_port, - our_isn, child->rcv_nxt, - TCP_SYN | TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); -} - -// Handle an ACK arriving on a SYN_RECEIVED socket (completes handshake) -static void tcp_synrcvd_recv_ack(tcp_socket* sock, uint32_t ack_num, - uint16_t peer_wnd) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - if (ack_num != sock->snd_nxt) { - sync::spin_unlock_irqrestore(sock->lock, irq); - return; - } - - sock->snd_una = ack_num; - sock->snd_wnd = peer_wnd; - sock->state = tcp_state::ESTABLISHED; - - // Allocate receive buffer for data transfer - if (!sock->rx_buf) { - sock->rx_buf = ring_buffer_create(TCP_RX_BUF_CAPACITY); - } - - sync::spin_unlock_irqrestore(sock->lock, irq); - - auto* obj = heap::kalloc_new(); - if (!obj) { - tcp_send_segment(sock->local_addr, sock->local_port, - sock->remote_addr, sock->remote_port, - sock->snd_nxt, 0, TCP_RST, 0, nullptr, 0); - tcp_destroy_socket(sock); - return; - } - - obj->type = resource::resource_type::SOCKET; - obj->ops = &g_tcp_ops; - obj->impl = sock; - - auto* pc = static_cast( - heap::kzalloc(sizeof(tcp_pending_conn))); - if (!pc) { - obj->impl = nullptr; - heap::kfree_delete(obj); - tcp_send_segment(sock->local_addr, sock->local_port, - sock->remote_addr, sock->remote_port, - sock->snd_nxt, 0, TCP_RST, 0, nullptr, 0); - tcp_destroy_socket(sock); - return; - } - - pc->conn_obj = obj; - - bool enqueued = false; - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_tcp_sock_lock); - - tcp_socket* parent = sock->parent; - if (parent && parent->state == tcp_state::LISTEN) { - parent->accept_queue.push_back(pc); - enqueued = true; - sync::wake_one(parent->accept_wq); - } - }); - - if (!enqueued) { - obj->impl = nullptr; - heap::kfree_delete(obj); - heap::kfree(pc); - tcp_send_segment(sock->local_addr, sock->local_port, - sock->remote_addr, sock->remote_port, - sock->snd_nxt, 0, TCP_RST, 0, nullptr, 0); - tcp_destroy_socket(sock); - } -} - -__PRIVILEGED_CODE void tcp_socket::ref_destroy(tcp_socket* self) { - if (!self) return; - - if (self->rx_buf) { - ring_buffer_destroy(self->rx_buf); - self->rx_buf = nullptr; - } - heap::kfree_delete(self); -} - -bool tcp_try_register(tcp_socket* sock) { - if (!sock || sock->local_port == 0) { - return false; - } - - bool reuse = (sock->so_options & static_cast(SO_REUSEADDR)) != 0; - bool registered = false; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_tcp_sock_lock); - - bool conflict = false; - for (tcp_socket* s = g_tcp_sock_list; s; s = s->next) { - if (s == sock) continue; - - if (s->local_port != sock->local_port) continue; - - if (sock->local_addr != 0 && s->local_addr != 0 - && s->local_addr != sock->local_addr) continue; - - if (reuse && (s->state == tcp_state::TIME_WAIT - || s->state == tcp_state::CLOSED)) { - continue; - } - - conflict = true; - break; - } - - if (!conflict) { - sock->next = g_tcp_sock_list; - g_tcp_sock_list = sock; - registered = true; - } - }); - - return registered; -} - -bool tcp_unregister_socket(tcp_socket* sock) { - if (!sock) return false; - - bool found = false; - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_tcp_sock_lock); - - tcp_socket** pp = &g_tcp_sock_list; - while (*pp) { - if (*pp == sock) { - *pp = sock->next; - sock->next = nullptr; - found = true; - break; - } - pp = &(*pp)->next; - } - }); - - return found; -} - -int32_t create_tcp_socket(resource::resource_object** out) { - if (!out) { - return resource::ERR_INVAL; - } - - auto* sock = heap::kalloc_new(); - if (!sock) { - return resource::ERR_NOMEM; - } - - sock->state = tcp_state::CLOSED; - sock->local_addr = 0; - sock->local_port = 0; - sock->remote_addr = 0; - sock->remote_port = 0; - sock->snd_una = 0; - sock->snd_nxt = 0; - sock->rcv_nxt = 0; - sock->snd_wnd = 0; - sock->rx_buf = nullptr; - sock->rx_wq.init(); - sock->parent = nullptr; - sock->backlog = 0; - sock->pending_count = 0; - sock->accept_queue.init(); - sock->accept_wq.init(); - sock->so_options = 0; - sock->shut_rd = false; - sock->shut_wr = false; - sock->lock = sync::SPINLOCK_INIT; - sock->next = nullptr; - - auto* obj = heap::kalloc_new(); - if (!obj) { - heap::kfree_delete(sock); - return resource::ERR_NOMEM; - } - - obj->type = resource::resource_type::SOCKET; - obj->ops = &g_tcp_ops; - obj->impl = sock; - - *out = obj; - return resource::OK; -} - -void tcp_recv(netif* iface, uint32_t src_ip, uint32_t dst_ip, - const uint8_t* data, size_t len) { - if (!iface || !data || len < sizeof(tcp_header)) { - return; - } - - const auto* hdr = reinterpret_cast(data); - - size_t hdr_len = tcp_header_len(hdr); - if (hdr_len < sizeof(tcp_header) || hdr_len > len) { - return; - } - - // Verify checksum over pseudo-header + full TCP segment - uint16_t computed = tcp_checksum(htonl(src_ip), htonl(dst_ip), data, len); - if (computed != 0) { - log::debug("tcp: bad checksum, dropping"); - return; - } - - uint16_t src_port = ntohs(hdr->src_port); - uint16_t dst_port = ntohs(hdr->dst_port); - uint32_t seq = ntohl(hdr->seq); - uint32_t ack_num = ntohl(hdr->ack); - uint8_t flags = hdr->flags; - size_t payload_len = len - hdr_len; - - // Look up matching socket - tcp_socket* sock = nullptr; - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_tcp_sock_lock); - sock = tcp_lookup(src_ip, src_port, dst_ip, dst_port); - }); - - if (!sock) { - return; - } - - // State machine dispatch - switch (sock->state) { - case tcp_state::LISTEN: - if (flags & TCP_SYN) { - tcp_listen_recv_syn(sock, src_ip, src_port, dst_ip, dst_port, seq); - } - break; - - case tcp_state::SYN_RECEIVED: - if (flags & TCP_ACK) { - tcp_synrcvd_recv_ack(sock, ack_num, ntohs(hdr->window)); - } - break; - - case tcp_state::SYN_SENT: { - // RFC 9293 Section 3.10.7.3 - SYN_SENT state processing (active open) - - // First, check ACK - bool ack_acceptable = false; - if (flags & TCP_ACK) { - // Unacceptable if SEG.ACK <= ISS (snd_una) or SEG.ACK > snd_nxt - if (ack_num <= sock->snd_una || ack_num > sock->snd_nxt) { - break; // drop segment - } - ack_acceptable = true; - } - - // Second, check RST - if (flags & TCP_RST) { - if (ack_acceptable) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - sock->state = tcp_state::CLOSED; - sync::spin_unlock_irqrestore(sock->lock, irq); - sync::wake_one(sock->accept_wq); - } - break; - } - - // Fourth, check SYN (this is the SYN-ACK we expect) - if (flags & TCP_SYN) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - - sock->rcv_nxt = seq + 1; - if (ack_acceptable) { - sock->snd_una = ack_num; - } - - // RFC: "If SND.UNA > ISS, our SYN has been ACKed" -> ESTABLISHED. - // With an acceptable ACK, snd_una was set to ack_num (= ISN+1), - // which is > ISN (= ISS). So ack_acceptable implies SYN ACKed. - if (ack_acceptable) { - sock->state = tcp_state::ESTABLISHED; - sock->snd_wnd = ntohs(hdr->window); - sock->rx_buf = ring_buffer_create(TCP_RX_BUF_CAPACITY); - - // Snapshot for send after unlock (avoid use-after-free) - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t s_nxt = sock->snd_nxt; - uint32_t r_nxt = sock->rcv_nxt; - sync::spin_unlock_irqrestore(sock->lock, irq); - - // Send ACK via deferred TX (RX context) - tcp_send_segment(l_addr, l_port, r_addr, r_port, - s_nxt, r_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - - sync::wake_one(sock->accept_wq); - } else { - // Simultaneous open (SYN without acceptable ACK) - sync::spin_unlock_irqrestore(sock->lock, irq); - } - } - break; - } - - case tcp_state::ESTABLISHED: { - // RFC 9293 Section 3.10.7.4 - ESTABLISHED state processing - - // ACK processing - if (flags & TCP_ACK) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (ack_num > sock->snd_nxt) { - sync::spin_unlock_irqrestore(sock->lock, irq); - break; - } - - if (ack_num > sock->snd_una && ack_num <= sock->snd_nxt) { - sock->snd_una = ack_num; - } - sock->snd_wnd = ntohs(hdr->window); - sync::spin_unlock_irqrestore(sock->lock, irq); - } - - // Process segment text (data) - if (payload_len > 0 && sock->rx_buf) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (sock->state != tcp_state::ESTABLISHED) { - sync::spin_unlock_irqrestore(sock->lock, irq); - break; - } - - if (seq != sock->rcv_nxt) { - sync::spin_unlock_irqrestore(sock->lock, irq); - break; - } - - // All-or-nothing: accept the full segment or none of it. - // Partial writes would desync rcv_nxt with the segment boundary. - ssize_t written = ring_buffer_write_all( - sock->rx_buf, - data + hdr_len, - payload_len, true); - - if (written > 0) { - sock->rcv_nxt += static_cast(written); - } - - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t s_nxt = sock->snd_nxt; - uint32_t r_nxt = sock->rcv_nxt; - sync::spin_unlock_irqrestore(sock->lock, irq); - - tcp_send_segment(l_addr, l_port, r_addr, r_port, - s_nxt, r_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - } - - // FIN processing - if (flags & TCP_FIN) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (sock->state != tcp_state::ESTABLISHED) { - sync::spin_unlock_irqrestore(sock->lock, irq); - break; - } - - if (seq + static_cast(payload_len) != sock->rcv_nxt) { - sync::spin_unlock_irqrestore(sock->lock, irq); - break; - } - - sock->rcv_nxt++; - sock->state = tcp_state::CLOSE_WAIT; - - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t s_nxt = sock->snd_nxt; - uint32_t r_nxt = sock->rcv_nxt; - sync::spin_unlock_irqrestore(sock->lock, irq); - - tcp_send_segment(l_addr, l_port, r_addr, r_port, - s_nxt, r_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - - if (sock->rx_buf) { - ring_buffer_close_write(sock->rx_buf); - } - } - break; - } - - case tcp_state::FIN_WAIT_1: { - // We sent FIN, waiting for peer's ACK and/or FIN - - // Peer can still send data per RFC 9293 section 3.6 - if (payload_len > 0 && sock->rx_buf) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (seq == sock->rcv_nxt) { - ssize_t written = ring_buffer_write_all( - sock->rx_buf, data + hdr_len, payload_len, true); - if (written > 0) { - sock->rcv_nxt += static_cast(written); - } - - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t s_nxt = sock->snd_nxt; - uint32_t r_nxt = sock->rcv_nxt; - sync::spin_unlock_irqrestore(sock->lock, irq); - - tcp_send_segment(l_addr, l_port, r_addr, r_port, - s_nxt, r_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - } else { - sync::spin_unlock_irqrestore(sock->lock, irq); - } - } - - // ACK processing - bool fw1_fin_acked = false; - if (flags & TCP_ACK) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (ack_num > sock->snd_una && ack_num <= sock->snd_nxt) { - sock->snd_una = ack_num; - } - fw1_fin_acked = (sock->snd_una == sock->snd_nxt); - - if (fw1_fin_acked && !(flags & TCP_FIN)) { - sock->state = tcp_state::FIN_WAIT_2; - } - sync::spin_unlock_irqrestore(sock->lock, irq); - } - - // FIN processing - if (flags & TCP_FIN) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (seq + static_cast(payload_len) != sock->rcv_nxt) { - sync::spin_unlock_irqrestore(sock->lock, irq); - break; - } - - sock->rcv_nxt++; - fw1_fin_acked = (sock->snd_una == sock->snd_nxt); - - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t s_nxt = sock->snd_nxt; - uint32_t r_nxt = sock->rcv_nxt; - - if (!fw1_fin_acked) { - sock->state = tcp_state::CLOSING; - } - sync::spin_unlock_irqrestore(sock->lock, irq); - - tcp_send_segment(l_addr, l_port, r_addr, r_port, - s_nxt, r_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - - if (sock->rx_buf) { - ring_buffer_close_write(sock->rx_buf); - } - - if (fw1_fin_acked) { - tcp_destroy_socket(sock); - } - } - break; - } - - case tcp_state::FIN_WAIT_2: { - // Our FIN ACKed, waiting for peer's FIN - - // Can still receive data per RFC - if (payload_len > 0 && sock->rx_buf) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (seq == sock->rcv_nxt) { - ssize_t written = ring_buffer_write_all( - sock->rx_buf, data + hdr_len, payload_len, true); - if (written > 0) { - sock->rcv_nxt += static_cast(written); - } - - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t s_nxt = sock->snd_nxt; - uint32_t r_nxt = sock->rcv_nxt; - sync::spin_unlock_irqrestore(sock->lock, irq); - - tcp_send_segment(l_addr, l_port, r_addr, r_port, - s_nxt, r_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - } else { - sync::spin_unlock_irqrestore(sock->lock, irq); - } - } - - // FIN processing - if (flags & TCP_FIN) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (seq + static_cast(payload_len) != sock->rcv_nxt) { - sync::spin_unlock_irqrestore(sock->lock, irq); - break; - } - - sock->rcv_nxt++; - - uint32_t l_addr = sock->local_addr; - uint16_t l_port = sock->local_port; - uint32_t r_addr = sock->remote_addr; - uint16_t r_port = sock->remote_port; - uint32_t s_nxt = sock->snd_nxt; - uint32_t r_nxt = sock->rcv_nxt; - sync::spin_unlock_irqrestore(sock->lock, irq); - - tcp_send_segment(l_addr, l_port, r_addr, r_port, - s_nxt, r_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - - if (sock->rx_buf) { - ring_buffer_close_write(sock->rx_buf); - } - - tcp_destroy_socket(sock); - } - break; - } - - case tcp_state::CLOSE_WAIT: - if (flags & TCP_ACK) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (ack_num > sock->snd_una && ack_num <= sock->snd_nxt) { - sock->snd_una = ack_num; - } - sock->snd_wnd = ntohs(hdr->window); - sync::spin_unlock_irqrestore(sock->lock, irq); - } - break; - - case tcp_state::LAST_ACK: - // We sent FIN after CLOSE_WAIT, waiting for peer's ACK - if (flags & TCP_ACK) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (ack_num > sock->snd_una && ack_num <= sock->snd_nxt) { - sock->snd_una = ack_num; - } - bool fin_acked = (sock->snd_una == sock->snd_nxt); - sync::spin_unlock_irqrestore(sock->lock, irq); - - if (fin_acked) { - tcp_destroy_socket(sock); - } - } - break; - - case tcp_state::CLOSING: - // Simultaneous close: we sent FIN, peer sent FIN, waiting for ACK of our FIN - if (flags & TCP_ACK) { - sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); - if (ack_num > sock->snd_una && ack_num <= sock->snd_nxt) { - sock->snd_una = ack_num; - } - bool fin_acked = (sock->snd_una == sock->snd_nxt); - sync::spin_unlock_irqrestore(sock->lock, irq); - - if (fin_acked) { - tcp_destroy_socket(sock); - } - } - break; - - case tcp_state::TIME_WAIT: - // Retransmitted FIN from peer: re-ACK it - if (flags & TCP_FIN) { - tcp_send_segment( - sock->local_addr, sock->local_port, - sock->remote_addr, sock->remote_port, - sock->snd_nxt, sock->rcv_nxt, - TCP_ACK, TCP_DEFAULT_WINDOW, - nullptr, 0); - } - break; - - default: - break; - } - - tcp_sock_release(sock); -} - -} // namespace net diff --git a/kernel/net/tcp.h b/kernel/net/tcp.h deleted file mode 100644 index fff591bc..00000000 --- a/kernel/net/tcp.h +++ /dev/null @@ -1,137 +0,0 @@ -#ifndef STELLUX_NET_TCP_H -#define STELLUX_NET_TCP_H - -#include "net/net.h" -#include "resource/resource.h" -#include "rc/ref_counted.h" -#include "sync/spinlock.h" -#include "sync/wait_queue.h" -#include "common/list.h" - -struct ring_buffer; - -namespace net { - -// TCP header flags (RFC 9293 Section 3.1) -constexpr uint8_t TCP_FIN = 0x01; // no more data from sender -constexpr uint8_t TCP_SYN = 0x02; // synchronize sequence numbers -constexpr uint8_t TCP_RST = 0x04; // reset the connection -constexpr uint8_t TCP_PSH = 0x08; // push buffered data to receiver -constexpr uint8_t TCP_ACK = 0x10; // acknowledgment field is valid -constexpr uint8_t TCP_URG = 0x20; // urgent pointer field is valid - -// TCP header (RFC 9293 Section 3.1), minimum 20 bytes. -// All multi-byte fields are in network byte order on the wire. -struct tcp_header { - uint16_t src_port; // source port - uint16_t dst_port; // destination port - uint32_t seq; // sequence number - uint32_t ack; // acknowledgment number - uint8_t data_off; // upper 4 bits: data offset (header length in 32-bit words) - uint8_t flags; // lower 6 bits: URG|ACK|PSH|RST|SYN|FIN - uint16_t window; // receive window size - uint16_t checksum; // checksum (pseudo-header + header + data) - uint16_t urgent_ptr; // urgent pointer (only valid if URG flag set) -} __attribute__((packed)); - -static_assert(sizeof(tcp_header) == 20, "tcp_header must be 20 bytes"); - -// Extract the header length in bytes from the data_off field. -// The upper 4 bits encode the number of 32-bit words in the header. -inline constexpr size_t tcp_header_len(const tcp_header* hdr) { - return static_cast((hdr->data_off >> 4) & 0xF) * 4; -} - -// TCP connection states (RFC 9293 Section 3.3.2) -enum class tcp_state : uint8_t { - CLOSED, - LISTEN, - SYN_SENT, - SYN_RECEIVED, - ESTABLISHED, - FIN_WAIT_1, - FIN_WAIT_2, - CLOSE_WAIT, - LAST_ACK, - TIME_WAIT, - CLOSING, -}; - -// Entry in a LISTEN socket's accept queue. -struct tcp_pending_conn { - list::node link; - resource::resource_object* conn_obj; -}; - -// TCP socket: one per connection (or one per listening port) -struct tcp_socket : rc::ref_counted { - tcp_state state; - - // Local endpoint (set by bind or connect) - uint32_t local_addr; // host byte order, 0 = any - uint16_t local_port; // host byte order, 0 = unbound - - // Remote endpoint (set by connect or accept, 0 for LISTEN sockets) - uint32_t remote_addr; // host byte order - uint16_t remote_port; // host byte order - - // Sequence number tracking (RFC 9293 Section 3.3.1) - uint32_t snd_una; // oldest unacknowledged seq (Send Unacknowledged) - uint32_t snd_nxt; // next seq we will send (Send Next) - uint32_t rcv_nxt; // next seq we expect to receive (Receive Next) - uint16_t snd_wnd; // peer's advertised receive window - - // ESTABLISHED state: receive buffer for incoming data - ring_buffer* rx_buf; - sync::wait_queue rx_wq; - - // LISTEN state: accept queue for completed connections - tcp_socket* parent; // backpointer to LISTEN socket (for child sockets) - uint32_t backlog; // max pending connections - uint32_t pending_count; // current pending connections - list::head accept_queue; - sync::wait_queue accept_wq; - - uint32_t so_options; // bitmask of socket options - bool shut_rd; // read side shutdown (SHUT_RD) - bool shut_wr; // write side shutdown (SHUT_WR / FIN sent) - sync::spinlock lock; - tcp_socket* next; // linked list for port registry - - /** - * @note Privilege: **required** - */ - __PRIVILEGED_CODE static void ref_destroy(tcp_socket* self); -}; - -/** - * Create an AF_INET SOCK_STREAM socket in CLOSED state. - */ -int32_t create_tcp_socket(resource::resource_object** out); - -/** - * Atomically check for binding conflicts and register if none found. - * @return true if registered, false if conflict. - */ -bool tcp_try_register(tcp_socket* sock); - -/** - * Unregister a TCP socket from the port registry. - * @return true if the socket was found and removed, false if not found. - */ -bool tcp_unregister_socket(tcp_socket* sock); - -/** - * Process a received TCP segment (after IPv4 header is stripped). - * Validates header, verifies checksum, and dispatches to connection state. - * @param src_ip Source IP in host byte order. - * @param dst_ip Destination IP in host byte order. - */ -void tcp_recv(netif* iface, uint32_t src_ip, uint32_t dst_ip, - const uint8_t* data, size_t len); - -extern const resource::resource_ops g_tcp_ops; - -} // namespace net - -#endif // STELLUX_NET_TCP_H diff --git a/kernel/net/udp.cpp b/kernel/net/udp.cpp deleted file mode 100644 index 9d1d7407..00000000 --- a/kernel/net/udp.cpp +++ /dev/null @@ -1,164 +0,0 @@ -#include "net/udp.h" -#include "net/inet_socket.h" -#include "net/dhcp.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "net/net.h" -#include "common/string.h" -#include "common/ring_buffer.h" -#include "mm/heap.h" -#include "sync/atomic.h" -#include "sync/spinlock.h" -#include "dynpriv/dynpriv.h" -#include "common/logging.h" - -namespace net { - -static inet_socket* g_udp_sock_list = nullptr; -static sync::spinlock g_udp_sock_lock = sync::SPINLOCK_INIT; -static sync::atomic g_ephemeral_next{UDP_PORT_EPHEMERAL_MIN}; - -// Ring buffer entry framing: [src_ip(4, net)] [src_port(2, net)] [payload_len(2, host)] [data(N)] -constexpr size_t RX_ENTRY_HEADER = 8; - -void udp_recv(netif* iface, uint32_t src_ip, uint32_t dst_ip, - const uint8_t* data, size_t len) { - if (!iface || !data || len < sizeof(udp_header)) { - return; - } - - const auto* hdr = reinterpret_cast(data); - - uint16_t udp_len = ntohs(hdr->length); - if (udp_len < sizeof(udp_header) || udp_len > len) { - return; - } - - // Verify checksum if present (checksum == 0 means "not computed" per RFC 768) - if (hdr->checksum != 0) { - uint16_t computed = udp_checksum(htonl(src_ip), htonl(dst_ip), data, udp_len); - if (computed != 0) { - log::debug("udp: bad checksum, dropping"); - return; - } - } - - uint16_t dst_port_net = hdr->dst_port; - const uint8_t* payload = data + sizeof(udp_header); - size_t payload_len = udp_len - sizeof(udp_header); - - // Port-68 packets also feed the DHCP client through a static buffer, - // so DHCP works even when no socket is registered on the port. - if (ntohs(dst_port_net) == DHCP_CLIENT_PORT && payload_len > 0) { - dhcp_rx_hook(payload, payload_len); - } - - // Build the framed ring buffer entry before taking the lock. - // This keeps heap alloc/free outside the IRQ-disabled critical section. - uint32_t src_ip_net = htonl(src_ip); - uint16_t src_port_net = hdr->src_port; - uint16_t plen = static_cast(payload_len); - - size_t entry_len = RX_ENTRY_HEADER + payload_len; - auto* entry = static_cast(heap::uzalloc(entry_len)); - if (!entry) return; - - string::memcpy(entry, &src_ip_net, 4); - string::memcpy(entry + 4, &src_port_net, 2); - string::memcpy(entry + 6, &plen, 2); - if (payload && payload_len > 0) { - string::memcpy(entry + RX_ENTRY_HEADER, payload, payload_len); - } - - // Lock only for socket lookup + ring buffer write - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_udp_sock_lock); - - for (inet_socket* s = g_udp_sock_list; s; s = s->next) { - if (htons(s->bound_port) == dst_port_net - && (s->bound_addr == 0 || s->bound_addr == dst_ip) - && s->rx_buf) { - (void)ring_buffer_write_all(s->rx_buf, entry, entry_len, true); - break; - } - } - }); - - heap::ufree(entry); -} - -void udp_register_socket(inet_socket* sock) { - if (!sock) return; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_udp_sock_lock); - - sock->next = g_udp_sock_list; - g_udp_sock_list = sock; - }); -} - -void udp_unregister_socket(inet_socket* sock) { - if (!sock) return; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_udp_sock_lock); - - inet_socket** pp = &g_udp_sock_list; - while (*pp) { - if (*pp == sock) { - *pp = sock->next; - sock->next = nullptr; - break; - } - pp = &(*pp)->next; - } - }); -} - -bool udp_try_register(inet_socket* sock) { - if (!sock || sock->bound_port == 0) { - return false; - } - - bool reuse = (sock->so_options & static_cast(SO_REUSEADDR)) != 0; - bool registered = false; - - RUN_ELEVATED({ - sync::irq_lock_guard guard(g_udp_sock_lock); - - bool conflict = false; - for (inet_socket* s = g_udp_sock_list; s; s = s->next) { - if (s == sock) continue; - - if (s->bound_port != sock->bound_port) continue; - - if (sock->bound_addr != 0 && s->bound_addr != 0 - && s->bound_addr != sock->bound_addr) continue; - - if (reuse && (s->so_options & static_cast(SO_REUSEADDR))) { - continue; - } - - conflict = true; - break; - } - - if (!conflict) { - sock->next = g_udp_sock_list; - g_udp_sock_list = sock; - registered = true; - } - }); - - return registered; -} - -uint16_t udp_alloc_ephemeral_port() { - uint32_t port = g_ephemeral_next.fetch_add_relaxed(1); - uint32_t range = UDP_PORT_EPHEMERAL_MAX - UDP_PORT_EPHEMERAL_MIN + 1; - return static_cast( - UDP_PORT_EPHEMERAL_MIN + (port - UDP_PORT_EPHEMERAL_MIN) % range); -} - -} // namespace net diff --git a/kernel/net/udp.h b/kernel/net/udp.h deleted file mode 100644 index 6c29376f..00000000 --- a/kernel/net/udp.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef STELLUX_NET_UDP_H -#define STELLUX_NET_UDP_H - -#include "common/types.h" -#include "net/net.h" - -namespace net { - -constexpr uint16_t UDP_PORT_EPHEMERAL_MIN = 49152; -constexpr uint16_t UDP_PORT_EPHEMERAL_MAX = 65535; - -struct udp_header { - uint16_t src_port; // network byte order - uint16_t dst_port; // network byte order - uint16_t length; // network byte order (header + payload) - uint16_t checksum; // network byte order (0 = not computed) -} __attribute__((packed)); - -static_assert(sizeof(udp_header) == 8, "udp_header must be 8 bytes"); - -struct inet_socket; - -/** - * Process a received UDP packet (after IPv4 header is stripped). - * Validates header, optionally verifies checksum, and delivers - * the payload to the matching socket by destination port. - * @param src_ip Source IP in host byte order. - * @param dst_ip Destination IP in host byte order (for checksum verification). - */ -void udp_recv(netif* iface, uint32_t src_ip, uint32_t dst_ip, - const uint8_t* data, size_t len); - -/** - * Register an inet socket to receive UDP packets on its bound_port. - * Called when the socket is first assigned a port. - */ -void udp_register_socket(inet_socket* sock); - -/** - * Unregister an inet socket from UDP delivery. - * Called during socket close. - */ -void udp_unregister_socket(inet_socket* sock); - -/** - * Atomically check for binding conflicts and register if none found. - * Uses POSIX overlap rule: conflict if ports match AND addresses overlap - * (a == 0 || b == 0 || a == b). All values in host byte order. - * The caller must have set sock->bound_port and sock->bound_addr before calling. - * @return true if registered successfully, false if a conflicting binding exists. - */ -bool udp_try_register(inet_socket* sock); - -/** - * Allocate an ephemeral port number (49152-65535). - * Thread-safe via atomic counter. - * @return Port number in host byte order. - */ -uint16_t udp_alloc_ephemeral_port(); - -} // namespace net - -#endif // STELLUX_NET_UDP_H diff --git a/kernel/syscall/handlers/sys_shutdown.cpp b/kernel/syscall/handlers/sys_shutdown.cpp index 549adfe2..989e6ce1 100644 --- a/kernel/syscall/handlers/sys_shutdown.cpp +++ b/kernel/syscall/handlers/sys_shutdown.cpp @@ -1,17 +1,19 @@ #include "syscall/handlers/sys_shutdown.h" #include "resource/resource.h" -#include "net/net.h" #include "sched/sched.h" #include "sched/task.h" +constexpr int32_t SHUT_RD = 0; +constexpr int32_t SHUT_WR = 1; +constexpr int32_t SHUT_RDWR = 2; + DEFINE_SYSCALL2(shutdown, fd, how) { sched::task* task = sched::current(); if (!task) return syscall::EIO; int32_t how_val = static_cast(how); - if (how_val != net::SHUT_RD && how_val != net::SHUT_WR && - how_val != net::SHUT_RDWR) { + if (how_val != SHUT_RD && how_val != SHUT_WR && how_val != SHUT_RDWR) { return syscall::EINVAL; } diff --git a/kernel/syscall/handlers/sys_sockaddr.cpp b/kernel/syscall/handlers/sys_sockaddr.cpp index c7b2e3b9..19939859 100644 --- a/kernel/syscall/handlers/sys_sockaddr.cpp +++ b/kernel/syscall/handlers/sys_sockaddr.cpp @@ -1,116 +1,38 @@ #include "syscall/handlers/sys_sockaddr.h" #include "resource/resource.h" -#include "net/tcp.h" -#include "net/inet_socket.h" -#include "net/byteorder.h" -#include "mm/uaccess.h" #include "sched/sched.h" #include "sched/task.h" -static void fill_inet_addr( - net::kernel_sockaddr_in* out, uint32_t host_ip, uint16_t host_port -) { - out->sin_family = net::AF_INET_VAL; - out->sin_port = net::htons(host_port); - out->sin_addr = net::htonl(host_ip); -} - -DEFINE_SYSCALL3(getsockname, fd, u_addr, u_addrlen) { - if (u_addr == 0 || u_addrlen == 0) return syscall::EFAULT; +// No socket family reports addresses yet, so both calls validate the +// descriptor and decline. A transport that carries addresses fills them in. +static int64_t reject_socket_address_query(uint64_t fd, uint64_t u_addr, uint64_t u_addrlen) { + if (u_addr == 0 || u_addrlen == 0) { + return syscall::EFAULT; + } sched::task* task = sched::current(); - if (!task) return syscall::EIO; + if (!task) { + return syscall::EIO; + } resource::resource_object* obj = nullptr; int32_t rc = resource::get_handle_object( task->handles, static_cast(fd), 0, &obj); - if (rc != resource::HANDLE_OK) return syscall::EBADF; - - if (obj->type != resource::resource_type::SOCKET) { - resource::resource_release(obj); - return syscall::ENOTSOCK; - } - - if (obj->ops != &net::g_tcp_ops || !obj->impl) { - resource::resource_release(obj); - return syscall::EOPNOTSUPP; + if (rc != resource::HANDLE_OK) { + return syscall::EBADF; } - auto* sock = static_cast(obj->impl); - net::kernel_sockaddr_in kaddr = {}; - fill_inet_addr(&kaddr, sock->local_addr, sock->local_port); + bool is_socket = obj->type == resource::resource_type::SOCKET; resource::resource_release(obj); - uint32_t user_len = 0; - int32_t copy_rc = mm::uaccess::copy_from_user( - &user_len, reinterpret_cast(u_addrlen), sizeof(user_len)); - if (copy_rc != mm::uaccess::OK) return syscall::EFAULT; - - uint32_t copy_len = user_len < sizeof(kaddr) - ? user_len : static_cast(sizeof(kaddr)); - if (copy_len > 0) { - copy_rc = mm::uaccess::copy_to_user( - reinterpret_cast(u_addr), &kaddr, copy_len); - if (copy_rc != mm::uaccess::OK) return syscall::EFAULT; - } - - uint32_t actual_len = static_cast(sizeof(kaddr)); - copy_rc = mm::uaccess::copy_to_user( - reinterpret_cast(u_addrlen), &actual_len, sizeof(actual_len)); - if (copy_rc != mm::uaccess::OK) return syscall::EFAULT; + return is_socket ? syscall::EOPNOTSUPP : syscall::ENOTSOCK; +} - return 0; +DEFINE_SYSCALL3(getsockname, fd, u_addr, u_addrlen) { + return reject_socket_address_query(fd, u_addr, u_addrlen); } DEFINE_SYSCALL3(getpeername, fd, u_addr, u_addrlen) { - if (u_addr == 0 || u_addrlen == 0) return syscall::EFAULT; - - sched::task* task = sched::current(); - if (!task) return syscall::EIO; - - resource::resource_object* obj = nullptr; - int32_t rc = resource::get_handle_object( - task->handles, static_cast(fd), 0, &obj); - if (rc != resource::HANDLE_OK) return syscall::EBADF; - - if (obj->type != resource::resource_type::SOCKET) { - resource::resource_release(obj); - return syscall::ENOTSOCK; - } - - if (obj->ops != &net::g_tcp_ops || !obj->impl) { - resource::resource_release(obj); - return syscall::EOPNOTSUPP; - } - - auto* sock = static_cast(obj->impl); - if (sock->remote_port == 0) { - resource::resource_release(obj); - return syscall::ENOTCONN; - } - - net::kernel_sockaddr_in kaddr = {}; - fill_inet_addr(&kaddr, sock->remote_addr, sock->remote_port); - resource::resource_release(obj); - - uint32_t user_len = 0; - int32_t copy_rc = mm::uaccess::copy_from_user( - &user_len, reinterpret_cast(u_addrlen), sizeof(user_len)); - if (copy_rc != mm::uaccess::OK) return syscall::EFAULT; - - uint32_t copy_len = user_len < sizeof(kaddr) - ? user_len : static_cast(sizeof(kaddr)); - if (copy_len > 0) { - copy_rc = mm::uaccess::copy_to_user( - reinterpret_cast(u_addr), &kaddr, copy_len); - if (copy_rc != mm::uaccess::OK) return syscall::EFAULT; - } - - uint32_t actual_len = static_cast(sizeof(kaddr)); - copy_rc = mm::uaccess::copy_to_user( - reinterpret_cast(u_addrlen), &actual_len, sizeof(actual_len)); - if (copy_rc != mm::uaccess::OK) return syscall::EFAULT; - - return 0; + return reject_socket_address_query(fd, u_addr, u_addrlen); } diff --git a/kernel/syscall/handlers/sys_socket.cpp b/kernel/syscall/handlers/sys_socket.cpp index f1bea8e7..6055c26e 100644 --- a/kernel/syscall/handlers/sys_socket.cpp +++ b/kernel/syscall/handlers/sys_socket.cpp @@ -1,8 +1,6 @@ #include "syscall/handlers/sys_socket.h" #include "socket/unix_socket.h" -#include "net/inet_socket.h" -#include "net/tcp.h" #include "resource/resource.h" #include "fs/fstypes.h" #include "sched/sched.h" @@ -11,12 +9,7 @@ #include "mm/heap.h" constexpr uint64_t AF_UNIX = 1; -constexpr uint64_t AF_INET = 2; constexpr uint64_t SOCK_STREAM = 1; -constexpr uint64_t SOCK_DGRAM = 2; -constexpr uint64_t IPPROTO_ICMP = 1; -constexpr uint64_t IPPROTO_TCP = 6; -constexpr uint64_t IPPROTO_UDP = 17; constexpr size_t SENDTO_MAX_ADDR = 128; constexpr size_t SENDTO_MAX_BUF = 4096; @@ -51,16 +44,6 @@ DEFINE_SYSCALL3(socket, domain, type, protocol) { } rc = socket::create_unbound_socket(&obj); - } else if (domain == AF_INET) { - if (type == SOCK_STREAM && (protocol == 0 || protocol == IPPROTO_TCP)) { - rc = net::create_tcp_socket(&obj); - } else if (type == SOCK_DGRAM && protocol == IPPROTO_ICMP) { - rc = net::create_inet_icmp_socket(&obj); - } else if (type == SOCK_DGRAM && (protocol == 0 || protocol == IPPROTO_UDP)) { - rc = net::create_inet_udp_socket(&obj); - } else { - return syscall::EPROTONOSUPPORT; - } } else { return syscall::EAFNOSUPPORT; } diff --git a/kernel/tests/net/dhcp.test.cpp b/kernel/tests/net/dhcp.test.cpp deleted file mode 100644 index 3fb8c286..00000000 --- a/kernel/tests/net/dhcp.test.cpp +++ /dev/null @@ -1,665 +0,0 @@ -#define STLX_TEST_TIER TIER_SCHED - -#include "stlx_unit_test.h" -#include "net/dhcp.h" -#include "net/ipv4.h" -#include "net/udp.h" -#include "net/ethernet.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "common/string.h" - -TEST_SUITE(dhcp_test); - -// Packet structure size - -TEST(dhcp_test, dhcp_packet_size) { - EXPECT_EQ(sizeof(net::dhcp_packet), static_cast(240)); -} - -// Build DISCOVER - -TEST(dhcp_test, build_discover_basic) { - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0xFF, sizeof(buf)); - - uint8_t mac[6] = {0x52, 0x54, 0x00, 0x12, 0x34, 0x56}; - uint32_t xid = net::htonl(0xDEADBEEF); - - size_t len = net::dhcp_build_discover(buf, sizeof(buf), mac, xid); - ASSERT_TRUE(len > sizeof(net::dhcp_packet)); - - auto* pkt = reinterpret_cast(buf); - - // Verify fixed header fields - EXPECT_EQ(pkt->op, net::DHCP_OP_BOOTREQUEST); - EXPECT_EQ(pkt->htype, net::DHCP_HTYPE_ETHERNET); - EXPECT_EQ(pkt->hlen, net::DHCP_HLEN_ETHERNET); - EXPECT_EQ(pkt->hops, static_cast(0)); - - // Transaction ID - EXPECT_EQ(pkt->xid, xid); - - // Flags: broadcast - EXPECT_EQ(net::ntohs(pkt->flags), net::DHCP_FLAG_BROADCAST); - - // All IP fields should be zero - EXPECT_EQ(pkt->ciaddr, static_cast(0)); - EXPECT_EQ(pkt->yiaddr, static_cast(0)); - EXPECT_EQ(pkt->siaddr, static_cast(0)); - EXPECT_EQ(pkt->giaddr, static_cast(0)); - - // MAC in chaddr - EXPECT_EQ(pkt->chaddr[0], static_cast(0x52)); - EXPECT_EQ(pkt->chaddr[1], static_cast(0x54)); - EXPECT_EQ(pkt->chaddr[2], static_cast(0x00)); - EXPECT_EQ(pkt->chaddr[3], static_cast(0x12)); - EXPECT_EQ(pkt->chaddr[4], static_cast(0x34)); - EXPECT_EQ(pkt->chaddr[5], static_cast(0x56)); - // Remaining chaddr bytes should be zero - EXPECT_EQ(pkt->chaddr[6], static_cast(0)); - EXPECT_EQ(pkt->chaddr[15], static_cast(0)); - - // DHCP magic cookie - EXPECT_EQ(net::ntohl(pkt->magic), net::DHCP_MAGIC_COOKIE); -} - -TEST(dhcp_test, build_discover_has_msg_type_option) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint8_t mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; - uint32_t xid = net::htonl(0x12345678); - - size_t len = net::dhcp_build_discover(buf, sizeof(buf), mac, xid); - ASSERT_TRUE(len > sizeof(net::dhcp_packet)); - - // Scan options for Message Type = DISCOVER - const uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t opts_len = len - sizeof(net::dhcp_packet); - bool found_msg_type = false; - - size_t pos = 0; - while (pos < opts_len) { - uint8_t code = opts[pos]; - if (code == net::DHCP_OPT_END) break; - if (code == net::DHCP_OPT_PAD) { pos++; continue; } - if (pos + 1 >= opts_len) break; - uint8_t opt_len = opts[pos + 1]; - if (pos + 2 + opt_len > opts_len) break; - - if (code == net::DHCP_OPT_MSG_TYPE && opt_len == 1) { - EXPECT_EQ(opts[pos + 2], net::DHCP_MSG_DISCOVER); - found_msg_type = true; - } - pos += 2 + opt_len; - } - - EXPECT_TRUE(found_msg_type); -} - -TEST(dhcp_test, build_discover_has_param_list) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint8_t mac[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55}; - uint32_t xid = net::htonl(0x00000001); - - size_t len = net::dhcp_build_discover(buf, sizeof(buf), mac, xid); - ASSERT_TRUE(len > sizeof(net::dhcp_packet)); - - const uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t opts_len = len - sizeof(net::dhcp_packet); - bool found_param_list = false; - - size_t pos = 0; - while (pos < opts_len) { - uint8_t code = opts[pos]; - if (code == net::DHCP_OPT_END) break; - if (code == net::DHCP_OPT_PAD) { pos++; continue; } - if (pos + 1 >= opts_len) break; - uint8_t opt_len = opts[pos + 1]; - if (pos + 2 + opt_len > opts_len) break; - - if (code == net::DHCP_OPT_PARAM_LIST) { - // Should request at least subnet mask, router, DNS - bool has_mask = false, has_router = false, has_dns = false; - for (uint8_t i = 0; i < opt_len; i++) { - if (opts[pos + 2 + i] == net::DHCP_OPT_SUBNET_MASK) has_mask = true; - if (opts[pos + 2 + i] == net::DHCP_OPT_ROUTER) has_router = true; - if (opts[pos + 2 + i] == net::DHCP_OPT_DNS) has_dns = true; - } - EXPECT_TRUE(has_mask); - EXPECT_TRUE(has_router); - EXPECT_TRUE(has_dns); - found_param_list = true; - } - pos += 2 + opt_len; - } - - EXPECT_TRUE(found_param_list); -} - -TEST(dhcp_test, build_discover_has_end_marker) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint8_t mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x01}; - uint32_t xid = net::htonl(0xCAFEBABE); - - size_t len = net::dhcp_build_discover(buf, sizeof(buf), mac, xid); - ASSERT_TRUE(len > sizeof(net::dhcp_packet)); - - // The last meaningful byte in the options should be END (255) - const uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t opts_len = len - sizeof(net::dhcp_packet); - bool found_end = false; - size_t pos = 0; - while (pos < opts_len) { - uint8_t code = opts[pos]; - if (code == net::DHCP_OPT_END) { found_end = true; break; } - if (code == net::DHCP_OPT_PAD) { pos++; continue; } - if (pos + 1 >= opts_len) break; - uint8_t opt_len = opts[pos + 1]; - pos += 2 + opt_len; - } - EXPECT_TRUE(found_end); -} - -TEST(dhcp_test, build_discover_buffer_too_small) { - uint8_t buf[16]; // Way too small - uint8_t mac[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; - uint32_t xid = net::htonl(1); - - size_t len = net::dhcp_build_discover(buf, sizeof(buf), mac, xid); - EXPECT_EQ(len, static_cast(0)); -} - -TEST(dhcp_test, build_discover_null_args) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint8_t mac[6] = {0}; - - EXPECT_EQ(net::dhcp_build_discover(nullptr, sizeof(buf), mac, 0), - static_cast(0)); - EXPECT_EQ(net::dhcp_build_discover(buf, sizeof(buf), nullptr, 0), - static_cast(0)); -} - -// Build REQUEST - -TEST(dhcp_test, build_request_basic) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint8_t mac[6] = {0x52, 0x54, 0x00, 0x12, 0x34, 0x56}; - uint32_t xid = net::htonl(0xDEADC0DE); - uint32_t offered_ip = net::ipv4_addr(10, 0, 2, 15); - uint32_t server_id = net::ipv4_addr(10, 0, 2, 2); - - size_t len = net::dhcp_build_request(buf, sizeof(buf), mac, xid, - offered_ip, server_id); - ASSERT_TRUE(len > sizeof(net::dhcp_packet)); - - auto* pkt = reinterpret_cast(buf); - EXPECT_EQ(pkt->op, net::DHCP_OP_BOOTREQUEST); - EXPECT_EQ(pkt->xid, xid); - EXPECT_EQ(net::ntohl(pkt->magic), net::DHCP_MAGIC_COOKIE); -} - -TEST(dhcp_test, build_request_has_correct_options) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint8_t mac[6] = {0x52, 0x54, 0x00, 0x12, 0x34, 0x56}; - uint32_t xid = net::htonl(0xAAAABBBB); - uint32_t offered_ip = net::ipv4_addr(192, 168, 1, 100); - uint32_t server_id = net::ipv4_addr(192, 168, 1, 1); - - size_t len = net::dhcp_build_request(buf, sizeof(buf), mac, xid, - offered_ip, server_id); - ASSERT_TRUE(len > sizeof(net::dhcp_packet)); - - const uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t opts_len = len - sizeof(net::dhcp_packet); - - bool found_msg_type = false; - bool found_requested_ip = false; - bool found_server_id = false; - - size_t pos = 0; - while (pos < opts_len) { - uint8_t code = opts[pos]; - if (code == net::DHCP_OPT_END) break; - if (code == net::DHCP_OPT_PAD) { pos++; continue; } - if (pos + 1 >= opts_len) break; - uint8_t opt_len = opts[pos + 1]; - if (pos + 2 + opt_len > opts_len) break; - - if (code == net::DHCP_OPT_MSG_TYPE && opt_len == 1) { - EXPECT_EQ(opts[pos + 2], net::DHCP_MSG_REQUEST); - found_msg_type = true; - } - - if (code == net::DHCP_OPT_REQUESTED_IP && opt_len == 4) { - uint32_t val; - string::memcpy(&val, opts + pos + 2, 4); - EXPECT_EQ(net::ntohl(val), offered_ip); - found_requested_ip = true; - } - - if (code == net::DHCP_OPT_SERVER_ID && opt_len == 4) { - uint32_t val; - string::memcpy(&val, opts + pos + 2, 4); - EXPECT_EQ(net::ntohl(val), server_id); - found_server_id = true; - } - - pos += 2 + opt_len; - } - - EXPECT_TRUE(found_msg_type); - EXPECT_TRUE(found_requested_ip); - EXPECT_TRUE(found_server_id); -} - -// Parse OFFER - -// Helper: build a hand-crafted DHCP OFFER packet for testing -static size_t build_test_offer(uint8_t* buf, size_t buf_size, uint32_t xid, - uint32_t yiaddr, uint32_t mask, uint32_t gw, - uint32_t dns, uint32_t server_id, - uint32_t lease_time) { - if (buf_size < sizeof(net::dhcp_packet) + 64) return 0; - - string::memset(buf, 0, buf_size); - auto* pkt = reinterpret_cast(buf); - - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->htype = net::DHCP_HTYPE_ETHERNET; - pkt->hlen = net::DHCP_HLEN_ETHERNET; - pkt->xid = xid; - pkt->yiaddr = net::htonl(yiaddr); - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t pos = 0; - - // Option 53: Message Type - opts[pos++] = net::DHCP_OPT_MSG_TYPE; - opts[pos++] = 1; - opts[pos++] = net::DHCP_MSG_OFFER; - - // Option 1: Subnet Mask - if (mask != 0) { - opts[pos++] = net::DHCP_OPT_SUBNET_MASK; - opts[pos++] = 4; - uint32_t v = net::htonl(mask); - string::memcpy(opts + pos, &v, 4); - pos += 4; - } - - // Option 3: Router - if (gw != 0) { - opts[pos++] = net::DHCP_OPT_ROUTER; - opts[pos++] = 4; - uint32_t v = net::htonl(gw); - string::memcpy(opts + pos, &v, 4); - pos += 4; - } - - // Option 6: DNS - if (dns != 0) { - opts[pos++] = net::DHCP_OPT_DNS; - opts[pos++] = 4; - uint32_t v = net::htonl(dns); - string::memcpy(opts + pos, &v, 4); - pos += 4; - } - - // Option 54: Server ID - if (server_id != 0) { - opts[pos++] = net::DHCP_OPT_SERVER_ID; - opts[pos++] = 4; - uint32_t v = net::htonl(server_id); - string::memcpy(opts + pos, &v, 4); - pos += 4; - } - - // Option 51: Lease Time - if (lease_time != 0) { - opts[pos++] = net::DHCP_OPT_LEASE_TIME; - opts[pos++] = 4; - uint32_t v = net::htonl(lease_time); - string::memcpy(opts + pos, &v, 4); - pos += 4; - } - - // Option 255: End - opts[pos++] = net::DHCP_OPT_END; - - return sizeof(net::dhcp_packet) + pos; -} - -TEST(dhcp_test, parse_offer_full) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint32_t xid = net::htonl(0x11223344); - uint32_t ip = net::ipv4_addr(10, 0, 2, 15); - uint32_t mask = net::ipv4_addr(255, 255, 255, 0); - uint32_t gw = net::ipv4_addr(10, 0, 2, 2); - uint32_t dns = net::ipv4_addr(10, 0, 2, 3); - uint32_t srv = net::ipv4_addr(10, 0, 2, 2); - - size_t len = build_test_offer(buf, sizeof(buf), xid, ip, mask, gw, dns, srv, 86400); - ASSERT_TRUE(len > sizeof(net::dhcp_packet)); - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response( - reinterpret_cast(buf), len, &cfg); - ASSERT_TRUE(ok); - EXPECT_TRUE(cfg.valid); - EXPECT_EQ(cfg.msg_type, net::DHCP_MSG_OFFER); - EXPECT_EQ(cfg.offered_ip, ip); - EXPECT_EQ(cfg.subnet_mask, mask); - EXPECT_EQ(cfg.gateway, gw); - EXPECT_EQ(cfg.dns_server, dns); - EXPECT_EQ(cfg.server_id, srv); - EXPECT_EQ(cfg.lease_time, static_cast(86400)); -} - -TEST(dhcp_test, parse_offer_minimal) { - // OFFER with only message type and yiaddr, no other options - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - auto* pkt = reinterpret_cast(buf); - - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->htype = net::DHCP_HTYPE_ETHERNET; - pkt->hlen = net::DHCP_HLEN_ETHERNET; - pkt->xid = net::htonl(0xAAAA); - pkt->yiaddr = net::htonl(net::ipv4_addr(192, 168, 0, 50)); - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - opts[0] = net::DHCP_OPT_MSG_TYPE; - opts[1] = 1; - opts[2] = net::DHCP_MSG_OFFER; - opts[3] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + 4, &cfg); - ASSERT_TRUE(ok); - EXPECT_EQ(cfg.msg_type, net::DHCP_MSG_OFFER); - EXPECT_EQ(cfg.offered_ip, net::ipv4_addr(192, 168, 0, 50)); - EXPECT_EQ(cfg.subnet_mask, static_cast(0)); - EXPECT_EQ(cfg.gateway, static_cast(0)); - EXPECT_EQ(cfg.dns_server, static_cast(0)); -} - -// Parse ACK - -TEST(dhcp_test, parse_ack) { - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - auto* pkt = reinterpret_cast(buf); - - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->htype = net::DHCP_HTYPE_ETHERNET; - pkt->hlen = net::DHCP_HLEN_ETHERNET; - pkt->xid = net::htonl(0xBBBB); - pkt->yiaddr = net::htonl(net::ipv4_addr(172, 16, 0, 10)); - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - opts[0] = net::DHCP_OPT_MSG_TYPE; - opts[1] = 1; - opts[2] = net::DHCP_MSG_ACK; - opts[3] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + 4, &cfg); - ASSERT_TRUE(ok); - EXPECT_EQ(cfg.msg_type, net::DHCP_MSG_ACK); - EXPECT_EQ(cfg.offered_ip, net::ipv4_addr(172, 16, 0, 10)); -} - -// Parse NAK - -TEST(dhcp_test, parse_nak) { - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - opts[0] = net::DHCP_OPT_MSG_TYPE; - opts[1] = 1; - opts[2] = net::DHCP_MSG_NAK; - opts[3] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + 4, &cfg); - ASSERT_TRUE(ok); - EXPECT_EQ(cfg.msg_type, net::DHCP_MSG_NAK); -} - -// Parse edge cases - -TEST(dhcp_test, parse_unknown_options_skipped) { - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->yiaddr = net::htonl(net::ipv4_addr(10, 10, 10, 10)); - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t pos = 0; - - // Unknown option 200, length 3 - opts[pos++] = 200; - opts[pos++] = 3; - opts[pos++] = 0xAA; - opts[pos++] = 0xBB; - opts[pos++] = 0xCC; - - // Message type (should still be found after unknown option) - opts[pos++] = net::DHCP_OPT_MSG_TYPE; - opts[pos++] = 1; - opts[pos++] = net::DHCP_MSG_OFFER; - - // Another unknown option - opts[pos++] = 250; - opts[pos++] = 1; - opts[pos++] = 0xFF; - - opts[pos++] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + pos, &cfg); - ASSERT_TRUE(ok); - EXPECT_EQ(cfg.msg_type, net::DHCP_MSG_OFFER); - EXPECT_EQ(cfg.offered_ip, net::ipv4_addr(10, 10, 10, 10)); -} - -TEST(dhcp_test, parse_pad_options_skipped) { - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t pos = 0; - - // Several PAD bytes before the message type - opts[pos++] = net::DHCP_OPT_PAD; - opts[pos++] = net::DHCP_OPT_PAD; - opts[pos++] = net::DHCP_OPT_PAD; - - opts[pos++] = net::DHCP_OPT_MSG_TYPE; - opts[pos++] = 1; - opts[pos++] = net::DHCP_MSG_ACK; - - opts[pos++] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + pos, &cfg); - ASSERT_TRUE(ok); - EXPECT_EQ(cfg.msg_type, net::DHCP_MSG_ACK); -} - -TEST(dhcp_test, parse_truncated_packet) { - // Packet shorter than the fixed header - uint8_t buf[100]; - string::memset(buf, 0, sizeof(buf)); - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response( - reinterpret_cast(buf), 100, &cfg); - // Should fail: packet is smaller than dhcp_packet (240 bytes) - EXPECT_FALSE(ok); -} - -TEST(dhcp_test, parse_no_msg_type) { - // Valid packet but no message type option - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - // Subnet mask but no message type - opts[0] = net::DHCP_OPT_SUBNET_MASK; - opts[1] = 4; - uint32_t mask = net::htonl(0xFFFFFF00); - string::memcpy(opts + 2, &mask, 4); - opts[6] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + 7, &cfg); - EXPECT_FALSE(ok); -} - -TEST(dhcp_test, parse_wrong_op_code) { - // BOOTREQUEST instead of BOOTREPLY - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREQUEST; // wrong! - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - opts[0] = net::DHCP_OPT_MSG_TYPE; - opts[1] = 1; - opts[2] = net::DHCP_MSG_OFFER; - opts[3] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + 4, &cfg); - EXPECT_FALSE(ok); -} - -TEST(dhcp_test, parse_wrong_magic) { - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->magic = net::htonl(0x12345678); // wrong magic - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - opts[0] = net::DHCP_OPT_MSG_TYPE; - opts[1] = 1; - opts[2] = net::DHCP_MSG_OFFER; - opts[3] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + 4, &cfg); - EXPECT_FALSE(ok); -} - -TEST(dhcp_test, parse_null_args) { - net::dhcp_config cfg = {}; - EXPECT_FALSE(net::dhcp_parse_response(nullptr, 300, &cfg)); - - uint8_t buf[net::DHCP_PACKET_MAX]; - EXPECT_FALSE(net::dhcp_parse_response( - reinterpret_cast(buf), 300, nullptr)); -} - -TEST(dhcp_test, parse_options_truncated_mid_option) { - // Option header says length=10 but only 2 bytes of data remain - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - opts[0] = net::DHCP_OPT_MSG_TYPE; - opts[1] = 1; - opts[2] = net::DHCP_MSG_OFFER; - - // Truncated option: code=1, length=10 but packet ends after 2 more bytes - opts[3] = net::DHCP_OPT_SUBNET_MASK; - opts[4] = 10; // claims 10 bytes of data - - // Only provide sizeof(dhcp_packet) + 5 bytes of options total - // The subnet mask option data would need bytes 5-14, but we end at 5 - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + 5, &cfg); - // Should still succeed with just the message type parsed before the truncation - ASSERT_TRUE(ok); - EXPECT_EQ(cfg.msg_type, net::DHCP_MSG_OFFER); - EXPECT_EQ(cfg.subnet_mask, static_cast(0)); // truncated, not parsed -} - -// Build DISCOVER -> Parse roundtrip - -TEST(dhcp_test, discover_roundtrip_mac_preserved) { - uint8_t buf[net::DHCP_PACKET_MAX]; - uint8_t mac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE}; - uint32_t xid = net::htonl(0x55667788); - - size_t len = net::dhcp_build_discover(buf, sizeof(buf), mac, xid); - ASSERT_TRUE(len > 0); - - auto* pkt = reinterpret_cast(buf); - EXPECT_EQ(string::memcmp(pkt->chaddr, mac, 6), 0); - EXPECT_EQ(pkt->xid, xid); -} - -// Multiple DNS/Router entries (only first used) - -TEST(dhcp_test, parse_multiple_dns_uses_first) { - uint8_t buf[net::DHCP_PACKET_MAX]; - string::memset(buf, 0, sizeof(buf)); - - auto* pkt = reinterpret_cast(buf); - pkt->op = net::DHCP_OP_BOOTREPLY; - pkt->magic = net::htonl(net::DHCP_MAGIC_COOKIE); - pkt->yiaddr = net::htonl(net::ipv4_addr(10, 0, 2, 15)); - - uint8_t* opts = buf + sizeof(net::dhcp_packet); - size_t pos = 0; - - opts[pos++] = net::DHCP_OPT_MSG_TYPE; - opts[pos++] = 1; - opts[pos++] = net::DHCP_MSG_ACK; - - // DNS with two entries (8 bytes) - opts[pos++] = net::DHCP_OPT_DNS; - opts[pos++] = 8; - uint32_t dns1 = net::htonl(net::ipv4_addr(8, 8, 8, 8)); - uint32_t dns2 = net::htonl(net::ipv4_addr(8, 8, 4, 4)); - string::memcpy(opts + pos, &dns1, 4); - pos += 4; - string::memcpy(opts + pos, &dns2, 4); - pos += 4; - - opts[pos++] = net::DHCP_OPT_END; - - net::dhcp_config cfg = {}; - bool ok = net::dhcp_parse_response(pkt, sizeof(net::dhcp_packet) + pos, &cfg); - ASSERT_TRUE(ok); - // Should use the first DNS server - EXPECT_EQ(cfg.dns_server, net::ipv4_addr(8, 8, 8, 8)); -} diff --git a/kernel/tests/net/genet.test.cpp b/kernel/tests/net/genet.test.cpp deleted file mode 100644 index b7c53621..00000000 --- a/kernel/tests/net/genet.test.cpp +++ /dev/null @@ -1,292 +0,0 @@ -#define STLX_TEST_TIER TIER_UTIL - -#include "stlx_unit_test.h" -#include "drivers/net/bcm_genet_regs.h" -#include "drivers/net/phy_regs.h" - -TEST_SUITE(genet_regs_test); - -// DMA descriptor offset tests -// Verify agreement with EDK2/FreeBSD/Linux values - -TEST(genet_regs_test, rx_desc_offsets_base) { - using namespace drivers::genet; - - // RX descriptor 0 should be at RX_BASE - EXPECT_EQ(RX_DESC_STATUS(0), RX_BASE); - EXPECT_EQ(RX_DESC_ADDR_LO(0), RX_BASE + 0x04u); - EXPECT_EQ(RX_DESC_ADDR_HI(0), RX_BASE + 0x08u); -} - -TEST(genet_regs_test, rx_desc_offsets_indexed) { - using namespace drivers::genet; - - // Each descriptor is 12 bytes - EXPECT_EQ(RX_DESC_STATUS(1), RX_BASE + 12u); - EXPECT_EQ(RX_DESC_STATUS(255), RX_BASE + 255u * 12u); - EXPECT_EQ(RX_DESC_ADDR_LO(10), RX_BASE + 10u * 12u + 0x04u); -} - -TEST(genet_regs_test, tx_desc_offsets_base) { - using namespace drivers::genet; - - // TX descriptor 0 should be at TX_BASE - EXPECT_EQ(TX_DESC_STATUS(0), TX_BASE); - EXPECT_EQ(TX_DESC_ADDR_LO(0), TX_BASE + 0x04u); - EXPECT_EQ(TX_DESC_ADDR_HI(0), TX_BASE + 0x08u); -} - -TEST(genet_regs_test, tx_desc_offsets_indexed) { - using namespace drivers::genet; - - EXPECT_EQ(TX_DESC_STATUS(1), TX_BASE + 12u); - EXPECT_EQ(TX_DESC_STATUS(255), TX_BASE + 255u * 12u); -} - -// DMA ring register offset tests - -TEST(genet_regs_test, rx_ring_base_default_queue) { - using namespace drivers::genet; - - // Default queue (16) ring base - uint32_t expected = RX_BASE + 0xC00 + DMA_RING_SIZE * DMA_DEFAULT_QUEUE; - EXPECT_EQ(RX_DMA_RING_BASE(DMA_DEFAULT_QUEUE), expected); - - // Ring register offsets from ring base - EXPECT_EQ(RX_DMA_WRITE_PTR_LO(DMA_DEFAULT_QUEUE), expected + 0x00u); - EXPECT_EQ(RX_DMA_PROD_INDEX(DMA_DEFAULT_QUEUE), expected + 0x08u); - EXPECT_EQ(RX_DMA_CONS_INDEX(DMA_DEFAULT_QUEUE), expected + 0x0Cu); - EXPECT_EQ(RX_DMA_RING_BUF_SIZE(DMA_DEFAULT_QUEUE), expected + 0x10u); - EXPECT_EQ(RX_DMA_START_ADDR_LO(DMA_DEFAULT_QUEUE), expected + 0x14u); - EXPECT_EQ(RX_DMA_END_ADDR_LO(DMA_DEFAULT_QUEUE), expected + 0x1Cu); -} - -TEST(genet_regs_test, tx_ring_base_default_queue) { - using namespace drivers::genet; - - uint32_t expected = TX_BASE + 0xC00 + DMA_RING_SIZE * DMA_DEFAULT_QUEUE; - EXPECT_EQ(TX_DMA_RING_BASE(DMA_DEFAULT_QUEUE), expected); - - EXPECT_EQ(TX_DMA_READ_PTR_LO(DMA_DEFAULT_QUEUE), expected + 0x00u); - EXPECT_EQ(TX_DMA_CONS_INDEX(DMA_DEFAULT_QUEUE), expected + 0x08u); - EXPECT_EQ(TX_DMA_PROD_INDEX(DMA_DEFAULT_QUEUE), expected + 0x0Cu); - EXPECT_EQ(TX_DMA_RING_BUF_SIZE(DMA_DEFAULT_QUEUE), expected + 0x10u); - EXPECT_EQ(TX_DMA_MBUF_DONE_THRES(DMA_DEFAULT_QUEUE), expected + 0x24u); - EXPECT_EQ(TX_DMA_WRITE_PTR_LO(DMA_DEFAULT_QUEUE), expected + 0x2Cu); -} - -// DMA control register offset tests - -TEST(genet_regs_test, dma_ctrl_offsets) { - using namespace drivers::genet; - - // Global DMA control registers - EXPECT_EQ(RX_DMA_RING_CFG, RX_BASE + 0x1040u); - EXPECT_EQ(RX_DMA_CTRL, RX_BASE + 0x1044u); - EXPECT_EQ(RX_SCB_BURST_SIZE, RX_BASE + 0x104Cu); - - EXPECT_EQ(TX_DMA_RING_CFG, TX_BASE + 0x1040u); - EXPECT_EQ(TX_DMA_CTRL, TX_BASE + 0x1044u); - EXPECT_EQ(TX_SCB_BURST_SIZE, TX_BASE + 0x104Cu); -} - -TEST(genet_regs_test, dma_ctrl_ring_enable_bits) { - using namespace drivers::genet; - - // Queue 0 enable bit should be bit 1 - EXPECT_EQ(DMA_CTRL_RING_EN(0), (1u << 1)); - // Queue 16 (default) enable bit should be bit 17 - EXPECT_EQ(DMA_CTRL_RING_EN(16), (1u << 17)); -} - -// Ring buffer size field encoding - -TEST(genet_regs_test, ring_buf_size_encoding) { - using namespace drivers::genet; - - uint32_t val = RING_BUF_SIZE_VAL(256, 1536); - // desc_count=256 in upper 16 bits, buf_len=1536 in lower 16 bits - EXPECT_EQ(val >> 16, 256u); - EXPECT_EQ(val & 0xFFFFu, 1536u); - - // 256 * 12 / 4 - 1 = 767 - EXPECT_EQ(DMA_END_ADDR(256), 767u); -} - -// XON/XOFF threshold encoding - -TEST(genet_regs_test, xon_xoff_encoding) { - using namespace drivers::genet; - - uint32_t val = XON_XOFF_VAL(5, DMA_DESC_COUNT >> 4); - EXPECT_EQ(val >> 16, 5u); - EXPECT_EQ(val & 0xFFFFu, 16u); // 256 >> 4 = 16 -} - -// Producer/consumer index math - -TEST(genet_regs_test, index_wrapping) { - using namespace drivers::genet; - - // Normal case - uint16_t prod = 10; - uint16_t cons = 5; - EXPECT_EQ((prod - cons) & DMA_INDEX_MASK, 5u); - - // Wrap-around case - prod = 3; - cons = 65530; - uint32_t pending = (prod - cons) & DMA_INDEX_MASK; - EXPECT_EQ(pending, 9u); // 65536 - 65530 + 3 = 9 - - // No pending - prod = 100; - cons = 100; - EXPECT_EQ((prod - cons) & DMA_INDEX_MASK, 0u); -} - -TEST(genet_regs_test, desc_index_from_cons) { - using namespace drivers::genet; - - // Consumer index maps to descriptor index via modulo - EXPECT_EQ(static_cast(0) % DMA_DESC_COUNT, 0u); - EXPECT_EQ(static_cast(255) % DMA_DESC_COUNT, 255u); - EXPECT_EQ(static_cast(256) % DMA_DESC_COUNT, 0u); - EXPECT_EQ(static_cast(257) % DMA_DESC_COUNT, 1u); - EXPECT_EQ(static_cast(512) % DMA_DESC_COUNT, 0u); -} - -// MDIO command word construction - -TEST(genet_regs_test, mdio_read_command) { - using namespace drivers::genet; - - uint8_t phy_addr = 1; - uint8_t reg = 0x02; // PHYIDR1 - - uint32_t cmd = MDIO_READ | MDIO_START_BUSY | - (static_cast(phy_addr) << MDIO_PMD_SHIFT) | - (static_cast(reg) << MDIO_REG_SHIFT); - - // Verify individual fields - EXPECT_BITS_SET(cmd, MDIO_READ); - EXPECT_BITS_SET(cmd, MDIO_START_BUSY); - EXPECT_EQ((cmd >> MDIO_PMD_SHIFT) & 0x1F, static_cast(phy_addr)); - EXPECT_EQ((cmd >> MDIO_REG_SHIFT) & 0x1F, static_cast(reg)); -} - -TEST(genet_regs_test, mdio_write_command) { - using namespace drivers::genet; - - uint8_t phy_addr = 1; - uint8_t reg = 0x00; // BMCR - uint16_t data = 0x1234; - - uint32_t cmd = MDIO_WRITE | MDIO_START_BUSY | - (static_cast(phy_addr) << MDIO_PMD_SHIFT) | - (static_cast(reg) << MDIO_REG_SHIFT) | - data; - - EXPECT_BITS_SET(cmd, MDIO_WRITE); - EXPECT_BITS_SET(cmd, MDIO_START_BUSY); - EXPECT_EQ(cmd & MDIO_VAL_MASK, static_cast(data)); -} - -// TX descriptor status construction - -TEST(genet_regs_test, tx_desc_status_construction) { - using namespace drivers::genet; - - uint32_t len = 1500; - uint32_t status = TX_DESC_SOP | TX_DESC_EOP | TX_DESC_CRC | - TX_DESC_QTAG_MASK | - (len << TX_DESC_BUFLEN_SHIFT); - - EXPECT_BITS_SET(status, TX_DESC_SOP); - EXPECT_BITS_SET(status, TX_DESC_EOP); - EXPECT_BITS_SET(status, TX_DESC_CRC); - EXPECT_EQ((status & TX_DESC_BUFLEN_MASK) >> TX_DESC_BUFLEN_SHIFT, len); -} - -// RX descriptor status extraction - -TEST(genet_regs_test, rx_desc_status_extraction) { - using namespace drivers::genet; - - // Simulate a received frame of 100 bytes with SOP+EOP set - uint32_t status = (100u << RX_DESC_BUFLEN_SHIFT) | RX_DESC_SOP | RX_DESC_EOP; - - uint32_t buf_len = (status & RX_DESC_BUFLEN_MASK) >> RX_DESC_BUFLEN_SHIFT; - EXPECT_EQ(buf_len, 100u); - EXPECT_BITS_SET(status, RX_DESC_SOP); - EXPECT_BITS_SET(status, RX_DESC_EOP); - EXPECT_EQ(status & RX_DESC_RX_ERROR, 0u); -} - -// Key register offsets, verify against known EDK2/FreeBSD values - -TEST(genet_regs_test, sys_register_offsets) { - using namespace drivers::genet; - - EXPECT_EQ(SYS_REV_CTRL, 0x000u); - EXPECT_EQ(SYS_PORT_CTRL, 0x004u); - EXPECT_EQ(SYS_RBUF_FLUSH_CTRL, 0x008u); - EXPECT_EQ(SYS_TBUF_FLUSH_CTRL, 0x00Cu); -} - -TEST(genet_regs_test, umac_register_offsets) { - using namespace drivers::genet; - - EXPECT_EQ(UMAC_CMD, 0x808u); - EXPECT_EQ(UMAC_MAC0, 0x80Cu); - EXPECT_EQ(UMAC_MAC1, 0x810u); - EXPECT_EQ(UMAC_MAX_FRAME_LEN, 0x814u); - EXPECT_EQ(UMAC_TX_FLUSH, 0xB34u); - EXPECT_EQ(UMAC_MIB_CTRL, 0xD80u); - EXPECT_EQ(MDIO_CMD, 0xE14u); - EXPECT_EQ(UMAC_MDF_CTRL, 0xE50u); -} - -TEST(genet_regs_test, interrupt_register_offsets) { - using namespace drivers::genet; - - EXPECT_EQ(INTRL2_CPU_STAT, 0x200u); - EXPECT_EQ(INTRL2_CPU_CLEAR, 0x208u); - EXPECT_EQ(INTRL2_CPU_STAT_MASK, 0x20Cu); - EXPECT_EQ(INTRL2_CPU_SET_MASK, 0x210u); - EXPECT_EQ(INTRL2_CPU_CLEAR_MASK, 0x214u); -} - -TEST(genet_regs_test, ext_and_rbuf_offsets) { - using namespace drivers::genet; - - EXPECT_EQ(EXT_RGMII_OOB_CTRL, 0x08Cu); - EXPECT_EQ(RBUF_CTRL, 0x300u); - EXPECT_EQ(RBUF_TBUF_SIZE_CTRL, 0x3B4u); -} - -// PHY register sanity - -TEST(genet_regs_test, phy_standard_registers) { - using namespace drivers::phy; - - EXPECT_EQ(BMCR, static_cast(0x00)); - EXPECT_EQ(BMSR, static_cast(0x01)); - EXPECT_EQ(PHYIDR1, static_cast(0x02)); - EXPECT_EQ(PHYIDR2, static_cast(0x03)); - EXPECT_EQ(ANAR, static_cast(0x04)); - EXPECT_EQ(ANLPAR, static_cast(0x05)); - EXPECT_EQ(GBCR, static_cast(0x09)); - EXPECT_EQ(GBSR, static_cast(0x0A)); -} - -// MDF register accessor - -TEST(genet_regs_test, mdf_addr_accessors) { - using namespace drivers::genet; - - EXPECT_EQ(UMAC_MDF_ADDR0(0), 0xE54u); - EXPECT_EQ(UMAC_MDF_ADDR1(0), 0xE58u); - EXPECT_EQ(UMAC_MDF_ADDR0(1), 0xE54u + 0x08u); - EXPECT_EQ(UMAC_MDF_ADDR1(1), 0xE58u + 0x08u); -} diff --git a/kernel/tests/net/inet_bind.test.cpp b/kernel/tests/net/inet_bind.test.cpp deleted file mode 100644 index ca65643d..00000000 --- a/kernel/tests/net/inet_bind.test.cpp +++ /dev/null @@ -1,351 +0,0 @@ -#define STLX_TEST_TIER TIER_SCHED - -#include "stlx_unit_test.h" -#include "net/net.h" -#include "net/loopback.h" -#include "net/route.h" -#include "net/ipv4.h" -#include "net/udp.h" -#include "net/inet_socket.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "net/ethernet.h" -#include "common/string.h" -#include "common/ring_buffer.h" -#include "resource/resource.h" -#include "mm/heap.h" - -TEST_SUITE(inet_bind_test); - -// Helper: build a kernel_sockaddr_in for binding -static net::kernel_sockaddr_in make_sin(uint32_t ip_host, uint16_t port_host) { - net::kernel_sockaddr_in sa{}; - sa.sin_family = net::AF_INET_VAL; - sa.sin_port = net::htons(port_host); - sa.sin_addr = net::htonl(ip_host); - return sa; -} - -// Helper: create a UDP socket and return (obj, sock) pair. -// Caller must clean up via close_udp_socket. -struct inet_bind_udp_pair { - resource::resource_object* obj; - net::inet_socket* sock; -}; - -static inet_bind_udp_pair create_udp() { - resource::resource_object* obj = nullptr; - int32_t rc = net::create_inet_udp_socket(&obj); - if (rc != resource::OK || !obj) { - return {nullptr, nullptr}; - } - - return {obj, static_cast(obj->impl)}; -} - -// Helper: bind a UDP socket to (ip, port) via ops -static int32_t do_bind(resource::resource_object* obj, uint32_t ip_host, - uint16_t port_host) { - auto sa = make_sin(ip_host, port_host); - return obj->ops->bind(obj, &sa, sizeof(sa)); -} - -// Helper: clean up a UDP socket (mirrors inet_close path) -static void close_udp_socket(resource::resource_object* obj) { - if (obj && obj->ops && obj->ops->close) { - obj->ops->close(obj); - } - if (obj) { - heap::kfree_delete(obj); - } -} - -// Basic bind - -TEST(inet_bind_test, udp_bind_specific_port) { - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - ASSERT_NOT_NULL(sock); - - int32_t rc = do_bind(obj, 0, 7777); - EXPECT_EQ(rc, resource::OK); - EXPECT_EQ(sock->bound_port, static_cast(7777)); - EXPECT_EQ(sock->bound_addr, static_cast(0)); - - close_udp_socket(obj); -} - -TEST(inet_bind_test, udp_bind_ephemeral) { - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - - int32_t rc = do_bind(obj, 0, 0); - EXPECT_EQ(rc, resource::OK); - EXPECT_GE(sock->bound_port, net::UDP_PORT_EPHEMERAL_MIN); - EXPECT_LE(sock->bound_port, net::UDP_PORT_EPHEMERAL_MAX); - - close_udp_socket(obj); -} - -TEST(inet_bind_test, udp_bind_already_bound) { - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - - ASSERT_EQ(do_bind(obj, 0, 7778), resource::OK); - EXPECT_EQ(do_bind(obj, 0, 7779), resource::ERR_INVAL); - EXPECT_EQ(sock->bound_port, static_cast(7778)); - - close_udp_socket(obj); -} - -// Conflict detection - -TEST(inet_bind_test, udp_bind_port_conflict_same_addr) { - auto [obj_a, sock_a] = create_udp(); - auto [obj_b, sock_b] = create_udp(); - ASSERT_NOT_NULL(obj_a); - ASSERT_NOT_NULL(obj_b); - - ASSERT_EQ(do_bind(obj_a, 0, 8888), resource::OK); - EXPECT_EQ(do_bind(obj_b, 0, 8888), resource::ERR_ADDRINUSE); - - close_udp_socket(obj_b); - close_udp_socket(obj_a); -} - -TEST(inet_bind_test, udp_bind_port_conflict_wildcard_vs_specific) { - auto [obj_a, sock_a] = create_udp(); - auto [obj_b, sock_b] = create_udp(); - ASSERT_NOT_NULL(obj_a); - ASSERT_NOT_NULL(obj_b); - - // Wildcard bind blocks specific-address bind on same port - ASSERT_EQ(do_bind(obj_a, 0, 5555), resource::OK); - EXPECT_EQ(do_bind(obj_b, net::ipv4_addr(127, 0, 0, 1), 5555), - resource::ERR_ADDRINUSE); - - close_udp_socket(obj_b); - close_udp_socket(obj_a); -} - -TEST(inet_bind_test, udp_bind_port_conflict_specific_vs_wildcard) { - auto [obj_a, sock_a] = create_udp(); - auto [obj_b, sock_b] = create_udp(); - ASSERT_NOT_NULL(obj_a); - ASSERT_NOT_NULL(obj_b); - - // Specific-address bind blocks wildcard bind on same port - ASSERT_EQ(do_bind(obj_a, net::ipv4_addr(127, 0, 0, 1), 5556), resource::OK); - EXPECT_EQ(do_bind(obj_b, 0, 5556), resource::ERR_ADDRINUSE); - - close_udp_socket(obj_b); - close_udp_socket(obj_a); -} - -TEST(inet_bind_test, udp_bind_same_port_different_addr_allowed) { - // Two specific different local IPs on the same port should be allowed. - // We need a mock NIC so there are two local IPs. - static net::netif mock_eth = {}; - string::memcpy(mock_eth.name, "bnd0", 5); - mock_eth.transmit = [](net::netif*, const uint8_t*, size_t) -> int32_t { - return net::OK; - }; - mock_eth.link_up = [](net::netif*) -> bool { return true; }; - mock_eth.poll = nullptr; - mock_eth.driver_data = nullptr; - - int32_t rc = net::register_netif(&mock_eth); - ASSERT_EQ(rc, net::OK); - rc = net::configure(&mock_eth, - net::ipv4_addr(10, 0, 88, 100), - net::ipv4_addr(255, 255, 255, 0), - 0); - ASSERT_EQ(rc, net::OK); - - auto [obj_a, sock_a] = create_udp(); - auto [obj_b, sock_b] = create_udp(); - ASSERT_NOT_NULL(obj_a); - ASSERT_NOT_NULL(obj_b); - - ASSERT_EQ(do_bind(obj_a, net::ipv4_addr(127, 0, 0, 1), 6666), resource::OK); - EXPECT_EQ(do_bind(obj_b, net::ipv4_addr(10, 0, 88, 100), 6666), resource::OK); - - close_udp_socket(obj_b); - close_udp_socket(obj_a); - - net::route_del_iface(&mock_eth); - net::unregister_netif(&mock_eth); -} - -// Implicit bind interaction - -TEST(inet_bind_test, udp_sendto_then_bind_fails) { - // After sendto assigns an ephemeral port, explicit bind must fail. - // We need a default netif for sendto to work. - static net::netif mock_eth = {}; - string::memcpy(mock_eth.name, "bnd1", 5); - mock_eth.transmit = [](net::netif*, const uint8_t*, size_t) -> int32_t { - return net::OK; - }; - mock_eth.link_up = [](net::netif*) -> bool { return true; }; - mock_eth.poll = nullptr; - mock_eth.driver_data = nullptr; - - int32_t rc = net::register_netif(&mock_eth); - ASSERT_EQ(rc, net::OK); - rc = net::configure(&mock_eth, - net::ipv4_addr(10, 0, 99, 50), - net::ipv4_addr(255, 255, 255, 0), - 0); - ASSERT_EQ(rc, net::OK); - - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - ASSERT_NOT_NULL(obj->ops); - ASSERT_TRUE(obj->ops->sendto != nullptr); - - // sendto triggers implicit ephemeral bind - auto dst = make_sin(net::ipv4_addr(127, 0, 0, 1), 9999); - uint8_t payload[] = "test"; - (void)obj->ops->sendto(obj, payload, 4, 0, &dst, sizeof(dst)); - - // The send itself may fail, but the implicit ephemeral bind happens - // before the send attempt, so bound_port must now be non-zero - EXPECT_NE(sock->bound_port, static_cast(0)); - - // Explicit bind must fail - EXPECT_EQ(do_bind(obj, 0, 12345), resource::ERR_INVAL); - - close_udp_socket(obj); - net::route_del_iface(&mock_eth); - net::unregister_netif(&mock_eth); -} - -// Address validation - -TEST(inet_bind_test, udp_bind_loopback_addr) { - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - - int32_t rc = do_bind(obj, net::ipv4_addr(127, 0, 0, 1), 9999); - EXPECT_EQ(rc, resource::OK); - EXPECT_EQ(sock->bound_addr, net::ipv4_addr(127, 0, 0, 1)); - EXPECT_EQ(sock->bound_port, static_cast(9999)); - - close_udp_socket(obj); -} - -TEST(inet_bind_test, udp_bind_nonlocal_addr_rejected) { - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - - int32_t rc = do_bind(obj, net::ipv4_addr(8, 8, 8, 8), 9999); - EXPECT_EQ(rc, resource::ERR_INVAL); - EXPECT_EQ(sock->bound_port, static_cast(0)); - - close_udp_socket(obj); -} - -TEST(inet_bind_test, udp_bind_bad_family) { - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - - net::kernel_sockaddr_in sa{}; - sa.sin_family = 99; // not AF_INET - sa.sin_port = net::htons(7777); - sa.sin_addr = 0; - int32_t rc = obj->ops->bind(obj, &sa, sizeof(sa)); - EXPECT_EQ(rc, resource::ERR_INVAL); - EXPECT_EQ(sock->bound_port, static_cast(0)); - - close_udp_socket(obj); -} - -// Address-filtered delivery via loopback - -TEST(inet_bind_test, udp_recv_delivers_to_bound_addr) { - // Bind a UDP socket to 127.0.0.1:7000, inject a UDP frame - // addressed to 127.0.0.1:7000, verify it is delivered. - auto [obj, sock] = create_udp(); - ASSERT_NOT_NULL(obj); - ASSERT_NOT_NULL(sock); - ASSERT_NOT_NULL(sock->rx_buf); - - ASSERT_EQ(do_bind(obj, net::ipv4_addr(127, 0, 0, 1), 7000), resource::OK); - - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - - // Build UDP payload - const uint8_t udp_payload[] = "hello-bind"; - constexpr size_t payload_len = 10; - - // Build UDP header + payload - size_t udp_total = sizeof(net::udp_header) + payload_len; - uint8_t udp_pkt[64]; - string::memset(udp_pkt, 0, sizeof(udp_pkt)); - auto* uhdr = reinterpret_cast(udp_pkt); - - uhdr->src_port = net::htons(12345); - uhdr->dst_port = net::htons(7000); - uhdr->length = net::htons(static_cast(udp_total)); - uhdr->checksum = 0; - string::memcpy(udp_pkt + sizeof(net::udp_header), udp_payload, payload_len); - - // Compute UDP checksum - uint16_t csum = net::udp_checksum( - net::htonl(net::ipv4_addr(127, 0, 0, 1)), - net::htonl(net::ipv4_addr(127, 0, 0, 1)), - udp_pkt, udp_total); - uhdr->checksum = (csum == 0) ? static_cast(0xFFFF) : csum; - - // Build IPv4 header - size_t ip_total = sizeof(net::ipv4_header) + udp_total; - uint8_t ip_pkt[128]; - string::memset(ip_pkt, 0, sizeof(ip_pkt)); - auto* ip_hdr = reinterpret_cast(ip_pkt); - - ip_hdr->ver_ihl = (4 << 4) | 5; - ip_hdr->total_len = net::htons(static_cast(ip_total)); - ip_hdr->ttl = 64; - ip_hdr->protocol = net::IPV4_PROTO_UDP; - ip_hdr->src_ip = net::htonl(net::ipv4_addr(127, 0, 0, 1)); - ip_hdr->dst_ip = net::htonl(net::ipv4_addr(127, 0, 0, 1)); - - ip_hdr->checksum = 0; - string::memcpy(ip_pkt + sizeof(net::ipv4_header), udp_pkt, udp_total); - ip_hdr->checksum = net::inet_checksum(ip_pkt, sizeof(net::ipv4_header)); - - // Build Ethernet frame - size_t frame_len = sizeof(net::eth_header) + ip_total; - uint8_t frame[256]; - string::memset(frame, 0, sizeof(frame)); - auto* eth_hdr = reinterpret_cast(frame); - - string::memset(eth_hdr->dst, 0, net::MAC_ADDR_LEN); - string::memset(eth_hdr->src, 0, net::MAC_ADDR_LEN); - eth_hdr->ethertype = net::htons(net::ETH_TYPE_IPV4); - string::memcpy(frame + sizeof(net::eth_header), ip_pkt, ip_total); - - // Transmit through loopback - int32_t tx_rc = lo->transmit(lo, frame, frame_len); - EXPECT_EQ(tx_rc, net::OK); - - // Read from the socket's ring buffer (nonblock) - // UDP RX framing: [4 src_ip_net][2 src_port_net][2 payload_len][N payload] - uint8_t read_buf[128]; - ssize_t nread = ring_buffer_read(sock->rx_buf, read_buf, sizeof(read_buf), true); - EXPECT_GT(nread, static_cast(0)); - - if (nread >= 8) { - uint16_t recv_payload_len; - string::memcpy(&recv_payload_len, read_buf + 6, 2); - EXPECT_EQ(recv_payload_len, static_cast(payload_len)); - - if (recv_payload_len == payload_len && nread >= 8 + static_cast(payload_len)) { - EXPECT_EQ(string::memcmp(read_buf + 8, udp_payload, payload_len), 0); - } - } - - close_udp_socket(obj); -} diff --git a/kernel/tests/net/loopback.test.cpp b/kernel/tests/net/loopback.test.cpp deleted file mode 100644 index 2cbc8467..00000000 --- a/kernel/tests/net/loopback.test.cpp +++ /dev/null @@ -1,530 +0,0 @@ -#define STLX_TEST_TIER TIER_SCHED - -#include "stlx_unit_test.h" -#include "net/net.h" -#include "net/loopback.h" -#include "net/netinfo.h" -#include "net/route.h" -#include "net/ipv4.h" -#include "net/icmp.h" -#include "net/arp.h" -#include "net/ethernet.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "net/inet_socket.h" -#include "common/string.h" -#include "common/ring_buffer.h" -#include "resource/resource.h" -#include "mm/heap.h" -#include "dynpriv/dynpriv.h" - -TEST_SUITE(loopback_test); - -// Interface existence and identity - -TEST(loopback_test, interface_exists) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); -} - -TEST(loopback_test, name_is_lo) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - EXPECT_STREQ(lo->name, "lo"); -} - -TEST(loopback_test, ip_configured) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - EXPECT_EQ(lo->ipv4_addr, net::ipv4_addr(127, 0, 0, 1)); - EXPECT_EQ(lo->ipv4_netmask, net::ipv4_addr(255, 0, 0, 0)); - EXPECT_EQ(lo->ipv4_gateway, static_cast(0)); -} - -TEST(loopback_test, is_configured_flag) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - EXPECT_TRUE(lo->configured); -} - -TEST(loopback_test, link_always_up) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - ASSERT_TRUE(lo->link_up != nullptr); - EXPECT_TRUE(lo->link_up(lo)); -} - -TEST(loopback_test, mac_is_zero) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - for (size_t i = 0; i < net::MAC_ADDR_LEN; i++) { - EXPECT_EQ(lo->mac[i], static_cast(0)); - } -} - -TEST(loopback_test, has_transmit_callback) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - ASSERT_TRUE(lo->transmit != nullptr); -} - -TEST(loopback_test, poll_is_null) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - EXPECT_TRUE(lo->poll == nullptr); -} - -// Default interface behavior - -TEST(loopback_test, not_default_interface) { - // With no hardware NICs the default outbound interface may be absent, - // but it must never be loopback. - net::netif* def = net::get_default_netif(); - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - - if (def != nullptr) { - EXPECT_NE(def, lo); - } -} - -// Interface list visibility - -TEST(loopback_test, visible_in_interface_list) { - net::net_status status = {}; - int32_t rc = net::query_status(&status); - ASSERT_EQ(rc, net::OK); - ASSERT_TRUE(status.if_count > 0); - - bool found_lo = false; - for (uint32_t i = 0; i < status.if_count; i++) { - if (string::strcmp(status.interfaces[i].name, "lo") == 0) { - found_lo = true; - // Verify the reported config matches - EXPECT_EQ(status.interfaces[i].ipv4_addr, - net::ipv4_addr(127, 0, 0, 1)); - EXPECT_EQ(status.interfaces[i].ipv4_netmask, - net::ipv4_addr(255, 0, 0, 0)); - EXPECT_BITS_SET(status.interfaces[i].flags, net::IFF_CONFIGURED); - EXPECT_BITS_SET(status.interfaces[i].flags, net::IFF_UP); - break; - } - } - EXPECT_TRUE(found_lo); -} - -// Loopback transmit, frame delivery - -TEST(loopback_test, transmit_delivers_to_rx) { - // Create an ICMP socket to receive packets delivered through loopback - resource::resource_object* obj = nullptr; - int32_t rc = net::create_inet_icmp_socket(&obj); - ASSERT_EQ(rc, resource::OK); - ASSERT_NOT_NULL(obj); - ASSERT_NOT_NULL(obj->impl); - - auto* sock = static_cast(obj->impl); - ASSERT_NOT_NULL(sock->rx_buf); - - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - - // Build an ICMP echo request - uint8_t icmp_pkt[64]; - string::memset(icmp_pkt, 0, sizeof(icmp_pkt)); - - auto* icmp_hdr = reinterpret_cast(icmp_pkt); - icmp_hdr->type = net::ICMP_TYPE_ECHO_REQUEST; - icmp_hdr->code = 0; - icmp_hdr->id = net::htons(0x1234); - icmp_hdr->sequence = net::htons(1); - icmp_hdr->checksum = 0; - - // Fill payload with a pattern - for (size_t i = sizeof(net::icmp_header); i < sizeof(icmp_pkt); i++) { - icmp_pkt[i] = static_cast(i & 0xFF); - } - icmp_hdr->checksum = net::inet_checksum(icmp_pkt, sizeof(icmp_pkt)); - - // Build IPv4 header - size_t ip_total = sizeof(net::ipv4_header) + sizeof(icmp_pkt); - uint8_t ip_pkt[128]; - string::memset(ip_pkt, 0, sizeof(ip_pkt)); - auto* ip_hdr = reinterpret_cast(ip_pkt); - - ip_hdr->ver_ihl = (4 << 4) | 5; - ip_hdr->total_len = net::htons(static_cast(ip_total)); - ip_hdr->ttl = 64; - ip_hdr->protocol = net::IPV4_PROTO_ICMP; - ip_hdr->src_ip = net::htonl(net::ipv4_addr(127, 0, 0, 1)); - ip_hdr->dst_ip = net::htonl(net::ipv4_addr(127, 0, 0, 1)); - - ip_hdr->checksum = 0; - string::memcpy(ip_pkt + sizeof(net::ipv4_header), icmp_pkt, sizeof(icmp_pkt)); - ip_hdr->checksum = net::inet_checksum(ip_pkt, sizeof(net::ipv4_header)); - - // Build Ethernet header - size_t frame_len = sizeof(net::eth_header) + ip_total; - uint8_t frame[256]; - string::memset(frame, 0, sizeof(frame)); - auto* eth_hdr = reinterpret_cast(frame); - - string::memset(eth_hdr->dst, 0, net::MAC_ADDR_LEN); - string::memset(eth_hdr->src, 0, net::MAC_ADDR_LEN); - eth_hdr->ethertype = net::htons(net::ETH_TYPE_IPV4); - string::memcpy(frame + sizeof(net::eth_header), ip_pkt, ip_total); - - // Transmit through loopback, this should deliver to ICMP handler - rc = lo->transmit(lo, frame, frame_len); - EXPECT_EQ(rc, net::OK); - - // The handler delivered the echo request to our socket and queued an - // echo reply on deferred TX, drain it now - net::drain_deferred_tx(); - - // Try to read from the socket's ring buffer (nonblock) - // The ICMP delivery format: [4 bytes src_ip_net] [2 bytes payload_len] [N bytes data] - uint8_t read_buf[256]; - ssize_t nread = ring_buffer_read(sock->rx_buf, read_buf, sizeof(read_buf), true); - - // We should have received at least the echo request delivery - EXPECT_GT(nread, static_cast(0)); - - // Clean up, unregister socket and destroy resources - net::icmp_unregister_socket(sock); - if (sock->rx_buf) { - ring_buffer_destroy(sock->rx_buf); - sock->rx_buf = nullptr; - } - heap::kfree_delete(sock); - heap::kfree_delete(obj); -} - -// ipv4_send to loopback destination - -TEST(loopback_test, ipv4_send_to_127_0_0_1) { - // Create an ICMP socket to catch delivered packets - resource::resource_object* obj = nullptr; - int32_t rc = net::create_inet_icmp_socket(&obj); - ASSERT_EQ(rc, resource::OK); - ASSERT_NOT_NULL(obj); - - auto* sock = static_cast(obj->impl); - ASSERT_NOT_NULL(sock); - ASSERT_NOT_NULL(sock->rx_buf); - - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - - // Build an ICMP echo request - uint8_t icmp_pkt[64]; - string::memset(icmp_pkt, 0, sizeof(icmp_pkt)); - - auto* icmp_hdr = reinterpret_cast(icmp_pkt); - icmp_hdr->type = net::ICMP_TYPE_ECHO_REQUEST; - icmp_hdr->code = 0; - icmp_hdr->id = net::htons(0x5678); - icmp_hdr->sequence = net::htons(2); - - icmp_hdr->checksum = 0; - icmp_hdr->checksum = net::inet_checksum(icmp_pkt, sizeof(icmp_pkt)); - - // Send to 127.0.0.1, the packet must loop back through the full - // receive path and land in the ICMP socket - rc = net::ipv4_send(lo, net::ipv4_addr(127, 0, 0, 1), - net::IPV4_PROTO_ICMP, icmp_pkt, sizeof(icmp_pkt)); - EXPECT_EQ(rc, net::OK); - - // Drain deferred TX, the echo reply was queued by icmp_recv - net::drain_deferred_tx(); - - // Read from socket, should have received the echo request - // ICMP delivery format: [4 bytes src_ip_net][2 bytes payload_len][N bytes data] - uint8_t read_buf[256]; - ssize_t nread = ring_buffer_read(sock->rx_buf, read_buf, sizeof(read_buf), true); - EXPECT_GT(nread, static_cast(0)); - - if (nread >= 6) { - // Parse the framing header - uint32_t src_ip_net; - string::memcpy(&src_ip_net, read_buf, 4); - uint32_t src_ip = net::ntohl(src_ip_net); - EXPECT_EQ(src_ip, net::ipv4_addr(127, 0, 0, 1)); - - uint16_t payload_len; - string::memcpy(&payload_len, read_buf + 4, 2); - EXPECT_GE(payload_len, static_cast(sizeof(net::icmp_header))); - - // Check the ICMP type in the delivered payload - if (payload_len >= sizeof(net::icmp_header)) { - auto* delivered = reinterpret_cast(read_buf + 6); - EXPECT_EQ(delivered->type, net::ICMP_TYPE_ECHO_REQUEST); - EXPECT_EQ(delivered->id, net::htons(0x5678)); - EXPECT_EQ(delivered->sequence, net::htons(2)); - } - } - - // There should also be a second packet: the echo reply from drain_deferred_tx - ssize_t nread2 = ring_buffer_read(sock->rx_buf, read_buf, sizeof(read_buf), true); - if (nread2 >= 6) { - uint16_t payload_len2; - string::memcpy(&payload_len2, read_buf + 4, 2); - if (payload_len2 >= sizeof(net::icmp_header)) { - auto* reply = reinterpret_cast(read_buf + 6); - EXPECT_EQ(reply->type, net::ICMP_TYPE_ECHO_REPLY); - } - } - - // Clean up - net::icmp_unregister_socket(sock); - if (sock->rx_buf) { - ring_buffer_destroy(sock->rx_buf); - sock->rx_buf = nullptr; - } - heap::kfree_delete(sock); - heap::kfree_delete(obj); -} - -// ipv4_send to other 127.x.x.x addresses - -TEST(loopback_test, ipv4_send_to_127_0_0_2) { - // Sending to 127.0.0.2 must also route through loopback, which - // accepts any destination in 127.0.0.0/8. - resource::resource_object* obj = nullptr; - int32_t rc = net::create_inet_icmp_socket(&obj); - ASSERT_EQ(rc, resource::OK); - ASSERT_NOT_NULL(obj); - - auto* sock = static_cast(obj->impl); - ASSERT_NOT_NULL(sock); - - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - - // Build a minimal ICMP echo request - uint8_t icmp_pkt[8]; - string::memset(icmp_pkt, 0, sizeof(icmp_pkt)); - - auto* icmp_hdr = reinterpret_cast(icmp_pkt); - icmp_hdr->type = net::ICMP_TYPE_ECHO_REQUEST; - icmp_hdr->code = 0; - icmp_hdr->id = net::htons(0xABCD); - icmp_hdr->sequence = net::htons(1); - - icmp_hdr->checksum = 0; - icmp_hdr->checksum = net::inet_checksum(icmp_pkt, sizeof(icmp_pkt)); - - // Send to 127.0.0.2 - rc = net::ipv4_send(lo, net::ipv4_addr(127, 0, 0, 2), - net::IPV4_PROTO_ICMP, icmp_pkt, sizeof(icmp_pkt)); - EXPECT_EQ(rc, net::OK); - - net::drain_deferred_tx(); - - // Should have received the packet - uint8_t read_buf[128]; - ssize_t nread = ring_buffer_read(sock->rx_buf, read_buf, sizeof(read_buf), true); - EXPECT_GT(nread, static_cast(0)); - - // Clean up - net::icmp_unregister_socket(sock); - if (sock->rx_buf) { - ring_buffer_destroy(sock->rx_buf); - sock->rx_buf = nullptr; - } - heap::kfree_delete(sock); - heap::kfree_delete(obj); -} - -// Loopback transmit with null/invalid args - -TEST(loopback_test, transmit_null_frame) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - ASSERT_TRUE(lo->transmit != nullptr); - - int32_t rc = lo->transmit(lo, nullptr, 100); - EXPECT_EQ(rc, net::ERR_INVAL); -} - -TEST(loopback_test, transmit_zero_length) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - ASSERT_TRUE(lo->transmit != nullptr); - - uint8_t dummy[1] = {0}; - int32_t rc = lo->transmit(lo, dummy, 0); - EXPECT_EQ(rc, net::ERR_INVAL); -} - -// Interface flags - -TEST(loopback_test, has_netif_flags) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - EXPECT_BITS_SET(lo->flags, net::NETIF_UP); - EXPECT_BITS_SET(lo->flags, net::NETIF_RUNNING); - EXPECT_BITS_SET(lo->flags, net::NETIF_LOOPBACK); -} - -TEST(loopback_test, iff_loopback_in_status) { - net::net_status status = {}; - int32_t rc = net::query_status(&status); - ASSERT_EQ(rc, net::OK); - - bool found_lo = false; - for (uint32_t i = 0; i < status.if_count; i++) { - if (string::strcmp(status.interfaces[i].name, "lo") == 0) { - found_lo = true; - EXPECT_BITS_SET(status.interfaces[i].flags, net::IFF_LOOPBACK); - break; - } - } - EXPECT_TRUE(found_lo); -} - -// Interface lookup - -TEST(loopback_test, find_netif_by_name) { - net::netif* lo = net::find_netif("lo"); - ASSERT_NOT_NULL(lo); - EXPECT_STREQ(lo->name, "lo"); - EXPECT_EQ(lo, net::get_loopback_netif()); -} - -TEST(loopback_test, find_netif_not_found) { - net::netif* eth99 = net::find_netif("eth99"); - EXPECT_TRUE(eth99 == nullptr); -} - -TEST(loopback_test, find_netif_null_name) { - net::netif* result = net::find_netif(nullptr); - EXPECT_TRUE(result == nullptr); -} - -TEST(loopback_test, find_netif_by_ip_loopback) { - net::netif* lo = net::find_netif_by_ip(net::ipv4_addr(127, 0, 0, 1)); - ASSERT_NOT_NULL(lo); - EXPECT_STREQ(lo->name, "lo"); -} - -TEST(loopback_test, find_netif_by_ip_not_found) { - net::netif* result = net::find_netif_by_ip(net::ipv4_addr(8, 8, 8, 8)); - EXPECT_TRUE(result == nullptr); -} - -TEST(loopback_test, find_netif_by_ip_zero) { - net::netif* result = net::find_netif_by_ip(0); - EXPECT_TRUE(result == nullptr); -} - -TEST(loopback_test, is_local_ip_loopback) { - EXPECT_TRUE(net::is_local_ip(net::ipv4_addr(127, 0, 0, 1))); -} - -TEST(loopback_test, is_local_ip_unknown) { - EXPECT_FALSE(net::is_local_ip(net::ipv4_addr(8, 8, 8, 8))); -} - -// Loopback transmit with null/invalid args - -TEST(loopback_test, transmit_null_iface) { - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - ASSERT_TRUE(lo->transmit != nullptr); - - uint8_t dummy[64] = {}; - int32_t rc = lo->transmit(nullptr, dummy, sizeof(dummy)); - EXPECT_EQ(rc, net::ERR_INVAL); -} - -// LOCAL route delivery for a non-127 own IP - -TEST(loopback_test, local_route_delivers_own_ip) { - // Sending to a configured interface's own IP must be delivered locally - // via the LOCAL route through loopback and accepted on receive. - - // Create and configure a mock interface - static net::netif mock_eth = {}; - string::memcpy(mock_eth.name, "mock0", 6); - - mock_eth.mac[0] = 0xAA; mock_eth.mac[1] = 0xBB; - mock_eth.mac[2] = 0xCC; mock_eth.mac[3] = 0xDD; - mock_eth.mac[4] = 0xEE; mock_eth.mac[5] = 0xFF; - - // Transmit callback, not used for LOCAL delivery, but required for register - mock_eth.transmit = [](net::netif*, const uint8_t*, size_t) -> int32_t { - return net::OK; - }; - mock_eth.link_up = [](net::netif*) -> bool { return true; }; - mock_eth.poll = nullptr; - mock_eth.driver_data = nullptr; - - int32_t rc = net::register_netif(&mock_eth); - ASSERT_EQ(rc, net::OK); - - // Configuring a non-loopback IP installs LOCAL 10.0.88.100/32 on lo - // and CONNECTED 10.0.88.0/24 on mock_eth - rc = net::configure(&mock_eth, - net::ipv4_addr(10, 0, 88, 100), - net::ipv4_addr(255, 255, 255, 0), - 0); - ASSERT_EQ(rc, net::OK); - - // Create an ICMP socket to receive packets - resource::resource_object* obj = nullptr; - rc = net::create_inet_icmp_socket(&obj); - ASSERT_EQ(rc, resource::OK); - ASSERT_NOT_NULL(obj); - - auto* sock = static_cast(obj->impl); - ASSERT_NOT_NULL(sock); - ASSERT_NOT_NULL(sock->rx_buf); - - // Build an ICMP echo request - uint8_t icmp_pkt[8]; - string::memset(icmp_pkt, 0, sizeof(icmp_pkt)); - - auto* icmp_hdr = reinterpret_cast(icmp_pkt); - icmp_hdr->type = net::ICMP_TYPE_ECHO_REQUEST; - icmp_hdr->code = 0; - icmp_hdr->id = net::htons(0xBEEF); - icmp_hdr->sequence = net::htons(1); - - icmp_hdr->checksum = 0; - icmp_hdr->checksum = net::inet_checksum(icmp_pkt, sizeof(icmp_pkt)); - - // Sending to our own IP must take the LOCAL /32 route through loopback - rc = net::ipv4_send(&mock_eth, net::ipv4_addr(10, 0, 88, 100), - net::IPV4_PROTO_ICMP, icmp_pkt, sizeof(icmp_pkt)); - EXPECT_EQ(rc, net::OK); - - // Drain deferred TX (echo reply) - net::drain_deferred_tx(); - - // Read from the socket, should have received the echo request - uint8_t read_buf[128]; - ssize_t nread = ring_buffer_read(sock->rx_buf, read_buf, sizeof(read_buf), true); - EXPECT_GT(nread, static_cast(0)); - - if (nread >= 6) { - // Parse framing: [4 bytes src_ip_net][2 bytes payload_len] - uint32_t src_ip_net; - string::memcpy(&src_ip_net, read_buf, 4); - uint32_t src_ip = net::ntohl(src_ip_net); - // Source IP should be 10.0.88.100 (our own address), NOT 127.0.0.1 - EXPECT_EQ(src_ip, net::ipv4_addr(10, 0, 88, 100)); - } - - // Clean up - net::icmp_unregister_socket(sock); - if (sock->rx_buf) { - ring_buffer_destroy(sock->rx_buf); - sock->rx_buf = nullptr; - } - heap::kfree_delete(sock); - heap::kfree_delete(obj); - - net::route_del_iface(&mock_eth); - net::unregister_netif(&mock_eth); -} diff --git a/kernel/tests/net/route.test.cpp b/kernel/tests/net/route.test.cpp deleted file mode 100644 index 0af62767..00000000 --- a/kernel/tests/net/route.test.cpp +++ /dev/null @@ -1,573 +0,0 @@ -#define STLX_TEST_TIER TIER_SCHED - -#include "stlx_unit_test.h" -#include "net/net.h" -#include "net/route.h" -#include "net/loopback.h" -#include "net/ipv4.h" -#include "net/icmp.h" -#include "net/byteorder.h" -#include "net/checksum.h" -#include "common/string.h" -#include "dynpriv/dynpriv.h" - -TEST_SUITE(route_test); - -// Dummy transmit callback for mock interfaces -static int32_t mock_tx(net::netif* /*iface*/, const uint8_t* /*frame*/, - size_t /*len*/) { - return net::OK; -} - -// Dummy link callback for mock interfaces -static bool mock_link_up(net::netif* /*iface*/) { - return true; -} - -// Basic route table state after init - -TEST(route_test, table_has_loopback_routes) { - // net::init() configures loopback with 127.0.0.1/8, which installs a - // CONNECTED route for 127.0.0.0/8, so at least one route must exist. - uint32_t count = net::route_count(); - EXPECT_GE(count, static_cast(1)); -} - -// route_add and route_count - -TEST(route_test, add_basic) { - uint32_t before = net::route_count(); - - // Create a mock interface for testing - net::netif mock = {}; - string::memcpy(mock.name, "test0", 6); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - - // Add a CONNECTED route for 192.168.100.0/24 with no gateway - int32_t rc = net::route_add( - net::ipv4_addr(192, 168, 100, 0), - net::ipv4_addr(255, 255, 255, 0), - 0, - &mock, - net::route_type::CONNECTED, - net::METRIC_CONNECTED); - - EXPECT_EQ(rc, net::OK); - EXPECT_EQ(net::route_count(), before + 1); - - // Clean up - net::route_del_iface(&mock); - EXPECT_EQ(net::route_count(), before); -} - -TEST(route_test, add_null_iface_rejected) { - int32_t rc = net::route_add(0, 0, 0, nullptr, net::route_type::CONNECTED, 100); - EXPECT_EQ(rc, net::ERR_INVAL); -} - -// route_lookup, connected route - -TEST(route_test, lookup_connected) { - net::netif mock = {}; - string::memcpy(mock.name, "mock0", 6); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - mock.configured = true; - mock.ipv4_addr = net::ipv4_addr(10, 0, 2, 15); - - int32_t rc = net::route_add( - net::ipv4_addr(10, 0, 2, 0), - net::ipv4_addr(255, 255, 255, 0), - 0, &mock, net::route_type::CONNECTED, net::METRIC_CONNECTED); - ASSERT_EQ(rc, net::OK); - - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(10, 0, 2, 100), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock); - EXPECT_EQ(rt.next_hop, net::ipv4_addr(10, 0, 2, 100)); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::CONNECTED)); - - net::route_del_iface(&mock); -} - -// route_lookup, gateway route - -TEST(route_test, lookup_gateway) { - net::netif mock = {}; - string::memcpy(mock.name, "mock1", 6); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - mock.configured = true; - - // Add default route via gateway 10.0.2.2 - int32_t rc = net::route_add( - 0, 0, - net::ipv4_addr(10, 0, 2, 2), - &mock, net::route_type::GATEWAY, net::METRIC_DEFAULT); - ASSERT_EQ(rc, net::OK); - - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(8, 8, 8, 8), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock); - EXPECT_EQ(rt.next_hop, net::ipv4_addr(10, 0, 2, 2)); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::GATEWAY)); - - net::route_del_iface(&mock); -} - -// route_lookup, longest prefix match - -TEST(route_test, lookup_longest_prefix) { - net::netif mock_broad = {}; - string::memcpy(mock_broad.name, "br0", 4); - mock_broad.transmit = mock_tx; - mock_broad.link_up = mock_link_up; - mock_broad.configured = true; - - net::netif mock_narrow = {}; - string::memcpy(mock_narrow.name, "nr0", 4); - mock_narrow.transmit = mock_tx; - mock_narrow.link_up = mock_link_up; - mock_narrow.configured = true; - - // Add a /8 route (broad) - int32_t rc = net::route_add( - net::ipv4_addr(10, 0, 0, 0), - net::ipv4_addr(255, 0, 0, 0), - 0, &mock_broad, net::route_type::CONNECTED, net::METRIC_CONNECTED); - ASSERT_EQ(rc, net::OK); - - // Add a /24 route (narrow, more specific) - rc = net::route_add( - net::ipv4_addr(10, 0, 2, 0), - net::ipv4_addr(255, 255, 255, 0), - 0, &mock_narrow, net::route_type::CONNECTED, net::METRIC_CONNECTED); - ASSERT_EQ(rc, net::OK); - - // Lookup 10.0.2.15, should match the /24 (longer prefix) - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(10, 0, 2, 15), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock_narrow); - - // Lookup 10.0.3.1, should match the /8 (only one that matches) - rc = net::route_lookup(net::ipv4_addr(10, 0, 3, 1), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock_broad); - - net::route_del_iface(&mock_broad); - net::route_del_iface(&mock_narrow); -} - -// route_lookup, metric tiebreak - -TEST(route_test, lookup_metric_tiebreak) { - net::netif mock_hi = {}; - string::memcpy(mock_hi.name, "hi0", 4); - mock_hi.transmit = mock_tx; - mock_hi.link_up = mock_link_up; - mock_hi.configured = true; - - net::netif mock_lo = {}; - string::memcpy(mock_lo.name, "lo0", 4); - mock_lo.transmit = mock_tx; - mock_lo.link_up = mock_link_up; - mock_lo.configured = true; - - // Add two routes for the same prefix, different metrics - int32_t rc = net::route_add( - net::ipv4_addr(172, 16, 0, 0), - net::ipv4_addr(255, 255, 0, 0), - 0, &mock_hi, net::route_type::CONNECTED, 500); - ASSERT_EQ(rc, net::OK); - - rc = net::route_add( - net::ipv4_addr(172, 16, 0, 0), - net::ipv4_addr(255, 255, 0, 0), - 0, &mock_lo, net::route_type::CONNECTED, 100); - ASSERT_EQ(rc, net::OK); - - // Lower metric should win - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(172, 16, 1, 1), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock_lo); - - net::route_del_iface(&mock_hi); - net::route_del_iface(&mock_lo); -} - -// route_lookup, local route - -TEST(route_test, lookup_local) { - net::netif mock = {}; - string::memcpy(mock.name, "lc0", 4); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - mock.configured = true; - - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - - // Add a LOCAL /32 host route through the real loopback interface - int32_t rc = net::route_add( - net::ipv4_addr(10, 0, 2, 15), - 0xFFFFFFFF, - 0, - lo, net::route_type::LOCAL, net::METRIC_LOCAL); - ASSERT_EQ(rc, net::OK); - - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(10, 0, 2, 15), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, lo); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::LOCAL)); - - // The LOCAL route on lo stays, route_del_iface(lo) would also drop the - // real loopback routes and 10.0.2.15 does not collide with other tests. - net::route_del_iface(&mock); -} - -// route_lookup, loopback - -TEST(route_test, lookup_loopback) { - // 127.0.0.1 must resolve to the loopback interface via the CONNECTED - // 127.0.0.0/8 route installed when loopback is configured. - net::route_result rt; - int32_t rc = net::route_lookup(net::ipv4_addr(127, 0, 0, 1), &rt); - ASSERT_EQ(rc, net::OK); - - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - EXPECT_EQ(rt.iface, lo); -} - -TEST(route_test, lookup_loopback_other_addr) { - // 127.0.0.2 should also be routed through loopback - // (matches 127.0.0.0/8 connected route) - net::route_result rt; - int32_t rc = net::route_lookup(net::ipv4_addr(127, 0, 0, 2), &rt); - ASSERT_EQ(rc, net::OK); - - net::netif* lo = net::get_loopback_netif(); - EXPECT_EQ(rt.iface, lo); -} - -// route_lookup, no match - -TEST(route_test, lookup_no_match) { - // The test environment has no default route, so an address outside - // every configured subnet must fail the lookup with ERR_NOIF. - net::route_result rt; - int32_t rc = net::route_lookup(net::ipv4_addr(192, 168, 99, 99), &rt); - EXPECT_EQ(rc, net::ERR_NOIF); -} - -// route_del_iface - -TEST(route_test, del_iface_clears_routes) { - net::netif mock = {}; - string::memcpy(mock.name, "del0", 5); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - mock.configured = true; - - uint32_t before = net::route_count(); - - // Add two routes for this interface - net::route_add(net::ipv4_addr(10, 10, 0, 0), - net::ipv4_addr(255, 255, 0, 0), - 0, &mock, net::route_type::CONNECTED, 100); - net::route_add(0, 0, net::ipv4_addr(10, 10, 0, 1), - &mock, net::route_type::GATEWAY, 1000); - - EXPECT_EQ(net::route_count(), before + 2); - - // Delete all routes for this interface - net::route_del_iface(&mock); - EXPECT_EQ(net::route_count(), before); -} - -// route_add_interface_routes, auto-populate - -TEST(route_test, add_interface_routes_populates) { - net::netif mock = {}; - string::memcpy(mock.name, "auto0", 6); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - - // Register the interface - int32_t rc = net::register_netif(&mock); - ASSERT_EQ(rc, net::OK); - - uint32_t before = net::route_count(); - - // Configure with IP, netmask, and gateway - // This should auto-add: LOCAL (own IP), CONNECTED (subnet), GATEWAY (default) - rc = net::configure(&mock, - net::ipv4_addr(10, 0, 5, 100), - net::ipv4_addr(255, 255, 255, 0), - net::ipv4_addr(10, 0, 5, 1)); - ASSERT_EQ(rc, net::OK); - - // Should have added 3 routes: LOCAL + CONNECTED + GATEWAY - EXPECT_EQ(net::route_count(), before + 3); - - // Verify LOCAL route for own IP - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(10, 0, 5, 100), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::LOCAL)); - - // Verify CONNECTED route for subnet - rc = net::route_lookup(net::ipv4_addr(10, 0, 5, 50), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::CONNECTED)); - - // Verify GATEWAY route catches external IPs - rc = net::route_lookup(net::ipv4_addr(8, 8, 8, 8), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock); - EXPECT_EQ(rt.next_hop, net::ipv4_addr(10, 0, 5, 1)); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::GATEWAY)); - - // Clean up - net::route_del_iface(&mock); - net::unregister_netif(&mock); -} - -// route_add_interface_routes, reconfiguration clears old routes - -TEST(route_test, reconfigure_replaces_routes) { - net::netif mock = {}; - string::memcpy(mock.name, "reconf", 7); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - - int32_t rc = net::register_netif(&mock); - ASSERT_EQ(rc, net::OK); - - uint32_t before = net::route_count(); - - // First configuration - rc = net::configure(&mock, - net::ipv4_addr(10, 0, 6, 100), - net::ipv4_addr(255, 255, 255, 0), - net::ipv4_addr(10, 0, 6, 1)); - ASSERT_EQ(rc, net::OK); - uint32_t after_first = net::route_count(); - EXPECT_EQ(after_first, before + 3); - - // Reconfigure with different IP, old routes should be replaced - rc = net::configure(&mock, - net::ipv4_addr(10, 0, 7, 200), - net::ipv4_addr(255, 255, 255, 0), - net::ipv4_addr(10, 0, 7, 1)); - ASSERT_EQ(rc, net::OK); - - // Same number of routes (old ones deleted, new ones added) - EXPECT_EQ(net::route_count(), after_first); - - // The old 10.0.6.0/24 CONNECTED route must be gone. The new default - // gateway may still match the old subnet, but only as a GATEWAY route. - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(10, 0, 6, 50), &rt); - if (rc == net::OK) { - if (rt.iface == &mock) { - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::GATEWAY)); - } - } - - // New IP should be routable - rc = net::route_lookup(net::ipv4_addr(10, 0, 7, 50), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(rt.iface, &mock); - - // Clean up - net::route_del_iface(&mock); - net::unregister_netif(&mock); -} - -// route_table_full - -TEST(route_test, table_full) { - // Save current count, then fill remaining slots - uint32_t current = net::route_count(); - uint32_t available = net::ROUTE_TABLE_SIZE - current; - - // Use unique mock interfaces so route_del_iface cleans them all up - net::netif mocks[net::ROUTE_TABLE_SIZE]; - uint32_t filled = 0; - - for (uint32_t i = 0; i < available; i++) { - string::memset(&mocks[i], 0, sizeof(net::netif)); - mocks[i].name[0] = 'f'; - mocks[i].name[1] = static_cast('0' + (i / 10) % 10); - mocks[i].name[2] = static_cast('0' + i % 10); - mocks[i].name[3] = '\0'; - - mocks[i].transmit = mock_tx; - mocks[i].link_up = mock_link_up; - mocks[i].configured = true; - - int32_t rc = net::route_add( - net::ipv4_addr(200, static_cast(i), 0, 0), - net::ipv4_addr(255, 255, 255, 0), - 0, &mocks[i], net::route_type::CONNECTED, 200); - if (rc == net::OK) { - filled++; - } else { - break; - } - } - - EXPECT_EQ(net::route_count(), net::ROUTE_TABLE_SIZE); - - // Adding one more should fail - net::netif extra = {}; - string::memcpy(extra.name, "over", 5); - extra.transmit = mock_tx; - extra.configured = true; - - int32_t rc = net::route_add(0, 0, 0, &extra, - net::route_type::CONNECTED, 999); - EXPECT_EQ(rc, net::ERR_NOMEM); - - // Clean up all the test routes - for (uint32_t i = 0; i < filled; i++) { - net::route_del_iface(&mocks[i]); - } - - EXPECT_EQ(net::route_count(), current); -} - -// Verify loopback ipv4_send delivers through the routing table - -TEST(route_test, ipv4_send_via_route_table) { - // ipv4_send must deliver to 127.0.0.1 through the route table lookup - // path, not through any hardcoded loopback shortcut. - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - - // Build a minimal ICMP echo request - uint8_t icmp_pkt[8]; - string::memset(icmp_pkt, 0, sizeof(icmp_pkt)); - - auto* hdr = reinterpret_cast(icmp_pkt); - hdr->type = net::ICMP_TYPE_ECHO_REQUEST; - hdr->code = 0; - hdr->id = net::htons(0x9999); - hdr->sequence = net::htons(1); - - hdr->checksum = 0; - hdr->checksum = net::inet_checksum(icmp_pkt, sizeof(icmp_pkt)); - - // ipv4_send should route via loopback - int32_t rc = net::ipv4_send(lo, net::ipv4_addr(127, 0, 0, 1), - net::IPV4_PROTO_ICMP, - icmp_pkt, sizeof(icmp_pkt)); - EXPECT_EQ(rc, net::OK); - - // Drain deferred TX (ICMP echo reply) - net::drain_deferred_tx(); -} - -// unregister_netif removes every route the interface owns - -TEST(route_test, unregister_cleans_routes) { - net::netif mock = {}; - string::memcpy(mock.name, "unreg0", 7); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - - int32_t rc = net::register_netif(&mock); - ASSERT_EQ(rc, net::OK); - - uint32_t before = net::route_count(); - - // Configure adds LOCAL + CONNECTED + GATEWAY = 3 routes - rc = net::configure(&mock, - net::ipv4_addr(10, 99, 0, 50), - net::ipv4_addr(255, 255, 255, 0), - net::ipv4_addr(10, 99, 0, 1)); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(net::route_count(), before + 3); - - // Verify routes exist - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(10, 99, 0, 50), &rt); - ASSERT_EQ(rc, net::OK); - - // Unregister should clean up ALL routes (including LOCAL -> loopback) - rc = net::unregister_netif(&mock); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(net::route_count(), before); - - // The old IP may still match some unrelated route, but the lookup - // must never return the unregistered interface - rc = net::route_lookup(net::ipv4_addr(10, 99, 0, 50), &rt); - if (rc == net::OK) { - EXPECT_NE(rt.iface, &mock); - } -} - -// Reconfiguring loopback must not destroy LOCAL routes that other -// interfaces own, route deletion is keyed on the owning interface - -TEST(route_test, loopback_reconfig_preserves_other_local_routes) { - // Set up a mock interface with a LOCAL route through loopback - net::netif mock = {}; - string::memcpy(mock.name, "own0", 5); - mock.transmit = mock_tx; - mock.link_up = mock_link_up; - - int32_t rc = net::register_netif(&mock); - ASSERT_EQ(rc, net::OK); - - rc = net::configure(&mock, - net::ipv4_addr(10, 50, 0, 100), - net::ipv4_addr(255, 255, 255, 0), - 0); - ASSERT_EQ(rc, net::OK); - - // Verify the LOCAL route exists and points through loopback - net::route_result rt; - rc = net::route_lookup(net::ipv4_addr(10, 50, 0, 100), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::LOCAL)); - - net::netif* lo = net::get_loopback_netif(); - ASSERT_NOT_NULL(lo); - EXPECT_EQ(rt.iface, lo); - - // Reconfiguring loopback triggers route_del_iface(lo), which must only - // remove routes owned by loopback, not the LOCAL route owned by mock. - rc = net::configure(lo, - net::ipv4_addr(127, 0, 0, 1), - net::ipv4_addr(255, 0, 0, 0), - 0); - ASSERT_EQ(rc, net::OK); - - // The LOCAL route for mock's IP (10.50.0.100) should still exist - rc = net::route_lookup(net::ipv4_addr(10, 50, 0, 100), &rt); - ASSERT_EQ(rc, net::OK); - EXPECT_EQ(static_cast(rt.type), - static_cast(net::route_type::LOCAL)); - EXPECT_EQ(rt.iface, lo); - - // Clean up - net::route_del_iface(&mock); - net::unregister_netif(&mock); -} diff --git a/kernel/tests/net/rtl8168.test.cpp b/kernel/tests/net/rtl8168.test.cpp deleted file mode 100644 index e2147cde..00000000 --- a/kernel/tests/net/rtl8168.test.cpp +++ /dev/null @@ -1,265 +0,0 @@ -#define STLX_TEST_TIER TIER_UTIL - -#include "stlx_unit_test.h" -#include "drivers/net/rtl8168_regs.h" - -TEST_SUITE(rtl8168_regs_test); - -// Descriptor struct sizes -// Verify agreement with RTL8168B datasheet Section 6.1 (16 bytes per descriptor) - -TEST(rtl8168_regs_test, descriptor_sizes) { - using namespace drivers::rtl8168; - - EXPECT_EQ(sizeof(tx_desc), 16u); - EXPECT_EQ(sizeof(rx_desc), 16u); -} - -// Register offsets, verify against RTL8168B datasheet Section 2.1 MAC Registers - -TEST(rtl8168_regs_test, mac_register_offsets) { - using namespace drivers::rtl8168; - - EXPECT_EQ(REG_IDR0, static_cast(0x0000)); - EXPECT_EQ(REG_IDR4, static_cast(0x0004)); - EXPECT_EQ(REG_MAR0, static_cast(0x0008)); - EXPECT_EQ(REG_MAR4, static_cast(0x000C)); - EXPECT_EQ(REG_DTCCR, static_cast(0x0010)); - EXPECT_EQ(REG_TNPDS, static_cast(0x0020)); - EXPECT_EQ(REG_THPDS, static_cast(0x0028)); - EXPECT_EQ(REG_CMD, static_cast(0x0037)); - EXPECT_EQ(REG_TPPOLL, static_cast(0x0038)); - EXPECT_EQ(REG_IMR, static_cast(0x003C)); - EXPECT_EQ(REG_ISR, static_cast(0x003E)); - EXPECT_EQ(REG_TCR, static_cast(0x0040)); - EXPECT_EQ(REG_RCR, static_cast(0x0044)); - EXPECT_EQ(REG_TCTR, static_cast(0x0048)); - EXPECT_EQ(REG_9346CR, static_cast(0x0050)); -} - -TEST(rtl8168_regs_test, config_register_offsets) { - using namespace drivers::rtl8168; - - EXPECT_EQ(REG_CONFIG0, static_cast(0x0051)); - EXPECT_EQ(REG_CONFIG1, static_cast(0x0052)); - EXPECT_EQ(REG_CONFIG2, static_cast(0x0053)); - EXPECT_EQ(REG_CONFIG3, static_cast(0x0054)); - EXPECT_EQ(REG_CONFIG4, static_cast(0x0055)); - EXPECT_EQ(REG_CONFIG5, static_cast(0x0056)); -} - -TEST(rtl8168_regs_test, phy_and_misc_register_offsets) { - using namespace drivers::rtl8168; - - EXPECT_EQ(REG_PHYAR, static_cast(0x0060)); - EXPECT_EQ(REG_PHYSTATUS, static_cast(0x006C)); - EXPECT_EQ(REG_RMS, static_cast(0x00DA)); - EXPECT_EQ(REG_CPCR, static_cast(0x00E0)); - EXPECT_EQ(REG_RDSAR, static_cast(0x00E4)); - EXPECT_EQ(REG_MTPS, static_cast(0x00EC)); - EXPECT_EQ(REG_MISC, static_cast(0x00F0)); -} - -// Command register bits - -TEST(rtl8168_regs_test, command_register_bits) { - using namespace drivers::rtl8168; - - EXPECT_EQ(CMD_RST, static_cast(0x10)); - EXPECT_EQ(CMD_RE, static_cast(0x08)); - EXPECT_EQ(CMD_TE, static_cast(0x04)); -} - -// Interrupt bit definitions - -TEST(rtl8168_regs_test, interrupt_bits) { - using namespace drivers::rtl8168; - - EXPECT_EQ(INT_ROK, static_cast(1u << 0)); - EXPECT_EQ(INT_RER, static_cast(1u << 1)); - EXPECT_EQ(INT_TOK, static_cast(1u << 2)); - EXPECT_EQ(INT_TER, static_cast(1u << 3)); - EXPECT_EQ(INT_RDU, static_cast(1u << 4)); - EXPECT_EQ(INT_LINKCHG, static_cast(1u << 5)); - EXPECT_EQ(INT_FOVW, static_cast(1u << 6)); - EXPECT_EQ(INT_TDU, static_cast(1u << 7)); - EXPECT_EQ(INT_SWINT, static_cast(1u << 8)); - EXPECT_EQ(INT_TIMEOUT, static_cast(1u << 14)); -} - -// PHYAR register construction - -TEST(rtl8168_regs_test, phyar_read_command) { - using namespace drivers::rtl8168; - - uint8_t reg = 0x01; // BMSR - uint32_t cmd = (static_cast(reg) << PHYAR_REG_SHIFT) & PHYAR_REG_MASK; - - // Read: bit 31 should be clear - EXPECT_EQ(cmd & PHYAR_FLAG, 0u); - EXPECT_EQ((cmd >> PHYAR_REG_SHIFT) & 0x1F, static_cast(reg)); -} - -TEST(rtl8168_regs_test, phyar_write_command) { - using namespace drivers::rtl8168; - - uint8_t reg = 0x00; // BMCR - uint16_t data = 0x9200; // reset + ANE + restart_AN - - uint32_t cmd = PHYAR_FLAG | - ((static_cast(reg) << PHYAR_REG_SHIFT) & PHYAR_REG_MASK) | - (data & PHYAR_DATA_MASK); - - EXPECT_BITS_SET(cmd, PHYAR_FLAG); - EXPECT_EQ((cmd >> PHYAR_REG_SHIFT) & 0x1F, static_cast(reg)); - EXPECT_EQ(cmd & PHYAR_DATA_MASK, static_cast(data)); -} - -// PHY Status register bits - -TEST(rtl8168_regs_test, phy_status_bits) { - using namespace drivers::rtl8168; - - EXPECT_EQ(PHYSTS_LINK, static_cast(1u << 1)); - EXPECT_EQ(PHYSTS_FULLDUP, static_cast(1u << 0)); - EXPECT_EQ(PHYSTS_10M, static_cast(1u << 2)); - EXPECT_EQ(PHYSTS_100M, static_cast(1u << 3)); - EXPECT_EQ(PHYSTS_1000MF, static_cast(1u << 4)); -} - -// TX descriptor opts1 construction - -TEST(rtl8168_regs_test, tx_desc_opts1_normal) { - using namespace drivers::rtl8168; - - uint32_t len = 1514; // max Ethernet frame - uint32_t opts1 = TX_OWN | TX_FS | TX_LS | (len & TX_LEN_MASK); - - EXPECT_BITS_SET(opts1, TX_OWN); - EXPECT_BITS_SET(opts1, TX_FS); - EXPECT_BITS_SET(opts1, TX_LS); - EXPECT_EQ(opts1 & TX_LEN_MASK, len); - EXPECT_EQ(opts1 & TX_LGSEN, 0u); // not TSO -} - -TEST(rtl8168_regs_test, tx_desc_eor_preserves_ring) { - using namespace drivers::rtl8168; - - uint32_t opts1 = TX_OWN | TX_FS | TX_LS | TX_EOR | (100u & TX_LEN_MASK); - EXPECT_BITS_SET(opts1, TX_EOR); - EXPECT_BITS_SET(opts1, TX_OWN); -} - -// RX descriptor opts1, command mode (OWN=1) - -TEST(rtl8168_regs_test, rx_desc_command_mode) { - using namespace drivers::rtl8168; - - uint32_t buf_size = RX_BUF_SIZE; - uint32_t opts1 = RX_OWN | (buf_size & RX_BUF_SIZE_MASK); - - EXPECT_BITS_SET(opts1, RX_OWN); - EXPECT_EQ(opts1 & RX_BUF_SIZE_MASK, buf_size); - - uint32_t last = opts1 | RX_EOR; - EXPECT_BITS_SET(last, RX_EOR); -} - -// RX descriptor opts1, status mode (OWN=0, after receive) - -TEST(rtl8168_regs_test, rx_desc_status_extraction) { - using namespace drivers::rtl8168; - - // Simulate: 100 byte frame (including CRC), first+last segment, no errors - uint32_t status = RX_FS | RX_LS | (100u & RX_FRAME_LEN_MASK); - - EXPECT_EQ(status & RX_OWN, 0u); - EXPECT_BITS_SET(status, RX_FS); - EXPECT_BITS_SET(status, RX_LS); - EXPECT_EQ(status & RX_RES, 0u); - EXPECT_EQ(status & RX_FRAME_LEN_MASK, 100u); -} - -TEST(rtl8168_regs_test, rx_desc_error_flags) { - using namespace drivers::rtl8168; - - uint32_t status = RX_FS | RX_LS | RX_RES | RX_CRC | (64u & RX_FRAME_LEN_MASK); - EXPECT_BITS_SET(status, RX_RES); - EXPECT_BITS_SET(status, RX_CRC); -} - -// Chip version XID extraction - -TEST(rtl8168_regs_test, chip_version_xid_extraction) { - using namespace drivers::rtl8168; - - // XID is (TxConfig >> 20) & TCR_XID_MASK. The RTL8168B XID 0x380 - // must survive the mask unchanged. - uint32_t xid = 0x380; - EXPECT_EQ(xid & TCR_XID_MASK, xid); - - // A value that exercises all bits in the mask - uint32_t full_mask_xid = TCR_XID_MASK; - EXPECT_EQ(full_mask_xid, 0x000007CFu); -} - -// Ring parameters - -TEST(rtl8168_regs_test, ring_parameters) { - using namespace drivers::rtl8168; - - EXPECT_EQ(TX_DESC_COUNT, 256u); - EXPECT_EQ(RX_DESC_COUNT, 256u); - EXPECT_EQ(RX_BUF_SIZE, 2048u); - - // RX buffer size must be multiple of 8 per datasheet - EXPECT_EQ(RX_BUF_SIZE % 8, 0u); - // RX buffer size must fit in 14-bit field - EXPECT_EQ(RX_BUF_SIZE <= 0x3FFF, true); -} - -// Config register lock/unlock values - -TEST(rtl8168_regs_test, config_lock_unlock) { - using namespace drivers::rtl8168; - - EXPECT_EQ(CFG_9346_LOCK, static_cast(0x00)); - EXPECT_EQ(CFG_9346_UNLOCK, static_cast(0xC0)); -} - -// MII PHY register addresses - -TEST(rtl8168_regs_test, phy_register_addresses) { - using namespace drivers::rtl8168::phy; - - EXPECT_EQ(BMCR, static_cast(0x00)); - EXPECT_EQ(BMSR, static_cast(0x01)); - EXPECT_EQ(PHYIDR1, static_cast(0x02)); - EXPECT_EQ(PHYIDR2, static_cast(0x03)); - EXPECT_EQ(ANAR, static_cast(0x04)); - EXPECT_EQ(ANLPAR, static_cast(0x05)); - EXPECT_EQ(GBCR, static_cast(0x09)); - EXPECT_EQ(GBSR, static_cast(0x0A)); -} - -// PHY BMCR bit definitions - -TEST(rtl8168_regs_test, phy_bmcr_bits) { - using namespace drivers::rtl8168::phy; - - EXPECT_EQ(BMCR_RESET, static_cast(1u << 15)); - EXPECT_EQ(BMCR_ANE, static_cast(1u << 12)); - EXPECT_EQ(BMCR_RESTART_AN, static_cast(1u << 9)); - EXPECT_EQ(BMCR_SPEED_1000, static_cast(1u << 6)); -} - -// TX max packet size register values - -TEST(rtl8168_regs_test, mtps_values) { - using namespace drivers::rtl8168; - - // MTPS_NORMAL = 0x0C -> 0x0C * 128 = 1536 bytes (covers 1518 frame + CRC) - EXPECT_EQ(static_cast(MTPS_NORMAL) * 128u, 1536u); - // MTPS_JUMBO = 0x3B -> 0x3B * 128 = 7552 bytes (covers 7440 jumbo) - EXPECT_EQ(static_cast(MTPS_JUMBO) * 128u, 7552u); -} From dbb79d4141ee37ec633a85746aad64d9c62fc522 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Fri, 4 Sep 2026 22:31:46 -0700 Subject: [PATCH 02/20] docs(rules): described the comment voice for public API blocks --- .cursor/rules/style.mdc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.cursor/rules/style.mdc b/.cursor/rules/style.mdc index 73ea88af..146a9ac5 100644 --- a/.cursor/rules/style.mdc +++ b/.cursor/rules/style.mdc @@ -45,6 +45,12 @@ dropbear, bearssl, stb_* headers, and kernel/boot/limine.h. column-aligned comment blocks in constant and register tables align within their block. - No decorative separators such as `// =====` or `// -----`. +- Voice, with `kernel/net/interface.h` as the reference: a class or module + block first says in plain prose what the thing is, for a reader with no + context, then who implements what and which direction each call travels. + Identifiers in prose are backticked. Obligations use "must". @param lines + stay short when the name already says it. Full sentences with periods, no + fragments. ## Stanza formatting From 8f3f819d4a32284d2ecdf5495805111659ac431d Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Fri, 4 Sep 2026 22:32:14 -0700 Subject: [PATCH 03/20] feat(net): added core network interface and packet abstractions --- kernel/net/interface.cpp | 30 +++++++++ kernel/net/interface.h | 73 +++++++++++++++++++++ kernel/net/packet.cpp | 80 +++++++++++++++++++++++ kernel/net/packet.h | 135 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 kernel/net/interface.cpp create mode 100644 kernel/net/interface.h create mode 100644 kernel/net/packet.cpp create mode 100644 kernel/net/packet.h diff --git a/kernel/net/interface.cpp b/kernel/net/interface.cpp new file mode 100644 index 00000000..a3be220b --- /dev/null +++ b/kernel/net/interface.cpp @@ -0,0 +1,30 @@ +#include "net/interface.h" +#include "sync/atomic.h" + +namespace net { + +// IDs start at 1 since 0 means "no interface" +static sync::atomic g_next_interface_id {1}; + +static uint64_t generate_interface_id() { + return g_next_interface_id.fetch_add_relaxed(1); +} + +interface::interface() + : m_id(generate_interface_id()) + , m_enabled(false) + , m_name{} + , m_counters{} + , m_mac{} + , m_mtu(0) {} + +int32_t interface::receive(packet* pkt) { + if (!pkt) { + return ERR_INVALID; + } + + // TODO: deliver the frame to the layer above + return OK; +} + +} // namespace net diff --git a/kernel/net/interface.h b/kernel/net/interface.h new file mode 100644 index 00000000..d4d6e182 --- /dev/null +++ b/kernel/net/interface.h @@ -0,0 +1,73 @@ +#ifndef STELLUX_NET_INTERFACE_H +#define STELLUX_NET_INTERFACE_H + +#include "common/types.h" + +namespace net { + +class packet; + +constexpr int32_t OK = 0; +constexpr int32_t ERR_INVALID = -1; // null packet or empty frame +constexpr int32_t ERR_BUSY = -2; // no transmit slot is free +constexpr int32_t ERR_TOO_LARGE = -3; // frame does not fit in one link transmission + +constexpr size_t MAC_ADDR_LEN = 6; +constexpr size_t IFACE_NAME_MAX = 16; + +struct iface_counters { + uint64_t frames_in; + uint64_t frames_out; + uint64_t bytes_in; + uint64_t bytes_out; + uint64_t drops; // frames discarded by policy, such as a full ring + uint64_t errors; // frames the hardware or the stack could not process +}; + +/** + * A network interface is a connection point between a network driver and the rest + * of the network stack. A network driver would derive from this class. + * `transmit` must be implemented by the driver and acts as a handoff point from the + * network stack to the driver. + * `receive` is already implemented by the interface and serves as the driver's + * entry point into the network stack and lets the driver push packets into it. + */ +class interface { +public: + interface(); + virtual ~interface() = default; + + /** + * @brief Hand a fully framed packet to the driver for transmission. + * The implementation must copy the frame into device memory before returning, + * and the caller still owns the packet afterwards. + * @param pkt Frame to send. + * @return OK on success, ERR_BUSY when no transmit slot is free, ERR_TOO_LARGE + * when the frame exceeds the link limit, ERR_INVALID for a null or + * empty packet. + */ + virtual int32_t transmit(packet* pkt) = 0; + + /** + * @brief Entry point for the network driver to push a received frame into + * the network stack. The interface takes ownership of the packet, so the + * driver must not touch it again after this call. + * @param pkt Received frame. + * @return OK when the frame was accepted, ERR_INVALID for a null packet. + */ + int32_t receive(packet* pkt); + +protected: + uint64_t m_id; // Nonzero and unique for the life of the kernel, 0 means no interface + bool m_enabled; // Administratively up, checked by the stack before frames move either way + char m_name[IFACE_NAME_MAX]; + iface_counters m_counters; + + // Link layer identity, filled in by the driver once the hardware reports it + uint8_t m_mac[MAC_ADDR_LEN]; + uint16_t m_mtu; // Largest payload carried in one frame, excluding the link header +}; + +} // namespace net + +#endif // STELLUX_NET_INTERFACE_H diff --git a/kernel/net/packet.cpp b/kernel/net/packet.cpp new file mode 100644 index 00000000..33e4fa5d --- /dev/null +++ b/kernel/net/packet.cpp @@ -0,0 +1,80 @@ +#include "net/packet.h" +#include "mm/heap.h" + +namespace net { + +packet* packet::alloc() { + void* mem = heap::uzalloc(sizeof(packet)); + if (!mem) { + return nullptr; + } + + return new (mem) packet(); +} + +void packet::free(packet* pkt) { + if (!pkt) { + return; + } + + pkt->~packet(); + heap::ufree(pkt); +} + +packet::packet() + : m_iface(nullptr) + , m_data(0) + , m_tail(0) + , m_link_header(HEADER_UNSET) + , m_network_header(HEADER_UNSET) + , m_transport_header(HEADER_UNSET) {} + +bool packet::reserve(size_t n) { + if (m_data != m_tail || n > tailroom()) { + return false; + } + + m_data += static_cast(n); + m_tail = m_data; + return true; +} + +uint8_t* packet::put(size_t n) { + if (n > tailroom()) { + return nullptr; + } + + uint8_t* start = m_buffer + m_tail; + m_tail += static_cast(n); + + return start; +} + +uint8_t* packet::push(size_t n) { + if (n > headroom()) { + return nullptr; + } + + m_data -= static_cast(n); + return m_buffer + m_data; +} + +uint8_t* packet::pull(size_t n) { + if (n > length()) { + return nullptr; + } + + m_data += static_cast(n); + return m_buffer + m_data; +} + +bool packet::trim(size_t len) { + if (len > length()) { + return false; + } + + m_tail = static_cast(m_data + len); + return true; +} + +} // namespace net diff --git a/kernel/net/packet.h b/kernel/net/packet.h new file mode 100644 index 00000000..42453a78 --- /dev/null +++ b/kernel/net/packet.h @@ -0,0 +1,135 @@ +#ifndef STELLUX_NET_PACKET_H +#define STELLUX_NET_PACKET_H + +#include "common/types.h" + +namespace net { + +class interface; + +constexpr size_t PACKET_OBJECT_SIZE = 2048; +constexpr size_t PACKET_METADATA_SIZE = 24; +constexpr size_t PACKET_CAPACITY = PACKET_OBJECT_SIZE - PACKET_METADATA_SIZE; + +/** + * A packet is the single buffer that carries one frame through the network + * stack. + * Headers are added in front of the payload on the way down and stripped from + * the front on the way up, so the class keeps a window `[data, tail)` inside a + * fixed buffer and every layer works only on that window. + * A packet has exactly one owner at any moment. It must be created with `alloc` + * and released with `free`, and it is never copied. + * Protocol layers own their header layouts and the helpers that read and write + * them, the packet itself only knows bytes. + */ +class packet { +public: + /** + * @brief Allocate an empty zeroed packet from the unprivileged heap. + * @return The packet, or nullptr when memory is exhausted. + */ + [[nodiscard]] static packet* alloc(); + + /** + * @brief Release a packet. + */ + static void free(packet* pkt); + + packet(const packet&) = delete; + packet& operator=(const packet&) = delete; + + /** + * @brief Leave `n` bytes of headroom in front of the window. + * Only allowed while the packet is empty, so it must be called before the + * first `put`. + * @return true on success, false when the packet is not empty or `n` does + * not fit. + */ + bool reserve(size_t n); + + /** + * @brief Grow the window at the end by `n` bytes. + * @return Pointer to the first new byte, or nullptr when `n` exceeds the + * tailroom. + */ + [[nodiscard]] uint8_t* put(size_t n); + + /** + * @brief Grow the window at the front by `n` bytes, making room for a header. + * @return Pointer to the new start of the window, or nullptr when `n` exceeds + * the headroom. + */ + [[nodiscard]] uint8_t* push(size_t n); + + /** + * @brief Shrink the window at the front by `n` bytes, stepping past a header. + * The bytes stay in the buffer and remain reachable through the header marks. + * @return Pointer to the new start of the window, or nullptr when `n` exceeds + * the window length. + */ + [[nodiscard]] uint8_t* pull(size_t n); + + /** + * @brief Shrink the window at the end to exactly `len` bytes, dropping link + * padding once the header has told us the true length. + * @return true on success, false when `len` exceeds the window length. + */ + bool trim(size_t len); + + uint8_t* data() { return m_buffer + m_data; } + const uint8_t* data() const { return m_buffer + m_data; } + + size_t length() const { return m_tail - m_data; } + size_t headroom() const { return m_data; } + size_t tailroom() const { return PACKET_CAPACITY - m_tail; } + + // Each layer marks where its header starts before pulling past it, so higher + // layers can still reach lower headers, such as UDP reading IP addresses for + // its checksum. + void mark_link_header() { m_link_header = m_data; } + void mark_network_header() { m_network_header = m_data; } + void mark_transport_header() { m_transport_header = m_data; } + + uint8_t* link_header() { return header_at(m_link_header); } + uint8_t* network_header() { return header_at(m_network_header); } + uint8_t* transport_header() { return header_at(m_transport_header); } + + const uint8_t* link_header() const { return header_at(m_link_header); } + const uint8_t* network_header() const { return header_at(m_network_header); } + const uint8_t* transport_header() const { return header_at(m_transport_header); } + + // Interface the frame arrived on or will be transmitted through + interface* iface() const { return m_iface; } + void set_iface(interface* iface) { m_iface = iface; } + +private: + static constexpr uint16_t HEADER_UNSET = 0xFFFF; + + packet(); + ~packet() = default; + + uint8_t* header_at(uint16_t offset) { + return offset == HEADER_UNSET ? nullptr : m_buffer + offset; + } + + const uint8_t* header_at(uint16_t offset) const { + return offset == HEADER_UNSET ? nullptr : m_buffer + offset; + } + + interface* m_iface; + + // Window bounds and header marks are offsets into m_buffer + uint16_t m_data; + uint16_t m_tail; + uint16_t m_link_header; + uint16_t m_network_header; + uint16_t m_transport_header; + + alignas(8) uint8_t m_buffer[PACKET_CAPACITY]; +}; + +static_assert(sizeof(packet) == PACKET_OBJECT_SIZE); + +} // namespace net + +#endif // STELLUX_NET_PACKET_H From b79ba63722ba47677928c3c3fd5fe29562b992cb Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Fri, 4 Sep 2026 23:01:30 -0700 Subject: [PATCH 04/20] feat(drivers): connected the virtio-net driver to the network interface --- kernel/drivers/net/virtio_net.cpp | 91 +++++++++++++++++++++---------- kernel/drivers/net/virtio_net.h | 14 +++-- kernel/net/interface.cpp | 39 ++++++++++++- kernel/net/interface.h | 6 +- 4 files changed, 112 insertions(+), 38 deletions(-) diff --git a/kernel/drivers/net/virtio_net.cpp b/kernel/drivers/net/virtio_net.cpp index bef81756..c84553f1 100644 --- a/kernel/drivers/net/virtio_net.cpp +++ b/kernel/drivers/net/virtio_net.cpp @@ -1,12 +1,12 @@ #include "drivers/net/virtio_net.h" #include "sync/atomic.h" #include "mm/vmm.h" -#include "mm/heap.h" #include "hw/mmio.h" -#include "common/logging.h" +#include "net/packet.h" #include "common/string.h" #include "dynpriv/dynpriv.h" #include "sched/sched.h" +#include "common/logging.h" namespace drivers { @@ -223,7 +223,7 @@ int32_t virtio_net_driver::read_mac() { return 0; } - for (int i = 0; i < 6; i++) { + for (size_t i = 0; i < net::MAC_ADDR_LEN; i++) { m_mac[i] = m_device_cfg->mac[i]; } @@ -401,6 +401,12 @@ int32_t virtio_net_driver::attach() { rc = read_mac(); if (rc != 0) return rc; + // Link identity for the stack. The interface comes + // up here until the stack owns that decision. + string::memcpy(net::interface::m_name, "eth0", 5); + m_mtu = ETHERNET_MTU; + m_enabled = true; + rc = init_queues(); if (rc != 0) { write_status(read_status() | VIRTIO_STATUS_FAILED); @@ -499,11 +505,29 @@ void virtio_net_driver::drain_rx_locked(rx_batch& batch) { } } -// Frames stop here until a protocol stack attaches, so this only hands the -// buffers back. It runs without m_vq_lock so delivery may later transmit. +// Copies each frame out of its DMA buffer into a packet and hands the packet to +// the stack. Runs without m_vq_lock so the stack may transmit from receive. void virtio_net_driver::deliver_rx_batch(rx_batch& batch) { for (uint16_t i = 0; i < batch.count; i++) { - m_rx_bufs[batch.entries[i].buf_idx].delivering = false; + rx_batch_entry& entry = batch.entries[i]; + net::packet* pkt = net::packet::alloc(); + + uint8_t* dst = pkt ? pkt->put(entry.len) : nullptr; + if (!dst) { + net::packet::free(pkt); + m_counters.drops++; + + m_rx_bufs[entry.buf_idx].delivering = false; + continue; + } + + string::memcpy(dst, entry.data, entry.len); + pkt->set_iface(this); + + m_rx_bufs[entry.buf_idx].delivering = false; + + // Hand the packet off to the network stack to handle + receive(pkt); } } @@ -562,15 +586,15 @@ void virtio_net_driver::run() { RUN_ELEVATED(sched::sleep_ms(1)); } - // Drain RX under lock, deliver frames without the lock so - // protocol handlers can transmit, then re-lock to replenish. + // Drain RX under lock, deliver lowered and without the lock so the + // stack can transmit from receive, then re-lock to replenish. rx_batch batch; RUN_ELEVATED({ sync::irq_lock_guard guard(m_vq_lock); drain_rx_locked(batch); process_tx_completions(); }); - RUN_ELEVATED(deliver_rx_batch(batch)); + deliver_rx_batch(batch); RUN_ELEVATED({ sync::irq_lock_guard guard(m_vq_lock); replenish_rx(); @@ -578,16 +602,23 @@ void virtio_net_driver::run() { } } -int32_t virtio_net_driver::transmit(const uint8_t* frame, size_t len) { - if (!frame || len == 0) { - return -1; +int32_t virtio_net_driver::transmit(net::packet* pkt) { + if (!pkt || pkt->length() == 0) { + return net::ERR_INVALID; } - int32_t result = -1; + size_t hdr_size = m_net_hdr_size; + size_t len = pkt->length(); + + if (hdr_size + len > TX_BUF_SIZE) { + return net::ERR_TOO_LARGE; + } + + int32_t result = net::ERR_BUSY; RUN_ELEVATED({ sync::irq_lock_guard guard(m_vq_lock); - // Find a free TX buffer + // Find a free TX buffer, reclaiming completed ones if none is free int32_t buf_idx = -1; for (uint16_t i = 0; i < TX_BUF_COUNT; i++) { if (!m_tx_bufs[i].in_use) { @@ -597,7 +628,6 @@ int32_t virtio_net_driver::transmit(const uint8_t* frame, size_t len) { } if (buf_idx < 0) { - // Process completions and try again process_tx_completions(); for (uint16_t i = 0; i < TX_BUF_COUNT; i++) { if (!m_tx_bufs[i].in_use) { @@ -610,23 +640,28 @@ int32_t virtio_net_driver::transmit(const uint8_t* frame, size_t len) { if (buf_idx >= 0) { auto& buf = m_tx_bufs[buf_idx]; - size_t hdr_size = m_net_hdr_size; - if (hdr_size + len <= TX_BUF_SIZE) { - auto* nethdr = reinterpret_cast(buf.vaddr); - string::memset(nethdr, 0, hdr_size); - nethdr->gso_type = VIRTIO_NET_HDR_GSO_NONE; + auto* nethdr = reinterpret_cast(buf.vaddr); + nethdr->gso_type = VIRTIO_NET_HDR_GSO_NONE; - string::memcpy(reinterpret_cast(buf.vaddr + hdr_size), frame, len); + string::memset(nethdr, 0, hdr_size); + string::memcpy(reinterpret_cast(buf.vaddr + hdr_size), pkt->data(), len); - int32_t rc = m_txq.add_buf(buf.phys, static_cast(hdr_size + len), 0); - if (rc >= 0) { - buf.in_use = true; - buf.desc_id = static_cast(rc); - m_txq.kick(m_tx_notify_addr); - result = 0; - } + int32_t rc = m_txq.add_buf(buf.phys, static_cast(hdr_size + len), 0); + if (rc >= 0) { + buf.in_use = true; + buf.desc_id = static_cast(rc); + + m_txq.kick(m_tx_notify_addr); + result = net::OK; } } + + if (result == net::OK) { + m_counters.frames_out++; + m_counters.bytes_out += len; + } else { + m_counters.drops++; + } }); return result; diff --git a/kernel/drivers/net/virtio_net.h b/kernel/drivers/net/virtio_net.h index 036be82b..38f54d71 100644 --- a/kernel/drivers/net/virtio_net.h +++ b/kernel/drivers/net/virtio_net.h @@ -4,12 +4,13 @@ #include "drivers/pci_driver.h" #include "drivers/net/virtio_pci.h" #include "drivers/net/virtio_queue.h" +#include "net/interface.h" #include "common/string.h" #include "sync/spinlock.h" namespace drivers { -class virtio_net_driver : public pci_driver { +class virtio_net_driver : public pci_driver, public net::interface { public: virtio_net_driver(pci::device* dev) : pci_driver("virtio_net", dev) @@ -20,7 +21,6 @@ class virtio_net_driver : public pci_driver { , m_device_cfg(nullptr) , m_rx_notify_addr(0) , m_tx_notify_addr(0) { - string::memset(m_mac, 0, sizeof(m_mac)); m_vq_lock = sync::SPINLOCK_INIT; } @@ -28,10 +28,15 @@ class virtio_net_driver : public pci_driver { int32_t detach() override; void run() override; + int32_t transmit(net::packet* pkt) override; + /** @note Privilege: **required** */ __PRIVILEGED_CODE void on_interrupt(uint32_t vector) override; private: + // Largest payload an Ethernet frame carries, excluding the 14-byte header + const uint16_t ETHERNET_MTU = 1500; + // Virtio initialization helpers int32_t parse_virtio_caps(); int32_t map_config_regions(); @@ -48,13 +53,13 @@ class virtio_net_driver : public pci_driver { size_t len; uint16_t buf_idx; // index into m_rx_bufs for clearing delivering flag }; + struct rx_batch { rx_batch_entry entries[RX_BATCH_MAX]; uint16_t count; }; // Packet I/O - int32_t transmit(const uint8_t* frame, size_t len); bool link_up(); void drain_rx_locked(rx_batch& batch); // requires m_vq_lock void deliver_rx_batch(rx_batch& batch); // called without m_vq_lock @@ -109,9 +114,6 @@ class virtio_net_driver : public pci_driver { // m_tx_bufs), held by run() and transmit(). sync::spinlock m_vq_lock; - // Hardware address, from the device or a fixed fallback - uint8_t m_mac[6]; - // Feature flags bool m_has_mac = false; bool m_has_status = false; diff --git a/kernel/net/interface.cpp b/kernel/net/interface.cpp index a3be220b..83835e0b 100644 --- a/kernel/net/interface.cpp +++ b/kernel/net/interface.cpp @@ -1,5 +1,7 @@ #include "net/interface.h" +#include "net/packet.h" #include "sync/atomic.h" +#include "common/logging.h" namespace net { @@ -23,7 +25,42 @@ int32_t interface::receive(packet* pkt) { return ERR_INVALID; } - // TODO: deliver the frame to the layer above + if (!m_enabled) { + m_counters.drops++; + packet::free(pkt); + return ERR_DOWN; + } + + m_counters.frames_in++; + m_counters.bytes_in += pkt->length(); + + // Debug logging + { + static const char HEX[] = "0123456789abcdef"; + const uint8_t* bytes = pkt->data(); + size_t len = pkt->length(); + + log::info("%s: received %lu bytes", m_name, len); + + for (size_t off = 0; off < len; off += 16) { + char line[3 * 16 + 1]; + size_t pos = 0; + + for (size_t i = off; i < off + 16 && i < len; i++) { + line[pos++] = HEX[bytes[i] >> 4]; + line[pos++] = HEX[bytes[i] & 0x0F]; + line[pos++] = ' '; + } + + line[pos] = '\0'; + log::info(" %04lx: %s", off, line); + } + } + + // At this point, the network stack is done + // processing the packet so we can safely free it. + packet::free(pkt); + return OK; } diff --git a/kernel/net/interface.h b/kernel/net/interface.h index d4d6e182..eb0ed3ca 100644 --- a/kernel/net/interface.h +++ b/kernel/net/interface.h @@ -11,6 +11,7 @@ constexpr int32_t OK = 0; constexpr int32_t ERR_INVALID = -1; // null packet or empty frame constexpr int32_t ERR_BUSY = -2; // no transmit slot is free constexpr int32_t ERR_TOO_LARGE = -3; // frame does not fit in one link transmission +constexpr int32_t ERR_DOWN = -4; // interface is administratively disabled constexpr size_t MAC_ADDR_LEN = 6; constexpr size_t IFACE_NAME_MAX = 16; @@ -42,9 +43,8 @@ class interface { * The implementation must copy the frame into device memory before returning, * and the caller still owns the packet afterwards. * @param pkt Frame to send. - * @return OK on success, ERR_BUSY when no transmit slot is free, ERR_TOO_LARGE - * when the frame exceeds the link limit, ERR_INVALID for a null or - * empty packet. + * @return OK when the frame was accepted, ERR_INVALID for a null packet, + * ERR_DOWN when the interface is disabled and the frame was dropped. */ virtual int32_t transmit(packet* pkt) = 0; From fa8826d395bd425fdf3125eda179046adc1e26f0 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 00:16:25 -0700 Subject: [PATCH 05/20] feat(net): added the ethernet layer and routed received frames through it --- kernel/drivers/net/virtio_net.cpp | 26 +++---- kernel/drivers/net/virtio_net.h | 3 - kernel/net/byteorder.h | 31 ++++++++ kernel/net/eth.cpp | 124 ++++++++++++++++++++++++++++++ kernel/net/eth.h | 83 ++++++++++++++++++++ kernel/net/interface.cpp | 35 ++------- kernel/net/interface.h | 28 +++++-- 7 files changed, 275 insertions(+), 55 deletions(-) create mode 100644 kernel/net/byteorder.h create mode 100644 kernel/net/eth.cpp create mode 100644 kernel/net/eth.h diff --git a/kernel/drivers/net/virtio_net.cpp b/kernel/drivers/net/virtio_net.cpp index c84553f1..09e8bc03 100644 --- a/kernel/drivers/net/virtio_net.cpp +++ b/kernel/drivers/net/virtio_net.cpp @@ -214,22 +214,17 @@ int32_t virtio_net_driver::negotiate_features() { int32_t virtio_net_driver::read_mac() { if (!m_device_cfg || !m_has_mac) { // Device provides no MAC, use a fixed locally administered address. - m_mac[0] = 0x52; - m_mac[1] = 0x54; - m_mac[2] = 0x00; - m_mac[3] = 0x12; - m_mac[4] = 0x34; - m_mac[5] = 0x56; + m_mac = {{0x52, 0x54, 0x00, 0x12, 0x34, 0x56}}; return 0; } - for (size_t i = 0; i < net::MAC_ADDR_LEN; i++) { - m_mac[i] = m_device_cfg->mac[i]; + for (size_t i = 0; i < net::eth::MAC_ADDR_LEN; i++) { + m_mac.bytes[i] = m_device_cfg->mac[i]; } log::info("virtio-net: MAC %02x:%02x:%02x:%02x:%02x:%02x", - m_mac[0], m_mac[1], m_mac[2], - m_mac[3], m_mac[4], m_mac[5]); + m_mac.bytes[0], m_mac.bytes[1], m_mac.bytes[2], + m_mac.bytes[3], m_mac.bytes[4], m_mac.bytes[5]); return 0; } @@ -404,7 +399,7 @@ int32_t virtio_net_driver::attach() { // Link identity for the stack. The interface comes // up here until the stack owns that decision. string::memcpy(net::interface::m_name, "eth0", 5); - m_mtu = ETHERNET_MTU; + m_mtu = net::eth::MTU; m_enabled = true; rc = init_queues(); @@ -512,10 +507,13 @@ void virtio_net_driver::deliver_rx_batch(rx_batch& batch) { rx_batch_entry& entry = batch.entries[i]; net::packet* pkt = net::packet::alloc(); - uint8_t* dst = pkt ? pkt->put(entry.len) : nullptr; + uint8_t* dst = nullptr; + if (pkt && pkt->reserve(net::eth::RX_ALIGN_PAD)) { + dst = pkt->put(entry.len); + } if (!dst) { net::packet::free(pkt); - m_counters.drops++; + record_packet_dropped(); m_rx_bufs[entry.buf_idx].delivering = false; continue; @@ -660,7 +658,7 @@ int32_t virtio_net_driver::transmit(net::packet* pkt) { m_counters.frames_out++; m_counters.bytes_out += len; } else { - m_counters.drops++; + record_packet_dropped(); } }); diff --git a/kernel/drivers/net/virtio_net.h b/kernel/drivers/net/virtio_net.h index 38f54d71..db53253b 100644 --- a/kernel/drivers/net/virtio_net.h +++ b/kernel/drivers/net/virtio_net.h @@ -34,9 +34,6 @@ class virtio_net_driver : public pci_driver, public net::interface { __PRIVILEGED_CODE void on_interrupt(uint32_t vector) override; private: - // Largest payload an Ethernet frame carries, excluding the 14-byte header - const uint16_t ETHERNET_MTU = 1500; - // Virtio initialization helpers int32_t parse_virtio_caps(); int32_t map_config_regions(); diff --git a/kernel/net/byteorder.h b/kernel/net/byteorder.h new file mode 100644 index 00000000..76dc2f2c --- /dev/null +++ b/kernel/net/byteorder.h @@ -0,0 +1,31 @@ +#ifndef STELLUX_NET_BYTEORDER_H +#define STELLUX_NET_BYTEORDER_H + +#include "common/types.h" + +namespace net { + +// Multibyte protocol fields travel most significant byte first, which RFC 1700 +// calls network byte order. These convert between that and the host's order and +// compile to a byte swap on little-endian hosts and to nothing on big-endian ones. +constexpr bool HOST_IS_LITTLE_ENDIAN = (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__); + +constexpr uint16_t bswap16(uint16_t v) { + return static_cast((v << 8) | (v >> 8)); +} + +constexpr uint32_t bswap32(uint32_t v) { + return ((v & 0x000000FFu) << 24) | + ((v & 0x0000FF00u) << 8) | + ((v & 0x00FF0000u) >> 8) | + ((v & 0xFF000000u) >> 24); +} + +constexpr uint16_t htons(uint16_t v) { return HOST_IS_LITTLE_ENDIAN ? bswap16(v) : v; } +constexpr uint16_t ntohs(uint16_t v) { return HOST_IS_LITTLE_ENDIAN ? bswap16(v) : v; } +constexpr uint32_t htonl(uint32_t v) { return HOST_IS_LITTLE_ENDIAN ? bswap32(v) : v; } +constexpr uint32_t ntohl(uint32_t v) { return HOST_IS_LITTLE_ENDIAN ? bswap32(v) : v; } + +} // namespace net + +#endif // STELLUX_NET_BYTEORDER_H diff --git a/kernel/net/eth.cpp b/kernel/net/eth.cpp new file mode 100644 index 00000000..3f87466c --- /dev/null +++ b/kernel/net/eth.cpp @@ -0,0 +1,124 @@ +#include "net/eth.h" +#include "net/packet.h" +#include "net/interface.h" +#include "common/logging.h" + +namespace net { +namespace eth { + +static int32_t drop(interface* iface, packet* pkt, int32_t rc) { + iface->record_packet_dropped(); + packet::free(pkt); + return rc; +} + +static int32_t reject(interface* iface, packet* pkt, int32_t rc) { + iface->record_iface_error(); + packet::free(pkt); + return rc; +} + +static void log_frame(const char* proto, const eth_header* hdr, size_t payload_len) { + const uint8_t* src = hdr->src.bytes; + log::info("eth: %s frame from %02x:%02x:%02x:%02x:%02x:%02x, %lu payload bytes", + proto, src[0], src[1], src[2], src[3], src[4], src[5], payload_len); +} + +int32_t input(packet* pkt) { + if (!pkt) { + log::warn("eth: input called with no packet"); + return ERR_INVALID; + } + + interface* iface = pkt->iface(); + if (!iface) { + log::warn("eth: input called with a packet that has no interface"); + packet::free(pkt); + return ERR_INVALID; + } + + // A frame shorter than its own header is damaged + if (pkt->length() < HEADER_LEN) { + return reject(iface, pkt, ERR_INVALID); + } + + pkt->mark_link_header(); + const eth_header* hdr = reinterpret_cast(pkt->link_header()); + + (void)pkt->pull(HEADER_LEN); + + // The NIC already filters to our address plus multicast, this is defense + // against a NIC in promiscuous mode or a driver that skipped its filter + if (hdr->dest != iface->mac() && !hdr->dest.is_multicast()) { + return drop(iface, pkt, OK); + } + + // Below TYPE_MIN the field is an IEEE 802.3 length, not a type + uint16_t type = ntohs(hdr->type); + if (type < TYPE_MIN) { + return drop(iface, pkt, OK); + } + + switch (type) { + case TYPE_ARP: + log_frame("ARP", hdr, pkt->length()); + packet::free(pkt); + break; + case TYPE_IPV4: + log_frame("IPv4", hdr, pkt->length()); + packet::free(pkt); + break; + default: + return drop(iface, pkt, OK); + } + + return OK; +} + +int32_t output(packet* pkt, const mac_addr& dest, uint16_t type) { + if (!pkt) { + log::warn("eth: output called with no packet"); + return ERR_INVALID; + } + + // Routing chooses the interface and records it on the packet before + // handing it down, so a missing one is a bug in the layer above. + interface* iface = pkt->iface(); + if (!iface) { + log::warn("eth: output called with a packet that has no interface"); + packet::free(pkt); + return ERR_INVALID; + } + + if (!iface->enabled()) { + return drop(iface, pkt, ERR_DOWN); + } + + // The window holds the payload at this point, and the MTU bounds exactly that + if (pkt->length() > iface->mtu()) { + return drop(iface, pkt, ERR_TOO_LARGE); + } + + // No headroom means the packet was allocated without reserving room for + // the headers below it, which is a bug in the layer that allocated it. + eth_header* hdr = reinterpret_cast(pkt->push(HEADER_LEN)); + if (!hdr) { + log::warn("eth: output packet has no headroom for the header"); + return reject(iface, pkt, ERR_INVALID); + } + + hdr->dest = dest; + hdr->src = iface->mac(); + hdr->type = htons(type); + pkt->mark_link_header(); + + // The interface borrows the packet and copies the frame out, + // so the packet is released here on every outcome. + int32_t rc = iface->transmit(pkt); + packet::free(pkt); + + return rc; +} + +} // namespace eth +} // namespace net diff --git a/kernel/net/eth.h b/kernel/net/eth.h new file mode 100644 index 00000000..f716e581 --- /dev/null +++ b/kernel/net/eth.h @@ -0,0 +1,83 @@ +#ifndef STELLUX_NET_ETH_H +#define STELLUX_NET_ETH_H + +#include "common/types.h" +#include "common/string.h" +#include "net/packet.h" +#include "net/byteorder.h" + +namespace net { +namespace eth { + +constexpr size_t MAC_ADDR_LEN = 6; +constexpr size_t HEADER_LEN = 14; +constexpr size_t MTU = 1500; // largest payload in one frame +constexpr size_t MIN_FRAME_LEN = 60; // shorter frames arrive padded +constexpr size_t MAX_FRAME_LEN = HEADER_LEN + MTU; // 1514 +constexpr size_t MIN_PAYLOAD_LEN = MIN_FRAME_LEN - HEADER_LEN; // 46 + +// Headroom a driver reserves before copying a received frame in. A 14-byte +// header leaves the network header that follows it misaligned, two bytes of +// padding put it back on a 4-byte boundary. +constexpr size_t RX_ALIGN_PAD = 2; + +// Values of the type field, in host byte order. Frames with a value below +// TYPE_MIN carry an IEEE 802.3 payload length there instead of a type. +constexpr uint16_t TYPE_MIN = 0x0600; +constexpr uint16_t TYPE_IPV4 = 0x0800; +constexpr uint16_t TYPE_ARP = 0x0806; +constexpr uint16_t TYPE_IPV6 = 0x86DD; + +struct mac_addr { + uint8_t bytes[MAC_ADDR_LEN]; + + bool operator==(const mac_addr& other) const { + return string::memcmp(bytes, other.bytes, MAC_ADDR_LEN) == 0; + } + + bool operator!=(const mac_addr& other) const { return !(*this == other); } + + bool is_multicast() const { return (bytes[0] & 0x01) != 0; } + bool is_broadcast() const; +} __attribute__((packed)); + +static_assert(sizeof(mac_addr) == MAC_ADDR_LEN); + +constexpr mac_addr BROADCAST_ADDR = {{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}; + +inline bool mac_addr::is_broadcast() const { return *this == BROADCAST_ADDR; } + +/** + * Ethernet II frame header (RFC 894, IEEE 802.3 Clause 3.1.1). + * `type` is stored in network byte order, so it must be read through `ntohs` + * and written through `htons`. + * https://www.rfc-editor.org/info/rfc894/ + */ +struct eth_header { + mac_addr dest; // Station the frame is for, or the broadcast address + mac_addr src; // Station that sent the frame + uint16_t type; // Type of the payload +} __attribute__((packed)); +static_assert(sizeof(eth_header) == HEADER_LEN); + +/* + * Entry point for the ethernet layer of the network stack to consume + * the packet. Refuses frames shorter than the header, marks the link + * header, pulls it, and dispatches on the type. Anything not handled is + * counted and freed here. + */ +int32_t input(packet* pkt); + +/* + * Exit point for the ethernet layer of the network stack to consume + * the packet. Refuses packets without headroom for the header or without + * an interface to leave through, pushes the header with the source taken + * from that interface, and hands the frame to it for transmission. The + * packet is freed here whether or not the interface accepted it. + */ +int32_t output(packet* pkt, const mac_addr& dest, uint16_t type); + +} // namespace eth +} // namespace net + +#endif // STELLUX_NET_ETH_H diff --git a/kernel/net/interface.cpp b/kernel/net/interface.cpp index 83835e0b..6bc8981e 100644 --- a/kernel/net/interface.cpp +++ b/kernel/net/interface.cpp @@ -1,7 +1,7 @@ #include "net/interface.h" #include "net/packet.h" +#include "net/eth.h" #include "sync/atomic.h" -#include "common/logging.h" namespace net { @@ -26,7 +26,7 @@ int32_t interface::receive(packet* pkt) { } if (!m_enabled) { - m_counters.drops++; + record_packet_dropped(); packet::free(pkt); return ERR_DOWN; } @@ -34,34 +34,9 @@ int32_t interface::receive(packet* pkt) { m_counters.frames_in++; m_counters.bytes_in += pkt->length(); - // Debug logging - { - static const char HEX[] = "0123456789abcdef"; - const uint8_t* bytes = pkt->data(); - size_t len = pkt->length(); - - log::info("%s: received %lu bytes", m_name, len); - - for (size_t off = 0; off < len; off += 16) { - char line[3 * 16 + 1]; - size_t pos = 0; - - for (size_t i = off; i < off + 16 && i < len; i++) { - line[pos++] = HEX[bytes[i] >> 4]; - line[pos++] = HEX[bytes[i] & 0x0F]; - line[pos++] = ' '; - } - - line[pos] = '\0'; - log::info(" %04lx: %s", off, line); - } - } - - // At this point, the network stack is done - // processing the packet so we can safely free it. - packet::free(pkt); - - return OK; + // Every interface is an Ethernet interface, so the link layer + // above is always Ethernet and it takes ownership from here. + return eth::input(pkt); } } // namespace net diff --git a/kernel/net/interface.h b/kernel/net/interface.h index eb0ed3ca..8482a588 100644 --- a/kernel/net/interface.h +++ b/kernel/net/interface.h @@ -1,7 +1,8 @@ #ifndef STELLUX_NET_INTERFACE_H #define STELLUX_NET_INTERFACE_H -#include "common/types.h" +#include "net/eth.h" +#include "sync/atomic.h" namespace net { @@ -13,7 +14,6 @@ constexpr int32_t ERR_BUSY = -2; // no transmit slot is free constexpr int32_t ERR_TOO_LARGE = -3; // frame does not fit in one link transmission constexpr int32_t ERR_DOWN = -4; // interface is administratively disabled -constexpr size_t MAC_ADDR_LEN = 6; constexpr size_t IFACE_NAME_MAX = 16; struct iface_counters { @@ -21,8 +21,8 @@ struct iface_counters { uint64_t frames_out; uint64_t bytes_in; uint64_t bytes_out; - uint64_t drops; // frames discarded by policy, such as a full ring - uint64_t errors; // frames the hardware or the stack could not process + sync::atomic drops; // frames discarded by policy, such as a full ring + sync::atomic errors; // frames the hardware or the stack could not process }; /** @@ -43,8 +43,9 @@ class interface { * The implementation must copy the frame into device memory before returning, * and the caller still owns the packet afterwards. * @param pkt Frame to send. - * @return OK when the frame was accepted, ERR_INVALID for a null packet, - * ERR_DOWN when the interface is disabled and the frame was dropped. + * @return OK on success, ERR_BUSY when no transmit slot is free, ERR_TOO_LARGE + * when the frame exceeds the link limit, ERR_INVALID for a null or + * empty packet. */ virtual int32_t transmit(packet* pkt) = 0; @@ -53,10 +54,21 @@ class interface { * the network stack. The interface takes ownership of the packet, so the * driver must not touch it again after this call. * @param pkt Received frame. - * @return OK when the frame was accepted, ERR_INVALID for a null packet. + * @return ERR_INVALID for a null packet, ERR_DOWN when the interface is + * disabled and the frame was dropped, otherwise the result of the + * link layer. */ int32_t receive(packet* pkt); + uint64_t id() const { return m_id; } + bool enabled() const { return m_enabled; } + + const eth::mac_addr& mac() const { return m_mac; } + uint16_t mtu() const { return m_mtu; } + + void record_packet_dropped() { m_counters.drops.fetch_add_relaxed(1); } + void record_iface_error() { m_counters.errors.fetch_add_relaxed(1); } + protected: uint64_t m_id; // Nonzero and unique for the life of the kernel, 0 means no interface bool m_enabled; // Administratively up, checked by the stack before frames move either way @@ -64,7 +76,7 @@ class interface { iface_counters m_counters; // Link layer identity, filled in by the driver once the hardware reports it - uint8_t m_mac[MAC_ADDR_LEN]; + eth::mac_addr m_mac; uint16_t m_mtu; // Largest payload carried in one frame, excluding the link header }; From ae56de3e0e711e3faeb3d560a28f6736bd966e8f Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 09:59:03 -0700 Subject: [PATCH 06/20] feat(net): added the network stack module header and initialization of the network stack daemon task --- kernel/boot/boot.cpp | 6 +++++- kernel/net/interface.h | 12 ++++++------ kernel/net/net.cpp | 37 +++++++++++++++++++++++++++++++++++++ kernel/net/net.h | 25 +++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 kernel/net/net.cpp create mode 100644 kernel/net/net.h diff --git a/kernel/boot/boot.cpp b/kernel/boot/boot.cpp index 8d00705a..de701dd4 100644 --- a/kernel/boot/boot.cpp +++ b/kernel/boot/boot.cpp @@ -20,8 +20,8 @@ #include "terminal/terminal.h" #include "hw/rtc.h" #include "pci/pci.h" -#include "dynpriv/dynpriv.h" #include "msi/msi.h" +#include "net/net.h" #include "drivers/pci_driver.h" #include "drivers/platform_driver.h" #include "drivers/graphics/gfxfb.h" @@ -153,6 +153,10 @@ extern "C" __PRIVILEGED_CODE void stlx_init() { log::warn("smp::init failed, continuing with single CPU"); } + if (net::init() != net::OK) { + log::warn("net::init failed, network stack will not function properly"); + } + #ifdef STLX_UNIT_TESTS_ENABLED stlx_test::run_all(); while (true) { diff --git a/kernel/net/interface.h b/kernel/net/interface.h index 8482a588..284726eb 100644 --- a/kernel/net/interface.h +++ b/kernel/net/interface.h @@ -1,19 +1,15 @@ #ifndef STELLUX_NET_INTERFACE_H #define STELLUX_NET_INTERFACE_H +#include "net/net.h" #include "net/eth.h" +#include "net/ipv4.h" #include "sync/atomic.h" namespace net { class packet; -constexpr int32_t OK = 0; -constexpr int32_t ERR_INVALID = -1; // null packet or empty frame -constexpr int32_t ERR_BUSY = -2; // no transmit slot is free -constexpr int32_t ERR_TOO_LARGE = -3; // frame does not fit in one link transmission -constexpr int32_t ERR_DOWN = -4; // interface is administratively disabled - constexpr size_t IFACE_NAME_MAX = 16; struct iface_counters { @@ -65,6 +61,7 @@ class interface { const eth::mac_addr& mac() const { return m_mac; } uint16_t mtu() const { return m_mtu; } + const ipv4::ipv4_config& ipv4_conf() const { return m_ipv4_conf; } void record_packet_dropped() { m_counters.drops.fetch_add_relaxed(1); } void record_iface_error() { m_counters.errors.fetch_add_relaxed(1); } @@ -78,6 +75,9 @@ class interface { // Link layer identity, filled in by the driver once the hardware reports it eth::mac_addr m_mac; uint16_t m_mtu; // Largest payload carried in one frame, excluding the link header + + // IPv4 identity, unspecified until configured by hand or through DHCP + ipv4::ipv4_config m_ipv4_conf; }; } // namespace net diff --git a/kernel/net/net.cpp b/kernel/net/net.cpp new file mode 100644 index 00000000..c4ff1367 --- /dev/null +++ b/kernel/net/net.cpp @@ -0,0 +1,37 @@ +#include "net/net.h" +#include "net/interface.h" +#include "sched/sched.h" +#include "dynpriv/dynpriv.h" +#include "common/logging.h" + +namespace net { + +static void netstk_daemon_task_start(void*) { + // All the network stack bookkeeping will be done here + + while (true) { + RUN_ELEVATED(sched::sleep_ms(100)); + } + + sched::exit(0); +} + +__PRIVILEGED_CODE int32_t init() { + // Create the interface table + // ... + + // Create and start the network stack daemon task + sched::task* daemon = sched::create_kernel_task( + netstk_daemon_task_start, nullptr, "netstkd"); + if (!daemon) { + log::error("net: failed to create network stack daemon"); + return ERR_NO_MEMORY; + } + + // Schedule the network stack daemon task + sched::enqueue(daemon); + + return OK; +} + +} // namespace net diff --git a/kernel/net/net.h b/kernel/net/net.h new file mode 100644 index 00000000..ebd4e1e6 --- /dev/null +++ b/kernel/net/net.h @@ -0,0 +1,25 @@ +#ifndef STELLUX_NET_NET_H +#define STELLUX_NET_NET_H + +#include "common/types.h" + +namespace net { + +// Result codes shared by every layer of the stack +constexpr int32_t OK = 0; +constexpr int32_t ERR_INVALID = -1; // null packet or empty frame +constexpr int32_t ERR_BUSY = -2; // no transmit slot is free +constexpr int32_t ERR_TOO_LARGE = -3; // frame does not fit in one link transmission +constexpr int32_t ERR_DOWN = -4; // interface is administratively disabled +constexpr int32_t ERR_NO_MEMORY = -5; // an allocation or task creation failed + +/** + * Initialize the network stack and start its daemon/bookkeeping task. + * @return OK on success, negative error code on failure. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t init(); + +} // namespace net + +#endif // STELLUX_NET_NET_H From 271b2f2aa7c216d7aaf5935629e89a2965168455 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 09:59:48 -0700 Subject: [PATCH 07/20] feat(net): added the IPv4 address type and per-interface IPv4 configuration --- kernel/drivers/net/virtio_net.cpp | 9 ++++ kernel/net/ipv4.h | 69 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 kernel/net/ipv4.h diff --git a/kernel/drivers/net/virtio_net.cpp b/kernel/drivers/net/virtio_net.cpp index 09e8bc03..5d3b8a73 100644 --- a/kernel/drivers/net/virtio_net.cpp +++ b/kernel/drivers/net/virtio_net.cpp @@ -12,6 +12,14 @@ namespace drivers { using namespace virtio; +// QEMU user-mode network defaults, used until the stack supports +// interface configuration +static constexpr net::ipv4::ipv4_config QEMU_STATIC_IPV4 = { + {{10, 0, 2, 15}}, + {{255, 255, 255, 0}}, + {{10, 0, 2, 2}}, +}; + // Virtio advertises its config regions as vendor-specific PCI capabilities // (id 0x09), distinguished by a cfg_type field. int32_t virtio_net_driver::parse_virtio_caps() { @@ -400,6 +408,7 @@ int32_t virtio_net_driver::attach() { // up here until the stack owns that decision. string::memcpy(net::interface::m_name, "eth0", 5); m_mtu = net::eth::MTU; + m_ipv4_conf = QEMU_STATIC_IPV4; m_enabled = true; rc = init_queues(); diff --git a/kernel/net/ipv4.h b/kernel/net/ipv4.h new file mode 100644 index 00000000..e892ffe3 --- /dev/null +++ b/kernel/net/ipv4.h @@ -0,0 +1,69 @@ +#ifndef STELLUX_NET_IPV4_H +#define STELLUX_NET_IPV4_H + +#include "common/types.h" +#include "common/string.h" + +namespace net { +namespace ipv4 { + +constexpr size_t ADDR_LEN = 4; + +/** + * A 32-bit IPv4 address held as its four wire bytes, most significant first, + * so it copies straight into and out of headers and there is never a byte + * order to remember. `bytes[0]` is the first octet of the dotted notation. + */ +struct ipv4_addr { + uint8_t bytes[ADDR_LEN]; + + bool operator==(const ipv4_addr& other) const { + return string::memcmp(bytes, other.bytes, ADDR_LEN) == 0; + } + + bool operator!=(const ipv4_addr& other) const { return !(*this == other); } + + // 0.0.0.0, no address assigned + bool is_unspecified() const; + + // 255.255.255.255, delivered to every host on the link + bool is_broadcast() const; + + // True when this address and `other` share the network that `mask` describes + bool in_same_subnet(const ipv4_addr& other, const ipv4_addr& mask) const; +} __attribute__((packed)); +static_assert(sizeof(ipv4_addr) == ADDR_LEN); + +constexpr ipv4_addr UNSPECIFIED_ADDR = {{0, 0, 0, 0}}; +constexpr ipv4_addr BROADCAST_ADDR = {{255, 255, 255, 255}}; + +inline bool ipv4_addr::is_unspecified() const { return *this == UNSPECIFIED_ADDR; } +inline bool ipv4_addr::is_broadcast() const { return *this == BROADCAST_ADDR; } + +inline bool ipv4_addr::in_same_subnet(const ipv4_addr& other, const ipv4_addr& mask) const { + for (size_t i = 0; i < ADDR_LEN; i++) { + if ((bytes[i] & mask.bytes[i]) != (other.bytes[i] & mask.bytes[i])) { + return false; + } + } + + return true; +} + +/** + * Network layer identity of one interface. `address` stays unspecified until + * configuration assigns one, by hand or through DHCP, and an interface without + * an address handles no network layer traffic, ARP included. + */ +struct ipv4_config { + ipv4_addr address; + ipv4_addr netmask; + ipv4_addr gateway; // Router for destinations outside the subnet, unspecified if none + + bool configured() const { return !address.is_unspecified(); } +}; + +} // namespace ipv4 +} // namespace net + +#endif // STELLUX_NET_IPV4_H From 5f1988c512e7d5f5cb3ad7bd808e5adba4ba791a Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 10:00:16 -0700 Subject: [PATCH 08/20] feat(net): added the ARP layer --- .cursor/rules/style.mdc | 4 +- kernel/net/arp.cpp | 81 +++++++++++++++++++++++++++++++++++++++++ kernel/net/arp.h | 76 ++++++++++++++++++++++++++++++++++++++ kernel/net/byteorder.h | 2 +- kernel/net/eth.cpp | 5 +-- kernel/net/eth.h | 6 +-- 6 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 kernel/net/arp.cpp create mode 100644 kernel/net/arp.h diff --git a/.cursor/rules/style.mdc b/.cursor/rules/style.mdc index 146a9ac5..88614160 100644 --- a/.cursor/rules/style.mdc +++ b/.cursor/rules/style.mdc @@ -50,7 +50,9 @@ dropbear, bearssl, stb_* headers, and kernel/boot/limine.h. context, then who implements what and which direction each call travels. Identifiers in prose are backticked. Obligations use "must". @param lines stay short when the name already says it. Full sentences with periods, no - fragments. + fragments. Plain technical vocabulary only: hosts and MAC addresses, not + "stations", protocols are "supported", not "spoken", and no figurative + phrasing such as packets that "travel" or entries that are "forgotten". ## Stanza formatting diff --git a/kernel/net/arp.cpp b/kernel/net/arp.cpp new file mode 100644 index 00000000..949a6e1c --- /dev/null +++ b/kernel/net/arp.cpp @@ -0,0 +1,81 @@ +#include "net/arp.h" +#include "net/packet.h" +#include "net/interface.h" +#include "common/logging.h" + +namespace net { +namespace arp { + +static int32_t drop(interface* iface, packet* pkt, int32_t rc) { + iface->record_packet_dropped(); + packet::free(pkt); + return rc; +} + +static int32_t reject(interface* iface, packet* pkt, int32_t rc) { + iface->record_iface_error(); + packet::free(pkt); + return rc; +} + +int32_t input(packet* pkt) { + if (!pkt) { + log::warn("arp: input called with no packet"); + return ERR_INVALID; + } + + interface* iface = pkt->iface(); + if (!iface) { + log::warn("arp: input called with a packet that has no interface"); + packet::free(pkt); + return ERR_INVALID; + } + + if (pkt->length() < HEADER_LEN) { + return reject(iface, pkt, ERR_INVALID); + } + + // Only Ethernet over IPv4 is supported + arp_header* hdr = reinterpret_cast(pkt->data()); + if ( + ntohs(hdr->hw_type) != HW_TYPE_ETHERNET || + ntohs(hdr->proto_type) != PROTO_TYPE_IPV4 || + hdr->hw_len != eth::MAC_ADDR_LEN || + hdr->proto_len != ipv4::ADDR_LEN + ) { + return drop(iface, pkt, OK); + } + + // Requests for other hosts are normal traffic, not drops + const ipv4::ipv4_config& conf = iface->ipv4_conf(); + if (!conf.configured() || hdr->target_proto_addr != conf.address) { + packet::free(pkt); + return OK; + } + + if (ntohs(hdr->opcode) != OP_REQUEST) { + return drop(iface, pkt, OK); + } + + // RFC 826: the request becomes the reply in place, sender and target swapped + hdr->target_hw_addr = hdr->sender_hw_addr; + hdr->target_proto_addr = hdr->sender_proto_addr; + hdr->sender_hw_addr = iface->mac(); + hdr->sender_proto_addr = conf.address; + hdr->opcode = htons(OP_REPLY); + + // Strip link padding so it is not sent back as payload + pkt->trim(HEADER_LEN); + + return output(pkt, hdr->target_hw_addr); +} + +int32_t output(packet* pkt, const eth::mac_addr& dest) { + return eth::output(pkt, dest, eth::TYPE_ARP); +} + +void sweep(uint64_t) { +} + +} // namespace arp +} // namespace net diff --git a/kernel/net/arp.h b/kernel/net/arp.h new file mode 100644 index 00000000..51893f47 --- /dev/null +++ b/kernel/net/arp.h @@ -0,0 +1,76 @@ +#ifndef STELLUX_NET_ARP_H +#define STELLUX_NET_ARP_H + +#include "net/eth.h" +#include "net/ipv4.h" + +namespace net { +namespace arp { + +constexpr size_t HEADER_LEN = 28; + +constexpr uint16_t HW_TYPE_ETHERNET = 1; +constexpr uint16_t PROTO_TYPE_IPV4 = eth::TYPE_IPV4; +constexpr uint16_t OP_REQUEST = 1; +constexpr uint16_t OP_REPLY = 2; + +// ARP table configuration +constexpr uint64_t NS_PER_SEC = 1000000000ULL; +constexpr size_t TABLE_SIZE = 16; +constexpr uint64_t ENTRY_LIFETIME_NS = 300 * NS_PER_SEC; // resolved entries expire after this +constexpr uint64_t PENDING_TIMEOUT_NS = 5 * NS_PER_SEC; // unanswered requests fail after this +constexpr size_t PENDING_QUEUE_DEPTH = 3; // packets held per unresolved entry + +/** + * ARP packet for Ethernet over IPv4 (RFC 826). The standard header is generic, + * which is what `hw_len` and `proto_len` describe, but only this one shape is + * supported by Stellux, so the addresses are typed and the lengths are checked on + * input rather than interpreted. Multibyte fields are in network byte order. + * https://www.rfc-editor.org/info/rfc826/ + */ +struct arp_header { + uint16_t hw_type; // HW_TYPE_ETHERNET + uint16_t proto_type; // PROTO_TYPE_IPV4 + uint8_t hw_len; // eth::MAC_ADDR_LEN + uint8_t proto_len; // ipv4::ADDR_LEN + uint16_t opcode; // OP_REQUEST or OP_REPLY + eth::mac_addr sender_hw_addr; // Sender MAC address + ipv4::ipv4_addr sender_proto_addr; // Sender IPv4 address + eth::mac_addr target_hw_addr; // Destination MAC, zero in a request + ipv4::ipv4_addr target_proto_addr; // Destination address being resolved +} __attribute__((packed)); +static_assert(sizeof(arp_header) == HEADER_LEN); + +/** + * Lifecycle of a table entry. An entry is `pending` from the moment a request goes + * out until the reply arrives, and packets for that address wait on it in the + * meantime. It is `resolved` while the hardware address is known and not expired. + * `empty` is zero so that zeroed memory is a free slot in the table. + */ +enum class arp_entry_state : uint8_t { + empty = 0, + pending = 1, + resolved = 2, +}; + +/* + * Consumes an ARP packet. A request for this host's + * address is answered in place, everything else is freed. + */ +int32_t input(packet* pkt); + +/* + * Consumes a finished ARP packet and targets it for `dest`, + * the requester's address for a reply or broadcast for a request. + */ +int32_t output(packet* pkt, const eth::mac_addr& dest); + +/* + * Ages the table on every daemon pass. `ts` is the current monotonic time. + */ +void sweep(uint64_t ts); + +} // namespace arp +} // namespace net + +#endif // STELLUX_NET_ARP_H diff --git a/kernel/net/byteorder.h b/kernel/net/byteorder.h index 76dc2f2c..b4ff0cd0 100644 --- a/kernel/net/byteorder.h +++ b/kernel/net/byteorder.h @@ -5,7 +5,7 @@ namespace net { -// Multibyte protocol fields travel most significant byte first, which RFC 1700 +// Multibyte protocol fields are sent most significant byte first, which RFC 1700 // calls network byte order. These convert between that and the host's order and // compile to a byte swap on little-endian hosts and to nothing on big-endian ones. constexpr bool HOST_IS_LITTLE_ENDIAN = (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__); diff --git a/kernel/net/eth.cpp b/kernel/net/eth.cpp index 3f87466c..9410c7b8 100644 --- a/kernel/net/eth.cpp +++ b/kernel/net/eth.cpp @@ -1,6 +1,7 @@ #include "net/eth.h" #include "net/packet.h" #include "net/interface.h" +#include "net/arp.h" #include "common/logging.h" namespace net { @@ -61,9 +62,7 @@ int32_t input(packet* pkt) { switch (type) { case TYPE_ARP: - log_frame("ARP", hdr, pkt->length()); - packet::free(pkt); - break; + return arp::input(pkt); case TYPE_IPV4: log_frame("IPv4", hdr, pkt->length()); packet::free(pkt); diff --git a/kernel/net/eth.h b/kernel/net/eth.h index f716e581..05cdc344 100644 --- a/kernel/net/eth.h +++ b/kernel/net/eth.h @@ -12,7 +12,7 @@ namespace eth { constexpr size_t MAC_ADDR_LEN = 6; constexpr size_t HEADER_LEN = 14; constexpr size_t MTU = 1500; // largest payload in one frame -constexpr size_t MIN_FRAME_LEN = 60; // shorter frames arrive padded +constexpr size_t MIN_FRAME_LEN = 60; // shorter frames are padded to this constexpr size_t MAX_FRAME_LEN = HEADER_LEN + MTU; // 1514 constexpr size_t MIN_PAYLOAD_LEN = MIN_FRAME_LEN - HEADER_LEN; // 46 @@ -54,8 +54,8 @@ inline bool mac_addr::is_broadcast() const { return *this == BROADCAST_ADDR; } * https://www.rfc-editor.org/info/rfc894/ */ struct eth_header { - mac_addr dest; // Station the frame is for, or the broadcast address - mac_addr src; // Station that sent the frame + mac_addr dest; // Destination MAC address or the broadcast address + mac_addr src; // Source MAC address uint16_t type; // Type of the payload } __attribute__((packed)); static_assert(sizeof(eth_header) == HEADER_LEN); From ba6ff0cf1fadf5aad0fadba9b7818b389daafc52 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 11:30:18 -0700 Subject: [PATCH 09/20] feat(sync): added an unprivileged lock guard primitive --- kernel/sync/spinlock.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/kernel/sync/spinlock.h b/kernel/sync/spinlock.h index 5c91e3b8..496c8c4e 100644 --- a/kernel/sync/spinlock.h +++ b/kernel/sync/spinlock.h @@ -49,6 +49,20 @@ __PRIVILEGED_CODE inline void spin_unlock_irqrestore(spinlock& lock, irq_state s cpu::irq_restore(state.flags); } +class lock_guard { +public: + explicit lock_guard(spinlock& lk) : m_lock(lk) { spin_lock(lk); } + ~lock_guard() { spin_unlock(m_lock); } + + lock_guard(const lock_guard&) = delete; + lock_guard& operator=(const lock_guard&) = delete; + lock_guard(lock_guard&&) = delete; + lock_guard& operator=(lock_guard&&) = delete; + +private: + spinlock& m_lock; +}; + class irq_lock_guard { public: /** From 27ade60d94daee4149f488f2940294d35829c458 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 11:52:12 -0700 Subject: [PATCH 10/20] feat(net): added the ARP neighbor table with resolution and aging policies --- kernel/net/arp.cpp | 332 ++++++++++++++++++++++++++++++++++++++- kernel/net/arp.h | 117 +++++++++++++- kernel/net/interface.cpp | 3 +- kernel/net/net.cpp | 14 +- kernel/net/net.h | 1 + kernel/net/packet.h | 9 +- 6 files changed, 459 insertions(+), 17 deletions(-) diff --git a/kernel/net/arp.cpp b/kernel/net/arp.cpp index 949a6e1c..1917e38e 100644 --- a/kernel/net/arp.cpp +++ b/kernel/net/arp.cpp @@ -1,11 +1,14 @@ #include "net/arp.h" #include "net/packet.h" #include "net/interface.h" +#include "clock/clock.h" #include "common/logging.h" namespace net { namespace arp { +static arp_table g_table; + static int32_t drop(interface* iface, packet* pkt, int32_t rc) { iface->record_packet_dropped(); packet::free(pkt); @@ -18,6 +21,246 @@ static int32_t reject(interface* iface, packet* pkt, int32_t rc) { return rc; } +static void move_all(packet_list& from, packet_list& to) { + while (packet* pkt = from.pop_front()) { + to.push_back(pkt); + } +} + +static void drop_all(packet_list& list) { + while (packet* pkt = list.pop_front()) { + pkt->iface()->record_packet_dropped(); + packet::free(pkt); + } +} + +// Broadcasts a request for `ip` from `iface`. Requests exist only +// for pending table entries, so this stays private to the module. +static int32_t send_arp_request(interface* iface, const ipv4::ipv4_addr& ip) { + packet* pkt = packet::alloc(); + if (!pkt) { + return ERR_NO_MEMORY; + } + + // Leave room for the link header in front of the payload + uint8_t* body = pkt->reserve(eth::HEADER_LEN) ? pkt->put(HEADER_LEN) : nullptr; + if (!body) { + packet::free(pkt); + return ERR_INVALID; + } + + arp_header* hdr = reinterpret_cast(body); + hdr->hw_type = htons(HW_TYPE_ETHERNET); + hdr->proto_type = htons(PROTO_TYPE_IPV4); + hdr->hw_len = eth::MAC_ADDR_LEN; + hdr->proto_len = ipv4::ADDR_LEN; + hdr->opcode = htons(OP_REQUEST); + hdr->sender_hw_addr = iface->mac(); + hdr->sender_proto_addr = iface->ipv4_conf().address; + hdr->target_hw_addr = {}; + hdr->target_proto_addr = ip; + + pkt->set_iface(iface); + return output(pkt, eth::BROADCAST_ADDR); +} + +void arp_table::init() { + m_lock = sync::SPINLOCK_INIT; + for (size_t i = 0; i < TABLE_SIZE; i++) { + m_entries[i].queue.init(); + clear_entry(m_entries[i]); + } +} + +void arp_table::clear_entry(arp_entry& entry) { + entry.state = arp_entry_state::empty; + entry.iface = nullptr; + entry.attempts = 0; + entry.ip = {}; + entry.mac = {}; + entry.timestamp = 0; +} + +arp_entry* arp_table::find_entry(interface* iface, const ipv4::ipv4_addr& ip) { + for (size_t i = 0; i < TABLE_SIZE; i++) { + arp_entry& entry = m_entries[i]; + if (entry.state != arp_entry_state::empty && entry.iface == iface && entry.ip == ip) { + return &entry; + } + } + return nullptr; +} + +// An empty slot or the oldest resolved entry, never a pending one, so taking +// it costs no queued packets. Returns nullptr when every entry is pending. +arp_entry* arp_table::take_free_entry() { + arp_entry* oldest_resolved = nullptr; + + for (size_t i = 0; i < TABLE_SIZE; i++) { + arp_entry& entry = m_entries[i]; + if (entry.state == arp_entry_state::empty) { + return &entry; + } + + if (entry.state == arp_entry_state::resolved && + (!oldest_resolved || entry.timestamp < oldest_resolved->timestamp)) { + oldest_resolved = &entry; + } + } + + if (oldest_resolved) { + clear_entry(*oldest_resolved); + } + + return oldest_resolved; +} + +// Falls back to evicting the oldest pending entry, handing its packets to `dropped` +arp_entry* arp_table::allocate_entry(packet_list& dropped) { + arp_entry* entry = take_free_entry(); + if (entry) { + return entry; + } + + arp_entry* oldest_pending = &m_entries[0]; + for (size_t i = 1; i < TABLE_SIZE; i++) { + if (m_entries[i].timestamp < oldest_pending->timestamp) { + oldest_pending = &m_entries[i]; + } + } + + move_all(oldest_pending->queue, dropped); + clear_entry(*oldest_pending); + return oldest_pending; +} + +arp_send_action arp_table::resolve( + interface* iface, + const ipv4::ipv4_addr& ip, + packet* pkt, + uint64_t timestamp, + eth::mac_addr* out_mac, + bool* send_request, + packet_list& dropped +) { + sync::lock_guard guard(m_lock); + *send_request = false; + + // If we have a resolved entry for this IP, we can use it to send the packet. + arp_entry* entry = find_entry(iface, ip); + if (entry && entry->state == arp_entry_state::resolved) { + *out_mac = entry->mac; + return arp_send_action::transmit; + } + + // Unknown address: open a pending entry and have the caller ask for it. + // A pending hit means a request is already in flight, the sweep retries it. + if (!entry) { + entry = allocate_entry(dropped); + entry->state = arp_entry_state::pending; + entry->iface = iface; + entry->ip = ip; + entry->attempts = 1; + entry->timestamp = timestamp; + *send_request = true; + } + + // The oldest waiting packet gives way, the newest is the most likely still wanted + if (pkt) { + if (entry->queue.size() >= PENDING_QUEUE_DEPTH) { + dropped.push_back(entry->queue.pop_front()); + } + + entry->queue.push_back(pkt); + } + + return arp_send_action::queued; +} + +void arp_table::sweep( + uint64_t timestamp, + packet_list& dropped, + arp_retry* retries, + size_t* retry_count +) { + sync::lock_guard guard(m_lock); + *retry_count = 0; + + for (size_t i = 0; i < TABLE_SIZE; i++) { + arp_entry& entry = m_entries[i]; + if (entry.state == arp_entry_state::empty) { + continue; + } + + uint64_t age = timestamp - entry.timestamp; + if (entry.state == arp_entry_state::resolved) { + if (age >= ENTRY_LIFETIME_NS) { + clear_entry(entry); + } + + continue; + } + + if (age < REQUEST_RETRY_NS) { + continue; + } + + // Pending with no reply for a full retry interval: ask again, + // or give up and release the packets that waited on it. + if (entry.attempts >= MAX_REQUEST_ATTEMPTS) { + move_all(entry.queue, dropped); + clear_entry(entry); + continue; + } + + entry.attempts++; + entry.timestamp = timestamp; + retries[*retry_count] = { entry.iface, entry.ip }; + + (*retry_count)++; + } +} + +bool arp_table::update_entry( + interface* iface, + const ipv4::ipv4_addr& ip, + const eth::mac_addr& mac, + uint64_t timestamp, + bool create, + packet_list& flushed +) { + sync::lock_guard guard(m_lock); + + arp_entry* entry = find_entry(iface, ip); + if (!entry) { + if (!create) { + return false; + } + + entry = take_free_entry(); + if (!entry) { + return false; + } + + entry->iface = iface; + entry->ip = ip; + } + + bool completed = entry->state == arp_entry_state::pending; + entry->state = arp_entry_state::resolved; + entry->mac = mac; + entry->attempts = 0; + entry->timestamp = timestamp; + move_all(entry->queue, flushed); + + return completed; +} + +int32_t init() { + g_table.init(); + return OK; +} + int32_t input(packet* pkt) { if (!pkt) { log::warn("arp: input called with no packet"); @@ -46,15 +289,53 @@ int32_t input(packet* pkt) { return drop(iface, pkt, OK); } - // Requests for other hosts are normal traffic, not drops + // No address yet, so there is nothing to learn or answer const ipv4::ipv4_config& conf = iface->ipv4_conf(); - if (!conf.configured() || hdr->target_proto_addr != conf.address) { + if (!conf.configured()) { packet::free(pkt); return OK; } - if (ntohs(hdr->opcode) != OP_REQUEST) { - return drop(iface, pkt, OK); + bool is_for_local_ip = hdr->target_proto_addr == conf.address; + bool is_request = ntohs(hdr->opcode) == OP_REQUEST; + + // Never learn from address probes, group addresses, or our own address + bool learnable_sender = + !hdr->sender_proto_addr.is_unspecified() && + !hdr->sender_hw_addr.is_multicast() && + hdr->sender_proto_addr != conf.address; + + if (learnable_sender) { + packet_list flushed; + flushed.init(); + + // Only requests addressed to this host may create entries + bool completed = g_table.update_entry( + iface, + hdr->sender_proto_addr, + hdr->sender_hw_addr, + clock::now_ns(), + is_for_local_ip && is_request, // prevent ARP poisoning + flushed + ); + + if (completed) { + const uint8_t* ip = hdr->sender_proto_addr.bytes; + const uint8_t* mac = hdr->sender_hw_addr.bytes; + log::info("arp: %u.%u.%u.%u is at %02x:%02x:%02x:%02x:%02x:%02x", + ip[0], ip[1], ip[2], ip[3], mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } + + // Packets that waited on this address can leave now that the lock is released + while (packet* waiting = flushed.pop_front()) { + eth::output(waiting, hdr->sender_hw_addr, eth::TYPE_IPV4); + } + } + + // Requests for other hosts and replies are normal traffic, not drops + if (!is_for_local_ip || !is_request) { + packet::free(pkt); + return OK; } // RFC 826: the request becomes the reply in place, sender and target swapped @@ -74,7 +355,48 @@ int32_t output(packet* pkt, const eth::mac_addr& dest) { return eth::output(pkt, dest, eth::TYPE_ARP); } -void sweep(uint64_t) { +int32_t resolve(interface* iface, const ipv4::ipv4_addr& ip, eth::mac_addr* out) { + if (!iface || !out) { + return ERR_INVALID; + } + + packet_list dropped; + dropped.init(); + + bool request = false; + + arp_send_action action = g_table.resolve( + iface, + ip, + nullptr, + clock::now_ns(), + out, + &request, + dropped + ); + + drop_all(dropped); + + // A lost request is retried by the sweep, so its result does not change the answer + if (request) { + send_arp_request(iface, ip); + } + + return action == arp_send_action::transmit ? OK : ERR_PENDING; +} + +void sweep(uint64_t ts) { + packet_list dropped; + dropped.init(); + arp_retry retries[TABLE_SIZE]; + size_t retry_count = 0; + + g_table.sweep(ts, dropped, retries, &retry_count); + drop_all(dropped); + + for (size_t i = 0; i < retry_count; i++) { + send_arp_request(retries[i].iface, retries[i].ip); + } } } // namespace arp diff --git a/kernel/net/arp.h b/kernel/net/arp.h index 51893f47..0c542950 100644 --- a/kernel/net/arp.h +++ b/kernel/net/arp.h @@ -1,10 +1,16 @@ #ifndef STELLUX_NET_ARP_H #define STELLUX_NET_ARP_H +#include "common/list.h" #include "net/eth.h" #include "net/ipv4.h" +#include "net/packet.h" +#include "sync/spinlock.h" namespace net { + +class interface; + namespace arp { constexpr size_t HEADER_LEN = 28; @@ -15,11 +21,12 @@ constexpr uint16_t OP_REQUEST = 1; constexpr uint16_t OP_REPLY = 2; // ARP table configuration -constexpr uint64_t NS_PER_SEC = 1000000000ULL; -constexpr size_t TABLE_SIZE = 16; -constexpr uint64_t ENTRY_LIFETIME_NS = 300 * NS_PER_SEC; // resolved entries expire after this -constexpr uint64_t PENDING_TIMEOUT_NS = 5 * NS_PER_SEC; // unanswered requests fail after this -constexpr size_t PENDING_QUEUE_DEPTH = 3; // packets held per unresolved entry +constexpr uint64_t NS_PER_SEC = 1000000000ULL; +constexpr size_t TABLE_SIZE = 16; +constexpr uint64_t ENTRY_LIFETIME_NS = 300 * NS_PER_SEC; // resolved entries expire after this +constexpr uint64_t REQUEST_RETRY_NS = 1 * NS_PER_SEC; // pending entries resend their request at this interval +constexpr uint8_t MAX_REQUEST_ATTEMPTS = 3; // pending entries fail after this many requests +constexpr size_t PENDING_QUEUE_DEPTH = 3; // packets held per unresolved entry /** * ARP packet for Ethernet over IPv4 (RFC 826). The standard header is generic, @@ -53,9 +60,97 @@ enum class arp_entry_state : uint8_t { resolved = 2, }; +using packet_list = list::head; + +struct arp_entry { + interface* iface; + arp_entry_state state; + uint8_t attempts; // Requests sent while pending + ipv4::ipv4_addr ip; + eth::mac_addr mac; + uint64_t timestamp; // Last request sent while pending, last confirmed while resolved + packet_list queue; +}; + +// Answer from the table when asked how to send to an address: the MAC is known +// and the caller transmits now, or the table has stored the packet in a pending entry. +enum class arp_send_action : uint8_t { + transmit = 0, + queued = 1, +}; + +struct arp_retry { + interface* iface; + ipv4::ipv4_addr ip; +}; + +class arp_table { +public: + arp_table() = default; + ~arp_table() = default; + + void init(); + + /* + * Finds the MAC for `ip` so a packet can be sent to it. Fills `out_mac` if + * the entry exists, otherwise stores `pkt` on a pending entry and sets + * `send_request` when a request must be broadcast. Packets evicted to make + * room are returned in `dropped` for the caller to free. + */ + arp_send_action resolve( + interface* iface, + const ipv4::ipv4_addr& ip, + packet* pkt, + uint64_t timestamp, + eth::mac_addr* out_mac, + bool* send_request, + packet_list& dropped + ); + + /* + * Records the ARP entry. Refreshes an existing entry, creates one only + * when `create` is set. Packets that waited on the entry are returned in + * `flushed` for the caller to send. + * Returns true when a pending entry was resolved. + */ + bool update_entry( + interface* iface, + const ipv4::ipv4_addr& ip, + const eth::mac_addr& mac, + uint64_t timestamp, + bool create, + packet_list& flushed + ); + + /* + * Expires resolved entries past their lifetime and retries or fails pending + * ones. Failed entries return their packets in `dropped` for the caller to + * free, entries due for another request are listed in `retries`. + */ + void sweep( + uint64_t timestamp, + packet_list& dropped, + arp_retry* retries, + size_t* retry_count + ); + +private: + arp_entry* find_entry(interface* iface, const ipv4::ipv4_addr& ip); + arp_entry* take_free_entry(); + arp_entry* allocate_entry(packet_list& dropped); + void clear_entry(arp_entry& entry); + + sync::spinlock m_lock; + arp_entry m_entries[TABLE_SIZE]; +}; + /* - * Consumes an ARP packet. A request for this host's - * address is answered in place, everything else is freed. + * Sets up the ARP table + */ +int32_t init(); + +/* + * Consumes an ARP packet. */ int32_t input(packet* pkt); @@ -66,7 +161,13 @@ int32_t input(packet* pkt); int32_t output(packet* pkt, const eth::mac_addr& dest); /* - * Ages the table on every daemon pass. `ts` is the current monotonic time. + * Fills `out` mac address and returns OK when `ip` is resolved, + * otherwise requests it and returns ERR_PENDING. + */ +int32_t resolve(interface* iface, const ipv4::ipv4_addr& ip, eth::mac_addr* out); + +/* + * Ages the table on every netstkd daemon pass. `ts` is the current monotonic time. */ void sweep(uint64_t ts); diff --git a/kernel/net/interface.cpp b/kernel/net/interface.cpp index 6bc8981e..92b90c46 100644 --- a/kernel/net/interface.cpp +++ b/kernel/net/interface.cpp @@ -18,7 +18,8 @@ interface::interface() , m_name{} , m_counters{} , m_mac{} - , m_mtu(0) {} + , m_mtu(0) + , m_ipv4_conf{} {} int32_t interface::receive(packet* pkt) { if (!pkt) { diff --git a/kernel/net/net.cpp b/kernel/net/net.cpp index c4ff1367..9f4a8fb3 100644 --- a/kernel/net/net.cpp +++ b/kernel/net/net.cpp @@ -1,5 +1,6 @@ #include "net/net.h" -#include "net/interface.h" +#include "clock/clock.h" +#include "net/arp.h" #include "sched/sched.h" #include "dynpriv/dynpriv.h" #include "common/logging.h" @@ -10,6 +11,11 @@ static void netstk_daemon_task_start(void*) { // All the network stack bookkeeping will be done here while (true) { + uint64_t ts = clock::now_ns(); + + // ARP layer sweep + arp::sweep(ts); + RUN_ELEVATED(sched::sleep_ms(100)); } @@ -20,6 +26,12 @@ __PRIVILEGED_CODE int32_t init() { // Create the interface table // ... + int32_t rc = arp::init(); + if (rc != OK) { + log::error("net: arp::init failed: %d", rc); + return rc; + } + // Create and start the network stack daemon task sched::task* daemon = sched::create_kernel_task( netstk_daemon_task_start, nullptr, "netstkd"); diff --git a/kernel/net/net.h b/kernel/net/net.h index ebd4e1e6..e5a0d1dd 100644 --- a/kernel/net/net.h +++ b/kernel/net/net.h @@ -12,6 +12,7 @@ constexpr int32_t ERR_BUSY = -2; // no transmit slot is free constexpr int32_t ERR_TOO_LARGE = -3; // frame does not fit in one link transmission constexpr int32_t ERR_DOWN = -4; // interface is administratively disabled constexpr int32_t ERR_NO_MEMORY = -5; // an allocation or task creation failed +constexpr int32_t ERR_PENDING = -6; // packet processing is in progress, retry later /** * Initialize the network stack and start its daemon/bookkeeping task. diff --git a/kernel/net/packet.h b/kernel/net/packet.h index 42453a78..dc2a1ce3 100644 --- a/kernel/net/packet.h +++ b/kernel/net/packet.h @@ -1,14 +1,14 @@ #ifndef STELLUX_NET_PACKET_H #define STELLUX_NET_PACKET_H -#include "common/types.h" +#include "common/list.h" namespace net { class interface; constexpr size_t PACKET_OBJECT_SIZE = 2048; -constexpr size_t PACKET_METADATA_SIZE = 24; +constexpr size_t PACKET_METADATA_SIZE = 40; constexpr size_t PACKET_CAPACITY = PACKET_OBJECT_SIZE - PACKET_METADATA_SIZE; /** @@ -102,6 +102,11 @@ class packet { interface* iface() const { return m_iface; } void set_iface(interface* iface) { m_iface = iface; } + // Linkage for the one queue that owns the packet while it waits, such as an + // ARP entry or a socket receive queue. A packet is on at most one queue and + // must be removed from it before anything else touches it. + list::node link; + private: static constexpr uint16_t HEADER_UNSET = 0xFFFF; From 8c10d1071fc9f798d932f8af09e545f7271f5c13 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 13:07:58 -0700 Subject: [PATCH 11/20] feat(net): added the interface registry --- kernel/drivers/net/virtio_net.cpp | 7 +- kernel/net/interface.cpp | 109 ++++++++++++++++++++++++++++++ kernel/net/interface.h | 21 ++++++ kernel/net/net.h | 1 + 4 files changed, 137 insertions(+), 1 deletion(-) diff --git a/kernel/drivers/net/virtio_net.cpp b/kernel/drivers/net/virtio_net.cpp index 5d3b8a73..e9a05bb6 100644 --- a/kernel/drivers/net/virtio_net.cpp +++ b/kernel/drivers/net/virtio_net.cpp @@ -406,11 +406,16 @@ int32_t virtio_net_driver::attach() { // Link identity for the stack. The interface comes // up here until the stack owns that decision. - string::memcpy(net::interface::m_name, "eth0", 5); m_mtu = net::eth::MTU; m_ipv4_conf = QEMU_STATIC_IPV4; m_enabled = true; + rc = net::register_interface(this, "eth"); + if (rc != net::OK) { + log::error("virtio-net: interface registration failed: %d", rc); + return rc; + } + rc = init_queues(); if (rc != 0) { write_status(read_status() | VIRTIO_STATUS_FAILED); diff --git a/kernel/net/interface.cpp b/kernel/net/interface.cpp index 92b90c46..fc28516e 100644 --- a/kernel/net/interface.cpp +++ b/kernel/net/interface.cpp @@ -2,16 +2,57 @@ #include "net/packet.h" #include "net/eth.h" #include "sync/atomic.h" +#include "sync/spinlock.h" +#include "common/string.h" namespace net { // IDs start at 1 since 0 means "no interface" static sync::atomic g_next_interface_id {1}; +// Slots are filled once and never removed. The count is published with a +// release store so readers can index below it without taking the lock. +static sync::spinlock g_registry_lock = sync::SPINLOCK_INIT; +static interface* g_interfaces[MAX_INTERFACES]; +static sync::atomic g_interface_count {0}; + static uint64_t generate_interface_id() { return g_next_interface_id.fetch_add_relaxed(1); } +// Writes `value` in decimal at `pos`, returns the new position or `cap` when +// the buffer is too small. +static size_t append_decimal(char* buf, size_t cap, size_t pos, size_t value) { + char digits[20]; + size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value != 0); + + if (pos + count >= cap) { + return cap; + } + + while (count > 0) { + buf[pos++] = digits[--count]; + } + + return pos; +} + +// Interfaces already registered under `prefix`, locked by the caller +static size_t count_prefix(const char* prefix, size_t prefix_len) { + size_t matches = 0; + size_t total = g_interface_count.load_relaxed(); + for (size_t i = 0; i < total; i++) { + if (string::strncmp(g_interfaces[i]->name(), prefix, prefix_len) == 0) { + matches++; + } + } + return matches; +} + interface::interface() : m_id(generate_interface_id()) , m_enabled(false) @@ -40,4 +81,72 @@ int32_t interface::receive(packet* pkt) { return eth::input(pkt); } +void interface::set_name(const char* name) { + size_t len = string::strlen(name); + if (len >= IFACE_NAME_MAX) { + len = IFACE_NAME_MAX - 1; + } + + string::memcpy(m_name, name, len); + m_name[len] = '\0'; +} + +int32_t register_interface(interface* iface, const char* prefix) { + if (!iface || !prefix) { + return ERR_INVALID; + } + + size_t prefix_len = string::strlen(prefix); + if (prefix_len == 0 || prefix_len >= IFACE_NAME_MAX) { + return ERR_INVALID; + } + + sync::lock_guard guard(g_registry_lock); + + size_t count = g_interface_count.load_relaxed(); + if (count >= MAX_INTERFACES) { + return ERR_FULL; + } + + char name[IFACE_NAME_MAX]; + string::memcpy(name, prefix, prefix_len); + + size_t end = append_decimal(name, IFACE_NAME_MAX, prefix_len, count_prefix(prefix, prefix_len)); + if (end >= IFACE_NAME_MAX) { + return ERR_INVALID; + } + + name[end] = '\0'; + iface->set_name(name); + + g_interfaces[count] = iface; + g_interface_count.store_release(count + 1); + return OK; +} + +size_t interface_count() { + return g_interface_count.load_acquire(); +} + +interface* interface_at(size_t index) { + if (index >= g_interface_count.load_acquire()) { + return nullptr; + } + + return g_interfaces[index]; +} + +interface* find_interface_by_address(const ipv4::ipv4_addr& addr) { + size_t count = g_interface_count.load_acquire(); + for (size_t i = 0; i < count; i++) { + const ipv4::ipv4_config& conf = g_interfaces[i]->ipv4_conf(); + + if (conf.configured() && conf.address == addr) { + return g_interfaces[i]; + } + } + + return nullptr; +} + } // namespace net diff --git a/kernel/net/interface.h b/kernel/net/interface.h index 284726eb..d3a86934 100644 --- a/kernel/net/interface.h +++ b/kernel/net/interface.h @@ -11,6 +11,7 @@ namespace net { class packet; constexpr size_t IFACE_NAME_MAX = 16; +constexpr size_t MAX_INTERFACES = 8; struct iface_counters { uint64_t frames_in; @@ -57,12 +58,17 @@ class interface { int32_t receive(packet* pkt); uint64_t id() const { return m_id; } + const char* name() const { return m_name; } + bool enabled() const { return m_enabled; } const eth::mac_addr& mac() const { return m_mac; } uint16_t mtu() const { return m_mtu; } const ipv4::ipv4_config& ipv4_conf() const { return m_ipv4_conf; } + // Assigned by the registry, truncated to IFACE_NAME_MAX + void set_name(const char* name); + void record_packet_dropped() { m_counters.drops.fetch_add_relaxed(1); } void record_iface_error() { m_counters.errors.fetch_add_relaxed(1); } @@ -80,6 +86,21 @@ class interface { ipv4::ipv4_config m_ipv4_conf; }; +/* + * Adds `iface` to the stack and names it ``, numbering within the + * prefix, such as eth0 or lo0. The registry owns the set and the names, the + * driver owns the object. + */ +int32_t register_interface(interface* iface, const char* prefix); + +size_t interface_count(); +interface* interface_at(size_t index); + +/* + * Finds the interface configured with `addr` or returns nullptr. + */ +interface* find_interface_by_address(const ipv4::ipv4_addr& addr); + } // namespace net #endif // STELLUX_NET_INTERFACE_H diff --git a/kernel/net/net.h b/kernel/net/net.h index e5a0d1dd..f622f903 100644 --- a/kernel/net/net.h +++ b/kernel/net/net.h @@ -13,6 +13,7 @@ constexpr int32_t ERR_TOO_LARGE = -3; // frame does not fit in one link transmis constexpr int32_t ERR_DOWN = -4; // interface is administratively disabled constexpr int32_t ERR_NO_MEMORY = -5; // an allocation or task creation failed constexpr int32_t ERR_PENDING = -6; // packet processing is in progress, retry later +constexpr int32_t ERR_FULL = -7; // a fixed-size table has no free slot /** * Initialize the network stack and start its daemon/bookkeeping task. From 50ec6715689e9cd8c70a3aa4e9a87464cf6af043 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 13:19:37 -0700 Subject: [PATCH 12/20] feat(net): added the IPv4 header --- kernel/net/checksum.cpp | 34 +++++++++++ kernel/net/checksum.h | 27 +++++++++ kernel/net/eth.cpp | 11 +--- kernel/net/ipv4.cpp | 121 ++++++++++++++++++++++++++++++++++++++++ kernel/net/ipv4.h | 58 ++++++++++++++++++- 5 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 kernel/net/checksum.cpp create mode 100644 kernel/net/checksum.h create mode 100644 kernel/net/ipv4.cpp diff --git a/kernel/net/checksum.cpp b/kernel/net/checksum.cpp new file mode 100644 index 00000000..a9f0128b --- /dev/null +++ b/kernel/net/checksum.cpp @@ -0,0 +1,34 @@ +#include "net/checksum.h" + +namespace net { + +uint32_t checksum_accumulate(uint32_t sum, const void* data, size_t len) { + const uint8_t* bytes = static_cast(data); + + // Words are summed as they appear on the wire, most significant byte first + while (len >= 2) { + sum += static_cast((bytes[0] << 8) | bytes[1]); + bytes += 2; + len -= 2; + } + + // An odd trailing byte is the high half of a word padded with zero + if (len == 1) { + sum += static_cast(bytes[0] << 8); + } + + return sum; +} + +uint16_t checksum_finish(uint32_t sum) { + while (sum >> 16) { + sum = (sum & 0xFFFF) + (sum >> 16); + } + return static_cast(~sum); +} + +uint16_t checksum(const void* data, size_t len) { + return checksum_finish(checksum_accumulate(0, data, len)); +} + +} // namespace net diff --git a/kernel/net/checksum.h b/kernel/net/checksum.h new file mode 100644 index 00000000..6383b04f --- /dev/null +++ b/kernel/net/checksum.h @@ -0,0 +1,27 @@ +#ifndef STELLUX_NET_CHECKSUM_H +#define STELLUX_NET_CHECKSUM_H + +#include "common/types.h" + +namespace net { + +/* + * Adds `len` bytes to a running RFC 1071 one's complement sum, so a header + * and its pseudo-header can be summed in separate calls. + */ +uint32_t checksum_accumulate(uint32_t sum, const void* data, size_t len); + +/* + * Folds the carries and complements the sum. Store the result with `htons`. + */ +uint16_t checksum_finish(uint32_t sum); + +/* + * Checksum of one buffer. Over a header whose checksum field is filled in, + * the result is zero when the header is intact. + */ +uint16_t checksum(const void* data, size_t len); + +} // namespace net + +#endif // STELLUX_NET_CHECKSUM_H diff --git a/kernel/net/eth.cpp b/kernel/net/eth.cpp index 9410c7b8..e2f9ef34 100644 --- a/kernel/net/eth.cpp +++ b/kernel/net/eth.cpp @@ -2,6 +2,7 @@ #include "net/packet.h" #include "net/interface.h" #include "net/arp.h" +#include "net/ipv4.h" #include "common/logging.h" namespace net { @@ -19,12 +20,6 @@ static int32_t reject(interface* iface, packet* pkt, int32_t rc) { return rc; } -static void log_frame(const char* proto, const eth_header* hdr, size_t payload_len) { - const uint8_t* src = hdr->src.bytes; - log::info("eth: %s frame from %02x:%02x:%02x:%02x:%02x:%02x, %lu payload bytes", - proto, src[0], src[1], src[2], src[3], src[4], src[5], payload_len); -} - int32_t input(packet* pkt) { if (!pkt) { log::warn("eth: input called with no packet"); @@ -64,9 +59,7 @@ int32_t input(packet* pkt) { case TYPE_ARP: return arp::input(pkt); case TYPE_IPV4: - log_frame("IPv4", hdr, pkt->length()); - packet::free(pkt); - break; + return ipv4::input(pkt); default: return drop(iface, pkt, OK); } diff --git a/kernel/net/ipv4.cpp b/kernel/net/ipv4.cpp new file mode 100644 index 00000000..5ae59c67 --- /dev/null +++ b/kernel/net/ipv4.cpp @@ -0,0 +1,121 @@ +#include "net/ipv4.h" +#include "net/interface.h" +#include "net/checksum.h" +#include "common/logging.h" + +namespace net { +namespace ipv4 { + +static int32_t drop(interface* iface, packet* pkt, int32_t rc) { + iface->record_packet_dropped(); + packet::free(pkt); + return rc; +} + +static int32_t reject(interface* iface, packet* pkt, int32_t rc) { + iface->record_iface_error(); + packet::free(pkt); + return rc; +} + +bool ipv4_config::is_subnet_broadcast(const ipv4_addr& addr) const { + if (!configured() || !addr.in_same_subnet(address, netmask)) { + return false; + } + + for (size_t i = 0; i < ADDR_LEN; i++) { + uint8_t host_bits = static_cast(~netmask.bytes[i]); + if ((addr.bytes[i] & host_bits) != host_bits) { + return false; + } + } + + return true; +} + +int32_t input(packet* pkt) { + if (!pkt) { + log::warn("ipv4: input called with no packet"); + return ERR_INVALID; + } + + interface* iface = pkt->iface(); + if (!iface) { + log::warn("ipv4: input called with a packet that has no interface"); + packet::free(pkt); + return ERR_INVALID; + } + + if (pkt->length() < HEADER_LEN) { + return reject(iface, pkt, ERR_INVALID); + } + + const ipv4_header* hdr = reinterpret_cast(pkt->data()); + if (hdr->version() != VERSION || hdr->ihl() < MIN_IHL || hdr->header_len() > pkt->length()) { + return reject(iface, pkt, ERR_INVALID); + } + + if (checksum(hdr, hdr->header_len()) != 0) { + return reject(iface, pkt, ERR_INVALID); + } + + // The header's own length is authoritative, anything past it is link padding + size_t total_len = ntohs(hdr->total_len); + if (total_len < hdr->header_len() || total_len > pkt->length()) { + return reject(iface, pkt, ERR_INVALID); + } + + pkt->trim(total_len); + + // Weak host model: any address this host owns is accepted on any interface, + // which is what local delivery through loopback needs. + bool for_local_host = find_interface_by_address(hdr->dst) != nullptr || + hdr->dst.is_broadcast() || + iface->ipv4_conf().is_subnet_broadcast(hdr->dst); + + if (!for_local_host) { + packet::free(pkt); + return OK; + } + + // Fragments are not reassembled + if (hdr->frag_off() != 0 || (hdr->flags() & FLAG_MF) != 0) { + return drop(iface, pkt, OK); + } + + pkt->mark_network_header(); + (void)pkt->pull(hdr->header_len()); + + const uint8_t* src = hdr->src.bytes; + switch (hdr->proto) { + case PROTO_ICMP: + log::info("ipv4: ICMP datagram from %u.%u.%u.%u, %lu payload bytes", + src[0], src[1], src[2], src[3], pkt->length()); + packet::free(pkt); + break; + case PROTO_UDP: + log::info("ipv4: UDP datagram from %u.%u.%u.%u, %lu payload bytes", + src[0], src[1], src[2], src[3], pkt->length()); + packet::free(pkt); + break; + case PROTO_TCP: + log::info("ipv4: TCP datagram from %u.%u.%u.%u, %lu payload bytes", + src[0], src[1], src[2], src[3], pkt->length()); + packet::free(pkt); + break; + default: + return drop(iface, pkt, OK); + } + + return OK; +} + +int32_t output(packet* pkt, const ipv4_addr& dest, uint8_t protocol) { + (void)dest; + (void)protocol; + packet::free(pkt); + return ERR_INVALID; +} + +} // namespace ipv4 +} // namespace net diff --git a/kernel/net/ipv4.h b/kernel/net/ipv4.h index e892ffe3..49bf2ba3 100644 --- a/kernel/net/ipv4.h +++ b/kernel/net/ipv4.h @@ -1,7 +1,8 @@ #ifndef STELLUX_NET_IPV4_H #define STELLUX_NET_IPV4_H -#include "common/types.h" +#include "net/packet.h" +#include "net/byteorder.h" #include "common/string.h" namespace net { @@ -9,6 +10,21 @@ namespace ipv4 { constexpr size_t ADDR_LEN = 4; +constexpr size_t HEADER_LEN = 20; +constexpr size_t MAX_HEADER_LEN = 60; + +constexpr uint8_t VERSION = 4; +constexpr uint8_t MIN_IHL = 5; +constexpr uint8_t DEFAULT_TTL = 64; + +constexpr uint8_t PROTO_ICMP = 1; +constexpr uint8_t PROTO_TCP = 6; +constexpr uint8_t PROTO_UDP = 17; + +constexpr uint16_t FLAG_DF = 0x4000; +constexpr uint16_t FLAG_MF = 0x2000; +constexpr uint16_t FRAG_OFFSET_MASK = 0x1FFF; + /** * A 32-bit IPv4 address held as its four wire bytes, most significant first, * so it copies straight into and out of headers and there is never a byte @@ -61,8 +77,48 @@ struct ipv4_config { ipv4_addr gateway; // Router for destinations outside the subnet, unspecified if none bool configured() const { return !address.is_unspecified(); } + + // True for the all-host-bits address of the current subnet + bool is_subnet_broadcast(const ipv4_addr& addr) const; }; +struct ipv4_header { + uint8_t version_ihl; + uint8_t tos; + uint16_t total_len; + uint16_t id; + uint16_t fl_frag_off; + uint8_t ttl; + uint8_t proto; + uint16_t checksum; + ipv4_addr src; + ipv4_addr dst; + + inline uint8_t version() const { return version_ihl >> 4; } + inline uint8_t ihl() const { return version_ihl & 0x0F; } + inline size_t header_len() const { return ihl() * sizeof(uint32_t); } + + void set_version_ihl(uint8_t version, uint8_t ihl) { + version_ihl = static_cast((version << 4) | (ihl & 0x0F)); + } + + inline uint16_t flags() const { return ntohs(fl_frag_off) & ~FRAG_OFFSET_MASK; } + inline uint16_t frag_off() const { return ntohs(fl_frag_off) & FRAG_OFFSET_MASK; } + +} __attribute__((packed)); +static_assert(sizeof(ipv4_header) == HEADER_LEN); + +/* + * Entry point into the IP layer of the network stack + */ +int32_t input(packet* pkt); + +/* + * Transmits a constructed packet for a given `dest` ipv4 address + */ +int32_t output(packet* pkt, const ipv4_addr& dest, uint8_t protocol); + + } // namespace ipv4 } // namespace net From e13f712e8bbc8548a0c2e6378f40255f6455b15a Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 17:42:06 -0700 Subject: [PATCH 13/20] feat(net): added route lookup with destination classification --- kernel/net/net.h | 1 + kernel/net/route.cpp | 88 ++++++++++++++++++++++++++++++++++++++++++++ kernel/net/route.h | 40 ++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 kernel/net/route.cpp create mode 100644 kernel/net/route.h diff --git a/kernel/net/net.h b/kernel/net/net.h index f622f903..2a8fa417 100644 --- a/kernel/net/net.h +++ b/kernel/net/net.h @@ -14,6 +14,7 @@ constexpr int32_t ERR_DOWN = -4; // interface is administratively disabled constexpr int32_t ERR_NO_MEMORY = -5; // an allocation or task creation failed constexpr int32_t ERR_PENDING = -6; // packet processing is in progress, retry later constexpr int32_t ERR_FULL = -7; // a fixed-size table has no free slot +constexpr int32_t ERR_NO_ROUTE = -8; // no interface can reach the destination /** * Initialize the network stack and start its daemon/bookkeeping task. diff --git a/kernel/net/route.cpp b/kernel/net/route.cpp new file mode 100644 index 00000000..249a0509 --- /dev/null +++ b/kernel/net/route.cpp @@ -0,0 +1,88 @@ +#include "net/route.h" +#include "net/interface.h" + +namespace net { +namespace route { + +// Matches `dest` against what `iface` reaches directly, ignoring its gateway +static bool match_link(interface* iface, const ipv4::ipv4_addr& dest, route_result* out) { + const ipv4::ipv4_config& conf = iface->ipv4_conf(); + if (!conf.configured()) { + return false; + } + + if (dest == conf.address) { + *out = { iface, dest, route_type::local }; + return true; + } + + if (dest.is_broadcast() || conf.is_subnet_broadcast(dest)) { + *out = { iface, dest, route_type::broadcast }; + return true; + } + + if (dest.in_same_subnet(conf.address, conf.netmask)) { + *out = { iface, dest, route_type::unicast }; + return true; + } + + return false; +} + +static bool match_gateway(interface* iface, route_result* out) { + const ipv4::ipv4_config& conf = iface->ipv4_conf(); + if (!conf.configured() || conf.gateway.is_unspecified()) { + return false; + } + + *out = { iface, conf.gateway, route_type::unicast }; + return true; +} + +int32_t lookup(const ipv4::ipv4_addr& dest, route_result* out) { + if (!out) { + return ERR_INVALID; + } + + // Every link is tried before any gateway, so an on-link host is never + // sent through a router + size_t count = interface_count(); + for (size_t i = 0; i < count; i++) { + if (match_link(interface_at(i), dest, out)) { + return OK; + } + } + + for (size_t i = 0; i < count; i++) { + if (match_gateway(interface_at(i), out)) { + return OK; + } + } + + return ERR_NO_ROUTE; +} + +int32_t lookup_on(interface* iface, const ipv4::ipv4_addr& dest, route_result* out) { + if (!iface || !out) { + return ERR_INVALID; + } + + // An unconfigured interface can still broadcast, which is how it asks for an address + if (!iface->ipv4_conf().configured()) { + if (!dest.is_broadcast()) { + return ERR_NO_ROUTE; + } + + *out = { iface, dest, route_type::broadcast }; + return OK; + } + + if (match_link(iface, dest, out) || match_gateway(iface, out)) { + return OK; + } + + return ERR_NO_ROUTE; +} + +} // namespace route +} // namespace net diff --git a/kernel/net/route.h b/kernel/net/route.h new file mode 100644 index 00000000..3f7a1d1a --- /dev/null +++ b/kernel/net/route.h @@ -0,0 +1,40 @@ +#ifndef STELLUX_NET_ROUTE_H +#define STELLUX_NET_ROUTE_H + +#include "common/types.h" +#include "net/ipv4.h" + +namespace net { + +class interface; + +namespace route { + +enum class route_type : uint8_t { + local = 0, // one of this host's own addresses + unicast = 1, // a single host, reached through `next_hop` + broadcast = 2, // every host on the interface's link +}; + +struct route_result { + interface* iface; // Interface the packet leaves through + ipv4::ipv4_addr next_hop; // The destination when on-link, otherwise the gateway + route_type type; +}; + +/* + * Chooses how to reach `dest` from any interface. + * Returns ERR_NO_ROUTE when none can. + */ +int32_t lookup(const ipv4::ipv4_addr& dest, route_result* out); + +/* + * Same purpose as `lookup`, but restricted to `iface` for callers that must send + * from a specific interface, such as one requesting an address before it has one. + */ +int32_t lookup_on(interface* iface, const ipv4::ipv4_addr& dest, route_result* out); + +} // namespace route +} // namespace net + +#endif // STELLUX_NET_ROUTE_H From 4404a354e9f33eaf861e2880491a1f9db10ff13d Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 17:43:13 -0700 Subject: [PATCH 14/20] feat(net): added IPv4 output and support for unicast datagrams through ARP resolution --- kernel/net/arp.cpp | 43 ++++++++++++++++++++++++++++++++ kernel/net/arp.h | 6 +++++ kernel/net/ipv4.cpp | 61 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/kernel/net/arp.cpp b/kernel/net/arp.cpp index 1917e38e..3a16f064 100644 --- a/kernel/net/arp.cpp +++ b/kernel/net/arp.cpp @@ -385,6 +385,49 @@ int32_t resolve(interface* iface, const ipv4::ipv4_addr& ip, eth::mac_addr* out) return action == arp_send_action::transmit ? OK : ERR_PENDING; } +int32_t resolve_and_send(packet* pkt, const ipv4::ipv4_addr& next_hop) { + if (!pkt) { + log::warn("arp: resolve_and_send called with no packet"); + return ERR_INVALID; + } + + interface* iface = pkt->iface(); + if (!iface) { + log::warn("arp: resolve_and_send called with a packet that has no interface"); + packet::free(pkt); + return ERR_INVALID; + } + + packet_list dropped; + dropped.init(); + + eth::mac_addr mac; + bool request = false; + + arp_send_action action = g_table.resolve( + iface, + next_hop, + pkt, + clock::now_ns(), + &mac, + &request, + dropped + ); + + drop_all(dropped); + + if (request) { + send_arp_request(iface, next_hop); + } + + // A queued packet belongs to the table now and leaves when the reply arrives + if (action == arp_send_action::queued) { + return OK; + } + + return eth::output(pkt, mac, eth::TYPE_IPV4); +} + void sweep(uint64_t ts) { packet_list dropped; dropped.init(); diff --git a/kernel/net/arp.h b/kernel/net/arp.h index 0c542950..57bf2a83 100644 --- a/kernel/net/arp.h +++ b/kernel/net/arp.h @@ -166,6 +166,12 @@ int32_t output(packet* pkt, const eth::mac_addr& dest); */ int32_t resolve(interface* iface, const ipv4::ipv4_addr& ip, eth::mac_addr* out); +/* + * Consumes an IPv4 packet and sends it to the unicast `next_hop` on the + * packet's interface, holding it in the table until the address resolves. + */ +int32_t resolve_and_send(packet* pkt, const ipv4::ipv4_addr& next_hop); + /* * Ages the table on every netstkd daemon pass. `ts` is the current monotonic time. */ diff --git a/kernel/net/ipv4.cpp b/kernel/net/ipv4.cpp index 5ae59c67..c8be3f9c 100644 --- a/kernel/net/ipv4.cpp +++ b/kernel/net/ipv4.cpp @@ -1,11 +1,18 @@ #include "net/ipv4.h" #include "net/interface.h" #include "net/checksum.h" +#include "net/route.h" +#include "net/arp.h" +#include "net/eth.h" +#include "sync/atomic.h" #include "common/logging.h" namespace net { namespace ipv4 { +// Identification field of the next datagram sent, only meaningful to reassembly +static sync::atomic g_next_id {0}; + static int32_t drop(interface* iface, packet* pkt, int32_t rc) { iface->record_packet_dropped(); packet::free(pkt); @@ -111,10 +118,56 @@ int32_t input(packet* pkt) { } int32_t output(packet* pkt, const ipv4_addr& dest, uint8_t protocol) { - (void)dest; - (void)protocol; - packet::free(pkt); - return ERR_INVALID; + if (!pkt) { + log::warn("ipv4: output called with no packet"); + return ERR_INVALID; + } + + route::route_result route; + int32_t rc = route::lookup(dest, &route); + if (rc != OK) { + packet::free(pkt); + return rc; + } + + // Local delivery through loopback interface + if (route.type == route::route_type::local) { + return drop(route.iface, pkt, ERR_NO_ROUTE); + } + + // Nothing is fragmented, so the payload must fit one frame behind the header + if (pkt->length() > static_cast(route.iface->mtu()) - HEADER_LEN) { + return drop(route.iface, pkt, ERR_TOO_LARGE); + } + + ipv4_header* hdr = reinterpret_cast(pkt->push(HEADER_LEN)); + if (!hdr) { + log::warn("ipv4: output packet has no headroom for the header"); + return reject(route.iface, pkt, ERR_INVALID); + } + + hdr->set_version_ihl(VERSION, MIN_IHL); + hdr->tos = 0; + hdr->total_len = htons(static_cast(pkt->length())); + hdr->id = htons(g_next_id.fetch_add_relaxed(1)); + hdr->fl_frag_off = htons(FLAG_DF); + hdr->ttl = DEFAULT_TTL; + hdr->proto = protocol; + hdr->src = route.iface->ipv4_conf().address; + hdr->dst = dest; + + // Computed last, over the finished header with the field itself zeroed + hdr->checksum = 0; + hdr->checksum = htons(checksum(hdr, HEADER_LEN)); + + pkt->mark_network_header(); + pkt->set_iface(route.iface); + + if (route.type == route::route_type::broadcast) { + return eth::output(pkt, eth::BROADCAST_ADDR, eth::TYPE_IPV4); + } + + return arp::resolve_and_send(pkt, route.next_hop); } } // namespace ipv4 From 6ae3d18bb2a7e184407efe7e47e23ce86766efdd Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 18:13:49 -0700 Subject: [PATCH 15/20] feat(net): added ICMP layer to the network stack --- kernel/net/icmp.cpp | 139 ++++++++++++++++++++++++++++++++++++++++++++ kernel/net/icmp.h | 83 ++++++++++++++++++++++++++ kernel/net/ipv4.cpp | 6 +- 3 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 kernel/net/icmp.cpp create mode 100644 kernel/net/icmp.h diff --git a/kernel/net/icmp.cpp b/kernel/net/icmp.cpp new file mode 100644 index 00000000..3c6bd281 --- /dev/null +++ b/kernel/net/icmp.cpp @@ -0,0 +1,139 @@ +#include "net/icmp.h" +#include "net/interface.h" +#include "net/checksum.h" +#include "net/eth.h" +#include "common/string.h" +#include "common/logging.h" + +namespace net { +namespace icmp { + +static int32_t drop(interface* iface, packet* pkt, int32_t rc) { + iface->record_packet_dropped(); + packet::free(pkt); + return rc; +} + +static int32_t reject(interface* iface, packet* pkt, int32_t rc) { + iface->record_iface_error(); + packet::free(pkt); + return rc; +} + +int32_t input(packet* pkt) { + if (!pkt) { + log::warn("icmp: input called with no packet"); + return ERR_INVALID; + } + + interface* iface = pkt->iface(); + const ipv4::ipv4_header* ip = reinterpret_cast(pkt->network_header()); + + if (!iface || !ip) { + log::warn("icmp: input called with a packet missing its interface or IP header"); + packet::free(pkt); + return ERR_INVALID; + } + + if (pkt->length() < HEADER_LEN) { + return reject(iface, pkt, ERR_INVALID); + } + + if (checksum(pkt->data(), pkt->length()) != 0) { + return reject(iface, pkt, ERR_INVALID); + } + + icmp_header* hdr = reinterpret_cast(pkt->data()); + const uint8_t* src = ip->src.bytes; + + switch (hdr->type) { + case TYPE_ECHO_REQUEST: { + // Requests sent to a broadcast address are ignored, answering them + // would let one packet make every host on the link reply at once. + if (ip->dst.is_broadcast() || iface->ipv4_conf().is_subnet_broadcast(ip->dst)) { + packet::free(pkt); + return OK; + } + + // The request becomes the reply in place, only the type changes + ipv4::ipv4_addr requester = ip->src; + hdr->type = TYPE_ECHO_REPLY; + + return output(pkt, requester); + } + case TYPE_ECHO_REPLY: { + log::info("icmp: echo reply from %u.%u.%u.%u, id 0x%04x seq %u, %lu payload bytes", + src[0], src[1], src[2], src[3], ntohs(hdr->echo.id), ntohs(hdr->echo.seq), + pkt->length() - HEADER_LEN); + packet::free(pkt); + return OK; + } + case TYPE_DEST_UNREACHABLE: + case TYPE_TIME_EXCEEDED: + case TYPE_PARAMETER_PROBLEM: + log::debug("icmp: error type %u code %u from %u.%u.%u.%u", + hdr->type, hdr->code, src[0], src[1], src[2], src[3]); + packet::free(pkt); + return OK; + default: + return drop(iface, pkt, OK); + } +} + +int32_t output(packet* pkt, const ipv4::ipv4_addr& dest) { + if (!pkt) { + log::warn("icmp: output called with no packet"); + return ERR_INVALID; + } + + if (pkt->length() < HEADER_LEN) { + log::warn("icmp: output called with a message shorter than its header"); + packet::free(pkt); + return ERR_INVALID; + } + + icmp_header* hdr = reinterpret_cast(pkt->data()); + hdr->checksum = 0; + hdr->checksum = htons(checksum(pkt->data(), pkt->length())); + + pkt->mark_transport_header(); + return ipv4::output(pkt, dest, ipv4::PROTO_ICMP); +} + +int32_t send_echo_request(const ipv4::ipv4_addr& dest, uint16_t id, uint16_t seq, + const void* payload, size_t len) { + if (len > 0 && !payload) { + return ERR_INVALID; + } + + packet* pkt = packet::alloc(); + if (!pkt) { + return ERR_NO_MEMORY; + } + + // Room for the two headers below, then the message itself + uint8_t* body = nullptr; + if (pkt->reserve(eth::HEADER_LEN + ipv4::HEADER_LEN)) { + body = pkt->put(HEADER_LEN + len); + } + + if (!body) { + packet::free(pkt); + return ERR_TOO_LARGE; + } + + icmp_header* hdr = reinterpret_cast(body); + hdr->type = TYPE_ECHO_REQUEST; + hdr->code = 0; + hdr->echo.id = htons(id); + hdr->echo.seq = htons(seq); + + if (len > 0) { + string::memcpy(body + HEADER_LEN, payload, len); + } + + return output(pkt, dest); +} + +} // namespace icmp +} // namespace net diff --git a/kernel/net/icmp.h b/kernel/net/icmp.h new file mode 100644 index 00000000..408d1038 --- /dev/null +++ b/kernel/net/icmp.h @@ -0,0 +1,83 @@ +#ifndef STELLUX_NET_ICMP_H +#define STELLUX_NET_ICMP_H + +#include "net/ipv4.h" + +namespace net { +namespace icmp { + +constexpr size_t HEADER_LEN = 8; + +constexpr uint8_t TYPE_ECHO_REPLY = 0; +constexpr uint8_t TYPE_DEST_UNREACHABLE = 3; +constexpr uint8_t TYPE_ECHO_REQUEST = 8; +constexpr uint8_t TYPE_TIME_EXCEEDED = 11; +constexpr uint8_t TYPE_PARAMETER_PROBLEM = 12; + +// Codes for TYPE_DEST_UNREACHABLE +constexpr uint8_t CODE_NET_UNREACHABLE = 0; +constexpr uint8_t CODE_HOST_UNREACHABLE = 1; +constexpr uint8_t CODE_PROTOCOL_UNREACHABLE = 2; +constexpr uint8_t CODE_PORT_UNREACHABLE = 3; +constexpr uint8_t CODE_FRAGMENTATION_NEEDED = 4; + +// Codes for TYPE_TIME_EXCEEDED +constexpr uint8_t CODE_TTL_EXCEEDED = 0; +constexpr uint8_t CODE_REASSEMBLY_EXCEEDED = 1; + +// Error messages quote the offending IP header plus this much of its payload +constexpr size_t ERROR_QUOTE_LEN = 8; + +/** + * ICMP message header (RFC 792). The last four bytes depend on the type: + * identifier and sequence for echo, zero for most errors, the next hop MTU + * for fragmentation needed. `checksum` covers the header and the body. + * https://www.rfc-editor.org/info/rfc792/ + */ +struct icmp_header { + uint8_t type; + uint8_t code; + uint16_t checksum; + union { + struct { + uint16_t id; + uint16_t seq; + } __attribute__((packed)) echo; + struct { + uint16_t unused; + uint16_t next_hop_mtu; + } __attribute__((packed)) frag; + uint32_t unused; + } __attribute__((packed)); +} __attribute__((packed)); +static_assert(sizeof(icmp_header) == HEADER_LEN); + +/* + * Consumes an ICMP message whose window starts at the header. Echo requests + * are answered in place, other messages are logged and freed. + */ +int32_t input(packet* pkt); + +/* + * Consumes a finished ICMP message, fills in its checksum, and hands it to + * IPv4 for `dest`. + */ +int32_t output(packet* pkt, const ipv4::ipv4_addr& dest); + +/* + * Builds and sends an echo request to `dest` carrying `len` bytes of `payload`. + */ +int32_t send_echo_request(const ipv4::ipv4_addr& dest, uint16_t id, uint16_t seq, + const void* payload, size_t len); + +/* + * Reports an error about `offending` packet whose window must start at its IPv4 + * header, quoting that header and the first bytes of its payload. Nothing is + * sent about broadcasts, later fragments, or other ICMP errors. + */ +int32_t send_error(const packet* offending, uint8_t type, uint8_t code); + +} // namespace icmp +} // namespace net + +#endif // STELLUX_NET_ICMP_H diff --git a/kernel/net/ipv4.cpp b/kernel/net/ipv4.cpp index c8be3f9c..6d46ca9d 100644 --- a/kernel/net/ipv4.cpp +++ b/kernel/net/ipv4.cpp @@ -4,6 +4,7 @@ #include "net/route.h" #include "net/arp.h" #include "net/eth.h" +#include "net/icmp.h" #include "sync/atomic.h" #include "common/logging.h" @@ -96,10 +97,7 @@ int32_t input(packet* pkt) { const uint8_t* src = hdr->src.bytes; switch (hdr->proto) { case PROTO_ICMP: - log::info("ipv4: ICMP datagram from %u.%u.%u.%u, %lu payload bytes", - src[0], src[1], src[2], src[3], pkt->length()); - packet::free(pkt); - break; + return icmp::input(pkt); case PROTO_UDP: log::info("ipv4: UDP datagram from %u.%u.%u.%u, %lu payload bytes", src[0], src[1], src[2], src[3], pkt->length()); From 726c2a3163d6ab7a32ae62f59c2c24f74bd88b9c Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 19:10:33 -0700 Subject: [PATCH 16/20] feat(net): added ICMP datagram sockets with the send path --- kernel/net/arp.h | 2 - kernel/net/icmp_socket.cpp | 218 +++++++++++++++++++++++++ kernel/net/icmp_socket.h | 52 ++++++ kernel/net/inet.cpp | 76 +++++++++ kernel/net/inet.h | 60 +++++++ kernel/net/packet.h | 2 + kernel/resource/resource.h | 2 + kernel/syscall/handlers/sys_socket.cpp | 13 +- kernel/syscall/syscall_table.h | 1 + 9 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 kernel/net/icmp_socket.cpp create mode 100644 kernel/net/icmp_socket.h create mode 100644 kernel/net/inet.cpp create mode 100644 kernel/net/inet.h diff --git a/kernel/net/arp.h b/kernel/net/arp.h index 57bf2a83..96d5e147 100644 --- a/kernel/net/arp.h +++ b/kernel/net/arp.h @@ -60,8 +60,6 @@ enum class arp_entry_state : uint8_t { resolved = 2, }; -using packet_list = list::head; - struct arp_entry { interface* iface; arp_entry_state state; diff --git a/kernel/net/icmp_socket.cpp b/kernel/net/icmp_socket.cpp new file mode 100644 index 00000000..5da55c17 --- /dev/null +++ b/kernel/net/icmp_socket.cpp @@ -0,0 +1,218 @@ +#include "net/icmp_socket.h" +#include "net/icmp.h" +#include "net/inet.h" +#include "net/eth.h" +#include "net/interface.h" +#include "resource/resource.h" +#include "mm/heap.h" +#include "common/string.h" + +namespace net { +namespace icmp { + +// The stack and the resource layer speak different result codes, this is the +// one place they are translated +static ssize_t map_net_error(int32_t rc) { + switch (rc) { + case OK: return 0; + case ERR_INVALID: return resource::ERR_INVAL; + case ERR_NO_MEMORY: return resource::ERR_NOMEM; + case ERR_TOO_LARGE: return resource::ERR_MSGSIZE; + case ERR_NO_ROUTE: return resource::ERR_HOSTUNREACH; + case ERR_DOWN: return resource::ERR_HOSTUNREACH; + default: return resource::ERR_IO; + } +} + +// Open sockets by slot. Written under g_sockets_lock from syscall +// context, read under it from the driver task on delivery. +static sync::spinlock g_sockets_lock = sync::SPINLOCK_INIT; +static icmp_socket* g_sockets[MAX_SOCKETS]; +static uint16_t g_next_id = 1; + +// Caller holds g_sockets_lock +static icmp_socket* find_locked(uint16_t id) { + for (size_t i = 0; i < MAX_SOCKETS; i++) { + if (g_sockets[i] && g_sockets[i]->id == id) { + return g_sockets[i]; + } + } + + return nullptr; +} + +// Caller holds g_sockets_lock. Identifiers wrap at 16 bits and skip 0 and any in use. +static uint16_t take_free_id() { + uint16_t id = g_next_id; + while (id == 0 || find_locked(id)) { + id++; + } + + g_next_id = static_cast(id + 1); + return id; +} + +icmp_socket* socket_open() { + icmp_socket* sock = heap::ualloc_new(); + if (!sock) { + return nullptr; + } + + sock->lock = sync::SPINLOCK_INIT; + sock->rx_queue.init(); + + sync::lock_guard guard(g_sockets_lock); + for (size_t i = 0; i < MAX_SOCKETS; i++) { + if (!g_sockets[i]) { + sock->id = take_free_id(); + g_sockets[i] = sock; + return sock; + } + } + + heap::ufree_delete(sock); + return nullptr; +} + +void socket_close(icmp_socket* sock) { + if (!sock) { + return; + } + + { + sync::lock_guard guard(g_sockets_lock); + for (size_t i = 0; i < MAX_SOCKETS; i++) { + if (g_sockets[i] == sock) { + g_sockets[i] = nullptr; + break; + } + } + } + + // Unregistered, so no more replies can arrive and the queue is ours alone + while (packet* pkt = sock->rx_queue.pop_front()) { + packet::free(pkt); + } + + heap::ufree_delete(sock); +} + +void socket_deliver(packet* pkt) { + if (!pkt) { + return; + } + + const icmp_header* hdr = reinterpret_cast(pkt->data()); + uint16_t id = ntohs(hdr->echo.id); + + // The socket lock is taken inside the table lock so the socket + // cannot be closed between the lookup and the enqueue. + sync::lock_guard table_guard(g_sockets_lock); + + icmp_socket* sock = find_locked(id); + if (!sock) { + pkt->iface()->record_packet_dropped(); + packet::free(pkt); + return; + } + + sync::lock_guard sock_guard(sock->lock); + if (sock->rx_queue.size() >= SOCKET_QUEUE_DEPTH) { + packet* oldest = sock->rx_queue.pop_front(); + oldest->iface()->record_packet_dropped(); + packet::free(oldest); + } + + sock->rx_queue.push_back(pkt); +} + +// Sends the caller's echo request to `kaddr`. The identifier is replaced with +// the socket's own so the reply can be matched back to it. +static ssize_t socket_sendto(resource::resource_object* obj, const void* ksrc, size_t count, + uint32_t, const void* kaddr, size_t addrlen) { + icmp_socket* sock = static_cast(obj->impl); + + ipv4::ipv4_addr dest; + uint16_t port = 0; + + if (inet::parse_sockaddr(kaddr, addrlen, &dest, &port) != OK) { + return resource::ERR_INVAL; + } + + // Only echo requests may be sent through a ping socket + if (count < HEADER_LEN) { + return resource::ERR_INVAL; + } + + const icmp_header* user_hdr = static_cast(ksrc); + if (user_hdr->type != TYPE_ECHO_REQUEST || user_hdr->code != 0) { + return resource::ERR_INVAL; + } + + packet* pkt = packet::alloc(); + if (!pkt) { + return resource::ERR_NOMEM; + } + + uint8_t* body = nullptr; + if (pkt->reserve(eth::HEADER_LEN + ipv4::HEADER_LEN)) { + body = pkt->put(count); + } + + if (!body) { + packet::free(pkt); + return resource::ERR_MSGSIZE; + } + + string::memcpy(body, ksrc, count); + reinterpret_cast(body)->echo.id = htons(sock->id); + + int32_t rc = output(pkt, dest); + if (rc != OK) { + return map_net_error(rc); + } + + return static_cast(count); +} + +static void socket_close(resource::resource_object* obj) { + if (!obj || !obj->impl) { + return; + } + + socket_close(static_cast(obj->impl)); + obj->impl = nullptr; +} + +static ssize_t socket_read(resource::resource_object*, void*, size_t, uint32_t) { + return resource::ERR_UNSUP; +} + +static ssize_t socket_write(resource::resource_object*, const void*, size_t, uint32_t) { + return resource::ERR_UNSUP; +} + +static const resource::resource_ops g_socket_ops = { + socket_read, + socket_write, + socket_close, + nullptr, // ioctl + nullptr, // mmap + socket_sendto, + nullptr, // recvfrom + nullptr, // bind + nullptr, // listen + nullptr, // accept + nullptr, // connect + nullptr, // setsockopt + nullptr, // getsockopt + nullptr, // poll + nullptr, // shutdown +}; + +const resource::resource_ops* socket_ops() { + return &g_socket_ops; +} + +} // namespace icmp +} // namespace net diff --git a/kernel/net/icmp_socket.h b/kernel/net/icmp_socket.h new file mode 100644 index 00000000..67613317 --- /dev/null +++ b/kernel/net/icmp_socket.h @@ -0,0 +1,52 @@ +#ifndef STELLUX_NET_ICMP_SOCKET_H +#define STELLUX_NET_ICMP_SOCKET_H + +#include "common/types.h" +#include "net/packet.h" +#include "sync/spinlock.h" + +namespace resource { struct resource_ops; } + +namespace net { +namespace icmp { + +constexpr size_t MAX_SOCKETS = 16; +constexpr size_t SOCKET_QUEUE_DEPTH = 8; // replies held per socket, oldest dropped first + +/** + * One ping socket. Requests sent through it carry `id`, and replies with that + * identifier wait in `rx_queue` until recvfrom takes them. Lives in + * unprivileged memory because the driver task enqueues replies while lowered. + */ +struct icmp_socket { + uint16_t id; + sync::spinlock lock; // Guards rx_queue + packet_list rx_queue; +}; + +/* + * Allocates a socket with an unused identifier and registers it. + * Returns nullptr when the table is full or memory is exhausted. + */ +icmp_socket* socket_open(); + +/* + * Unregisters the socket and frees it with any replies still waiting. + */ +void socket_close(icmp_socket* sock); + +/* + * Consumes an echo reply, queueing it on the socket that owns its identifier + * or freeing it when no socket does. + */ +void socket_deliver(packet* pkt); + +/* + * The resource operations a ping socket's resource object dispatches through. + */ +const resource::resource_ops* socket_ops(); + +} // namespace icmp +} // namespace net + +#endif // STELLUX_NET_ICMP_SOCKET_H diff --git a/kernel/net/inet.cpp b/kernel/net/inet.cpp new file mode 100644 index 00000000..91e503a8 --- /dev/null +++ b/kernel/net/inet.cpp @@ -0,0 +1,76 @@ +#include "net/inet.h" +#include "net/byteorder.h" +#include "net/icmp_socket.h" +#include "resource/resource.h" +#include "mm/heap.h" +#include "common/string.h" + +namespace net { +namespace inet { + +int32_t parse_sockaddr(const void* addr, size_t len, ipv4::ipv4_addr* out_addr, uint16_t* out_port) { + if (!addr || !out_addr || !out_port || len < SOCKADDR_IN_LEN) { + return ERR_INVALID; + } + + const sockaddr_in* sa = static_cast(addr); + if (sa->family != AF_INET) { + return ERR_INVALID; + } + + *out_addr = sa->addr; + *out_port = ntohs(sa->port); + return OK; +} + +int32_t fill_sockaddr(void* addr, size_t* len, const ipv4::ipv4_addr& ip, uint16_t port) { + if (!addr || !len || *len < SOCKADDR_IN_LEN) { + return ERR_INVALID; + } + + sockaddr_in* sa = static_cast(addr); + sa->family = AF_INET; + sa->port = htons(port); + sa->addr = ip; + string::memset(sa->zero, 0, sizeof(sa->zero)); + + *len = SOCKADDR_IN_LEN; + return OK; +} + +/** + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t create_socket(uint32_t type, uint32_t protocol, + resource::resource_object** out) { + if (!out) { + return resource::ERR_INVAL; + } + + if (type != SOCK_DGRAM || protocol != IPPROTO_ICMP) { + return resource::ERR_UNSUP; + } + + icmp::icmp_socket* sock = icmp::socket_open(); + if (!sock) { + return resource::ERR_NOMEM; + } + + // The object stays in privileged memory with the rest of the resource + // layer, only the protocol state behind `impl` is reachable while lowered + auto* obj = heap::kalloc_new(); + if (!obj) { + icmp::socket_close(sock); + return resource::ERR_NOMEM; + } + + obj->type = resource::resource_type::SOCKET; + obj->ops = icmp::socket_ops(); + obj->impl = sock; + + *out = obj; + return resource::OK; +} + +} // namespace inet +} // namespace net diff --git a/kernel/net/inet.h b/kernel/net/inet.h new file mode 100644 index 00000000..10c3e457 --- /dev/null +++ b/kernel/net/inet.h @@ -0,0 +1,60 @@ +#ifndef STELLUX_NET_INET_H +#define STELLUX_NET_INET_H + +#include "common/types.h" +#include "net/net.h" +#include "net/ipv4.h" + +namespace resource { struct resource_object; } + +namespace net { +namespace inet { + +// Values shared with userland through the socket system calls +constexpr uint16_t AF_INET = 2; +constexpr uint32_t SOCK_STREAM = 1; +constexpr uint32_t SOCK_DGRAM = 2; +constexpr uint32_t IPPROTO_IP = 0; +constexpr uint32_t IPPROTO_ICMP = ipv4::PROTO_ICMP; +constexpr uint32_t IPPROTO_TCP = ipv4::PROTO_TCP; +constexpr uint32_t IPPROTO_UDP = ipv4::PROTO_UDP; + +constexpr size_t SOCKADDR_IN_LEN = 16; + +/** + * An IPv4 endpoint as userland passes it to the socket calls, padded to the + * size of the generic `sockaddr`. `port` is in network byte order. + */ +struct sockaddr_in { + uint16_t family; // AF_INET + uint16_t port; + ipv4::ipv4_addr addr; + uint8_t zero[8]; +} __attribute__((packed)); +static_assert(sizeof(sockaddr_in) == SOCKADDR_IN_LEN); + +/* + * Validates `len` bytes of a user-supplied address and extracts the endpoint. + * `port` is returned in host byte order. + */ +int32_t parse_sockaddr(const void* addr, size_t len, ipv4::ipv4_addr* out_addr, uint16_t* out_port); + +/* + * Writes the endpoint into a buffer of `*len` bytes and sets `*len` to the + * size written. `port` is taken in host byte order. + */ +int32_t fill_sockaddr(void* addr, size_t* len, const ipv4::ipv4_addr& ip, uint16_t port); + +/** + * Creates the resource object for an AF_INET socket of `type` and `protocol`, + * with one reference held by the caller. Speaks resource result codes, and + * ERR_UNSUP for a combination no protocol provides. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t create_socket(uint32_t type, uint32_t protocol, + resource::resource_object** out); + +} // namespace inet +} // namespace net + +#endif // STELLUX_NET_INET_H diff --git a/kernel/net/packet.h b/kernel/net/packet.h index dc2a1ce3..ea831248 100644 --- a/kernel/net/packet.h +++ b/kernel/net/packet.h @@ -135,6 +135,8 @@ class packet { static_assert(sizeof(packet) == PACKET_OBJECT_SIZE); +using packet_list = list::head; + } // namespace net #endif // STELLUX_NET_PACKET_H diff --git a/kernel/resource/resource.h b/kernel/resource/resource.h index 565598af..1b6fa0be 100644 --- a/kernel/resource/resource.h +++ b/kernel/resource/resource.h @@ -87,6 +87,8 @@ constexpr int32_t ERR_EXIST = -17; constexpr int32_t ERR_INTR = -18; constexpr int32_t ERR_NOPROTOOPT = -19; constexpr int32_t ERR_LOOP = -20; +constexpr int32_t ERR_MSGSIZE = -21; +constexpr int32_t ERR_HOSTUNREACH = -22; /** * @brief Allocate a private handle table and attach it to the task. diff --git a/kernel/syscall/handlers/sys_socket.cpp b/kernel/syscall/handlers/sys_socket.cpp index 6055c26e..da0d9163 100644 --- a/kernel/syscall/handlers/sys_socket.cpp +++ b/kernel/syscall/handlers/sys_socket.cpp @@ -1,6 +1,7 @@ #include "syscall/handlers/sys_socket.h" #include "socket/unix_socket.h" +#include "net/inet.h" #include "resource/resource.h" #include "fs/fstypes.h" #include "sched/sched.h" @@ -25,6 +26,9 @@ static inline int64_t map_socket_op_error(int32_t rc) { case resource::ERR_NOTDIR: return syscall::ENOTDIR; case resource::ERR_INTR: return syscall::ERESTARTSYS; case resource::ERR_NOPROTOOPT: return syscall::ENOPROTOOPT; + case resource::ERR_MSGSIZE: return syscall::EMSGSIZE; + case resource::ERR_HOSTUNREACH: return syscall::EHOSTUNREACH; + case resource::ERR_UNSUP: return syscall::EOPNOTSUPP; default: return syscall::EIO; } } @@ -44,6 +48,13 @@ DEFINE_SYSCALL3(socket, domain, type, protocol) { } rc = socket::create_unbound_socket(&obj); + } else if (domain == net::inet::AF_INET) { + rc = net::inet::create_socket(static_cast(type), + static_cast(protocol), &obj); + + if (rc == resource::ERR_UNSUP) { + return syscall::EPROTONOSUPPORT; + } } else { return syscall::EAFNOSUPPORT; } @@ -373,7 +384,7 @@ DEFINE_SYSCALL6(sendto, fd, buf, len, flags, dest_addr, addrlen) { resource::resource_release(obj); if (result < 0) { - return syscall::EIO; + return map_socket_op_error(static_cast(result)); } return result; diff --git a/kernel/syscall/syscall_table.h b/kernel/syscall/syscall_table.h index e3b3d166..969db747 100644 --- a/kernel/syscall/syscall_table.h +++ b/kernel/syscall/syscall_table.h @@ -45,6 +45,7 @@ constexpr int64_t EISCONN = -106; constexpr int64_t ENOTCONN = -107; constexpr int64_t ETIMEDOUT = -110; constexpr int64_t ECONNREFUSED = -111; +constexpr int64_t EHOSTUNREACH = -113; // Interrupted restartable wait, resolved at the syscall-return boundary // (rewind for re-execution or EINTR) and never visible to userspace. From 347d93dc3427a2b519e49cdbd32fdc5e9976ddd1 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 19:24:55 -0700 Subject: [PATCH 17/20] feat(net): implemented delivering echo replies to the ICMP socket that sent the request --- kernel/net/icmp.cpp | 6 ++---- kernel/net/icmp_socket.cpp | 32 +++++++++++++++++++++++++++++++- kernel/net/inet.h | 1 + 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/kernel/net/icmp.cpp b/kernel/net/icmp.cpp index 3c6bd281..85cc8899 100644 --- a/kernel/net/icmp.cpp +++ b/kernel/net/icmp.cpp @@ -1,4 +1,5 @@ #include "net/icmp.h" +#include "net/icmp_socket.h" #include "net/interface.h" #include "net/checksum.h" #include "net/eth.h" @@ -62,10 +63,7 @@ int32_t input(packet* pkt) { return output(pkt, requester); } case TYPE_ECHO_REPLY: { - log::info("icmp: echo reply from %u.%u.%u.%u, id 0x%04x seq %u, %lu payload bytes", - src[0], src[1], src[2], src[3], ntohs(hdr->echo.id), ntohs(hdr->echo.seq), - pkt->length() - HEADER_LEN); - packet::free(pkt); + socket_deliver(pkt); return OK; } case TYPE_DEST_UNREACHABLE: diff --git a/kernel/net/icmp_socket.cpp b/kernel/net/icmp_socket.cpp index 5da55c17..be989a5a 100644 --- a/kernel/net/icmp_socket.cpp +++ b/kernel/net/icmp_socket.cpp @@ -175,6 +175,36 @@ static ssize_t socket_sendto(resource::resource_object* obj, const void* ksrc, s return static_cast(count); } +// Hands the oldest waiting reply to the caller, truncated to +// `count` bytes, with its source address in `kaddr`. +static ssize_t socket_recvfrom(resource::resource_object* obj, void* kdst, size_t count, + uint32_t, void* kaddr, size_t* addrlen) { + icmp_socket* sock = static_cast(obj->impl); + + packet* pkt = nullptr; + { + sync::lock_guard guard(sock->lock); + pkt = sock->rx_queue.pop_front(); + } + + if (!pkt) { + return resource::ERR_AGAIN; + } + + size_t copied = pkt->length() < count ? pkt->length() : count; + string::memcpy(kdst, pkt->data(), copied); + + if (kaddr && addrlen) { + const ipv4::ipv4_header* ip = reinterpret_cast(pkt->network_header()); + if (inet::fill_sockaddr(kaddr, addrlen, ip->src, 0) != OK) { + *addrlen = 0; + } + } + + packet::free(pkt); + return static_cast(copied); +} + static void socket_close(resource::resource_object* obj) { if (!obj || !obj->impl) { return; @@ -199,7 +229,7 @@ static const resource::resource_ops g_socket_ops = { nullptr, // ioctl nullptr, // mmap socket_sendto, - nullptr, // recvfrom + socket_recvfrom, nullptr, // bind nullptr, // listen nullptr, // accept diff --git a/kernel/net/inet.h b/kernel/net/inet.h index 10c3e457..2f9a154a 100644 --- a/kernel/net/inet.h +++ b/kernel/net/inet.h @@ -18,6 +18,7 @@ constexpr uint32_t IPPROTO_IP = 0; constexpr uint32_t IPPROTO_ICMP = ipv4::PROTO_ICMP; constexpr uint32_t IPPROTO_TCP = ipv4::PROTO_TCP; constexpr uint32_t IPPROTO_UDP = ipv4::PROTO_UDP; +constexpr uint32_t MSG_DONTWAIT = 0x40; constexpr size_t SOCKADDR_IN_LEN = 16; From 8b6e8857208e5d7f58656fc0c034ce8c79ebaa51 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 19:49:55 -0700 Subject: [PATCH 18/20] refactor(resource): decoupled old monolithic resource ops table into default nullable generic ops and socket specific ops --- kernel/net/icmp_socket.cpp | 26 ++++------ kernel/pipe/pipe.cpp | 36 +++----------- kernel/pty/pty.cpp | 40 ++++------------ kernel/resource/providers/file_provider.cpp | 21 +++----- kernel/resource/providers/proc_provider.cpp | 19 ++------ kernel/resource/providers/shmem_provider.cpp | 19 ++------ kernel/resource/resource.h | 42 +++++----------- kernel/resource/socket_ops.h | 49 +++++++++++++++++++ kernel/socket/unix_socket.cpp | 28 +++++------ kernel/syscall/handlers/sys_shutdown.cpp | 7 +-- kernel/syscall/handlers/sys_socket.cpp | 50 ++++++++++++-------- kernel/terminal/terminal.cpp | 19 ++------ kernel/tests/resource/resource.test.cpp | 14 ++---- 13 files changed, 155 insertions(+), 215 deletions(-) create mode 100644 kernel/resource/socket_ops.h diff --git a/kernel/net/icmp_socket.cpp b/kernel/net/icmp_socket.cpp index be989a5a..a65132de 100644 --- a/kernel/net/icmp_socket.cpp +++ b/kernel/net/icmp_socket.cpp @@ -3,7 +3,7 @@ #include "net/inet.h" #include "net/eth.h" #include "net/interface.h" -#include "resource/resource.h" +#include "resource/socket_ops.h" #include "mm/heap.h" #include "common/string.h" @@ -222,22 +222,16 @@ static ssize_t socket_write(resource::resource_object*, const void*, size_t, uin return resource::ERR_UNSUP; } +static const resource::socket_ops g_icmp_socket_ops = { + .sendto = socket_sendto, + .recvfrom = socket_recvfrom, +}; + static const resource::resource_ops g_socket_ops = { - socket_read, - socket_write, - socket_close, - nullptr, // ioctl - nullptr, // mmap - socket_sendto, - socket_recvfrom, - nullptr, // bind - nullptr, // listen - nullptr, // accept - nullptr, // connect - nullptr, // setsockopt - nullptr, // getsockopt - nullptr, // poll - nullptr, // shutdown + .read = socket_read, + .write = socket_write, + .close = socket_close, + .socket = &g_icmp_socket_ops, }; const resource::resource_ops* socket_ops() { diff --git a/kernel/pipe/pipe.cpp b/kernel/pipe/pipe.cpp index ba12afcc..a2eab7e1 100644 --- a/kernel/pipe/pipe.cpp +++ b/kernel/pipe/pipe.cpp @@ -128,39 +128,15 @@ static uint32_t pipe_write_poll( // Ops tables static const resource::resource_ops g_pipe_read_ops = { - pipe_read, // read - nullptr, // write - pipe_read_close, // close - nullptr, // ioctl - nullptr, // mmap - nullptr, // sendto - nullptr, // recvfrom - nullptr, // bind - nullptr, // listen - nullptr, // accept - nullptr, // connect - nullptr, // setsockopt - nullptr, // getsockopt - pipe_read_poll, // poll - nullptr, // shutdown + .read = pipe_read, + .close = pipe_read_close, + .poll = pipe_read_poll, }; static const resource::resource_ops g_pipe_write_ops = { - nullptr, // read - pipe_write, // write - pipe_write_close, // close - nullptr, // ioctl - nullptr, // mmap - nullptr, // sendto - nullptr, // recvfrom - nullptr, // bind - nullptr, // listen - nullptr, // accept - nullptr, // connect - nullptr, // setsockopt - nullptr, // getsockopt - pipe_write_poll, // poll - nullptr, // shutdown + .write = pipe_write, + .close = pipe_write_close, + .poll = pipe_write_poll, }; // Pair creation diff --git a/kernel/pty/pty.cpp b/kernel/pty/pty.cpp index 15c5e6cc..183bc77e 100644 --- a/kernel/pty/pty.cpp +++ b/kernel/pty/pty.cpp @@ -368,39 +368,19 @@ static uint32_t pty_slave_poll( // Ops tables static const resource::resource_ops g_pty_master_ops = { - pty_master_read, - pty_master_write, - pty_master_close, - pty_ioctl, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - pty_master_poll, - nullptr, + .read = pty_master_read, + .write = pty_master_write, + .close = pty_master_close, + .ioctl = pty_ioctl, + .poll = pty_master_poll, }; static const resource::resource_ops g_pty_slave_ops = { - pty_slave_read, - pty_slave_write, - pty_slave_close, - pty_ioctl, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - pty_slave_poll, - nullptr, + .read = pty_slave_read, + .write = pty_slave_write, + .close = pty_slave_close, + .ioctl = pty_ioctl, + .poll = pty_slave_poll, }; // Pair creation diff --git a/kernel/resource/providers/file_provider.cpp b/kernel/resource/providers/file_provider.cpp index 534dfa81..0981816f 100644 --- a/kernel/resource/providers/file_provider.cpp +++ b/kernel/resource/providers/file_provider.cpp @@ -141,21 +141,12 @@ __PRIVILEGED_CODE static uint32_t file_poll( } static const resource_ops g_file_ops = { - file_read, - file_write, - file_close, - file_ioctl, - file_mmap, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - file_poll, - nullptr, + .read = file_read, + .write = file_write, + .close = file_close, + .ioctl = file_ioctl, + .mmap = file_mmap, + .poll = file_poll, }; /** diff --git a/kernel/resource/providers/proc_provider.cpp b/kernel/resource/providers/proc_provider.cpp index 52823f6c..4dd5434e 100644 --- a/kernel/resource/providers/proc_provider.cpp +++ b/kernel/resource/providers/proc_provider.cpp @@ -85,21 +85,10 @@ __PRIVILEGED_CODE static uint32_t proc_poll( } static const resource_ops g_proc_ops = { - proc_read, - proc_write, - proc_close, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - proc_poll, - nullptr, + .read = proc_read, + .write = proc_write, + .close = proc_close, + .poll = proc_poll, }; __PRIVILEGED_CODE int32_t create_proc_resource( diff --git a/kernel/resource/providers/shmem_provider.cpp b/kernel/resource/providers/shmem_provider.cpp index a4c6eb71..567405a2 100644 --- a/kernel/resource/providers/shmem_provider.cpp +++ b/kernel/resource/providers/shmem_provider.cpp @@ -87,21 +87,10 @@ static uint32_t shmem_resource_poll( } static const resource_ops g_shmem_resource_ops = { - shmem_resource_read, - shmem_resource_write, - shmem_resource_close, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - shmem_resource_poll, - nullptr, + .read = shmem_resource_read, + .write = shmem_resource_write, + .close = shmem_resource_close, + .poll = shmem_resource_poll, }; int32_t create_shmem_resource( diff --git a/kernel/resource/resource.h b/kernel/resource/resource.h index 1b6fa0be..261a7089 100644 --- a/kernel/resource/resource.h +++ b/kernel/resource/resource.h @@ -12,6 +12,7 @@ namespace sync { struct poll_table; } namespace resource { struct resource_object; +struct socket_ops; using read_fn = ssize_t (*)(resource_object* obj, void* kdst, size_t count, uint32_t flags); using write_fn = ssize_t (*)(resource_object* obj, const void* ksrc, size_t count, uint32_t flags); @@ -20,38 +21,21 @@ using ioctl_fn = int32_t (*)(resource_object* obj, uint32_t cmd, uint64_t arg); using mmap_fn = int32_t (*)(resource_object* obj, mm::mm_context* mm_ctx, uintptr_t addr, size_t length, uint32_t prot, uint32_t map_flags, uint64_t offset, uintptr_t* out_addr); -using sendto_fn = ssize_t (*)(resource_object* obj, const void* ksrc, size_t count, - uint32_t flags, const void* kaddr, size_t addrlen); -using recvfrom_fn = ssize_t (*)(resource_object* obj, void* kdst, size_t count, - uint32_t flags, void* kaddr, size_t* addrlen); -using bind_fn = int32_t (*)(resource_object* obj, const void* kaddr, size_t addrlen); -using listen_fn = int32_t (*)(resource_object* obj, int32_t backlog); -using accept_fn = int32_t (*)(resource_object* obj, resource_object** new_obj, - void* kaddr, size_t* addrlen, bool nonblock); -using connect_fn = int32_t (*)(resource_object* obj, const void* kaddr, size_t addrlen); -using setsockopt_fn = int32_t (*)(resource_object* obj, int32_t level, - int32_t optname, const void* optval, size_t optlen); -using getsockopt_fn = int32_t (*)(resource_object* obj, int32_t level, - int32_t optname, void* optval, size_t* optlen); using poll_fn = uint32_t (*)(resource_object* obj, sync::poll_table* pt); -using shutdown_fn = int32_t (*)(resource_object* obj, int32_t how); +/** + * Operations every resource may provide. Tables list only the entries they + * implement, the rest are null. `socket` is set exactly for sockets and holds + * the operations only they have. + */ struct resource_ops { - read_fn read; - write_fn write; - close_fn close; - ioctl_fn ioctl; // nullable - mmap_fn mmap; // nullable - sendto_fn sendto; // nullable, for datagram/raw sockets - recvfrom_fn recvfrom; // nullable, for datagram/raw sockets - bind_fn bind; // nullable, for sockets - listen_fn listen; // nullable, for stream sockets - accept_fn accept; // nullable, for listening sockets - connect_fn connect; // nullable, for stream sockets - setsockopt_fn setsockopt; // nullable, for sockets - getsockopt_fn getsockopt; // nullable, for sockets - poll_fn poll; // nullable, returns readiness mask, subscribes on wait queues - shutdown_fn shutdown; // nullable, for sockets + read_fn read = nullptr; + write_fn write = nullptr; + close_fn close = nullptr; + ioctl_fn ioctl = nullptr; + mmap_fn mmap = nullptr; + poll_fn poll = nullptr; + const socket_ops* socket = nullptr; }; struct resource_object : rc::ref_counted { diff --git a/kernel/resource/socket_ops.h b/kernel/resource/socket_ops.h new file mode 100644 index 00000000..22fa6f4e --- /dev/null +++ b/kernel/resource/socket_ops.h @@ -0,0 +1,49 @@ +#ifndef STELLUX_RESOURCE_SOCKET_OPS_H +#define STELLUX_RESOURCE_SOCKET_OPS_H + +#include "resource/resource.h" + +namespace resource { + +using sendto_fn = ssize_t (*)(resource_object* obj, const void* ksrc, size_t count, + uint32_t flags, const void* kaddr, size_t addrlen); +using recvfrom_fn = ssize_t (*)(resource_object* obj, void* kdst, size_t count, + uint32_t flags, void* kaddr, size_t* addrlen); +using bind_fn = int32_t (*)(resource_object* obj, const void* kaddr, size_t addrlen); +using listen_fn = int32_t (*)(resource_object* obj, int32_t backlog); +using accept_fn = int32_t (*)(resource_object* obj, resource_object** new_obj, + void* kaddr, size_t* addrlen, bool nonblock); +using connect_fn = int32_t (*)(resource_object* obj, const void* kaddr, size_t addrlen); +using getname_fn = int32_t (*)(resource_object* obj, void* kaddr, size_t* addrlen, bool peer); +using setsockopt_fn = int32_t (*)(resource_object* obj, int32_t level, + int32_t optname, const void* optval, size_t optlen); +using getsockopt_fn = int32_t (*)(resource_object* obj, int32_t level, + int32_t optname, void* optval, size_t* optlen); +using shutdown_fn = int32_t (*)(resource_object* obj, int32_t how); + +/** + * Operations only sockets have. Every entry is nullable, the syscall layer + * reports EOPNOTSUPP for a missing one. `getname` returns the local address, + * or the peer's when `peer` is set. + */ +struct socket_ops { + bind_fn bind = nullptr; + listen_fn listen = nullptr; + accept_fn accept = nullptr; + connect_fn connect = nullptr; + sendto_fn sendto = nullptr; + recvfrom_fn recvfrom = nullptr; + getname_fn getname = nullptr; + setsockopt_fn setsockopt = nullptr; + getsockopt_fn getsockopt = nullptr; + shutdown_fn shutdown = nullptr; +}; + +// The socket operations of `obj`, or nullptr when it is not a socket +inline const socket_ops* socket_ops_of(const resource_object* obj) { + return obj && obj->ops ? obj->ops->socket : nullptr; +} + +} // namespace resource + +#endif // STELLUX_RESOURCE_SOCKET_OPS_H diff --git a/kernel/socket/unix_socket.cpp b/kernel/socket/unix_socket.cpp index a01ec468..d22a3310 100644 --- a/kernel/socket/unix_socket.cpp +++ b/kernel/socket/unix_socket.cpp @@ -1,4 +1,5 @@ #include "socket/unix_socket.h" +#include "resource/socket_ops.h" #include "mm/heap.h" #include "sync/spinlock.h" #include "sync/wait_queue.h" @@ -449,22 +450,19 @@ __PRIVILEGED_CODE static uint32_t socket_poll( return 0; } +static const resource::socket_ops g_unix_socket_ops = { + .bind = unix_bind, + .listen = unix_listen, + .accept = unix_accept, + .connect = unix_connect, +}; + static const resource::resource_ops g_socket_ops = { - socket_read, - socket_write, - socket_close, - nullptr, // ioctl - nullptr, // mmap - nullptr, // sendto - nullptr, // recvfrom - unix_bind, - unix_listen, - unix_accept, - unix_connect, - nullptr, // setsockopt - nullptr, // getsockopt - socket_poll, - nullptr, // shutdown + .read = socket_read, + .write = socket_write, + .close = socket_close, + .poll = socket_poll, + .socket = &g_unix_socket_ops, }; const resource::resource_ops* get_socket_ops() { diff --git a/kernel/syscall/handlers/sys_shutdown.cpp b/kernel/syscall/handlers/sys_shutdown.cpp index 989e6ce1..f6818dbe 100644 --- a/kernel/syscall/handlers/sys_shutdown.cpp +++ b/kernel/syscall/handlers/sys_shutdown.cpp @@ -1,6 +1,6 @@ #include "syscall/handlers/sys_shutdown.h" -#include "resource/resource.h" +#include "resource/socket_ops.h" #include "sched/sched.h" #include "sched/task.h" @@ -27,12 +27,13 @@ DEFINE_SYSCALL2(shutdown, fd, how) { return syscall::ENOTSOCK; } - if (!obj->ops || !obj->ops->shutdown) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->shutdown) { resource::resource_release(obj); return syscall::EOPNOTSUPP; } - int32_t result = obj->ops->shutdown(obj, how_val); + int32_t result = sockops->shutdown(obj, how_val); resource::resource_release(obj); if (result == resource::OK) return 0; diff --git a/kernel/syscall/handlers/sys_socket.cpp b/kernel/syscall/handlers/sys_socket.cpp index da0d9163..68a89c1f 100644 --- a/kernel/syscall/handlers/sys_socket.cpp +++ b/kernel/syscall/handlers/sys_socket.cpp @@ -2,7 +2,7 @@ #include "socket/unix_socket.h" #include "net/inet.h" -#include "resource/resource.h" +#include "resource/socket_ops.h" #include "fs/fstypes.h" #include "sched/sched.h" #include "sched/task.h" @@ -164,7 +164,8 @@ DEFINE_SYSCALL3(bind, fd, addr, addrlen) { return syscall::EINVAL; } - if (!obj->ops || !obj->ops->bind) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->bind) { resource::resource_release(obj); return syscall::EOPNOTSUPP; } @@ -179,7 +180,7 @@ DEFINE_SYSCALL3(bind, fd, addr, addrlen) { return syscall::EFAULT; } - int32_t result = obj->ops->bind(obj, kaddr, klen); + int32_t result = sockops->bind(obj, kaddr, klen); resource::resource_release(obj); return (result == resource::OK) ? 0 : map_socket_op_error(result); } @@ -199,12 +200,13 @@ DEFINE_SYSCALL2(listen, fd, backlog) { return syscall::EINVAL; } - if (!obj->ops || !obj->ops->listen) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->listen) { resource::resource_release(obj); return syscall::EOPNOTSUPP; } - int32_t result = obj->ops->listen(obj, static_cast(backlog)); + int32_t result = sockops->listen(obj, static_cast(backlog)); resource::resource_release(obj); return (result == resource::OK) ? 0 : map_socket_op_error(result); } @@ -228,7 +230,8 @@ DEFINE_SYSCALL3(connect, fd, addr, addrlen) { return syscall::EINVAL; } - if (!obj->ops || !obj->ops->connect) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->connect) { resource::resource_release(obj); return syscall::EOPNOTSUPP; } @@ -243,7 +246,7 @@ DEFINE_SYSCALL3(connect, fd, addr, addrlen) { return syscall::EFAULT; } - int32_t result = obj->ops->connect(obj, kaddr, klen); + int32_t result = sockops->connect(obj, kaddr, klen); resource::resource_release(obj); return (result == resource::OK) ? 0 : map_socket_op_error(result); } @@ -264,7 +267,8 @@ DEFINE_SYSCALL3(accept, fd, addr, addrlen) { return syscall::EINVAL; } - if (!listen_obj->ops || !listen_obj->ops->accept) { + const resource::socket_ops* sockops = resource::socket_ops_of(listen_obj); + if (!sockops || !sockops->accept) { resource::resource_release(listen_obj); return syscall::EOPNOTSUPP; } @@ -275,7 +279,7 @@ DEFINE_SYSCALL3(accept, fd, addr, addrlen) { size_t kaddr_len = sizeof(kaddr); resource::resource_object* new_obj = nullptr; - int32_t result = listen_obj->ops->accept( + int32_t result = sockops->accept( listen_obj, &new_obj, kaddr, &kaddr_len, nonblock); if (result != resource::OK) { @@ -335,7 +339,8 @@ DEFINE_SYSCALL6(sendto, fd, buf, len, flags, dest_addr, addrlen) { return syscall::EBADF; } - if (!obj->ops || !obj->ops->sendto) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->sendto) { resource::resource_release(obj); return syscall::EOPNOTSUPP; } @@ -377,9 +382,9 @@ DEFINE_SYSCALL6(sendto, fd, buf, len, flags, dest_addr, addrlen) { } } - ssize_t result = obj->ops->sendto(obj, kbuf, data_len, - static_cast(flags), - kaddr, addr_len); + ssize_t result = sockops->sendto(obj, kbuf, data_len, + static_cast(flags), + kaddr, addr_len); heap::kfree(kbuf); resource::resource_release(obj); @@ -409,7 +414,8 @@ DEFINE_SYSCALL6(recvfrom, fd, buf, len, flags, src_addr, addrlen) { return syscall::EBADF; } - if (!obj->ops || !obj->ops->recvfrom) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->recvfrom) { resource::resource_release(obj); return syscall::EOPNOTSUPP; } @@ -428,9 +434,9 @@ DEFINE_SYSCALL6(recvfrom, fd, buf, len, flags, src_addr, addrlen) { uint8_t kaddr[SENDTO_MAX_ADDR] = {}; size_t kaddr_len = sizeof(kaddr); - ssize_t result = obj->ops->recvfrom(obj, kbuf, data_len, - static_cast(flags), - kaddr, &kaddr_len); + ssize_t result = sockops->recvfrom(obj, kbuf, data_len, + static_cast(flags), + kaddr, &kaddr_len); if (result < 0) { heap::kfree(kbuf); @@ -497,7 +503,8 @@ DEFINE_SYSCALL5(setsockopt, fd, level, optname, optval, optlen) { return syscall::EINVAL; } - if (!obj->ops || !obj->ops->setsockopt) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->setsockopt) { resource::resource_release(obj); return syscall::ENOPROTOOPT; } @@ -516,7 +523,7 @@ DEFINE_SYSCALL5(setsockopt, fd, level, optname, optval, optlen) { return syscall::EFAULT; } - int32_t result = obj->ops->setsockopt( + int32_t result = sockops->setsockopt( obj, static_cast(level), static_cast(optname), kval, klen); resource::resource_release(obj); @@ -540,7 +547,8 @@ DEFINE_SYSCALL5(getsockopt, fd, level, optname, optval, optlen) { return syscall::EINVAL; } - if (!obj->ops || !obj->ops->getsockopt) { + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops || !sockops->getsockopt) { resource::resource_release(obj); return syscall::ENOPROTOOPT; } @@ -560,7 +568,7 @@ DEFINE_SYSCALL5(getsockopt, fd, level, optname, optval, optlen) { } uint8_t kval[64] = {}; - int32_t result = obj->ops->getsockopt( + int32_t result = sockops->getsockopt( obj, static_cast(level), static_cast(optname), kval, &klen); if (result != resource::OK) { diff --git a/kernel/terminal/terminal.cpp b/kernel/terminal/terminal.cpp index e8400b6e..2f2c0d49 100644 --- a/kernel/terminal/terminal.cpp +++ b/kernel/terminal/terminal.cpp @@ -120,21 +120,10 @@ __PRIVILEGED_CODE static uint32_t terminal_poll( } static const resource::resource_ops g_terminal_ops = { - terminal_read, - terminal_write, - terminal_close, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - terminal_poll, - nullptr, + .read = terminal_read, + .write = terminal_write, + .close = terminal_close, + .poll = terminal_poll, }; const resource::resource_ops* get_terminal_ops() { diff --git a/kernel/tests/resource/resource.test.cpp b/kernel/tests/resource/resource.test.cpp index 40d6ad9e..06a26bad 100644 --- a/kernel/tests/resource/resource.test.cpp +++ b/kernel/tests/resource/resource.test.cpp @@ -139,11 +139,7 @@ TEST(resource_test, missing_provider_ops_return_err_unsup) { sched::task* task = sched::current(); ASSERT_NOT_NULL(task); - static const resource::resource_ops no_rw_ops = { - nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - nullptr, - }; + static const resource::resource_ops no_rw_ops = {}; auto* read_obj = heap::kalloc_new(); ASSERT_NOT_NULL(read_obj); @@ -195,9 +191,7 @@ TEST(resource_test, terminal_release_invokes_close_once) { ASSERT_NOT_NULL(task); static const resource::resource_ops close_counter_ops = { - nullptr, nullptr, close_counter_close, nullptr, nullptr, nullptr, nullptr, - nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - nullptr, + .close = close_counter_close, }; resource_close_counter counter{0}; @@ -319,9 +313,7 @@ TEST(resource_test, dup2_replaces_and_closes_target) { ASSERT_NOT_NULL(task); static const resource::resource_ops victim_ops = { - nullptr, nullptr, close_counter_close, nullptr, nullptr, nullptr, nullptr, - nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - nullptr, + .close = close_counter_close, }; resource_close_counter counter{0}; From 4b3e6547114edea90ca3ff53e2d3a0a1c309d134 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 20:08:45 -0700 Subject: [PATCH 19/20] feat(syscall): implemented getsockname and getpeername syscalls through the socket ops table --- kernel/net/icmp_socket.cpp | 15 +++++++ kernel/syscall/handlers/sys_error_map.h | 21 +++++++++ kernel/syscall/handlers/sys_sockaddr.cpp | 57 ++++++++++++++++++++---- kernel/syscall/handlers/sys_socket.cpp | 34 ++++---------- 4 files changed, 93 insertions(+), 34 deletions(-) diff --git a/kernel/net/icmp_socket.cpp b/kernel/net/icmp_socket.cpp index a65132de..0cfdd023 100644 --- a/kernel/net/icmp_socket.cpp +++ b/kernel/net/icmp_socket.cpp @@ -205,6 +205,20 @@ static ssize_t socket_recvfrom(resource::resource_object* obj, void* kdst, size_ return static_cast(copied); } +// Reports the socket as unbound with its identifier in the port field +static int32_t socket_getname(resource::resource_object* obj, void* kaddr, size_t* addrlen, bool peer) { + if (peer) { + return resource::ERR_NOTCONN; + } + + icmp_socket* sock = static_cast(obj->impl); + if (inet::fill_sockaddr(kaddr, addrlen, ipv4::UNSPECIFIED_ADDR, sock->id) != OK) { + return resource::ERR_INVAL; + } + + return resource::OK; +} + static void socket_close(resource::resource_object* obj) { if (!obj || !obj->impl) { return; @@ -225,6 +239,7 @@ static ssize_t socket_write(resource::resource_object*, const void*, size_t, uin static const resource::socket_ops g_icmp_socket_ops = { .sendto = socket_sendto, .recvfrom = socket_recvfrom, + .getname = socket_getname, }; static const resource::resource_ops g_socket_ops = { diff --git a/kernel/syscall/handlers/sys_error_map.h b/kernel/syscall/handlers/sys_error_map.h index f53b83e0..3c638eae 100644 --- a/kernel/syscall/handlers/sys_error_map.h +++ b/kernel/syscall/handlers/sys_error_map.h @@ -3,9 +3,30 @@ #include "syscall/syscall_table.h" #include "fs/fs.h" +#include "resource/resource.h" namespace syscall::error_map { +inline int64_t map_socket_op_error(int32_t rc) { + switch (rc) { + case resource::ERR_INVAL: return syscall::EINVAL; + case resource::ERR_NOMEM: return syscall::ENOMEM; + case resource::ERR_ADDRINUSE: return syscall::EADDRINUSE; + case resource::ERR_CONNREFUSED: return syscall::ECONNREFUSED; + case resource::ERR_ISCONN: return syscall::EISCONN; + case resource::ERR_NOTCONN: return syscall::ENOTCONN; + case resource::ERR_AGAIN: return syscall::EAGAIN; + case resource::ERR_NOENT: return syscall::ENOENT; + case resource::ERR_NOTDIR: return syscall::ENOTDIR; + case resource::ERR_INTR: return syscall::ERESTARTSYS; + case resource::ERR_NOPROTOOPT: return syscall::ENOPROTOOPT; + case resource::ERR_MSGSIZE: return syscall::EMSGSIZE; + case resource::ERR_HOSTUNREACH: return syscall::EHOSTUNREACH; + case resource::ERR_UNSUP: return syscall::EOPNOTSUPP; + default: return syscall::EIO; + } +} + inline int64_t map_fs_error(int32_t rc) { switch (rc) { case fs::ERR_NOENT: diff --git a/kernel/syscall/handlers/sys_sockaddr.cpp b/kernel/syscall/handlers/sys_sockaddr.cpp index 19939859..56262e34 100644 --- a/kernel/syscall/handlers/sys_sockaddr.cpp +++ b/kernel/syscall/handlers/sys_sockaddr.cpp @@ -1,12 +1,14 @@ #include "syscall/handlers/sys_sockaddr.h" +#include "syscall/handlers/sys_error_map.h" -#include "resource/resource.h" +#include "resource/socket_ops.h" #include "sched/sched.h" #include "sched/task.h" +#include "mm/uaccess.h" -// No socket family reports addresses yet, so both calls validate the -// descriptor and decline. A transport that carries addresses fills them in. -static int64_t reject_socket_address_query(uint64_t fd, uint64_t u_addr, uint64_t u_addrlen) { +constexpr size_t SOCKADDR_STORAGE_LEN = 128; // Largest address any family reports + +static int64_t query_socket_address(uint64_t fd, uint64_t u_addr, uint64_t u_addrlen, bool peer) { if (u_addr == 0 || u_addrlen == 0) { return syscall::EFAULT; } @@ -19,20 +21,59 @@ static int64_t reject_socket_address_query(uint64_t fd, uint64_t u_addr, uint64_ resource::resource_object* obj = nullptr; int32_t rc = resource::get_handle_object( task->handles, static_cast(fd), 0, &obj); + if (rc != resource::HANDLE_OK) { return syscall::EBADF; } - bool is_socket = obj->type == resource::resource_type::SOCKET; + const resource::socket_ops* sockops = resource::socket_ops_of(obj); + if (!sockops) { + resource::resource_release(obj); + return syscall::ENOTSOCK; + } + + if (!sockops->getname) { + resource::resource_release(obj); + return syscall::EOPNOTSUPP; + } + + uint8_t kaddr[SOCKADDR_STORAGE_LEN] = {}; + size_t kaddr_len = sizeof(kaddr); + + int32_t result = sockops->getname(obj, kaddr, &kaddr_len, peer); resource::resource_release(obj); + + if (result != resource::OK) { + return syscall::error_map::map_socket_op_error(result); + } + + // The caller gets as much of the address as fits, + // and the full length so a short buffer is detectable. + uint32_t user_addrlen = 0; + if (mm::uaccess::copy_from_user(&user_addrlen, reinterpret_cast(u_addrlen), + sizeof(user_addrlen)) != mm::uaccess::OK) { + return syscall::EFAULT; + } + + size_t copy_len = kaddr_len < user_addrlen ? kaddr_len : user_addrlen; + if (copy_len > 0 && + mm::uaccess::copy_to_user(reinterpret_cast(u_addr), kaddr, copy_len) != mm::uaccess::OK) { + return syscall::EFAULT; + } + + uint32_t out_len = static_cast(kaddr_len); + if (mm::uaccess::copy_to_user(reinterpret_cast(u_addrlen), &out_len, + sizeof(out_len)) != mm::uaccess::OK) { + return syscall::EFAULT; + } - return is_socket ? syscall::EOPNOTSUPP : syscall::ENOTSOCK; + return 0; } DEFINE_SYSCALL3(getsockname, fd, u_addr, u_addrlen) { - return reject_socket_address_query(fd, u_addr, u_addrlen); + return query_socket_address(fd, u_addr, u_addrlen, false); } DEFINE_SYSCALL3(getpeername, fd, u_addr, u_addrlen) { - return reject_socket_address_query(fd, u_addr, u_addrlen); + return query_socket_address(fd, u_addr, u_addrlen, true); } diff --git a/kernel/syscall/handlers/sys_socket.cpp b/kernel/syscall/handlers/sys_socket.cpp index 68a89c1f..0fa7173e 100644 --- a/kernel/syscall/handlers/sys_socket.cpp +++ b/kernel/syscall/handlers/sys_socket.cpp @@ -1,4 +1,5 @@ #include "syscall/handlers/sys_socket.h" +#include "syscall/handlers/sys_error_map.h" #include "socket/unix_socket.h" #include "net/inet.h" @@ -14,25 +15,6 @@ constexpr uint64_t SOCK_STREAM = 1; constexpr size_t SENDTO_MAX_ADDR = 128; constexpr size_t SENDTO_MAX_BUF = 4096; -static inline int64_t map_socket_op_error(int32_t rc) { - switch (rc) { - case resource::ERR_INVAL: return syscall::EINVAL; - case resource::ERR_NOMEM: return syscall::ENOMEM; - case resource::ERR_ADDRINUSE: return syscall::EADDRINUSE; - case resource::ERR_CONNREFUSED: return syscall::ECONNREFUSED; - case resource::ERR_ISCONN: return syscall::EISCONN; - case resource::ERR_AGAIN: return syscall::EAGAIN; - case resource::ERR_NOENT: return syscall::ENOENT; - case resource::ERR_NOTDIR: return syscall::ENOTDIR; - case resource::ERR_INTR: return syscall::ERESTARTSYS; - case resource::ERR_NOPROTOOPT: return syscall::ENOPROTOOPT; - case resource::ERR_MSGSIZE: return syscall::EMSGSIZE; - case resource::ERR_HOSTUNREACH: return syscall::EHOSTUNREACH; - case resource::ERR_UNSUP: return syscall::EOPNOTSUPP; - default: return syscall::EIO; - } -} - DEFINE_SYSCALL3(socket, domain, type, protocol) { sched::task* task = sched::current(); if (!task) { @@ -182,7 +164,7 @@ DEFINE_SYSCALL3(bind, fd, addr, addrlen) { int32_t result = sockops->bind(obj, kaddr, klen); resource::resource_release(obj); - return (result == resource::OK) ? 0 : map_socket_op_error(result); + return (result == resource::OK) ? 0 : syscall::error_map::map_socket_op_error(result); } DEFINE_SYSCALL2(listen, fd, backlog) { @@ -208,7 +190,7 @@ DEFINE_SYSCALL2(listen, fd, backlog) { int32_t result = sockops->listen(obj, static_cast(backlog)); resource::resource_release(obj); - return (result == resource::OK) ? 0 : map_socket_op_error(result); + return (result == resource::OK) ? 0 : syscall::error_map::map_socket_op_error(result); } DEFINE_SYSCALL3(connect, fd, addr, addrlen) { @@ -248,7 +230,7 @@ DEFINE_SYSCALL3(connect, fd, addr, addrlen) { int32_t result = sockops->connect(obj, kaddr, klen); resource::resource_release(obj); - return (result == resource::OK) ? 0 : map_socket_op_error(result); + return (result == resource::OK) ? 0 : syscall::error_map::map_socket_op_error(result); } DEFINE_SYSCALL3(accept, fd, addr, addrlen) { @@ -284,7 +266,7 @@ DEFINE_SYSCALL3(accept, fd, addr, addrlen) { if (result != resource::OK) { resource::resource_release(listen_obj); - return map_socket_op_error(result); + return syscall::error_map::map_socket_op_error(result); } resource::handle_t new_handle = -1; @@ -389,7 +371,7 @@ DEFINE_SYSCALL6(sendto, fd, buf, len, flags, dest_addr, addrlen) { resource::resource_release(obj); if (result < 0) { - return map_socket_op_error(static_cast(result)); + return syscall::error_map::map_socket_op_error(static_cast(result)); } return result; @@ -527,7 +509,7 @@ DEFINE_SYSCALL5(setsockopt, fd, level, optname, optval, optlen) { obj, static_cast(level), static_cast(optname), kval, klen); resource::resource_release(obj); - return (result == resource::OK) ? 0 : map_socket_op_error(result); + return (result == resource::OK) ? 0 : syscall::error_map::map_socket_op_error(result); } DEFINE_SYSCALL5(getsockopt, fd, level, optname, optval, optlen) { @@ -573,7 +555,7 @@ DEFINE_SYSCALL5(getsockopt, fd, level, optname, optval, optlen) { kval, &klen); if (result != resource::OK) { resource::resource_release(obj); - return map_socket_op_error(result); + return syscall::error_map::map_socket_op_error(result); } copy_rc = mm::uaccess::copy_to_user( From d876ab55ef2e0c76f1e2c4809120b3b0b14d5665 Mon Sep 17 00:00:00 2001 From: FlareCoding Date: Sat, 5 Sep 2026 21:06:11 -0700 Subject: [PATCH 20/20] feat(userland): made ping learn its echo identifier from the kernel --- userland/apps/ping/src/ping.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/userland/apps/ping/src/ping.c b/userland/apps/ping/src/ping.c index 16503c53..760a2cb1 100644 --- a/userland/apps/ping/src/ping.c +++ b/userland/apps/ping/src/ping.c @@ -273,6 +273,16 @@ int main(int argc, char* argv[]) { return 1; } + // The kernel stamps its own identifier on every request and reports it as the + // local port, the stack address is only a fallback where that query fails. + uint16_t ping_id = (uint16_t)(uintptr_t)&fd; + struct sockaddr_in local; + socklen_t local_len = sizeof(local); + + if (getsockname(fd, (struct sockaddr*)&local, &local_len) == 0 && local_len == sizeof(local)) { + ping_id = my_ntohs(local.sin_port); + } + char ip_str[32]; format_ip(dst_ip_host, ip_str, sizeof(ip_str)); if (parse_ipv4(target) != 0) { @@ -291,7 +301,6 @@ int main(int argc, char* argv[]) { uint32_t rtt_min = 0xFFFFFFFF; uint32_t rtt_max = 0; uint64_t rtt_total = 0; - uint16_t ping_id = (uint16_t)(uintptr_t)&fd; // unique-ish per process for (int i = 0; i < count; i++) { // Build ICMP echo request