diff --git a/Makefile b/Makefile index 0b8f6c4..699550a 100644 --- a/Makefile +++ b/Makefile @@ -287,7 +287,7 @@ endif CONFIG_SRCS-y += gui/gui.c gui/gui_gfx.c gui/gui_cursor.c gui/gui_wm.c gui/gui_desktop.c gui/gui_apps.c \ gui/gui_widgets.c gui/gui_icons.c gui/gui_dialog.c gui/gui_apps_ext.c gui/gui_terminal.c \ - gui/gui_app_sorry.c + gui/gui_app_sorry.c gui/gui_app_netmon.c # Deduplicate CONFIG_SRCS-y := $(sort $(CONFIG_SRCS-y)) diff --git a/gui/gui_app_netmon.c b/gui/gui_app_netmon.c new file mode 100644 index 0000000..23b4db8 --- /dev/null +++ b/gui/gui_app_netmon.c @@ -0,0 +1,328 @@ +/* + * SUB-OS Network Monitor. + * + * A live view of the networking stack: the interface's configuration, a + * rolling chart of receive and transmit rates taken from the NIC's own + * counters, and the TCP engine's connection table with every connection's + * real state and direction -- the same data `netstat` prints, refreshed in + * place. + * + * Rates are differences between successive samples of the driver counters, + * taken on a fixed ~2 Hz cadence rather than once per repaint, so the chart + * reads in packets per half-second regardless of how often the compositor + * happens to redraw the window. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NETMON_HISTORY 64 +#define NETMON_PERIOD_MS 500 // sampling cadence for the rate chart + +#define NETMON_RX_COLOR GUI_THEME_SUCCESS +#define NETMON_TX_COLOR GUI_THEME_PRIMARY + +typedef struct { + uint16_t rx[NETMON_HISTORY]; // packets received per sample interval + uint16_t tx[NETMON_HISTORY]; + int head; // next slot to write + int count; + + uint64_t last_sample_ms; + uint64_t prev_rx_pkts; + uint64_t prev_tx_pkts; + bool primed; // a baseline reading has been taken + + int scroll; // first connection row shown +} netmon_data_t; + +// =========================================================================== +// Sampling +// =========================================================================== +static void netmon_sample(netmon_data_t* nd, uint64_t now_ms) { + uint64_t rx = e1000_get_rx_packets(); + uint64_t tx = e1000_get_tx_packets(); + + // The first reading only establishes a baseline: without it the whole + // counter total since boot would be charted as one enormous first spike. + if (!nd->primed) { + nd->prev_rx_pkts = rx; + nd->prev_tx_pkts = tx; + nd->primed = true; + nd->last_sample_ms = now_ms; + return; + } + + if (now_ms - nd->last_sample_ms < NETMON_PERIOD_MS) return; + nd->last_sample_ms = now_ms; + + uint64_t drx = (rx >= nd->prev_rx_pkts) ? rx - nd->prev_rx_pkts : 0; + uint64_t dtx = (tx >= nd->prev_tx_pkts) ? tx - nd->prev_tx_pkts : 0; + nd->prev_rx_pkts = rx; + nd->prev_tx_pkts = tx; + + nd->rx[nd->head] = (drx > 0xFFFF) ? 0xFFFF : (uint16_t)drx; + nd->tx[nd->head] = (dtx > 0xFFFF) ? 0xFFFF : (uint16_t)dtx; + nd->head = (nd->head + 1) % NETMON_HISTORY; + if (nd->count < NETMON_HISTORY) nd->count++; +} + +// Oldest-first read of a ring, so the chart scrolls left with time. +static uint16_t ring_at(const uint16_t* ring, int head, int count, int i) { + int start = (head + NETMON_HISTORY - count) % NETMON_HISTORY; + return ring[(start + i) % NETMON_HISTORY]; +} + +// =========================================================================== +// Rate chart +// =========================================================================== +static void netmon_draw_chart(netmon_data_t* nd, int ox, int oy, int x, int y, int w, int h) { + gui_gfx_fill_rect(ox + x, oy + y, w, h, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(ox + x, oy + y, w, h, GUI_THEME_BORDER); + + for (int gy = y + h / 4; gy < y + h; gy += h / 4) { + gui_gfx_draw_line(ox + x + 1, oy + gy, ox + x + w - 2, oy + gy, 0xFF1E293B); + } + + // Scale to the tallest sample in view, with a floor so an idle link does + // not magnify single stray packets into a full-height sawtooth. + uint16_t peak = 4; + for (int i = 0; i < nd->count; i++) { + uint16_t r = ring_at(nd->rx, nd->head, nd->count, i); + uint16_t t = ring_at(nd->tx, nd->head, nd->count, i); + if (r > peak) peak = r; + if (t > peak) peak = t; + } + + if (nd->count >= 2) { + int step = (w - 8) / (NETMON_HISTORY - 1); + if (step < 1) step = 1; + + for (int pass = 0; pass < 2; pass++) { + const uint16_t* ring = pass ? nd->tx : nd->rx; + uint32_t col = pass ? NETMON_TX_COLOR : NETMON_RX_COLOR; + + int prev_x = 0, prev_y = 0; + for (int i = 0; i < nd->count; i++) { + uint16_t v = ring_at(ring, nd->head, nd->count, i); + int px = x + 4 + i * step; + int py = y + h - 4 - ((int)v * (h - 10)) / peak; + if (px > x + w - 3) break; + + if (i > 0) { + gui_gfx_draw_line(ox + prev_x, oy + prev_y, ox + px, oy + py, col); + } + prev_x = px; + prev_y = py; + } + } + } else { + gui_gfx_draw_string(ox + x + 8, oy + y + h / 2 - 4, + "sampling...", GUI_THEME_TEXT_DIM); + } + + char buf[40]; + snprintf(buf, sizeof(buf), "peak %u pkt/%ums", peak, NETMON_PERIOD_MS); + gui_gfx_draw_string(ox + x + 6, oy + y + 5, buf, GUI_THEME_TEXT_DIM); + + // Legend, right-aligned inside the plot. + int lx = x + w - 88; + gui_gfx_fill_rect(ox + lx, oy + y + 7, 8, 2, NETMON_RX_COLOR); + gui_gfx_draw_string(ox + lx + 12, oy + y + 5, "rx", NETMON_RX_COLOR); + gui_gfx_fill_rect(ox + lx + 34, oy + y + 7, 8, 2, NETMON_TX_COLOR); + gui_gfx_draw_string(ox + lx + 46, oy + y + 5, "tx", NETMON_TX_COLOR); +} + +// =========================================================================== +// Paint +// =========================================================================== +static void netmon_paint(gui_window_t* win) { + netmon_data_t* nd = (netmon_data_t*)win->user_data; + if (!nd) return; + + netmon_sample(nd, ktime_ms()); + + gui_widget_begin(win); + int w = gui_widget_client_width(); + int h = gui_widget_client_height(); + int ox = win->x + 1; + int oy = win->y + GUI_TITLEBAR_HEIGHT + 1; + if (w < 200 || h < 140) { gui_widget_end(); return; } + + char buf[96]; + net_if_t* nif = net_get_primary_if(); + bool link = e1000_is_link_up(); + + // --- interface header ------------------------------------------------- + char ip[16], mask[16], gw[16]; + if (nif) { + ip_to_str(nif->ip, ip); + ip_to_str(nif->subnet, mask); + ip_to_str(nif->gateway, gw); + } else { + strcpy(ip, "0.0.0.0"); strcpy(mask, "0.0.0.0"); strcpy(gw, "0.0.0.0"); + } + + gui_gfx_draw_string_16_shadow(ox + 10, oy + 8, nif ? nif->name : "eth0", + GUI_THEME_PRIMARY, GUI_COLOR_BLACK); + gui_badge(58, 10, link ? "UP" : "DOWN", + link ? GUI_THEME_SUCCESS : GUI_THEME_DANGER, GUI_COLOR_BLACK); + + uint8_t mac[6]; + e1000_get_mac(mac); + snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + gui_label_aligned(0, 12, w - 12, buf, GUI_THEME_TEXT_DIM, GUI_ALIGN_RIGHT); + + snprintf(buf, sizeof(buf), "inet %s mask %s gw %s", ip, mask, gw); + gui_label(10, 30, buf, GUI_THEME_TEXT_MUTED); + + // --- rate chart ------------------------------------------------------- + int chart_y = 46; + int chart_h = (h < 300) ? 70 : 96; + netmon_draw_chart(nd, ox, oy, 10, chart_y, w - 20, chart_h); + + // --- counters --------------------------------------------------------- + int row = chart_y + chart_h + 8; + snprintf(buf, sizeof(buf), "RX %llu pkts / %llu B", + (unsigned long long)e1000_get_rx_packets(), + (unsigned long long)e1000_get_rx_bytes()); + gui_label(10, row, buf, NETMON_RX_COLOR); + + snprintf(buf, sizeof(buf), "TX %llu pkts / %llu B", + (unsigned long long)e1000_get_tx_packets(), + (unsigned long long)e1000_get_tx_bytes()); + gui_label(10, row + 12, buf, NETMON_TX_COLOR); + + uint64_t urx, utx, udrop; + udp_get_stats(&urx, &utx, &udrop); + snprintf(buf, sizeof(buf), "udp %llu in %llu out %llu dropped", + (unsigned long long)urx, (unsigned long long)utx, + (unsigned long long)udrop); + gui_label_aligned(0, row, w - 12, buf, GUI_THEME_TEXT_MUTED, GUI_ALIGN_RIGHT); + + snprintf(buf, sizeof(buf), "tcp %u connection(s) %d listener(s)", + (unsigned)tcp_get_connections_count(), tcp_get_listener_count()); + gui_label_aligned(0, row + 12, w - 12, buf, GUI_THEME_TEXT_MUTED, GUI_ALIGN_RIGHT); + + // --- connection table ------------------------------------------------- + int tbl_y = row + 30; + gui_separator(10, tbl_y - 4, w - 20); + gui_label_bold(10, tbl_y, "PROTO LOCAL", GUI_THEME_TEXT_DIM); + gui_label(w / 2 + 6, tbl_y, "REMOTE", GUI_THEME_TEXT_DIM); + gui_label_aligned(0, tbl_y, w - 12, "STATE", GUI_THEME_TEXT_DIM, GUI_ALIGN_RIGHT); + + // Reserve the bottom strip for the scroll controls whether or not they are + // needed, so the last row never ends up underneath them. + int line_h = 12; + int list_top = tbl_y + 14; + int rows_fit = (h - 26 - list_top) / line_h; + if (rows_fit < 1) rows_fit = 1; + + // Build the display list: listeners first, then live connections. It is + // rebuilt every frame because the engine's table is the only source of + // truth and entries come and go on their own. + int shown = 0; + int index = 0; + + for (int i = 0; i < tcp_get_listener_count(); i++) { + uint16_t lp = tcp_get_listener_port(i); + if (!lp) continue; + if (index++ < nd->scroll) continue; + if (shown >= rows_fit) break; + + int ly = list_top + shown * line_h; + snprintf(buf, sizeof(buf), "tcp 0.0.0.0:%u", lp); + gui_label(10, ly, buf, GUI_THEME_TEXT_MAIN); + gui_label(w / 2 + 6, ly, "*:*", GUI_THEME_TEXT_DIM); + snprintf(buf, sizeof(buf), "LISTEN (%d)", tcp_get_listener_backlog(i)); + gui_label_aligned(0, ly, w - 12, buf, GUI_THEME_WARNING, GUI_ALIGN_RIGHT); + shown++; + } + + for (size_t i = 0; i < 32; i++) { + const tcp_conn_t* c = tcp_get_connection(i); + if (!c) continue; + if (index++ < nd->scroll) continue; + if (shown >= rows_fit) break; + + int ly = list_top + shown * line_h; + char lip[16], rip[16]; + ip_to_str(c->local_ip, lip); + ip_to_str(c->remote_ip, rip); + + snprintf(buf, sizeof(buf), "tcp %s:%u", lip, c->local_port); + gui_label(10, ly, buf, GUI_THEME_TEXT_MAIN); + + snprintf(buf, sizeof(buf), "%s:%u", rip, c->remote_port); + gui_label(w / 2 + 6, ly, buf, GUI_THEME_TEXT_MUTED); + + // Established is the state worth spotting at a glance; the transient + // handshake and teardown states are dimmer. + uint32_t col = (c->state == TCP_STATE_ESTABLISHED) ? GUI_THEME_SUCCESS + : (c->state == TCP_STATE_CLOSED) ? GUI_THEME_TEXT_DIM + : GUI_THEME_WARNING; + snprintf(buf, sizeof(buf), "%s %s", tcp_state_name(c->state), + c->role == TCP_ROLE_CLIENT ? "out" : + c->role == TCP_ROLE_ACCEPTED ? "in" : "svc"); + gui_label_aligned(0, ly, w - 12, buf, col, GUI_ALIGN_RIGHT); + shown++; + } + + if (shown == 0) { + gui_label(10, list_top, "(no sockets open)", GUI_THEME_TEXT_DIM); + } + + // Scroll only matters once the list outgrows the window. + if (index > rows_fit) { + snprintf(buf, sizeof(buf), "%d-%d of %d", nd->scroll + 1, + nd->scroll + shown, index); + gui_label(10, h - 20, buf, GUI_THEME_TEXT_DIM); + + if (gui_button(1, w - 58, h - 24, 24, 18, "^") && nd->scroll > 0) nd->scroll--; + if (gui_button(2, w - 32, h - 24, 24, 18, "v") && nd->scroll < index - rows_fit) { + nd->scroll++; + } + } else { + nd->scroll = 0; + } + + gui_widget_end(); +} + +static void netmon_event(gui_window_t* win, const gui_event_t* ev) { + gui_widget_feed_event(win, ev); + + if (ev->type == GUI_EVENT_CLOSE && win->user_data) { + kfree(win->user_data); + win->user_data = NULL; + } +} + +void gui_app_netmon_launch(int x, int y, int w, int h) { + gui_window_t* win = gui_wm_create_window("Network Monitor", x, y, + (w > 0) ? w : 560, (h > 0) ? h : 400); + if (!win) return; + + netmon_data_t* nd = (netmon_data_t*)kzalloc(sizeof(netmon_data_t)); + if (!nd) { + gui_wm_destroy_window(win->id); + return; + } + + win->user_data = nd; + win->paint = netmon_paint; + win->handle_event = netmon_event; +} diff --git a/gui/gui_desktop.c b/gui/gui_desktop.c index 35a8195..78b4440 100644 --- a/gui/gui_desktop.c +++ b/gui/gui_desktop.c @@ -65,6 +65,7 @@ static const desktop_app_t g_apps[] = { { "Settings", GUI_ICON_SETTINGS, gui_app_settings_launch, 160, 60, 400, 320 }, { "Analytics", GUI_ICON_MONITOR, gui_app_analytics_launch,320, 70, 560, 400 }, { "Sorry", GUI_ICON_HEART, gui_app_sorry_launch, 260, 60, 660, 470 }, + { "Network", GUI_ICON_NETWORK, gui_app_netmon_launch, 300, 80, 560, 400 }, { "About", GUI_ICON_INFO, gui_app_about_launch, 240, 140, 330, 170 }, }; #define APP_COUNT ((int)(sizeof(g_apps) / sizeof(g_apps[0]))) @@ -72,8 +73,8 @@ static const desktop_app_t g_apps[] = { // Which of the above appear on the desktop itself. // Indices into g_apps[]: Terminal, Files, Monitor, Kernel Log, KTest, Life, // Rust Lab, Settings, Analytics, Sorry. (Objects/RegEdit live in the start -// menu.) -static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 9, 10, 14, 15, 16 }; +// menu.) Network sits alongside them. +static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 9, 10, 14, 15, 16, 17 }; #define DESKTOP_ICON_COUNT ((int)(sizeof(g_desktop_icons) / sizeof(g_desktop_icons[0]))) static bool g_start_menu_open = false; diff --git a/gui/gui_icons.c b/gui/gui_icons.c index 85d2e7c..1c03998 100644 --- a/gui/gui_icons.c +++ b/gui/gui_icons.c @@ -314,12 +314,31 @@ static const char* g_icons[GUI_ICON_COUNT][GUI_ICON_SIZE] = { "0000000990000000", "0000000000000000", "0000000000000000", + }, + // GUI_ICON_NETWORK + { + "0000000000000000", + "0000005555000000", + "0000556666550000", + "0005666556665000", + "0056666556666500", + "0555555555555550", + "0566666556666650", + "5666666556666665", + "5666666556666665", + "0566666556666650", + "0555555555555550", + "0056666556666500", + "0005666556665000", + "0000556666550000", + "0000005555000000", + "0000000000000000", },}; static const char* g_icon_names[GUI_ICON_COUNT] = { "Terminal", "Monitor", "Folder", "Calculator", "Paint", "Info", "Settings", "Tasks", "Log", "Tests", "Clock", "File", "Power", "Warning", - "Heart" + "Heart", "Network" }; static uint8_t hex_index(char c) { diff --git a/include/gui/gui_apps.h b/include/gui/gui_apps.h index cb59aaf..97490f0 100644 --- a/include/gui/gui_apps.h +++ b/include/gui/gui_apps.h @@ -15,5 +15,6 @@ void gui_app_objbrowse_launch(int x, int y, int w, int h); void gui_app_regedit_launch(int x, int y, int w, int h); void gui_app_analytics_launch(int x, int y, int w, int h); void gui_app_sorry_launch(int x, int y, int w, int h); +void gui_app_netmon_launch(int x, int y, int w, int h); #endif // _GUI_APPS_H diff --git a/include/gui/gui_icons.h b/include/gui/gui_icons.h index 582ac19..03141a9 100644 --- a/include/gui/gui_icons.h +++ b/include/gui/gui_icons.h @@ -26,6 +26,7 @@ typedef enum { GUI_ICON_POWER, GUI_ICON_WARNING, GUI_ICON_HEART, + GUI_ICON_NETWORK, GUI_ICON_COUNT } gui_icon_id_t; diff --git a/include/net/socket.h b/include/net/socket.h index 4080db2..14f142b 100644 --- a/include/net/socket.h +++ b/include/net/socket.h @@ -23,6 +23,11 @@ typedef struct sockaddr_in { char sin_zero[8]; } sockaddr_in_t; +// Values for socket_t.state. +#define SOCK_STATE_OPEN 0 // created, no peer or port committed yet +#define SOCK_STATE_CONNECTED 1 // peer address known (and, for a stream, open) +#define SOCK_STATE_LISTEN 2 // stream socket accepting inbound connections + typedef struct socket { int domain; int type; @@ -39,6 +44,12 @@ void socket_subsystem_init(void); int sys_socket(int domain, int type, int protocol); int sys_bind(int sockfd, const struct sockaddr_in* addr, size_t addrlen); int sys_connect(int sockfd, const struct sockaddr_in* addr, size_t addrlen); + +// Stream sockets only. sys_listen claims the bound port; sys_accept returns a +// new socket descriptor for the next connection to complete its handshake, or +// -1 if none arrives within the wait. `addr`, when given, receives the peer. +int sys_listen(int sockfd, int backlog); +int sys_accept(int sockfd, struct sockaddr_in* addr, size_t* addrlen); ssize_t sys_send(int sockfd, const void* buf, size_t len, int flags); ssize_t sys_recv(int sockfd, void* buf, size_t len, int flags); diff --git a/include/net/tcp.h b/include/net/tcp.h index 41bd4b3..a7d4e9d 100644 --- a/include/net/tcp.h +++ b/include/net/tcp.h @@ -34,10 +34,17 @@ typedef struct { uint16_t urgent_pointer; } __attribute__((packed)) tcp_header_t; -// Which end opened the connection. Server connections are created by an -// inbound SYN to a listening service; client connections by tcp_connect(). -#define TCP_ROLE_SERVER 0 -#define TCP_ROLE_CLIENT 1 +// Which end opened the connection, and how it is serviced. +// +// SERVICE -- created by an inbound SYN to one of the built-in servers +// (sshd, httpd), which are answered inline from the receive +// path and never hand a stream back to a caller. +// CLIENT -- opened by tcp_connect(). +// ACCEPTED -- opened by a peer against a tcp_listen() port and handed to +// tcp_accept(). Carries a byte stream exactly like a CLIENT. +#define TCP_ROLE_SERVICE 0 +#define TCP_ROLE_CLIENT 1 +#define TCP_ROLE_ACCEPTED 2 #define TCP_RX_BUF_SIZE 4096 #define TCP_MSS 1460 @@ -103,6 +110,31 @@ bool tcp_is_established(const tcp_conn_t* c); // True once the peer's FIN has been received, so no further data can arrive. bool tcp_peer_closed(const tcp_conn_t* c); +// --- Passive open (server side) ----------------------------------------- +// +// tcp_listen claims a port; inbound SYNs to it are completed by the receive +// path and the finished connections queue up until tcp_accept collects them. +// This is the general path: the built-in sshd and httpd predate it and are +// still answered inline, so listening on their ports is not useful. + +#define TCP_MAX_LISTENERS 8 +#define TCP_BACKLOG_MAX 4 + +// Returns 0 on success, -1 if the port is taken or the table is full. +int tcp_listen(uint16_t port, int backlog); + +// Collect the next established connection, waiting up to timeout_ms. +// Returns NULL if none arrived. The caller owns it and must tcp_close it. +tcp_conn_t* tcp_accept(uint16_t port, uint32_t timeout_ms); + +// Stop listening. Connections already accepted are unaffected; any still +// waiting in the backlog are reset. +void tcp_unlisten(uint16_t port); + +int tcp_get_listener_count(void); +uint16_t tcp_get_listener_port(int idx); +int tcp_get_listener_backlog(int idx); + size_t tcp_get_connections_count(void); const tcp_conn_t* tcp_get_connection(size_t index); const char* tcp_state_name(uint8_t state); diff --git a/net/core/socket.c b/net/core/socket.c index 81de026..6dc65bf 100644 --- a/net/core/socket.c +++ b/net/core/socket.c @@ -20,6 +20,7 @@ #define SOCK_CONNECT_TIMEOUT_MS 4000 #define SOCK_RECV_TIMEOUT_MS 2000 +#define SOCK_ACCEPT_TIMEOUT_MS 5000 typedef struct { uint32_t src_ip; @@ -36,6 +37,7 @@ typedef struct { int count; // queued datagrams bool bound; // a UDP binding is installed for local_port tcp_conn_t* tcp; // stream sockets only: the connection sys_connect opened + bool listening; // stream sockets only: a TCP listener holds local_port } sock_priv_t; static socket_t socket_table[MAX_SOCKETS]; @@ -144,15 +146,56 @@ int sys_connect(int sockfd, const struct sockaddr_in* addr, size_t addrlen) { if (!pv->tcp) return -1; s->local_ip = pv->tcp->local_ip; s->local_port = pv->tcp->local_port; - s->state = 1; + s->state = SOCK_STATE_CONNECTED; return 0; } - s->state = 1; // "connected" (address memorised for send/recv) + s->state = SOCK_STATE_CONNECTED; // address memorised for send/recv if (s->type == SOCK_DGRAM) return dgram_ensure_bound(sockfd); return 0; } +int sys_listen(int sockfd, int backlog) { + if (!valid_fd(sockfd)) return -1; + socket_t* s = &socket_table[sockfd]; + if (s->type != SOCK_STREAM || s->local_port == 0) return -1; + + if (tcp_listen(s->local_port, backlog) != 0) return -1; + socket_priv[sockfd].listening = true; + s->state = SOCK_STATE_LISTEN; + return 0; +} + +int sys_accept(int sockfd, struct sockaddr_in* addr, size_t* addrlen) { + if (!valid_fd(sockfd)) return -1; + socket_t* s = &socket_table[sockfd]; + if (s->type != SOCK_STREAM || !socket_priv[sockfd].listening) return -1; + + tcp_conn_t* c = tcp_accept(s->local_port, SOCK_ACCEPT_TIMEOUT_MS); + if (!c) return -1; + + // The accepted connection needs a descriptor of its own; the listening + // socket keeps its port and stays available for the next caller. + int fd = sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (fd < 0) { tcp_close(c); return -1; } + + socket_t* ns = &socket_table[fd]; + ns->local_ip = c->local_ip; + ns->local_port = c->local_port; + ns->remote_ip = c->remote_ip; + ns->remote_port = c->remote_port; + ns->state = SOCK_STATE_CONNECTED; + socket_priv[fd].tcp = c; + + if (addr) { + addr->sin_family = AF_INET; + addr->sin_addr = c->remote_ip; + addr->sin_port = htons(c->remote_port); + if (addrlen) *addrlen = sizeof(struct sockaddr_in); + } + return fd; +} + ssize_t sys_sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr_in* dest, size_t addrlen) { (void)flags; (void)addrlen; @@ -164,7 +207,7 @@ ssize_t sys_sendto(int sockfd, const void* buf, size_t len, int flags, uint32_t dip; uint16_t dport; if (dest) { dip = dest->sin_addr; dport = ntohs(dest->sin_port); } - else if (s->state == 1) { dip = s->remote_ip; dport = s->remote_port; } + else if (s->state == SOCK_STATE_CONNECTED) { dip = s->remote_ip; dport = s->remote_port; } else return -1; if (dgram_ensure_bound(sockfd) != 0) return -1; @@ -235,6 +278,7 @@ int sys_close_socket(int sockfd) { if (!valid_fd(sockfd)) return -1; socket_t* s = &socket_table[sockfd]; sock_priv_t* pv = &socket_priv[sockfd]; + if (pv->listening && s->local_port) tcp_unlisten(s->local_port); if (pv->tcp) tcp_close(pv->tcp); if (pv->bound && s->local_port) udp_unbind(s->local_port); memset(s, 0, sizeof(socket_t)); diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c index 420a8b3..4cd9322 100644 --- a/net/ipv4/tcp.c +++ b/net/ipv4/tcp.c @@ -48,6 +48,18 @@ static tcp_conn_t tcp_connections[MAX_TCP_CONNS]; +// A claimed port, plus the connections whose handshakes have completed on it +// and are waiting to be collected by tcp_accept. +typedef struct { + uint16_t port; + bool in_use; + int backlog; + tcp_conn_t* queue[TCP_BACKLOG_MAX]; + int count; +} tcp_listener_t; + +static tcp_listener_t tcp_listeners[TCP_MAX_LISTENERS]; + static uint16_t tcp_checksum(uint32_t src_ip, uint32_t dst_ip, const tcp_header_t* tcp, const void* payload, uint16_t payload_len) { uint16_t tcp_total_len = (uint16_t)(sizeof(tcp_header_t) + payload_len); @@ -71,6 +83,7 @@ static uint16_t tcp_checksum(uint32_t src_ip, uint32_t dst_ip, const tcp_header_ void tcp_init(void) { memset(tcp_connections, 0, sizeof(tcp_connections)); + memset(tcp_listeners, 0, sizeof(tcp_listeners)); printk(KERN_INFO "NET: TCP state machine online (active + passive open)\n"); } @@ -201,13 +214,34 @@ static tcp_conn_t* alloc_conn_server(uint32_t r_ip, uint16_t r_port, uint16_t l_ c->remote_ip = r_ip; c->remote_port = r_port; c->local_port = l_port; - c->role = TCP_ROLE_SERVER; + c->role = TCP_ROLE_SERVICE; c->state = TCP_STATE_CLOSED; c->seq_num = 1000; c->ack_num = 0; return c; } +// A connection born from an inbound SYN on a listening port. It carries a +// stream, so unlike the inline-service role it needs a receive buffer. +static tcp_conn_t* alloc_conn_accepted(uint32_t r_ip, uint16_t r_port, uint16_t l_port) { + tcp_conn_t* c = alloc_conn(); + if (!c) return NULL; + + c->rx_buf = (uint8_t*)kzalloc(TCP_RX_BUF_SIZE); + if (!c->rx_buf) { conn_free(c); return NULL; } + + net_if_t* netif = net_get_primary_if(); + c->local_ip = netif ? netif->ip : 0x0A00020F; + c->remote_ip = r_ip; + c->remote_port = r_port; + c->local_port = l_port; + c->role = TCP_ROLE_ACCEPTED; + c->iss = prng_rand32(); + c->seq_num = c->iss; + c->snd_una = c->iss; + return c; +} + static uint16_t alloc_ephemeral_port(void) { for (int attempt = 0; attempt < 512; attempt++) { uint16_t p = (uint16_t)(TCP_EPHEMERAL_LO + @@ -243,9 +277,36 @@ static uint16_t rx_append(tcp_conn_t* c, const uint8_t* data, uint16_t len) { } // =========================================================================== -// Client state machine +// Listener table // =========================================================================== -static void tcp_client_input(tcp_conn_t* c, uint32_t seq, uint32_t ack, uint8_t flags, +static tcp_listener_t* find_listener(uint16_t port) { + for (int i = 0; i < TCP_MAX_LISTENERS; i++) { + if (tcp_listeners[i].in_use && tcp_listeners[i].port == port) { + return &tcp_listeners[i]; + } + } + return NULL; +} + +// A handshake finished on a listening port: hand the connection to whoever is +// in tcp_accept. If nobody has drained the backlog, the peer is refused rather +// than left believing it has an open connection nobody will ever read. +static void listener_enqueue(tcp_conn_t* c) { + tcp_listener_t* l = find_listener(c->local_port); + if (!l) return; + + if (l->count >= l->backlog) { + tcp_tx(c, TCP_FLAG_RST, NULL, 0); + conn_free(c); + return; + } + l->queue[l->count++] = c; +} + +// =========================================================================== +// Stream state machine, shared by active opens and accepted connections +// =========================================================================== +static void tcp_stream_input(tcp_conn_t* c, uint32_t seq, uint32_t ack, uint8_t flags, const uint8_t* payload, uint16_t payload_len) { if (flags & TCP_FLAG_RST) { c->reset = true; @@ -275,15 +336,24 @@ static void tcp_client_input(tcp_conn_t* c, uint32_t seq, uint32_t ack, uint8_t if ((flags & TCP_FLAG_ACK) && seq_ge(ack, c->iss + 1)) { c->snd_una = ack; c->state = TCP_STATE_ESTABLISHED; + // A passive open is only complete now: this is the third leg of + // the handshake, so the connection can finally be handed out. + if (c->role == TCP_ROLE_ACCEPTED) listener_enqueue(c); } return; } if (c->state == TCP_STATE_CLOSED) return; - // A lingering connection only has to keep acknowledging a retransmitted FIN. + // A lingering connection only has to keep acknowledging the peer's FIN, + // whether it is a retransmission or the peer's first one arriving after we + // stopped waiting for it. if (c->state == TCP_STATE_TIME_WAIT) { - if (flags & TCP_FLAG_FIN) tcp_tx(c, TCP_FLAG_ACK, NULL, 0); + if (flags & TCP_FLAG_FIN) { + if (seq == c->ack_num) c->ack_num++; // the FIN itself + c->peer_fin = true; + tcp_tx(c, TCP_FLAG_ACK, NULL, 0); + } return; } @@ -401,6 +471,16 @@ static void tcp_server_input(tcp_conn_t* c, uint32_t src_ip, uint16_t src_port, tcp_send_packet(src_ip, dst_port, src_port, c->seq_num, c->ack_num, TCP_FLAG_PSH | TCP_FLAG_ACK, resp, (uint16_t)rlen); c->seq_num += rlen; + + // The response advertises "Connection: close", so honour it: send + // the FIN now and let the connection linger in TIME_WAIT until the + // reaper takes it. Without this every served request held its slot + // for good and the 32-entry table filled up after 32 hits. + tcp_send_packet(src_ip, dst_port, src_port, c->seq_num, c->ack_num, + TCP_FLAG_FIN | TCP_FLAG_ACK, NULL, 0); + c->seq_num++; + c->state = TCP_STATE_TIME_WAIT; + c->closed_at = pit_get_ticks(); } } } @@ -427,16 +507,36 @@ void tcp_receive(const uint8_t* packet, uint16_t length, uint32_t src_ip) { // 1. An established connection -- ours or a peer's -- owns this segment. tcp_conn_t* conn = find_conn(src_ip, src_port, dst_port); if (conn) { - if (conn->role == TCP_ROLE_CLIENT) { - tcp_client_input(conn, seq_num, ack_num, flags, payload, payload_len); - } else { + // Lingering is role-independent: whichever half opened the connection, + // all a TIME_WAIT entry owes the peer is an acknowledgement of its FIN. + if (conn->state == TCP_STATE_TIME_WAIT) { + tcp_stream_input(conn, seq_num, ack_num, flags, payload, payload_len); + } else if (conn->role == TCP_ROLE_SERVICE) { tcp_server_input(conn, src_ip, src_port, dst_port, seq_num, flags, payload, payload_len); + } else { + tcp_stream_input(conn, seq_num, ack_num, flags, payload, payload_len); } return; } - // 2. Otherwise a listening service may want to accept it. + // 2. A SYN to a port someone called tcp_listen on starts a passive open. + // Only a SYN may do so: anything else for an unknown 4-tuple is stale. + if ((flags & TCP_FLAG_SYN) && !(flags & TCP_FLAG_ACK) && find_listener(dst_port)) { + conn = alloc_conn_accepted(src_ip, src_port, dst_port); + if (conn) { + conn->ack_num = seq_num + 1; + conn->state = TCP_STATE_SYN_RCVD; + tcp_tx_seq(conn, conn->iss, TCP_FLAG_SYN | TCP_FLAG_ACK, NULL, 0); + conn->seq_num = conn->iss + 1; // our SYN consumes a sequence number + } else { + tcp_send_packet(src_ip, dst_port, src_port, 0, seq_num + 1, + TCP_FLAG_RST | TCP_FLAG_ACK, NULL, 0); + } + return; + } + + // 3. Otherwise one of the built-in services may want it. bool is_ssh_port = (dst_port == sshd_get_port() && sshd_is_running()); bool is_http_port = (dst_port == 80 && httpd_is_running()); if (is_ssh_port || is_http_port) { @@ -596,6 +696,15 @@ void tcp_close(tcp_conn_t* c) { } } + // A close we started may not have finished inside that wait: the peer is + // entitled to take its time sending its own FIN. Falling into TIME_WAIT + // rather than freeing the slot means that late FIN gets acknowledged + // properly instead of being answered with a reset. + if (c->state == TCP_STATE_FIN_WAIT1 || c->state == TCP_STATE_FIN_WAIT2 || + c->state == TCP_STATE_CLOSING) { + c->state = TCP_STATE_TIME_WAIT; + } + if (c->state == TCP_STATE_TIME_WAIT) { // Hold the slot so a late duplicate cannot land on a new connection. // The receive buffer is not needed for that, so give it back now. @@ -608,6 +717,83 @@ void tcp_close(tcp_conn_t* c) { conn_free(c); } +// =========================================================================== +// Passive open API +// =========================================================================== +int tcp_listen(uint16_t port, int backlog) { + if (port == 0) return -1; + if (find_listener(port)) return -1; // already claimed + if (backlog < 1) backlog = 1; + if (backlog > TCP_BACKLOG_MAX) backlog = TCP_BACKLOG_MAX; + + for (int i = 0; i < TCP_MAX_LISTENERS; i++) { + if (tcp_listeners[i].in_use) continue; + memset(&tcp_listeners[i], 0, sizeof(tcp_listener_t)); + tcp_listeners[i].in_use = true; + tcp_listeners[i].port = port; + tcp_listeners[i].backlog = backlog; + return 0; + } + return -1; +} + +tcp_conn_t* tcp_accept(uint16_t port, uint32_t timeout_ms) { + tcp_listener_t* l = find_listener(port); + if (!l) return NULL; + + uint64_t t0 = pit_get_ticks(); + uint64_t wait = ms_to_ticks(timeout_ms); + + while (l->count == 0) { + if ((pit_get_ticks() - t0) >= wait) return NULL; + arch_halt(); + } + + tcp_conn_t* c = l->queue[0]; + for (int i = 1; i < l->count; i++) l->queue[i - 1] = l->queue[i]; + l->count--; + return c; +} + +void tcp_unlisten(uint16_t port) { + tcp_listener_t* l = find_listener(port); + if (!l) return; + + // Anything still queued was never handed to a reader, so refuse it rather + // than leaving the peer with a connection nobody owns. + for (int i = 0; i < l->count; i++) { + tcp_conn_t* c = l->queue[i]; + if (!c || !c->in_use) continue; + tcp_tx(c, TCP_FLAG_RST, NULL, 0); + conn_free(c); + } + memset(l, 0, sizeof(*l)); +} + +int tcp_get_listener_count(void) { + int n = 0; + for (int i = 0; i < TCP_MAX_LISTENERS; i++) if (tcp_listeners[i].in_use) n++; + return n; +} + +uint16_t tcp_get_listener_port(int idx) { + int n = 0; + for (int i = 0; i < TCP_MAX_LISTENERS; i++) { + if (!tcp_listeners[i].in_use) continue; + if (n++ == idx) return tcp_listeners[i].port; + } + return 0; +} + +int tcp_get_listener_backlog(int idx) { + int n = 0; + for (int i = 0; i < TCP_MAX_LISTENERS; i++) { + if (!tcp_listeners[i].in_use) continue; + if (n++ == idx) return tcp_listeners[i].count; + } + return 0; +} + bool tcp_is_established(const tcp_conn_t* c) { return c && c->in_use && c->state == TCP_STATE_ESTABLISHED; } @@ -617,6 +803,12 @@ bool tcp_peer_closed(const tcp_conn_t* c) { } size_t tcp_get_connections_count(void) { + // Expired TIME_WAIT slots are normally reclaimed when the next connection + // is allocated, which never happens if none follows. netstat and the + // Network Monitor poll this, so it doubles as the periodic tick that stops + // finished connections lingering in the table for good. + reap_time_wait(); + size_t count = 0; for (size_t i = 0; i < MAX_TCP_CONNS; i++) { if (tcp_connections[i].in_use) count++; diff --git a/userland/lazybox/lazybox.c b/userland/lazybox/lazybox.c index e820259..f6f52ea 100644 --- a/userland/lazybox/lazybox.c +++ b/userland/lazybox/lazybox.c @@ -2216,6 +2216,14 @@ static int applet_netstat(int argc, char** argv) { } } + // Ports claimed through tcp_listen (the general passive-open path). + for (int i = 0; i < tcp_get_listener_count(); i++) { + uint16_t lp = tcp_get_listener_port(i); + if (!lp) continue; + printk("tcp 0 0 0.0.0.0:%-5u 0.0.0.0:* " + "LISTEN (%d queued)\n", lp, tcp_get_listener_backlog(i)); + } + // Live connections from the TCP engine's own table: inbound ones accepted // by a listener, outbound ones opened by tcp_connect. for (size_t i = 0; i < 32; i++) { @@ -2271,8 +2279,9 @@ static int applet_sockstat(int argc, char** argv) { snprintf(rbuf, sizeof(rbuf), "%s:%u", ra, s->remote_port); const char* ty = (s->type == SOCK_DGRAM) ? "dgram" : (s->type == SOCK_STREAM) ? "stream" : "raw"; - printk("%-4d %-8s %-24s %-24s %s\n", i, ty, lbuf, rbuf, - s->state == 1 ? "CONNECTED" : "OPEN"); + const char* st = (s->state == SOCK_STATE_CONNECTED) ? "CONNECTED" : + (s->state == SOCK_STATE_LISTEN) ? "LISTEN" : "OPEN"; + printk("%-4d %-8s %-24s %-24s %s\n", i, ty, lbuf, rbuf, st); shown++; } if (!shown) printk(" (no open sockets)\n"); @@ -2356,6 +2365,76 @@ static int applet_tcpconnect(int argc, char** argv) { return 0; } +// Listen on a port, take one connection, echo what the peer sends, and hang +// up. This is the passive-open path end to end, through the BSD socket API +// rather than the TCP engine directly: bind, listen, accept, recv, send, close. +static int applet_tcpserve(int argc, char** argv) { + if (argc < 2) { + printk("usage: tcpserve [seconds to wait]\n"); + return 1; + } + + uint16_t port = (uint16_t)atoi(argv[1]); + if (port == 0) { + printk(ANSI_BRIGHT_RED "tcpserve: invalid port '%s'\n" ANSI_RESET, argv[1]); + return 1; + } + + int fd = sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (fd < 0) { + printk(ANSI_BRIGHT_RED "tcpserve: no socket available\n" ANSI_RESET); + return 1; + } + + sockaddr_in_t local; + memset(&local, 0, sizeof(local)); + local.sin_family = AF_INET; + local.sin_port = htons(port); + local.sin_addr = 0; // all interfaces + + if (sys_bind(fd, &local, sizeof(local)) != 0 || sys_listen(fd, 4) != 0) { + printk(ANSI_BRIGHT_RED "tcpserve: cannot listen on port %u " + "(already claimed?)\n" ANSI_RESET, port); + sys_close_socket(fd); + return 1; + } + + printk(ANSI_BRIGHT_GREEN "Listening" ANSI_RESET " on 0.0.0.0:%u -- waiting for a " + "connection ...\n", port); + + sockaddr_in_t peer; + size_t plen = sizeof(peer); + int cfd = sys_accept(fd, &peer, &plen); + if (cfd < 0) { + printk(ANSI_YELLOW "tcpserve: nobody connected; giving up\n" ANSI_RESET); + sys_close_socket(fd); + return 1; + } + + char pip[16]; + ip_to_str(peer.sin_addr, pip); + printk(ANSI_BRIGHT_GREEN "Accepted" ANSI_RESET " connection from %s:%u\n", + pip, ntohs(peer.sin_port)); + + char buf[513]; + ssize_t n = sys_recv(cfd, buf, sizeof(buf) - 1, 0); + if (n > 0) { + buf[n] = '\0'; + printk(ANSI_YELLOW "--- %d byte(s) received ---\n" ANSI_RESET, (int)n); + printk("%s\n", buf); + + const char* reply = "SUB-OS accepted your connection. Goodbye.\r\n"; + sys_send(cfd, reply, strlen(reply), 0); + } else { + printk(ANSI_YELLOW "tcpserve: peer sent nothing\n" ANSI_RESET); + } + + sys_close_socket(cfd); + sys_close_socket(fd); + printk("Connection closed, port %u released.\n", port); + return 0; +} + // A minimal HTTP/1.0 client over the active-open path. static int applet_httpget(int argc, char** argv) { if (argc < 2) { @@ -2727,6 +2806,7 @@ static const lazybox_applet_t applets[] = { {"sockstat", applet_sockstat, "sockstat", "Socket table & UDP stats", "Network"}, {"tcpconnect", applet_tcpconnect, "tcpconnect [text]", "Open an outbound TCP stream", "Network"}, {"httpget", applet_httpget, "httpget [path] [port]", "Fetch a URL over TCP", "Network"}, + {"tcpserve", applet_tcpserve, "tcpserve ", "Accept one inbound TCP stream", "Network"}, // Storage & Devices {"lsblk", applet_lsblk, "lsblk", "List block storage devices", "Storage"},