Fix TCP error handling

Add unit tests for TCP error handling
Account for options in TCP header for payload start address calculation
Take UDP payload length from UDP header
This commit is contained in:
Sergio R. Caprile committed 2025-08-29 10:34:17 -03:00
1 parent c1152f5eeb
commit 7339c7dfe7
3 files changed
+472 -139

No files matched your search

+66 -58
View File
@@ -4729,13 +4729,17 @@ static size_t tx_tcp(struct mg_tcpip_if *ifp, uint8_t *dst_mac, uint32_t dst_ip,
return ether_output(ifp, PDIFF(ifp->tx.buf, tcp + 1) + len);
}
static size_t tx_tcp_pkt(struct mg_tcpip_if *ifp, struct pkt *pkt,
uint8_t flags, uint32_t seq, const void *buf,
size_t len) {
uint32_t delta = (pkt->tcp->flags & (TH_SYN | TH_FIN)) ? 1 : 0;
static size_t tx_tcp_ctrlresp(struct mg_tcpip_if *ifp, struct pkt *pkt,
uint8_t flags, uint32_t seqno) {
uint32_t ackno = mg_htonl(mg_ntohl(pkt->tcp->seq) + (uint32_t) pkt->pay.len +
((pkt->tcp->flags & (TH_SYN | TH_FIN)) ? 1 : 0));
return tx_tcp(ifp, pkt->eth->src, pkt->ip->src, flags, pkt->tcp->dport,
pkt->tcp->sport, seq, mg_htonl(mg_ntohl(pkt->tcp->seq) + delta),
buf, len);
pkt->tcp->sport, seqno, ackno, NULL, 0);
}
static size_t tx_tcp_rst(struct mg_tcpip_if *ifp, struct pkt *pkt, bool toack) {
return tx_tcp_ctrlresp(ifp, pkt, toack ? TH_RST : (TH_RST | TH_ACK),
toack ? pkt->tcp->ack : 0);
}
static struct mg_connection *accept_conn(struct mg_connection *lsn,
@@ -4934,10 +4938,9 @@ static void read_conn(struct mg_connection *c, struct pkt *pkt) {
}
}
// TCP backlog
struct mg_backlog {
uint16_t port, mss; // use port=0 for available entries
uint16_t port, mss; // use port=0 for available entries
uint8_t age;
};
@@ -4947,7 +4950,7 @@ static int backlog_insert(struct mg_connection *c, uint16_t port,
size_t i;
for (i = 0; i < sizeof(c->data) / sizeof(*p); i++) {
if (p[i].port != 0) continue;
p[i].age = 2; // remove after two calls, average 1.5 call rate
p[i].age = 2; // remove after two calls, average 1.5 call rate
p[i].port = port, p[i].mss = mss;
return (int) i;
}
@@ -4970,7 +4973,7 @@ static void backlog_remove(struct mg_connection *c, uint16_t key) {
static void backlog_maintain(struct mg_connection *c) {
struct mg_backlog *p = (struct mg_backlog *) c->data;
size_t i; // dec age and remove those where it reaches 0
size_t i; // dec age and remove those where it reaches 0
for (i = 0; i < sizeof(c->data) / sizeof(*p); i++) {
if (p[i].port == 0) continue;
if (p[i].age != 0) --p[i].age;
@@ -5007,70 +5010,74 @@ static void handle_opt(struct connstate *s, struct tcp *tcp) {
static void rx_tcp(struct mg_tcpip_if *ifp, struct pkt *pkt) {
struct mg_connection *c = getpeer(ifp->mgr, pkt, false);
struct connstate *s = c == NULL ? NULL : (struct connstate *) (c + 1);
#if 0
MG_INFO(("%lu %hhu %d", c ? c->id : 0, pkt->tcp->flags, (int) pkt->pay.len));
#endif
// Order is VERY important; RFC-9293 3.5.2
// - check clients (Group 1) and established connections (Group 3)
if (c != NULL && c->is_connecting && pkt->tcp->flags == (TH_SYN | TH_ACK)) {
// client got a server connection accept
handle_opt(s, pkt->tcp); // process options (MSS)
s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq) + 1;
tx_tcp_pkt(ifp, pkt, TH_ACK, pkt->tcp->ack, NULL, 0);
tx_tcp_ctrlresp(ifp, pkt, TH_ACK, pkt->tcp->ack);
c->is_connecting = 0; // Client connected
settmout(c, MIP_TTYPE_KEEPALIVE);
mg_call(c, MG_EV_CONNECT, NULL); // Let user know
if (c->is_tls_hs) mg_tls_handshake(c);
if (!c->is_tls_hs) c->is_tls = 0; // user did not call mg_tls_init()
} else if (c != NULL && c->is_connecting && pkt->tcp->flags != TH_ACK) {
// mg_hexdump(pkt->raw.buf, pkt->raw.len);
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
mg_error(c, "connection refused");
} else if (c != NULL && pkt->tcp->flags & TH_RST) {
// TODO(): validate RST is within window (and optional with proper ACK)
mg_error(c, "peer RST"); // RFC-1122 4.2.2.13
} else if (c != NULL) {
#if 0
MG_DEBUG(("%lu %d %M:%hu -> %M:%hu", c->id, (int) pkt->raw.len,
mg_print_ip4, &pkt->ip->src, mg_ntohs(pkt->tcp->sport),
mg_print_ip4, &pkt->ip->dst, mg_ntohs(pkt->tcp->dport)));
mg_hexdump(pkt->pay.buf, pkt->pay.len);
#endif
// process segment
s->tmiss = 0; // Reset missed keep-alive counter
if (s->ttype == MIP_TTYPE_KEEPALIVE) // Advance keep-alive timer
settmout(c,
MIP_TTYPE_KEEPALIVE); // unless a former ACK timeout is pending
read_conn(c, pkt); // Override timer with ACK timeout if needed
} else if ((c = getpeer(ifp->mgr, pkt, true)) == NULL) {
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
} else if (pkt->tcp->flags & TH_RST) {
if (c->is_accepted) mg_error(c, "peer RST"); // RFC-1122 4.2.2.13
// ignore RST if not connected
} else if (pkt->tcp->flags & TH_SYN) {
struct connstate cs; // At this point, s = NULL, there is no connection
int key;
uint32_t isn;
if (pkt->tcp->sport != 0) {
handle_opt(&cs, pkt->tcp); // process options (MSS)
key = backlog_insert(c, pkt->tcp->sport, cs.dmss); // backlog options (MSS)
if (key < 0) return; // no room in backlog, discard SYN, client retries
// Use peer's src port and bl key as ISN, to later identify the handshake
isn = (mg_htonl(((uint32_t)key << 16) | mg_ntohs(pkt->tcp->sport)));
tx_tcp_pkt(ifp, pkt, TH_SYN | TH_ACK, isn, NULL, 0);
} // what should we do when port=0 ? Linux takes port 0 as any other port
} else if (pkt->tcp->flags & TH_FIN) {
tx_tcp_pkt(ifp, pkt, TH_FIN | TH_ACK, pkt->tcp->ack, NULL, 0);
} else if ((uint16_t) (mg_htonl(pkt->tcp->ack) - 1) ==
mg_htons(pkt->tcp->sport)) {
uint16_t key = (uint16_t) ((mg_htonl(pkt->tcp->ack) - 1) >> 16);
struct mg_backlog *b = backlog_retrieve(c, key, pkt->tcp->sport);
if (b != NULL) {
accept_conn(c, pkt, b->mss); // pass options
backlog_remove(c, key);
} else if (!c->is_accepted) { // not an actual match, reset
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
// TODO(scaprile): revisit this and below, weird scenarios
}
} else if (!c->is_accepted) { // no peer
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
} else {
// MG_VERBOSE(("dropped silently.."));
}
} else
// - we don't listen on that port; RFC-9293 3.5.2 Group 1
// - check listening connections; RFC-9293 3.5.2 Group 2
if ((c = getpeer(ifp->mgr, pkt, true)) == NULL) {
// not listening on that port
if (!(pkt->tcp->flags & TH_RST)) {
tx_tcp_rst(ifp, pkt, pkt->tcp->flags & TH_ACK);
} // else silently discard
} else if (pkt->tcp->flags == TH_SYN) {
// listener receives a connection request
struct connstate cs; // At this point, s = NULL, there is no connection
int key;
uint32_t isn;
if (pkt->tcp->sport != 0) {
handle_opt(&cs, pkt->tcp); // process options (MSS)
key = backlog_insert(c, pkt->tcp->sport,
cs.dmss); // backlog options (MSS)
if (key < 0) return; // no room in backlog, discard SYN, client retries
// Use peer's src port and bl key as ISN, to later identify the
// handshake
isn = (mg_htonl(((uint32_t) key << 16) | mg_ntohs(pkt->tcp->sport)));
tx_tcp_ctrlresp(ifp, pkt, TH_SYN | TH_ACK, isn);
} // what should we do when port=0 ? Linux takes port 0 as any other
// port
} else if (pkt->tcp->flags == TH_ACK) {
// listener receives an ACK
struct mg_backlog *b = NULL;
if ((uint16_t) (mg_htonl(pkt->tcp->ack) - 1) ==
mg_htons(pkt->tcp->sport)) {
uint16_t key = (uint16_t) ((mg_htonl(pkt->tcp->ack) - 1) >> 16);
b = backlog_retrieve(c, key, pkt->tcp->sport);
if (b != NULL) { // ACK is a response to a SYN+ACK
accept_conn(c, pkt, b->mss); // pass options
backlog_remove(c, key);
} // else not an actual match, reset
}
if (b == NULL) tx_tcp_rst(ifp, pkt, true);
} else if (pkt->tcp->flags & TH_RST) {
// silently discard
} else if (pkt->tcp->flags & TH_ACK) { // ACK + something else != RST
tx_tcp_rst(ifp, pkt, true);
} else if (pkt->tcp->flags & TH_SYN) { // SYN + something else != ACK
tx_tcp_rst(ifp, pkt, false);
} // else silently discard
}
static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) {
@@ -5090,6 +5097,7 @@ static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) {
pkt->udp = (struct udp *) (pkt->ip + 1);
if (pkt->pay.len < sizeof(*pkt->udp)) return;
mkpay(pkt, pkt->udp + 1);
if (pkt->udp->len < pkt->pay.len) pkt->pay.len = pkt->udp->len;
MG_VERBOSE(("UDP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src,
mg_ntohs(pkt->udp->sport), mg_print_ip4, &pkt->ip->dst,
mg_ntohs(pkt->udp->dport), (int) pkt->pay.len));
@@ -5108,7 +5116,7 @@ static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) {
uint16_t iplen, off;
pkt->tcp = (struct tcp *) (pkt->ip + 1);
if (pkt->pay.len < sizeof(*pkt->tcp)) return;
mkpay(pkt, pkt->tcp + 1);
mkpay(pkt, (uint32_t *) pkt->tcp + (pkt->tcp->off >> 4)); // may have opts
iplen = mg_ntohs(pkt->ip->len);
off = (uint16_t) (sizeof(*pkt->ip) + ((pkt->tcp->off >> 4) * 4U));
if (iplen >= off) pkt->pay.len = (size_t) (iplen - off);
+66 -58
View File
@@ -572,13 +572,17 @@ static size_t tx_tcp(struct mg_tcpip_if *ifp, uint8_t *dst_mac, uint32_t dst_ip,
return ether_output(ifp, PDIFF(ifp->tx.buf, tcp + 1) + len);
}
static size_t tx_tcp_pkt(struct mg_tcpip_if *ifp, struct pkt *pkt,
uint8_t flags, uint32_t seq, const void *buf,
size_t len) {
uint32_t delta = (pkt->tcp->flags & (TH_SYN | TH_FIN)) ? 1 : 0;
static size_t tx_tcp_ctrlresp(struct mg_tcpip_if *ifp, struct pkt *pkt,
uint8_t flags, uint32_t seqno) {
uint32_t ackno = mg_htonl(mg_ntohl(pkt->tcp->seq) + (uint32_t) pkt->pay.len +
((pkt->tcp->flags & (TH_SYN | TH_FIN)) ? 1 : 0));
return tx_tcp(ifp, pkt->eth->src, pkt->ip->src, flags, pkt->tcp->dport,
pkt->tcp->sport, seq, mg_htonl(mg_ntohl(pkt->tcp->seq) + delta),
buf, len);
pkt->tcp->sport, seqno, ackno, NULL, 0);
}
static size_t tx_tcp_rst(struct mg_tcpip_if *ifp, struct pkt *pkt, bool toack) {
return tx_tcp_ctrlresp(ifp, pkt, toack ? TH_RST : (TH_RST | TH_ACK),
toack ? pkt->tcp->ack : 0);
}
static struct mg_connection *accept_conn(struct mg_connection *lsn,
@@ -777,10 +781,9 @@ static void read_conn(struct mg_connection *c, struct pkt *pkt) {
}
}
// TCP backlog
struct mg_backlog {
uint16_t port, mss; // use port=0 for available entries
uint16_t port, mss; // use port=0 for available entries
uint8_t age;
};
@@ -790,7 +793,7 @@ static int backlog_insert(struct mg_connection *c, uint16_t port,
size_t i;
for (i = 0; i < sizeof(c->data) / sizeof(*p); i++) {
if (p[i].port != 0) continue;
p[i].age = 2; // remove after two calls, average 1.5 call rate
p[i].age = 2; // remove after two calls, average 1.5 call rate
p[i].port = port, p[i].mss = mss;
return (int) i;
}
@@ -813,7 +816,7 @@ static void backlog_remove(struct mg_connection *c, uint16_t key) {
static void backlog_maintain(struct mg_connection *c) {
struct mg_backlog *p = (struct mg_backlog *) c->data;
size_t i; // dec age and remove those where it reaches 0
size_t i; // dec age and remove those where it reaches 0
for (i = 0; i < sizeof(c->data) / sizeof(*p); i++) {
if (p[i].port == 0) continue;
if (p[i].age != 0) --p[i].age;
@@ -850,70 +853,74 @@ static void handle_opt(struct connstate *s, struct tcp *tcp) {
static void rx_tcp(struct mg_tcpip_if *ifp, struct pkt *pkt) {
struct mg_connection *c = getpeer(ifp->mgr, pkt, false);
struct connstate *s = c == NULL ? NULL : (struct connstate *) (c + 1);
#if 0
MG_INFO(("%lu %hhu %d", c ? c->id : 0, pkt->tcp->flags, (int) pkt->pay.len));
#endif
// Order is VERY important; RFC-9293 3.5.2
// - check clients (Group 1) and established connections (Group 3)
if (c != NULL && c->is_connecting && pkt->tcp->flags == (TH_SYN | TH_ACK)) {
// client got a server connection accept
handle_opt(s, pkt->tcp); // process options (MSS)
s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq) + 1;
tx_tcp_pkt(ifp, pkt, TH_ACK, pkt->tcp->ack, NULL, 0);
tx_tcp_ctrlresp(ifp, pkt, TH_ACK, pkt->tcp->ack);
c->is_connecting = 0; // Client connected
settmout(c, MIP_TTYPE_KEEPALIVE);
mg_call(c, MG_EV_CONNECT, NULL); // Let user know
if (c->is_tls_hs) mg_tls_handshake(c);
if (!c->is_tls_hs) c->is_tls = 0; // user did not call mg_tls_init()
} else if (c != NULL && c->is_connecting && pkt->tcp->flags != TH_ACK) {
// mg_hexdump(pkt->raw.buf, pkt->raw.len);
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
mg_error(c, "connection refused");
} else if (c != NULL && pkt->tcp->flags & TH_RST) {
// TODO(): validate RST is within window (and optional with proper ACK)
mg_error(c, "peer RST"); // RFC-1122 4.2.2.13
} else if (c != NULL) {
#if 0
MG_DEBUG(("%lu %d %M:%hu -> %M:%hu", c->id, (int) pkt->raw.len,
mg_print_ip4, &pkt->ip->src, mg_ntohs(pkt->tcp->sport),
mg_print_ip4, &pkt->ip->dst, mg_ntohs(pkt->tcp->dport)));
mg_hexdump(pkt->pay.buf, pkt->pay.len);
#endif
// process segment
s->tmiss = 0; // Reset missed keep-alive counter
if (s->ttype == MIP_TTYPE_KEEPALIVE) // Advance keep-alive timer
settmout(c,
MIP_TTYPE_KEEPALIVE); // unless a former ACK timeout is pending
read_conn(c, pkt); // Override timer with ACK timeout if needed
} else if ((c = getpeer(ifp->mgr, pkt, true)) == NULL) {
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
} else if (pkt->tcp->flags & TH_RST) {
if (c->is_accepted) mg_error(c, "peer RST"); // RFC-1122 4.2.2.13
// ignore RST if not connected
} else if (pkt->tcp->flags & TH_SYN) {
struct connstate cs; // At this point, s = NULL, there is no connection
int key;
uint32_t isn;
if (pkt->tcp->sport != 0) {
handle_opt(&cs, pkt->tcp); // process options (MSS)
key = backlog_insert(c, pkt->tcp->sport, cs.dmss); // backlog options (MSS)
if (key < 0) return; // no room in backlog, discard SYN, client retries
// Use peer's src port and bl key as ISN, to later identify the handshake
isn = (mg_htonl(((uint32_t)key << 16) | mg_ntohs(pkt->tcp->sport)));
tx_tcp_pkt(ifp, pkt, TH_SYN | TH_ACK, isn, NULL, 0);
} // what should we do when port=0 ? Linux takes port 0 as any other port
} else if (pkt->tcp->flags & TH_FIN) {
tx_tcp_pkt(ifp, pkt, TH_FIN | TH_ACK, pkt->tcp->ack, NULL, 0);
} else if ((uint16_t) (mg_htonl(pkt->tcp->ack) - 1) ==
mg_htons(pkt->tcp->sport)) {
uint16_t key = (uint16_t) ((mg_htonl(pkt->tcp->ack) - 1) >> 16);
struct mg_backlog *b = backlog_retrieve(c, key, pkt->tcp->sport);
if (b != NULL) {
accept_conn(c, pkt, b->mss); // pass options
backlog_remove(c, key);
} else if (!c->is_accepted) { // not an actual match, reset
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
// TODO(scaprile): revisit this and below, weird scenarios
}
} else if (!c->is_accepted) { // no peer
tx_tcp_pkt(ifp, pkt, TH_RST | TH_ACK, pkt->tcp->ack, NULL, 0);
} else {
// MG_VERBOSE(("dropped silently.."));
}
} else
// - we don't listen on that port; RFC-9293 3.5.2 Group 1
// - check listening connections; RFC-9293 3.5.2 Group 2
if ((c = getpeer(ifp->mgr, pkt, true)) == NULL) {
// not listening on that port
if (!(pkt->tcp->flags & TH_RST)) {
tx_tcp_rst(ifp, pkt, pkt->tcp->flags & TH_ACK);
} // else silently discard
} else if (pkt->tcp->flags == TH_SYN) {
// listener receives a connection request
struct connstate cs; // At this point, s = NULL, there is no connection
int key;
uint32_t isn;
if (pkt->tcp->sport != 0) {
handle_opt(&cs, pkt->tcp); // process options (MSS)
key = backlog_insert(c, pkt->tcp->sport,
cs.dmss); // backlog options (MSS)
if (key < 0) return; // no room in backlog, discard SYN, client retries
// Use peer's src port and bl key as ISN, to later identify the
// handshake
isn = (mg_htonl(((uint32_t) key << 16) | mg_ntohs(pkt->tcp->sport)));
tx_tcp_ctrlresp(ifp, pkt, TH_SYN | TH_ACK, isn);
} // what should we do when port=0 ? Linux takes port 0 as any other
// port
} else if (pkt->tcp->flags == TH_ACK) {
// listener receives an ACK
struct mg_backlog *b = NULL;
if ((uint16_t) (mg_htonl(pkt->tcp->ack) - 1) ==
mg_htons(pkt->tcp->sport)) {
uint16_t key = (uint16_t) ((mg_htonl(pkt->tcp->ack) - 1) >> 16);
b = backlog_retrieve(c, key, pkt->tcp->sport);
if (b != NULL) { // ACK is a response to a SYN+ACK
accept_conn(c, pkt, b->mss); // pass options
backlog_remove(c, key);
} // else not an actual match, reset
}
if (b == NULL) tx_tcp_rst(ifp, pkt, true);
} else if (pkt->tcp->flags & TH_RST) {
// silently discard
} else if (pkt->tcp->flags & TH_ACK) { // ACK + something else != RST
tx_tcp_rst(ifp, pkt, true);
} else if (pkt->tcp->flags & TH_SYN) { // SYN + something else != ACK
tx_tcp_rst(ifp, pkt, false);
} // else silently discard
}
static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) {
@@ -933,6 +940,7 @@ static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) {
pkt->udp = (struct udp *) (pkt->ip + 1);
if (pkt->pay.len < sizeof(*pkt->udp)) return;
mkpay(pkt, pkt->udp + 1);
if (pkt->udp->len < pkt->pay.len) pkt->pay.len = pkt->udp->len;
MG_VERBOSE(("UDP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src,
mg_ntohs(pkt->udp->sport), mg_print_ip4, &pkt->ip->dst,
mg_ntohs(pkt->udp->dport), (int) pkt->pay.len));
@@ -951,7 +959,7 @@ static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) {
uint16_t iplen, off;
pkt->tcp = (struct tcp *) (pkt->ip + 1);
if (pkt->pay.len < sizeof(*pkt->tcp)) return;
mkpay(pkt, pkt->tcp + 1);
mkpay(pkt, (uint32_t *) pkt->tcp + (pkt->tcp->off >> 4)); // may have opts
iplen = mg_ntohs(pkt->ip->len);
off = (uint16_t) (sizeof(*pkt->ip) + ((pkt->tcp->off >> 4) * 4U));
if (iplen >= off) pkt->pay.len = (size_t) (iplen - off);
+340 -23
View File
@@ -54,6 +54,17 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
(void) c, (void) ev, (void) ev_data;
}
static void tcpclosure_fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_ACCEPT) c->is_draining = 1;
(void) c, (void) ev_data;
}
static void client_fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_ERROR || ev == MG_EV_CONNECT)
(*(int *) c->fn_data) = ev;
(void) c, (void) ev_data;
}
static void frag_recv_fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_ERROR) {
if (s_sent_fragment > 0) {
@@ -83,6 +94,7 @@ static void frag_send_fn(struct mg_connection *c, int ev, void *ev_data) {
(void) c, (void) ev_data;
}
static void test_poll(void) {
int count = 0, i;
struct mg_tcpip_if mif;
@@ -194,47 +206,118 @@ static void init_tcp_handshake(struct eth *e, struct ip *ip,
struct tcp *t = (struct tcp *)(s_driver_data.buf + sizeof(*e) + sizeof(*ip));
// SYN
create_tcp_simpleseg(e, ip, 1000, 0, TH_SYN | TH_ACK, 0);
create_tcp_simpleseg(e, ip, 1000, 0, TH_SYN, 0);
MG_VERBOSE(("SYN -->"));
mg_mgr_poll(mgr, 0); // make sure we clean former stuff in buffer
// SYN-ACK
while (!received_response(&s_driver_data)) mg_mgr_poll(mgr, 0);
ASSERT((t->flags == (TH_SYN | TH_ACK)));
ASSERT((t->ack == mg_htonl(1001)));
MG_VERBOSE(("SYN+ACK <--"));
// ACK
create_tcp_simpleseg(e, ip, 1001, 2, TH_ACK, 0);
mg_mgr_poll(mgr, 0);
MG_VERBOSE(("ACK -->"));
mg_mgr_poll(mgr, 0); // this may have data on return !
}
// DHCP discovery works as a 1 second timeout, we take advantage of it
// (something is received within 1s) and we mask it when doing longer waits
// (verify received data is TCP by checking IP's protocol field)
static void test_tcp_basics(void) {
struct mg_mgr mgr;
struct eth e;
struct ip ip;
struct tcp *t = (struct tcp *) (s_driver_data.buf + sizeof(e) + sizeof(ip));
// uint64_t start, now;
// bool response_recv = true;
struct ip *i = (struct ip *) (s_driver_data.buf + sizeof(e));
uint64_t start, now;
struct mg_tcpip_driver driver;
struct mg_tcpip_if mif;
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, fn);
// send SYN to a non-used port, expect RST + ACK
create_tcp_seg(&e, &ip, 1234, 1, TH_SYN, 0, 69, 0, NULL, 0);
// https://datatracker.ietf.org/doc/html/rfc9293#section-3.5.2 Reset Generation
// non-used port. Group 1 in RFC
// send SYN, expect RST + ACK
create_tcp_seg(&e, &ip, 1234, 4321, TH_SYN, 1, 69, 0, NULL, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->flags == (TH_RST | TH_ACK)));
ASSERT((t->ack == mg_htonl(1235)));
ASSERT(t->flags == (TH_RST | TH_ACK));
ASSERT(t->seq == mg_htonl(0));
ASSERT(t->ack == mg_htonl(1235));
// send data to a non-used port, expect a RST + ACK
create_tcp_seg(&e, &ip, 1234, 1, TH_PUSH, 0, 69, 0, NULL, 0);
// send SYN+ACK, expect RST
create_tcp_seg(&e, &ip, 1234, 4321, TH_SYN | TH_ACK, 1, 69, 0, NULL, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == TH_RST);
ASSERT(t->seq == mg_htonl(4321));
// send data, expect RST + ACK
create_tcp_seg(&e, &ip, 1234, 4321, TH_PUSH, 1, 69, 2, NULL, 0);
mg_mgr_poll(&mgr, 0);
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->flags == (TH_RST | TH_ACK)));
ASSERT((t->ack == mg_htonl(1234)));
ASSERT(t->flags == (TH_RST | TH_ACK));
ASSERT(t->seq == mg_htonl(0));
ASSERT(t->ack == mg_htonl(1236));
// send ACK, expect RST
create_tcp_seg(&e, &ip, 1234, 4321, TH_ACK, 1, 69, 0, NULL, 0);
mg_mgr_poll(&mgr, 0);
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == TH_RST);
ASSERT(t->seq == mg_htonl(4321));
// send FIN, expect RST + ACK
create_tcp_seg(&e, &ip, 1234, 4321, TH_FIN, 1, 69, 0, NULL, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == (TH_RST | TH_ACK)); // Linux answers RST only
ASSERT(t->seq == mg_htonl(0));
ASSERT(t->ack == mg_htonl(1235));
// send FIN+ACK, expect RST
create_tcp_seg(&e, &ip, 1234, 4321, TH_FIN | TH_ACK, 1, 69, 0, NULL, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == TH_RST);
ASSERT(t->seq == mg_htonl(4321));
// listening, non-connected port. Group 2 in RFC
// send data, expect no response
create_tcp_seg(&e, &ip, 1234, 4321, TH_PUSH, 1, 80, 2, NULL, 0);
mg_mgr_poll(&mgr, 0);
ASSERT(!received_response(&s_driver_data));
// send ACK, expect RST
create_tcp_seg(&e, &ip, 1234, 4321, TH_ACK, 1, 80, 0, NULL, 0);
mg_mgr_poll(&mgr, 0);
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == TH_RST);
ASSERT(t->seq == mg_htonl(4321));
// send SYN+ACK, expect RST
create_tcp_seg(&e, &ip, 1234, 4321, TH_SYN | TH_ACK, 1, 80, 0, NULL, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == TH_RST);
ASSERT(t->seq == mg_htonl(4321));
// send FIN, expect no response
create_tcp_seg(&e, &ip, 1234, 4321, TH_FIN, 1, 80, 0, NULL, 0);
mg_mgr_poll(&mgr, 0);
ASSERT(!received_response(&s_driver_data));
// send FIN+ACK, expect RST
create_tcp_seg(&e, &ip, 1234, 4321, TH_FIN | TH_ACK, 1, 80, 0, NULL, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == TH_RST);
ASSERT(t->seq == mg_htonl(4321));
// we currently don't validate checksum, no silently discarded segment test
init_tcp_handshake(&e, &ip, &mgr); // starts with seq_no=1000, ackno=2
@@ -255,7 +338,238 @@ static void test_tcp_basics(void) {
ASSERT((t->flags == TH_ACK));
ASSERT((t->ack == mg_htonl(1001))); // expecting 1001, dude
// we currently don't validate checksum, no silently discarded segment test
// Initiate closure, send FIN (test client-initiated closure)
// https://datatracker.ietf.org/doc/html/rfc9293#section-3.6
// We are case 1, Mongoose is case 2
create_tcp_simpleseg(&e, &ip, 1001, 2, TH_FIN, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
// Mongoose does a fast reduced ("3-way instead of 4-way" closure)
ASSERT((t->flags == (TH_FIN | TH_ACK))); // Mongoose ACKs our FIN, sends FIN
ASSERT((t->seq == mg_htonl(2)));
ASSERT((t->ack == mg_htonl(1002)));
// make sure it is still open
ASSERT(mgr.conns->next != NULL); // more than one connection: the listener + us
create_tcp_simpleseg(&e, &ip, 1002, 3, TH_ACK, 0); // ACK Mongoose FIN
mg_mgr_poll(&mgr, 0);
ASSERT(!received_response(&s_driver_data));
// make sure it is closed
ASSERT(mgr.conns->next == NULL); // only one connection: the listener
s_driver_data.len = 0;
mg_mgr_free(&mgr);
// Test client-initiated closure timeout, do not ACK
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, fn);
init_tcp_handshake(&e, &ip, &mgr); // starts with seq_no=1000, ackno=2
create_tcp_simpleseg(&e, &ip, 1001, 2, TH_FIN, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
// Mongoose does a fast reduced ("3-way instead of 4-way" closure)
ASSERT((t->flags == (TH_FIN | TH_ACK))); // Mongoose ACKs our FIN, sends FIN
ASSERT((t->seq == mg_htonl(2)));
ASSERT((t->ack == mg_htonl(1002)));
// make sure it is still open
ASSERT(mgr.conns->next != NULL); // more than one connection: the listener + us
s_driver_data.len = 0; // avoid Mongoose "receiving itself"
start = mg_millis();
now = 0;
do {
mg_mgr_poll(&mgr, 0);
if (received_response(&s_driver_data) && i->proto == 6) break; // check first
now = mg_millis() - start;
} while (now < (12 * MIP_TCP_FIN_MS)/10);
ASSERT(now > MIP_TCP_FIN_MS);
// make sure it is closed
ASSERT(mgr.conns->next == NULL); // only one connection: the listener
s_driver_data.len = 0;
mg_mgr_free(&mgr);
// Test server-initiated closure, abbreviated 3-way: respond FIN+ACK
// https://datatracker.ietf.org/doc/html/rfc9293#section-3.6
// We are case 2, Mongoose is case 1
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, tcpclosure_fn);
init_tcp_handshake(&e, &ip, &mgr); // starts with seq_no=1000, ackno=2
// we should have already received the FIN due to the call above
start = mg_millis();
while (!received_response(&s_driver_data)) {
mg_mgr_poll(&mgr, 0);
now = mg_millis() - start;
if (now > 2 * MIP_TCP_ACK_MS)
ASSERT(0); // response should have been received by now
}
ASSERT((t->seq == mg_htonl(2)));
ASSERT((t->ack == mg_htonl(1001)));
ASSERT(t->flags == (TH_FIN | TH_ACK)); // Mongoose ACKs last data, sends FIN
// send FIN + ACK
create_tcp_simpleseg(&e, &ip, 1001, 3, TH_FIN | TH_ACK, 0); // ACK FIN, send FIN
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->flags == TH_ACK)); // Mongoose ACKs our FIN
ASSERT((t->seq == mg_htonl(3)));
ASSERT((t->ack == mg_htonl(1002)));
// make sure it is closed
ASSERT(mgr.conns->next == NULL); // only one connection: the listener
s_driver_data.len = 0;
mg_mgr_free(&mgr);
// Test server-initiated closure, long 4-way closure: respond ACK
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, tcpclosure_fn);
init_tcp_handshake(&e, &ip, &mgr); // starts with seq_no=1000, ackno=2
// we should have already received the FIN, tested in above tst
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->seq == mg_htonl(2)));
ASSERT((t->ack == mg_htonl(1001)));
ASSERT(t->flags == (TH_FIN | TH_ACK)); // Mongoose ACKs last data, sends FIN
// ACK Mongoose FIN, do *not* send FIN yet
create_tcp_simpleseg(&e, &ip, 1001, 3, TH_ACK, 0); // ACK FIN
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
start = mg_millis();
now = 0;
do {
if (received_response(&s_driver_data)) break; // check first
mg_mgr_poll(&mgr, 0);
now = mg_millis() - start;
} while (now < 2 * MIP_TCP_ACK_MS); // keep timeout below 1s (DHCP discover)
ASSERT(now >= 2 * MIP_TCP_ACK_MS);
// make sure it is still open
ASSERT(mgr.conns->next != NULL); // more than one connection: the listener + us
create_tcp_simpleseg(&e, &ip, 1001, 3, TH_FIN, 0); // send FIN
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->flags == TH_ACK)); // Mongoose ACKs our FIN
ASSERT((t->seq == mg_htonl(3)));
ASSERT((t->ack == mg_htonl(1002)));
// make sure it is closed
ASSERT(mgr.conns->next == NULL); // only one connection: the listener
s_driver_data.len = 0;
mg_mgr_free(&mgr);
// Test server-initiated closure, FIN retransmission: do not ACK FIN
// Actual data retransmission is tested on another unit test
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, tcpclosure_fn);
init_tcp_handshake(&e, &ip, &mgr); // starts with seq_no=1000, ackno=2
// we should have already received the FIN, tested in some tst above
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->seq == mg_htonl(2)));
ASSERT((t->ack == mg_htonl(1001)));
ASSERT(t->flags == (TH_FIN | TH_ACK)); // Mongoose ACKs last data, sends FIN
s_driver_data.len = 0; // avoid Mongoose "receiving itself"
start = mg_millis();
now = 0;
do {
if (received_response(&s_driver_data)) break; // check first
mg_mgr_poll(&mgr, 0);
now = mg_millis() - start;
} while (now < 2 * MIP_TCP_ACK_MS); // keep timeout below 1s (DHCP discover)
// ASSERT(now < 2 * MIP_TCP_ACK_MS); ******** WE FAIL THIS, Mongoose does not retransmit, FIN is not an additional element in the stream
// ASSERT((t->seq == mg_htonl(2)));
// ASSERT((t->ack == mg_htonl(1001)));
// ASSERT(t->flags == (TH_FIN | TH_ACK)); // Mongoose retransmits FIN
// send FIN + ACK
create_tcp_simpleseg(&e, &ip, 1001, 3, TH_FIN | TH_ACK, 0); // ACK FIN, send FIN
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->flags == TH_ACK)); // Mongoose ACKs our FIN
ASSERT((t->seq == mg_htonl(3)));
ASSERT((t->ack == mg_htonl(1002)));
// make sure it is closed
ASSERT(mgr.conns->next == NULL); // only one connection: the listener
s_driver_data.len = 0;
mg_mgr_free(&mgr);
// Test simultaneous closure
// https://datatracker.ietf.org/doc/html/rfc9293#section-3.6 case 3
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, tcpclosure_fn);
init_tcp_handshake(&e, &ip, &mgr); // starts with seq_no=1000, ackno=2
// we should have already received the FIN due to the call above
start = mg_millis();
while (!received_response(&s_driver_data)) {
mg_mgr_poll(&mgr, 0);
now = mg_millis() - start;
if (now > 2 * MIP_TCP_ACK_MS)
ASSERT(0); // response should have been received by now
}
ASSERT((t->seq == mg_htonl(2)));
ASSERT((t->ack == mg_htonl(1001)));
ASSERT(t->flags == (TH_FIN | TH_ACK)); // Mongoose ACKs last data, sends FIN
// Also initiate closure, send FIN, do *not* ACK Mongoose FIN
create_tcp_simpleseg(&e, &ip, 1001, 2, TH_FIN, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT((t->flags == TH_ACK)); // Mongoose ACKs our FIN
ASSERT((t->seq == mg_htonl(3)));
ASSERT((t->ack == mg_htonl(1002)));
// make sure it is still open ******** WE FAIL THIS, Mongoose closes immediately, does not wait to retransmit its ACK nor to get the other end ACK
// ASSERT(mgr.conns->next != NULL); // more than one connection: the listener + us
// create_tcp_simpleseg(&e, &ip, 1002, 3, TH_ACK, 0); // ACK FIN
// mg_mgr_poll(&mgr, 0);
// make sure it is closed
ASSERT(mgr.conns->next == NULL); // only one connection: the listener
s_driver_data.len = 0;
mg_mgr_free(&mgr);
// Test responses to a connecting client
// https://datatracker.ietf.org/doc/html/rfc9293#section-3.5
// NOTE: Mongoose ignores any data until connection is actually established
// NOTE: Mongoose does not support the concept of "simultaneous open", Mongoose is either client or server
{
struct mg_connection *c;
int event = 255;
uint32_t ackno;
// this creates a listener we won't use
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, tcpclosure_fn);
c = mg_connect(&mgr, "tcp://1.2.3.4:1234/", client_fn, &event);
ASSERT(c!=NULL);
ASSERT(received_response(&s_driver_data));
ASSERT((t->flags == TH_SYN));
ASSERT(event == 255);
// invalid SYN + ACK to connecting client (after SYN...), send ACK out of seq
ackno = mg_ntohl(t->seq) + 1000;
// create_tcp_seg(&e, &ip, 4321, ackno, TH_SYN | TH_ACK, 1234, mg_ntohs(c->loc.port), 0, NULL, 0);
// mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
// while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
// ASSERT((t->flags == (TH_RST | TH_ACK))); // ***************** WHAT DOES LINUX DO HERE ????
// ******** WE FAIL THIS, Mongoose does not validate the ACK number
// ASSERT((t->seq == mg_htonl(ackno)));
// ASSERT((t->ack == mg_htonl(4322)));
// connect
ackno = mg_ntohl(t->seq) + 1;
create_tcp_seg(&e, &ip, 4321, ackno, TH_SYN | TH_ACK, 1234, mg_ntohs(c->loc.port), 0, NULL, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == TH_ACK);
ASSERT(t->seq == mg_htonl(ackno));
ASSERT((t->ack == mg_htonl(4322)));
ASSERT(event == MG_EV_CONNECT);
event = 255;
s_driver_data.len = 0;
mg_mgr_free(&mgr);
// test connection failure, send RST+ACK
// this creates a listener we won't use
init_tcp_tests(&mgr, &e, &ip, &driver, &mif, tcpclosure_fn);
c = mg_connect(&mgr, "tcp://1.2.3.4:1234/", client_fn, &event);
received_response(&s_driver_data); // get the SYN
ackno = mg_ntohl(t->seq) + 1;
create_tcp_seg(&e, &ip, 4321, ackno, TH_RST + TH_ACK, 1234, mg_ntohs(c->loc.port), 0, NULL, 0);
mg_mgr_poll(&mgr, 0);
MG_DEBUG(("event: %d", event));
ASSERT(event == MG_EV_ERROR);
ASSERT(!received_response(&s_driver_data));
}
// TODO(): RST handling
// https://datatracker.ietf.org/doc/html/rfc9293#section-3.5.3
// all reset (RST) segments are validated by checking their SEQ fields. A reset is valid if its sequence number is in the window; otherwise, it is silently discarded
s_driver_data.len = 0;
mg_mgr_free(&mgr);
@@ -405,7 +719,6 @@ static void test_tcp_backlog(void) {
struct tcp *t = (struct tcp *) (s_driver_data.buf + sizeof(e) + sizeof(ip));
struct ip *i = (struct ip *) (s_driver_data.buf + sizeof(e));
uint64_t start, now;
//bool response_recv = true;
struct mg_tcpip_driver driver;
struct mg_tcpip_if mif;
uint16_t opts[4 / 2]; // Send MSS, RFC-9293 3.7.1
@@ -421,18 +734,22 @@ static void test_tcp_backlog(void) {
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == (TH_SYN | TH_ACK));
// delay ACK so it is removed from the backlog
// delay ACK so conn attempt is removed from the backlog
s_driver_data.len = 0; // avoid Mongoose "receiving itself"
start = mg_millis();
do {
mg_mgr_poll(&mgr, 0);
now = mg_millis() - start;
} while (now < 2100);
// Mongoose may have retransmitted SYN + ACK, so
create_tcp_simpleseg(&e, &ip, 1235, 2, TH_ACK, 0);
mg_mgr_poll(&mgr, 0); // make sure we clean former stuff in buffer
while (!received_response(&s_driver_data) || i->proto != 6) mg_mgr_poll(&mgr, 0);
ASSERT(t->flags == (TH_RST | TH_ACK));
// check backlog is empty
c = mgr.conns;
ASSERT(c->next == NULL);
for (j = 0; j < LOGSZ; j++) {
struct mg_backlog *b = (struct mg_backlog *)(c->data) + j;
ASSERT(b->port == 0);
}
// Mongoose may have retransmitted SYN + ACK, and DHCP sent discover
received_response(&s_driver_data); // make sure we clean buffer
opts[0] = mg_htons(0x0204); // RFC-9293 3.2
// fill the backlog