diff --git a/include/net/net.h b/include/net/net.h index 16b05be..cb2f70d 100644 --- a/include/net/net.h +++ b/include/net/net.h @@ -102,6 +102,15 @@ arp_entry_t* net_get_arp_table(int* count_out); uint32_t ip_parse(const char* ip_str); void ip_to_str(uint32_t ip, char* buf); + +// One's-complement checksum, summed a byte at a time so the loads cannot be +// reordered around a caller's store into the buffer being summed, and so no +// alignment is assumed of a packed header. Words are read big-endian; +// net_csum_finish returns the result already in network byte order, ready to +// be assigned straight to a header's checksum field. +uint32_t net_csum_add(uint32_t sum, const void* data, size_t len); +uint16_t net_csum_finish(uint32_t sum); +uint16_t net_checksum(const void* data, size_t len); void mac_to_str(const uint8_t* mac, char* buf); #endif // _NET_NET_H diff --git a/include/net/tcp.h b/include/net/tcp.h index 5442a7e..41bd4b3 100644 --- a/include/net/tcp.h +++ b/include/net/tcp.h @@ -34,15 +34,35 @@ 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 + +#define TCP_RX_BUF_SIZE 4096 +#define TCP_MSS 1460 + typedef struct tcp_conn { uint32_t local_ip; uint32_t remote_ip; uint16_t local_port; uint16_t remote_port; uint8_t state; - uint32_t seq_num; - uint32_t ack_num; + uint32_t seq_num; // next sequence number we will send + uint32_t ack_num; // next sequence number we expect to receive bool in_use; + + // --- client / stream state, unused by the server path ---------------- + uint8_t role; + uint32_t iss; // initial send sequence number + uint32_t snd_una; // oldest sequence number not yet acknowledged + uint8_t* rx_buf; // receive ring, allocated on active open + uint16_t rx_head; // read cursor + uint16_t rx_count; // bytes available to the reader + bool peer_fin; // peer sent FIN: no more data will arrive + bool reset; // peer sent RST + uint64_t timer; // tick of the last transmission awaiting an ACK + uint64_t closed_at; // tick TIME_WAIT began } tcp_conn_t; void tcp_init(void); @@ -51,7 +71,40 @@ void tcp_send_packet(uint32_t dst_ip, uint16_t src_port, uint16_t dst_port, uint32_t seq, uint32_t ack, uint8_t flags, const void* payload, uint16_t payload_len); +// --- Active open (client side) ------------------------------------------ +// +// Every call below is bounded: a silent or unreachable peer produces a +// timeout, never a hang. They busy-wait against the timer tick with the CPU +// halted between checks, the same way net_ping and the DNS resolver do, so +// they must be called from a context where inbound packets can still be +// serviced by the NIC interrupt. + +// Perform the three-way handshake with dst_ip:dst_port, retransmitting the +// SYN until the handshake completes or timeout_ms elapses. Returns the +// connection, or NULL if it could not be established. +tcp_conn_t* tcp_connect(uint32_t dst_ip, uint16_t dst_port, uint32_t timeout_ms); + +// Send len bytes, segmented to the MSS. Stop-and-wait: each segment is +// retransmitted until acknowledged before the next is sent. Returns the number +// of bytes acknowledged, or -1 if the connection is unusable. +int tcp_send(tcp_conn_t* c, const void* data, uint16_t len); + +// Read up to len bytes, waiting up to timeout_ms for data to arrive. Returns +// the byte count, or -1 on reset. A return of 0 means no data became +// available: use tcp_peer_closed() to tell a finished stream from an idle one. +int tcp_recv(tcp_conn_t* c, void* buf, uint16_t len, uint32_t timeout_ms); + +// Orderly shutdown, then release the connection. The handle must not be used +// afterwards. +void tcp_close(tcp_conn_t* c); + +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); + 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); #endif // _NET_TCP_H diff --git a/net/core/net.c b/net/core/net.c index 819df79..b50745e 100644 --- a/net/core/net.c +++ b/net/core/net.c @@ -22,21 +22,41 @@ static volatile uint64_t ping_time = 0; static const uint8_t eth_broadcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; -static uint16_t net_checksum(const void* data, size_t len) { - const uint16_t* ptr = (const uint16_t*)data; - uint32_t sum = 0; +/* + * One's-complement checksum, accumulated a byte at a time. + * + * The byte-wise form is not just about portability. Reading through an + * unsigned char pointer may alias any object, so these loads cannot be + * reordered around a caller's `hdr->checksum = 0` store into the very buffer + * being summed. A word-at-a-time version is free to be reordered, and the TCP + * one was: it folded a stale checksum field into its own result and put a + * wrong checksum on every segment the kernel sent. It also drops the + * assumption that a packed header may be read as uint16_t, which is a fault + * rather than a slowdown on the ARM targets. + * + * Words are read big-endian, so the accumulator carries the checksum's true + * numeric value and it is converted to network order once, at the end. + */ +uint32_t net_csum_add(uint32_t sum, const void* data, size_t len) { + const uint8_t* p = (const uint8_t*)data; while (len > 1) { - sum += *ptr++; + sum += ((uint32_t)p[0] << 8) | (uint32_t)p[1]; + p += 2; len -= 2; } - if (len > 0) { - sum += *(const uint8_t*)ptr; - } - while (sum >> 16) { - sum = (sum & 0xFFFF) + (sum >> 16); - } - return (uint16_t)(~sum); + if (len) sum += (uint32_t)p[0] << 8; // odd tail byte is the high half + + return sum; +} + +uint16_t net_csum_finish(uint32_t sum) { + while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); + return htons((uint16_t)(~sum)); +} + +uint16_t net_checksum(const void* data, size_t len) { + return net_csum_finish(net_csum_add(0, data, len)); } void ip_to_str(uint32_t ip, char* buf) { @@ -235,6 +255,16 @@ void net_receive(const uint8_t* packet, uint16_t length) { if (payload_len >= sizeof(ip_header_t)) { const ip_header_t* ip = (const ip_header_t*)payload; uint8_t ihl = (ip->ihl_version & 0x0F) * 4; + + /* Ethernet pads every frame out to 60 bytes, so the frame length + * overstates a short datagram. The IP header's own total_length is + * what says where the datagram really ends; without this trim the + * padding is handed to TCP/UDP/ICMP as protocol payload, which + * makes a bare ACK or FIN look like it carried several bytes and + * drags the acknowledgement number out of step with the peer. */ + uint16_t ip_total = ntohs(ip->total_length); + if (ip_total >= ihl && ip_total < payload_len) payload_len = ip_total; + if (payload_len >= ihl) { // Dynamically learn sender's MAC address in ARP cache arp_cache_insert(ip->src_ip, eth->src_mac); diff --git a/net/core/socket.c b/net/core/socket.c index 85decec..81de026 100644 --- a/net/core/socket.c +++ b/net/core/socket.c @@ -1,12 +1,15 @@ // BSD-style socket layer for SUB-OS. // -// Implements connectionless (SOCK_DGRAM) sockets on top of the UDP transport. +// Implements connectionless (SOCK_DGRAM) sockets on top of the UDP transport +// and connection-oriented (SOCK_STREAM) sockets on top of the TCP engine. +// // Binding a datagram socket registers a UDP handler that appends inbound // datagrams to the socket's fixed-depth receive ring; sys_recv/sys_recvfrom pop -// the oldest datagram. Stream (SOCK_STREAM) sockets are accepted and tracked so -// the API is uniform, but their data path is served by the in-kernel TCP engine. +// the oldest datagram. A stream socket instead holds a TCP connection opened by +// sys_connect, and its send/recv calls carry a byte stream through that. #include #include +#include #include #include #include @@ -15,6 +18,9 @@ #define RX_RING_DEPTH 8 #define DGRAM_MAX 1472 // 1500 MTU - 20 IP - 8 UDP +#define SOCK_CONNECT_TIMEOUT_MS 4000 +#define SOCK_RECV_TIMEOUT_MS 2000 + typedef struct { uint32_t src_ip; uint16_t src_port; @@ -29,6 +35,7 @@ typedef struct { int head; // next slot to read 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 } sock_priv_t; static socket_t socket_table[MAX_SOCKETS]; @@ -125,8 +132,22 @@ int sys_connect(int sockfd, const struct sockaddr_in* addr, size_t addrlen) { (void)addrlen; if (!valid_fd(sockfd) || !addr) return -1; socket_t* s = &socket_table[sockfd]; + sock_priv_t* pv = &socket_priv[sockfd]; s->remote_ip = addr->sin_addr; s->remote_port = ntohs(addr->sin_port); + + // A stream socket really connects: the call returns only once the + // three-way handshake has completed, or fails on timeout. + if (s->type == SOCK_STREAM) { + if (pv->tcp) return 0; // already connected + pv->tcp = tcp_connect(s->remote_ip, s->remote_port, SOCK_CONNECT_TIMEOUT_MS); + if (!pv->tcp) return -1; + s->local_ip = pv->tcp->local_ip; + s->local_port = pv->tcp->local_port; + s->state = 1; + return 0; + } + s->state = 1; // "connected" (address memorised for send/recv) if (s->type == SOCK_DGRAM) return dgram_ensure_bound(sockfd); return 0; @@ -153,6 +174,18 @@ ssize_t sys_sendto(int sockfd, const void* buf, size_t len, int flags, } ssize_t sys_send(int sockfd, const void* buf, size_t len, int flags) { + if (!valid_fd(sockfd) || !buf) return -1; + socket_t* s = &socket_table[sockfd]; + + if (s->type == SOCK_STREAM) { + sock_priv_t* pv = &socket_priv[sockfd]; + if (!pv->tcp) return -1; // not connected + if (len > 0xFFFF) len = 0xFFFF; + int n = tcp_send(pv->tcp, buf, (uint16_t)len); + if (n > 0) stat_tx++; + return (n < 0) ? -1 : (ssize_t)n; + } + return sys_sendto(sockfd, buf, len, flags, NULL, 0); } @@ -183,6 +216,18 @@ ssize_t sys_recvfrom(int sockfd, void* buf, size_t len, int flags, } ssize_t sys_recv(int sockfd, void* buf, size_t len, int flags) { + if (!valid_fd(sockfd) || !buf) return -1; + socket_t* s = &socket_table[sockfd]; + + if (s->type == SOCK_STREAM) { + sock_priv_t* pv = &socket_priv[sockfd]; + if (!pv->tcp) return -1; // not connected + if (len > 0xFFFF) len = 0xFFFF; + int n = tcp_recv(pv->tcp, buf, (uint16_t)len, SOCK_RECV_TIMEOUT_MS); + if (n > 0) stat_rx++; + return (n < 0) ? -1 : (ssize_t)n; + } + return sys_recvfrom(sockfd, buf, len, flags, NULL, NULL); } @@ -190,6 +235,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->tcp) tcp_close(pv->tcp); if (pv->bound && s->local_port) udp_unbind(s->local_port); memset(s, 0, sizeof(socket_t)); memset(pv, 0, sizeof(sock_priv_t)); diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c index dda345a..420a8b3 100644 --- a/net/ipv4/tcp.c +++ b/net/ipv4/tcp.c @@ -1,3 +1,29 @@ +/* + * TCP for SUB-OS. + * + * The engine drives both ends of a connection: + * + * Passive open -- an inbound SYN to a listening service (sshd, httpd) + * creates a server connection, which is answered straight + * out of the receive path. + * Active open -- tcp_connect() performs a real three-way handshake, after + * which tcp_send/tcp_recv carry a byte stream and + * tcp_close() shuts it down in order. + * + * Inbound segments are matched against the connection table first and only + * then offered to the listeners, which is what lets a reply to one of our own + * outbound connections through -- previously anything that was not addressed + * to a listening port was answered with RST, so the SYN-ACK completing our own + * handshake would have been rejected. + * + * Transmission is stop-and-wait rather than a sliding window: one segment is + * outstanding at a time and is retransmitted until acknowledged. That is + * enough to be correct over a lossy path and keeps the retransmission state to + * a single sequence number, at the cost of throughput on a long fat link. + * + * Every blocking call is bounded against the timer tick and halts the CPU + * between checks, so a silent peer costs a timeout rather than a hang. + */ #include #include #include @@ -6,66 +32,68 @@ #include #include #include +#include +#include #define MAX_TCP_CONNS 32 -typedef struct { - uint32_t src_ip; - uint32_t dst_ip; - uint8_t zero; - uint8_t protocol; - uint16_t tcp_len; -} __attribute__((packed)) tcp_pseudo_header_t; +// The timer tick is 100 Hz, so one tick is 10 ms. +#define TCP_RTO_TICKS 50 // 500 ms retransmission timeout +#define TCP_MAX_RETRIES 5 +#define TCP_CLOSE_TICKS 50 // how long tcp_close waits for the shutdown +#define TCP_TIME_WAIT_TICKS 200 // 2 s; a deliberately shortened 2*MSL + +#define TCP_EPHEMERAL_LO 49152 +#define TCP_EPHEMERAL_HI 65535 static tcp_conn_t tcp_connections[MAX_TCP_CONNS]; -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 = sizeof(tcp_header_t) + payload_len; +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); - tcp_pseudo_header_t ph; - ph.src_ip = src_ip; - ph.dst_ip = dst_ip; - ph.zero = 0; - ph.protocol = IP_PROTO_TCP; - ph.tcp_len = htons(tcp_total_len); + // The pseudo-header, laid out explicitly rather than as a packed struct: + // src and dst already hold network order, so their bytes are summed as-is. + uint8_t tail[4] = { + 0, IP_PROTO_TCP, + (uint8_t)(tcp_total_len >> 8), (uint8_t)(tcp_total_len & 0xFF) + }; uint32_t sum = 0; - const uint16_t* ptr = (const uint16_t*)&ph; - for (size_t i = 0; i < sizeof(ph) / 2; i++) { - sum += *ptr++; - } + sum = net_csum_add(sum, &src_ip, sizeof(src_ip)); + sum = net_csum_add(sum, &dst_ip, sizeof(dst_ip)); + sum = net_csum_add(sum, tail, sizeof(tail)); + sum = net_csum_add(sum, tcp, sizeof(tcp_header_t)); + if (payload && payload_len > 0) sum = net_csum_add(sum, payload, payload_len); - ptr = (const uint16_t*)tcp; - for (size_t i = 0; i < sizeof(tcp_header_t) / 2; i++) { - sum += *ptr++; - } - - if (payload && payload_len > 0) { - ptr = (const uint16_t*)payload; - size_t len = payload_len; - while (len > 1) { - sum += *ptr++; - len -= 2; - } - if (len > 0) { - sum += *(const uint8_t*)ptr; - } - } - - while (sum >> 16) { - sum = (sum & 0xFFFF) + (sum >> 16); - } - return (uint16_t)(~sum); + return net_csum_finish(sum); } void tcp_init(void) { memset(tcp_connections, 0, sizeof(tcp_connections)); - printk(KERN_INFO "NET: TCP (Transmission Control Protocol) state machine online\n"); + printk(KERN_INFO "NET: TCP state machine online (active + passive open)\n"); } -void tcp_send_packet(uint32_t dst_ip, uint16_t src_port, uint16_t dst_port, - uint32_t seq, uint32_t ack, uint8_t flags, - const void* payload, uint16_t payload_len) { +// =========================================================================== +// Sequence arithmetic +// +// Sequence numbers wrap at 2^32, so they are compared by the sign of their +// difference rather than by magnitude. +// =========================================================================== +static inline bool seq_ge(uint32_t a, uint32_t b) { return (int32_t)(a - b) >= 0; } +static inline bool seq_gt(uint32_t a, uint32_t b) { return (int32_t)(a - b) > 0; } + +static inline uint64_t ms_to_ticks(uint32_t ms) { + uint64_t t = ms / 10; + return t ? t : 1; +} + +// =========================================================================== +// Transmission +// =========================================================================== +static void tcp_tx_full(uint32_t dst_ip, uint16_t src_port, uint16_t dst_port, + uint32_t seq, uint32_t ack, uint8_t flags, + const void* payload, uint16_t payload_len, uint16_t window) { net_if_t* netif = net_get_primary_if(); uint32_t src_ip = netif ? netif->ip : 0x0A00020F; // 10.0.2.15 @@ -80,7 +108,7 @@ void tcp_send_packet(uint32_t dst_ip, uint16_t src_port, uint16_t dst_port, tcp->ack_num = htonl(ack); tcp->data_offset_reserved = (sizeof(tcp_header_t) / 4) << 4; tcp->flags = flags; - tcp->window_size = htons(65535); + tcp->window_size = htons(window); tcp->urgent_pointer = 0; if (payload && payload_len > 0) { @@ -94,31 +122,292 @@ void tcp_send_packet(uint32_t dst_ip, uint16_t src_port, uint16_t dst_port, kfree(buf); } -static tcp_conn_t* find_or_alloc_conn(uint32_t r_ip, uint16_t r_port, uint16_t l_port) { +void tcp_send_packet(uint32_t dst_ip, uint16_t src_port, uint16_t dst_port, + uint32_t seq, uint32_t ack, uint8_t flags, + const void* payload, uint16_t payload_len) { + tcp_tx_full(dst_ip, src_port, dst_port, seq, ack, flags, payload, payload_len, 65535); +} + +// Free space in the receive ring, which is what we advertise as our window so +// a peer stops before overrunning a reader that has fallen behind. +static uint16_t tcp_window(const tcp_conn_t* c) { + if (!c->rx_buf) return 65535; + return (uint16_t)(TCP_RX_BUF_SIZE - c->rx_count); +} + +// Send on a connection at an explicit sequence number. Retransmissions reuse +// the original segment's sequence number, which is why this is separate from +// the connection's current send position. +static void tcp_tx_seq(tcp_conn_t* c, uint32_t seq, uint8_t flags, + const void* payload, uint16_t payload_len) { + tcp_tx_full(c->remote_ip, c->local_port, c->remote_port, + seq, c->ack_num, flags, payload, payload_len, tcp_window(c)); +} + +static void tcp_tx(tcp_conn_t* c, uint8_t flags, const void* payload, uint16_t payload_len) { + tcp_tx_seq(c, c->seq_num, flags, payload, payload_len); +} + +// =========================================================================== +// Connection table +// =========================================================================== +static void conn_free(tcp_conn_t* c) { + if (c->rx_buf) { kfree(c->rx_buf); c->rx_buf = NULL; } + memset(c, 0, sizeof(*c)); +} + +// A connection in TIME_WAIT still occupies its slot so that a stray duplicate +// from the old incarnation cannot be mistaken for traffic on a new one. +static void reap_time_wait(void) { + uint64_t now = pit_get_ticks(); for (size_t i = 0; i < MAX_TCP_CONNS; i++) { - if (tcp_connections[i].in_use && - tcp_connections[i].remote_ip == r_ip && - tcp_connections[i].remote_port == r_port && - tcp_connections[i].local_port == l_port) { - return &tcp_connections[i]; + tcp_conn_t* c = &tcp_connections[i]; + if (c->in_use && c->state == TCP_STATE_TIME_WAIT && + (now - c->closed_at) >= TCP_TIME_WAIT_TICKS) { + conn_free(c); } } +} + +static tcp_conn_t* find_conn(uint32_t r_ip, uint16_t r_port, uint16_t l_port) { + for (size_t i = 0; i < MAX_TCP_CONNS; i++) { + tcp_conn_t* c = &tcp_connections[i]; + if (c->in_use && c->remote_ip == r_ip && + c->remote_port == r_port && c->local_port == l_port) { + return c; + } + } + return NULL; +} + +static tcp_conn_t* alloc_conn(void) { + reap_time_wait(); for (size_t i = 0; i < MAX_TCP_CONNS; i++) { if (!tcp_connections[i].in_use) { + memset(&tcp_connections[i], 0, sizeof(tcp_conn_t)); tcp_connections[i].in_use = true; - tcp_connections[i].remote_ip = r_ip; - tcp_connections[i].remote_port = r_port; - tcp_connections[i].local_port = l_port; - tcp_connections[i].local_ip = 0x0A00020F; - tcp_connections[i].state = TCP_STATE_CLOSED; - tcp_connections[i].seq_num = 1000; - tcp_connections[i].ack_num = 0; return &tcp_connections[i]; } } return NULL; } +static tcp_conn_t* alloc_conn_server(uint32_t r_ip, uint16_t r_port, uint16_t l_port) { + tcp_conn_t* c = alloc_conn(); + if (!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_SERVER; + c->state = TCP_STATE_CLOSED; + c->seq_num = 1000; + c->ack_num = 0; + return c; +} + +static uint16_t alloc_ephemeral_port(void) { + for (int attempt = 0; attempt < 512; attempt++) { + uint16_t p = (uint16_t)(TCP_EPHEMERAL_LO + + (prng_rand32() % (TCP_EPHEMERAL_HI - TCP_EPHEMERAL_LO + 1))); + bool taken = false; + for (size_t i = 0; i < MAX_TCP_CONNS; i++) { + if (tcp_connections[i].in_use && tcp_connections[i].local_port == p) { + taken = true; + break; + } + } + if (!taken) return p; + } + return 0; +} + +// =========================================================================== +// Receive ring +// =========================================================================== + +// Store what fits and report how much was taken, so the acknowledgement only +// ever covers bytes actually kept. +static uint16_t rx_append(tcp_conn_t* c, const uint8_t* data, uint16_t len) { + if (!c->rx_buf || !data) return 0; + + uint16_t stored = 0; + while (stored < len && c->rx_count < TCP_RX_BUF_SIZE) { + uint16_t tail = (uint16_t)((c->rx_head + c->rx_count) % TCP_RX_BUF_SIZE); + c->rx_buf[tail] = data[stored++]; + c->rx_count++; + } + return stored; +} + +// =========================================================================== +// Client state machine +// =========================================================================== +static void tcp_client_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; + c->state = TCP_STATE_CLOSED; + return; + } + + if (c->state == TCP_STATE_SYN_SENT) { + if ((flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)) == (TCP_FLAG_SYN | TCP_FLAG_ACK)) { + // The SYN we sent occupies one sequence number, so a SYN-ACK that + // completes our handshake acknowledges exactly iss + 1. + if (ack != c->iss + 1) return; + c->snd_una = ack; + c->ack_num = seq + 1; + c->state = TCP_STATE_ESTABLISHED; + tcp_tx(c, TCP_FLAG_ACK, NULL, 0); + } else if (flags & TCP_FLAG_SYN) { + // Simultaneous open: both ends sent a SYN at once. + c->ack_num = seq + 1; + c->state = TCP_STATE_SYN_RCVD; + tcp_tx_seq(c, c->iss, TCP_FLAG_SYN | TCP_FLAG_ACK, NULL, 0); + } + return; + } + + if (c->state == TCP_STATE_SYN_RCVD) { + if ((flags & TCP_FLAG_ACK) && seq_ge(ack, c->iss + 1)) { + c->snd_una = ack; + c->state = TCP_STATE_ESTABLISHED; + } + return; + } + + if (c->state == TCP_STATE_CLOSED) return; + + // A lingering connection only has to keep acknowledging a retransmitted FIN. + if (c->state == TCP_STATE_TIME_WAIT) { + if (flags & TCP_FLAG_FIN) tcp_tx(c, TCP_FLAG_ACK, NULL, 0); + return; + } + + // Advance the send window, ignoring anything that acknowledges more than + // has actually been sent. + if ((flags & TCP_FLAG_ACK) && seq_gt(ack, c->snd_una) && seq_ge(c->seq_num, ack)) { + c->snd_una = ack; + } + + // Accept in-order data. Anything else earns a duplicate acknowledgement + // telling the peer where we actually are. + if (payload_len > 0 && !c->peer_fin) { + if (seq == c->ack_num) { + c->ack_num += rx_append(c, payload, payload_len); + } + tcp_tx(c, TCP_FLAG_ACK, NULL, 0); + } + + // A FIN sits after the segment's data, so it is only in order once every + // preceding byte has been taken. + if ((flags & TCP_FLAG_FIN) && !c->peer_fin && c->ack_num == seq + payload_len) { + c->ack_num++; + c->peer_fin = true; + tcp_tx(c, TCP_FLAG_ACK, NULL, 0); + + if (c->state == TCP_STATE_ESTABLISHED) c->state = TCP_STATE_CLOSE_WAIT; + else if (c->state == TCP_STATE_FIN_WAIT1) c->state = TCP_STATE_CLOSING; + else if (c->state == TCP_STATE_FIN_WAIT2) { + c->state = TCP_STATE_TIME_WAIT; + c->closed_at = pit_get_ticks(); + } + } + + // Our own FIN also consumes a sequence number: once snd_una reaches + // seq_num there is nothing of ours left outstanding. + if (c->snd_una == c->seq_num) { + if (c->state == TCP_STATE_FIN_WAIT1) { + c->state = TCP_STATE_FIN_WAIT2; + } else if (c->state == TCP_STATE_CLOSING) { + c->state = TCP_STATE_TIME_WAIT; + c->closed_at = pit_get_ticks(); + } else if (c->state == TCP_STATE_LAST_ACK) { + c->state = TCP_STATE_CLOSED; + } + } +} + +// =========================================================================== +// Server state machine +// +// Behaviour is unchanged from the original listener: SSH and HTTP are served +// straight out of the receive path, one request per segment. +// =========================================================================== +static void tcp_server_input(tcp_conn_t* c, uint32_t src_ip, uint16_t src_port, + uint16_t dst_port, uint32_t seq_num, uint8_t flags, + const uint8_t* payload, uint16_t payload_len) { + bool is_ssh_port = (dst_port == sshd_get_port() && sshd_is_running()); + bool is_http_port = (dst_port == 80 && httpd_is_running()); + + if (flags & TCP_FLAG_SYN) { + c->state = TCP_STATE_SYN_RCVD; + c->ack_num = seq_num + 1; + c->seq_num = 10000; + + tcp_send_packet(src_ip, dst_port, src_port, c->seq_num, c->ack_num, + TCP_FLAG_SYN | TCP_FLAG_ACK, NULL, 0); + c->seq_num++; + return; + } + + if (flags & TCP_FLAG_FIN) { + c->ack_num = seq_num + 1; + tcp_send_packet(src_ip, dst_port, src_port, c->seq_num, c->ack_num, + TCP_FLAG_ACK | TCP_FLAG_FIN, NULL, 0); + conn_free(c); + return; + } + + if (flags & TCP_FLAG_RST) { + conn_free(c); + return; + } + + if (c->state == TCP_STATE_SYN_RCVD) { + c->state = TCP_STATE_ESTABLISHED; + if (is_ssh_port && payload_len == 0) { + const char* banner = SSH_BANNER; + uint16_t blen = (uint16_t)strlen(banner); + tcp_send_packet(src_ip, dst_port, src_port, c->seq_num, c->ack_num, + TCP_FLAG_PSH | TCP_FLAG_ACK, banner, blen); + c->seq_num += blen; + return; + } + } + c->state = TCP_STATE_ESTABLISHED; + + if (payload_len == 0) return; + c->ack_num = seq_num + payload_len; + + if (is_ssh_port) { + char resp[1024]; + int rlen = sshd_process_packet((const char*)payload, payload_len, resp, sizeof(resp)); + if (rlen > 0) { + 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; + } else { + tcp_send_packet(src_ip, dst_port, src_port, c->seq_num, c->ack_num, + TCP_FLAG_ACK, NULL, 0); + } + } else if (is_http_port) { + char resp[2048]; + int rlen = httpd_handle_request((const char*)payload, resp, sizeof(resp)); + if (rlen > 0) { + 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; + } + } +} + +// =========================================================================== +// Receive dispatch +// =========================================================================== void tcp_receive(const uint8_t* packet, uint16_t length, uint32_t src_ip) { if (!packet || length < sizeof(tcp_header_t)) return; @@ -127,96 +416,204 @@ void tcp_receive(const uint8_t* packet, uint16_t length, uint32_t src_ip) { uint16_t dst_port = ntohs(tcp->dst_port); uint32_t seq_num = ntohl(tcp->seq_num); uint32_t ack_num = ntohl(tcp->ack_num); - uint8_t flags = tcp->flags; - (void)ack_num; + uint8_t flags = tcp->flags; uint8_t header_len = (tcp->data_offset_reserved >> 4) * 4; + if (header_len < sizeof(tcp_header_t) || header_len > length) return; + const uint8_t* payload = packet + header_len; - uint16_t payload_len = length > header_len ? length - header_len : 0; + uint16_t payload_len = length - header_len; - // Check if target port is SSH (22) or HTTP (80) - bool is_ssh_port = (dst_port == sshd_get_port() && sshd_is_running()); + // 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 { + tcp_server_input(conn, src_ip, src_port, dst_port, seq_num, flags, + payload, payload_len); + } + return; + } + + // 2. Otherwise a listening service may want to accept 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) { - // Port not open: send TCP RST - if (!(flags & TCP_FLAG_RST)) { - tcp_send_packet(src_ip, dst_port, src_port, 0, seq_num + 1, TCP_FLAG_RST | TCP_FLAG_ACK, NULL, 0); + if (is_ssh_port || is_http_port) { + conn = alloc_conn_server(src_ip, src_port, dst_port); + if (conn) { + tcp_server_input(conn, src_ip, src_port, dst_port, seq_num, flags, + payload, payload_len); } return; } - tcp_conn_t* conn = find_or_alloc_conn(src_ip, src_port, dst_port); - if (!conn) return; - - // Handle TCP SYN (Connection Initiation from Client) - if (flags & TCP_FLAG_SYN) { - conn->state = TCP_STATE_SYN_RCVD; - conn->ack_num = seq_num + 1; - conn->seq_num = 10000; - - // Send SYN-ACK - tcp_send_packet(src_ip, dst_port, src_port, conn->seq_num, conn->ack_num, - TCP_FLAG_SYN | TCP_FLAG_ACK, NULL, 0); - conn->seq_num++; - return; + // 3. Nothing is listening and nothing matches: refuse the segment. + if (!(flags & TCP_FLAG_RST)) { + tcp_send_packet(src_ip, dst_port, src_port, 0, seq_num + 1, + TCP_FLAG_RST | TCP_FLAG_ACK, NULL, 0); } +} - // Handle TCP FIN (Connection Termination) - if (flags & TCP_FLAG_FIN) { - conn->ack_num = seq_num + 1; - tcp_send_packet(src_ip, dst_port, src_port, conn->seq_num, conn->ack_num, - TCP_FLAG_ACK | TCP_FLAG_FIN, NULL, 0); - conn->in_use = false; - conn->state = TCP_STATE_CLOSED; - return; - } +// =========================================================================== +// Active open API +// =========================================================================== +tcp_conn_t* tcp_connect(uint32_t dst_ip, uint16_t dst_port, uint32_t timeout_ms) { + if (dst_ip == 0 || dst_port == 0) return NULL; - // Handle TCP RST - if (flags & TCP_FLAG_RST) { - conn->in_use = false; - conn->state = TCP_STATE_CLOSED; - return; - } + tcp_conn_t* c = alloc_conn(); + if (!c) return NULL; - // Handle Data / ACK - if (conn->state == TCP_STATE_SYN_RCVD) { - conn->state = TCP_STATE_ESTABLISHED; - if (is_ssh_port && payload_len == 0) { - const char* banner = SSH_BANNER; - uint16_t blen = (uint16_t)strlen(banner); - tcp_send_packet(src_ip, dst_port, src_port, conn->seq_num, conn->ack_num, - TCP_FLAG_PSH | TCP_FLAG_ACK, banner, blen); - conn->seq_num += blen; - return; + c->rx_buf = (uint8_t*)kzalloc(TCP_RX_BUF_SIZE); + if (!c->rx_buf) { conn_free(c); return NULL; } + + c->local_port = alloc_ephemeral_port(); + if (c->local_port == 0) { conn_free(c); return NULL; } + + net_if_t* netif = net_get_primary_if(); + c->local_ip = netif ? netif->ip : 0x0A00020F; + c->remote_ip = dst_ip; + c->remote_port = dst_port; + c->role = TCP_ROLE_CLIENT; + c->iss = prng_rand32(); + c->seq_num = c->iss; + c->snd_una = c->iss; + c->ack_num = 0; + c->state = TCP_STATE_SYN_SENT; + + tcp_tx_seq(c, c->iss, TCP_FLAG_SYN, NULL, 0); + c->seq_num = c->iss + 1; // the SYN consumes one sequence number + + uint64_t t0 = pit_get_ticks(); + uint64_t wait = ms_to_ticks(timeout_ms); + uint64_t last = t0; + + while ((pit_get_ticks() - t0) < wait) { + if (c->state == TCP_STATE_ESTABLISHED) return c; + if (c->reset || c->state == TCP_STATE_CLOSED) break; + + if ((pit_get_ticks() - last) >= TCP_RTO_TICKS) { + tcp_tx_seq(c, c->iss, TCP_FLAG_SYN, NULL, 0); + last = pit_get_ticks(); } + arch_halt(); } - conn->state = TCP_STATE_ESTABLISHED; - if (payload_len > 0) { - conn->ack_num = seq_num + payload_len; + conn_free(c); + return NULL; +} - if (is_ssh_port) { - char resp[1024]; - int rlen = sshd_process_packet((const char*)payload, payload_len, resp, sizeof(resp)); - if (rlen > 0) { - tcp_send_packet(src_ip, dst_port, src_port, conn->seq_num, conn->ack_num, - TCP_FLAG_PSH | TCP_FLAG_ACK, resp, (uint16_t)rlen); - conn->seq_num += rlen; - } else { - tcp_send_packet(src_ip, dst_port, src_port, conn->seq_num, conn->ack_num, - TCP_FLAG_ACK, NULL, 0); - } - } else if (is_http_port) { - char resp[2048]; - int rlen = httpd_handle_request((const char*)payload, resp, sizeof(resp)); - if (rlen > 0) { - tcp_send_packet(src_ip, dst_port, src_port, conn->seq_num, conn->ack_num, - TCP_FLAG_PSH | TCP_FLAG_ACK, resp, (uint16_t)rlen); - conn->seq_num += rlen; +int tcp_send(tcp_conn_t* c, const void* data, uint16_t len) { + if (!c || !c->in_use || !data) return -1; + if (c->state != TCP_STATE_ESTABLISHED && c->state != TCP_STATE_CLOSE_WAIT) return -1; + if (len == 0) return 0; + + const uint8_t* p = (const uint8_t*)data; + uint16_t sent = 0; + + while (sent < len) { + uint16_t chunk = (uint16_t)(len - sent); + if (chunk > TCP_MSS) chunk = TCP_MSS; + + uint32_t seg_seq = c->seq_num; + uint32_t seg_end = seg_seq + chunk; + + // Publish the segment as sent before waiting: the acknowledgement is + // only accepted if it falls within what seq_num says we have sent. + c->seq_num = seg_end; + + int tries = 0; + for (;;) { + tcp_tx_seq(c, seg_seq, TCP_FLAG_PSH | TCP_FLAG_ACK, p + sent, chunk); + + uint64_t t0 = pit_get_ticks(); + while ((pit_get_ticks() - t0) < TCP_RTO_TICKS) { + if (seq_ge(c->snd_una, seg_end) || c->reset) break; + arch_halt(); } + + if (seq_ge(c->snd_una, seg_end)) break; + if (c->reset) return sent ? (int)sent : -1; + if (++tries >= TCP_MAX_RETRIES) return sent ? (int)sent : -1; + } + + sent += chunk; + } + + return (int)sent; +} + +int tcp_recv(tcp_conn_t* c, void* buf, uint16_t len, uint32_t timeout_ms) { + if (!c || !c->in_use || !buf || len == 0) return -1; + + uint64_t t0 = pit_get_ticks(); + uint64_t wait = ms_to_ticks(timeout_ms); + + while (c->rx_count == 0) { + if (c->reset) return -1; + if (c->peer_fin) return 0; // stream ended + if ((pit_get_ticks() - t0) >= wait) return 0; // nothing arrived + arch_halt(); + } + + uint16_t before = c->rx_count; + uint16_t n = (c->rx_count < len) ? c->rx_count : len; + + uint8_t* out = (uint8_t*)buf; + for (uint16_t i = 0; i < n; i++) { + out[i] = c->rx_buf[(c->rx_head + i) % TCP_RX_BUF_SIZE]; + } + + c->rx_head = (uint16_t)((c->rx_head + n) % TCP_RX_BUF_SIZE); + c->rx_count = (uint16_t)(c->rx_count - n); + + // If the window had closed down far enough that the peer may have stalled, + // tell it immediately that there is room again. + if (before >= TCP_RX_BUF_SIZE / 2 && c->state == TCP_STATE_ESTABLISHED) { + tcp_tx(c, TCP_FLAG_ACK, NULL, 0); + } + + return (int)n; +} + +void tcp_close(tcp_conn_t* c) { + if (!c || !c->in_use) return; + + if (c->state == TCP_STATE_ESTABLISHED || c->state == TCP_STATE_CLOSE_WAIT) { + // Closing after the peer already sent its FIN is the passive case and + // finishes in LAST_ACK; closing first is the active case. + bool passive = (c->state == TCP_STATE_CLOSE_WAIT); + + tcp_tx(c, TCP_FLAG_FIN | TCP_FLAG_ACK, NULL, 0); + c->seq_num++; // our FIN consumes a sequence number + c->state = passive ? TCP_STATE_LAST_ACK : TCP_STATE_FIN_WAIT1; + + uint64_t t0 = pit_get_ticks(); + while ((pit_get_ticks() - t0) < TCP_CLOSE_TICKS) { + if (c->reset) break; + if (c->state == TCP_STATE_CLOSED || c->state == TCP_STATE_TIME_WAIT) break; + arch_halt(); } } + + 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. + if (c->rx_buf) { kfree(c->rx_buf); c->rx_buf = NULL; } + c->rx_head = c->rx_count = 0; + c->closed_at = pit_get_ticks(); + return; + } + + conn_free(c); +} + +bool tcp_is_established(const tcp_conn_t* c) { + return c && c->in_use && c->state == TCP_STATE_ESTABLISHED; +} + +bool tcp_peer_closed(const tcp_conn_t* c) { + return c && c->in_use && c->peer_fin; } size_t tcp_get_connections_count(void) { @@ -231,3 +628,12 @@ const tcp_conn_t* tcp_get_connection(size_t index) { if (index >= MAX_TCP_CONNS || !tcp_connections[index].in_use) return NULL; return &tcp_connections[index]; } + +const char* tcp_state_name(uint8_t state) { + static const char* const names[] = { + "CLOSED", "LISTEN", "SYN_SENT", "SYN_RCVD", "ESTABLISHED", + "FIN_WAIT1", "FIN_WAIT2", "CLOSE_WAIT", "CLOSING", "LAST_ACK", "TIME_WAIT" + }; + if (state >= sizeof(names) / sizeof(names[0])) return "UNKNOWN"; + return names[state]; +} diff --git a/net/ipv4/udp.c b/net/ipv4/udp.c index 13ea069..c14d4dc 100644 --- a/net/ipv4/udp.c +++ b/net/ipv4/udp.c @@ -41,29 +41,22 @@ static volatile uint64_t stat_drop = 0; static uint16_t udp_checksum(uint32_t src_ip, uint32_t dst_ip, const udp_header_t* udp, const void* payload, uint16_t payload_len) { - udp_pseudo_header_t ph; - ph.src_ip = src_ip; - ph.dst_ip = dst_ip; - ph.zero = 0; - ph.protocol = IP_PROTO_UDP; - ph.udp_len = htons((uint16_t)(sizeof(udp_header_t) + payload_len)); + uint16_t udp_len = (uint16_t)(sizeof(udp_header_t) + payload_len); + + // The pseudo-header, laid out explicitly rather than as a packed struct: + // src and dst already hold network order, so their bytes are summed as-is. + uint8_t tail[4] = { + 0, IP_PROTO_UDP, (uint8_t)(udp_len >> 8), (uint8_t)(udp_len & 0xFF) + }; uint32_t sum = 0; - const uint16_t* p = (const uint16_t*)&ph; - for (size_t i = 0; i < sizeof(ph) / 2; i++) sum += *p++; + sum = net_csum_add(sum, &src_ip, sizeof(src_ip)); + sum = net_csum_add(sum, &dst_ip, sizeof(dst_ip)); + sum = net_csum_add(sum, tail, sizeof(tail)); + sum = net_csum_add(sum, udp, sizeof(udp_header_t)); + if (payload && payload_len > 0) sum = net_csum_add(sum, payload, payload_len); - p = (const uint16_t*)udp; - for (size_t i = 0; i < sizeof(udp_header_t) / 2; i++) sum += *p++; - - if (payload && payload_len > 0) { - p = (const uint16_t*)payload; - size_t len = payload_len; - while (len > 1) { sum += *p++; len -= 2; } - if (len > 0) sum += *(const uint8_t*)p; - } - - while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); - uint16_t cksum = (uint16_t)(~sum); + uint16_t cksum = net_csum_finish(sum); // A zero checksum means "not computed" on the wire; send 0xFFFF instead. return cksum ? cksum : 0xFFFF; } @@ -189,14 +182,8 @@ void udp_send_broadcast(uint16_t src_port, uint16_t dst_port, ip->src_ip = 0x00000000; ip->dst_ip = 0xFFFFFFFF; - // IPv4 header checksum. - { - uint32_t sum = 0; - const uint16_t* p = (const uint16_t*)ip; - for (size_t i = 0; i < sizeof(ip_header_t) / 2; i++) sum += *p++; - while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); - ip->checksum = (uint16_t)(~sum); - } + // IPv4 header checksum, over the header just written (with checksum zero). + ip->checksum = net_checksum(ip, sizeof(ip_header_t)); udp_header_t* udp = (udp_header_t*)(buf + sizeof(ip_header_t)); udp->src_port = htons(src_port); diff --git a/userland/lazybox/lazybox.c b/userland/lazybox/lazybox.c index 3f9bae8..e820259 100644 --- a/userland/lazybox/lazybox.c +++ b/userland/lazybox/lazybox.c @@ -2216,6 +2216,23 @@ static int applet_netstat(int argc, char** argv) { } } + // 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++) { + const tcp_conn_t* c = tcp_get_connection(i); + if (!c) continue; + + char lip[16], rip[16], laddr[24], raddr[24]; + ip_to_str(c->local_ip, lip); + ip_to_str(c->remote_ip, rip); + snprintf(laddr, sizeof(laddr), "%s:%u", lip, c->local_port); + snprintf(raddr, sizeof(raddr), "%s:%u", rip, c->remote_port); + + printk("tcp %6u 0 %-23s %-23s %s%s\n", + c->rx_count, laddr, raddr, tcp_state_name(c->state), + c->role == TCP_ROLE_CLIENT ? " (out)" : " (in)"); + } + // Live UDP port bindings (DNS resolver, DHCP client, datagram sockets). uint16_t ports[32]; int np = udp_get_bindings(ports, 32); @@ -2267,6 +2284,131 @@ static int applet_sockstat(int argc, char** argv) { return 0; } +// Resolve a host argument that may be either a dotted quad or a name. +static uint32_t resolve_host_arg(const char* s) { + uint32_t ip = ip_parse(s); + if (ip != 0) return ip; + return dns_resolve(s); +} + +// Open a TCP connection the way a client does, optionally push a line, and +// print whatever the peer sends back. This is the active-open path end to end: +// handshake, stream write, stream read, orderly close. +static int applet_tcpconnect(int argc, char** argv) { + if (argc < 3) { + printk("usage: tcpconnect [text to send]\n"); + return 1; + } + + uint32_t ip = resolve_host_arg(argv[1]); + if (ip == 0) { + printk(ANSI_BRIGHT_RED "tcpconnect: cannot resolve %s\n" ANSI_RESET, argv[1]); + return 1; + } + + uint16_t port = (uint16_t)atoi(argv[2]); + if (port == 0) { + printk(ANSI_BRIGHT_RED "tcpconnect: invalid port '%s'\n" ANSI_RESET, argv[2]); + return 1; + } + + char ipstr[16]; + ip_to_str(ip, ipstr); + printk("Connecting to %s:%u ...\n", ipstr, port); + + tcp_conn_t* c = tcp_connect(ip, port, 4000); + if (!c) { + printk(ANSI_BRIGHT_RED "tcpconnect: connection to %s:%u failed " + "(no SYN-ACK, or refused)\n" ANSI_RESET, ipstr, port); + return 1; + } + + printk(ANSI_BRIGHT_GREEN "Connected" ANSI_RESET " (local port %u, state %s)\n", + c->local_port, tcp_state_name(c->state)); + + if (argc >= 4) { + char line[256]; + int n = 0; + for (int i = 3; i < argc && n < (int)sizeof(line) - 3; i++) { + if (i > 3) line[n++] = ' '; + const char* w = argv[i]; + while (*w && n < (int)sizeof(line) - 3) line[n++] = *w++; + } + line[n++] = '\r'; + line[n++] = '\n'; + + int sent = tcp_send(c, line, (uint16_t)n); + printk("Sent %d byte(s)\n", sent); + } + + char buf[513]; + int total = 0; + for (;;) { + int n = tcp_recv(c, buf, sizeof(buf) - 1, 1500); + if (n <= 0) break; + buf[n] = '\0'; + printk("%s", buf); + total += n; + } + + printk("\n" ANSI_YELLOW "--- %d byte(s) received, closing ---\n" ANSI_RESET, total); + tcp_close(c); + return 0; +} + +// A minimal HTTP/1.0 client over the active-open path. +static int applet_httpget(int argc, char** argv) { + if (argc < 2) { + printk("usage: httpget [path] [port]\n"); + return 1; + } + + const char* host = argv[1]; + const char* path = (argc >= 3) ? argv[2] : "/"; + uint16_t port = (argc >= 4) ? (uint16_t)atoi(argv[3]) : 80; + + uint32_t ip = resolve_host_arg(host); + if (ip == 0) { + printk(ANSI_BRIGHT_RED "httpget: cannot resolve %s\n" ANSI_RESET, host); + return 1; + } + + char ipstr[16]; + ip_to_str(ip, ipstr); + printk("GET http://%s%s -> %s:%u\n", host, path, ipstr, port); + + tcp_conn_t* c = tcp_connect(ip, port, 4000); + if (!c) { + printk(ANSI_BRIGHT_RED "httpget: connection failed\n" ANSI_RESET); + return 1; + } + + char req[320]; + int rl = snprintf(req, sizeof(req), + "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: SUB-OS/lazybox\r\n" + "Connection: close\r\n\r\n", path, host); + if (tcp_send(c, req, (uint16_t)rl) < 0) { + printk(ANSI_BRIGHT_RED "httpget: send failed\n" ANSI_RESET); + tcp_close(c); + return 1; + } + + char buf[513]; + int total = 0; + for (;;) { + int n = tcp_recv(c, buf, sizeof(buf) - 1, 2000); + if (n <= 0) break; + buf[n] = '\0'; + printk("%s", buf); + total += n; + } + + printk("\n" ANSI_YELLOW "--- %d byte(s), %s ---\n" ANSI_RESET, + total, tcp_peer_closed(c) ? "server closed the stream" : "timed out"); + tcp_close(c); + return 0; +} + static int applet_htop(int argc, char** argv) { (void)argc; (void)argv; system_metrics_t m; @@ -2583,6 +2725,8 @@ static const lazybox_applet_t applets[] = { {"iptables", applet_iptables, "iptables", "Firewall packet filter rules","Network"}, {"netstat", applet_netstat, "netstat", "Network port connections", "Network"}, {"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"}, // Storage & Devices {"lsblk", applet_lsblk, "lsblk", "List block storage devices", "Storage"},