-
Notifications
You must be signed in to change notification settings - Fork 27
feature: TCP MSS clamping support (bidirectional) #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2056,6 +2056,11 @@ static int handle_PACKET (struct n3n_runtime_data * eee, | |
| } | ||
| } | ||
|
|
||
| /* Clamp TCP MSS on incoming VPN packets before writing to TAP. */ | ||
| if(eee->conf.clamp_mss) { | ||
| clamp_mss(eee, eth_payload, eth_size); | ||
| } | ||
|
|
||
| /* Write ethernet packet to tap device. */ | ||
| traceEvent(TRACE_DEBUG, "sending data of size %u to TAP", (unsigned int)eth_size); | ||
| data_sent_len = tuntap_write(&(eee->device), eth_payload, eth_size); | ||
|
|
@@ -2193,6 +2198,188 @@ static int find_peer_destination (struct n3n_runtime_data * eee, | |
|
|
||
| /* ***************************************************** */ | ||
|
|
||
| /** | ||
| * Incremental TCP checksum update (RFC 1624). | ||
| * | ||
| * When a 16-bit field in the TCP header is modified, the checksum can be | ||
| * updated incrementally rather than recalculated from scratch: | ||
| * HC' = ~(~HC + ~m + m') | ||
| * | ||
| * All values are in host byte order. | ||
| */ | ||
| uint16_t tcp_csum_update (uint16_t old_csum, uint16_t old_val, uint16_t new_val) { | ||
| uint32_t sum = (uint32_t)(~old_csum & 0xFFFF) | ||
| + (uint32_t)(~old_val & 0xFFFF) | ||
| + (uint32_t) new_val; | ||
| sum = (sum & 0xFFFF) + (sum >> 16); | ||
| sum += sum >> 16; | ||
| return (uint16_t)(~sum & 0xFFFF); | ||
| } | ||
|
|
||
| /** | ||
| * Clamp the TCP MSS value in SYN/SYN-ACK packets to prevent fragmentation. | ||
| * Supports IPv4/IPv6, VLAN/QinQ, IP-in-IP, and IPv6 extension headers. | ||
| * Uses byte-shift reads for alignment safety on ARM/MIPS. | ||
| * Inspired by tinc's clamp_mss; expanded with full protocol support. | ||
| * TCP checksum update follows RFC 1624 incremental one's-complement method. | ||
| */ | ||
| void clamp_mss (struct n3n_runtime_data *eee, uint8_t *tap_pkt, size_t len) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is quite a lot of tricky logic and corner cases in this function. Did this code come from somewhere? Please attribute it so that the provenance can be checked and the appropriate licensing can be reviewed. Also, in the future, fixes from upstream could be cherry picked.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The foundational workflow is inspired by tinc's clamp_mss, which I have expanded for full protocol support using Qwen Code. The handling of IPv4/IPv6, VLAN/QinQ, IP-in-IP, and IPv6 ext headers is based on my domain expertise, with Wikipedia used to cross-reference specifications. While AI assisted in the coding, I personally directed the logic and verified the implementation through manual and Gemini AI review. |
||
|
|
||
| if(eee->conf.mtu <= 0) { | ||
| return; | ||
| } | ||
|
|
||
| uint16_t mtu = eee->conf.mtu; | ||
|
|
||
| /* ---- Ethernet ---- */ | ||
| if(len < sizeof(ether_hdr_t)) { | ||
| return; | ||
| } | ||
|
|
||
| size_t offset = sizeof(ether_hdr_t); | ||
| uint16_t ethertype = (tap_pkt[12] << 8) | tap_pkt[13]; | ||
|
|
||
| /* Skip stacked VLAN tags (802.1Q / 802.1ad QinQ) */ | ||
| while(ethertype == 0x8100 || ethertype == 0x88A8 || ethertype == 0x9100) { | ||
| if(len < offset + 4) { | ||
| return; | ||
| } | ||
| ethertype = (tap_pkt[offset + 2] << 8) | tap_pkt[offset + 3]; | ||
| offset += 4; | ||
| } | ||
|
|
||
| /* ---- IP ---- */ | ||
| uint8_t ip_proto = 0; | ||
|
|
||
| if(ethertype == 0x0800) { | ||
| /* --- IPv4 --- */ | ||
| if(len < offset + 20 || (tap_pkt[offset] >> 4) != 4) { | ||
| return; | ||
| } | ||
|
|
||
| /* Check for fragmentation: offset != 0 OR More Fragments flag set */ | ||
| if(((tap_pkt[offset + 6] << 8) | tap_pkt[offset + 7]) & 0x3FFF) { | ||
| return; | ||
| } | ||
|
|
||
| uint16_t ihl_bytes = (tap_pkt[offset] & 0x0F) * 4; | ||
| ip_proto = tap_pkt[offset + 9]; | ||
|
|
||
| /* Support for IP-in-IP encapsulation (RFC 2003) */ | ||
| if(ip_proto == 4) { | ||
| offset += ihl_bytes; | ||
| if(len < offset + 20 || (tap_pkt[offset] >> 4) != 4) { | ||
| return; | ||
| } | ||
| if(((tap_pkt[offset + 6] << 8) | tap_pkt[offset + 7]) & 0x3FFF) { | ||
| return; | ||
| } | ||
| ihl_bytes = (tap_pkt[offset] & 0x0F) * 4; | ||
| ip_proto = tap_pkt[offset + 9]; | ||
| } | ||
| offset += ihl_bytes; | ||
|
|
||
| } else if(ethertype == 0x86DD) { | ||
| /* --- IPv6 --- */ | ||
| if(len < offset + 40 || (tap_pkt[offset] >> 4) != 6) { | ||
| return; | ||
| } | ||
| uint8_t next_hdr = tap_pkt[offset + 6]; | ||
| offset += 40; | ||
|
|
||
| /* Walk through Extension Headers */ | ||
| while(next_hdr != IPPROTO_TCP && next_hdr != IPPROTO_NONE) { | ||
| if(len < offset + 2) { | ||
| return; | ||
| } | ||
| if(next_hdr == 0 || next_hdr == 43 || next_hdr == 60) { | ||
| uint16_t ext_len = (tap_pkt[offset + 1] + 1) * 8; | ||
| if(len < offset + ext_len) { | ||
| return; | ||
| } | ||
| next_hdr = tap_pkt[offset]; | ||
| offset += ext_len; | ||
| } else if(next_hdr == 44 || next_hdr == 51) { | ||
| /* Abort on Fragment (44) or Auth Header (51) */ | ||
| return; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| ip_proto = next_hdr; | ||
| } else { | ||
| return; /* not IP */ | ||
| } | ||
|
|
||
| /* ---- TCP ---- */ | ||
| if(ip_proto != IPPROTO_TCP || len < offset + 20) { | ||
| return; | ||
| } | ||
|
|
||
| /* Only process SYN or SYN-ACK packets (Flag bit 2) */ | ||
| if(!(tap_pkt[offset + 13] & 0x02)) { | ||
| return; | ||
| } | ||
|
|
||
| uint8_t tcp_doff = tap_pkt[offset + 12] >> 4; | ||
| size_t tcp_hdr_len = tcp_doff * 4; | ||
| if(tcp_doff < 5 || len < offset + tcp_hdr_len) { | ||
| return; | ||
| } | ||
|
|
||
| /* Calculate dynamic Max MSS based on total IP overhead found */ | ||
| size_t ip_overhead = offset - 14; | ||
| if(mtu <= (ip_overhead + 20)) { | ||
| return; | ||
| } | ||
| uint16_t max_mss = mtu - (uint16_t)ip_overhead - 20; | ||
| if(max_mss == 0) { | ||
| return; | ||
| } | ||
|
|
||
| /* ---- Walk TCP options for MSS (kind=2, len=4) ---- */ | ||
| uint8_t *opts = tap_pkt + offset + 20; | ||
| size_t opts_len = tcp_hdr_len - 20; | ||
|
|
||
| for(size_t i = 0; i < opts_len; ) { | ||
| uint8_t kind = opts[i]; | ||
|
|
||
| if(kind == 0) break; /* End of Options */ | ||
| if(kind == 1) { | ||
| i++; continue; | ||
| } /* NOP */ | ||
|
|
||
| if(i + 1 >= opts_len) break; | ||
| uint8_t olen = opts[i + 1]; | ||
| if(olen < 2 || i + olen > opts_len) break; | ||
|
|
||
| if(kind == 2 && olen == 4) { | ||
| uint16_t old_mss = (opts[i + 2] << 8) | opts[i + 3]; | ||
| if(old_mss > max_mss) { | ||
| uint16_t new_mss = max_mss; | ||
|
|
||
| traceEvent(TRACE_INFO, "Clamping MSS from %u to %u", old_mss, new_mss); | ||
|
|
||
| /* Update MSS value in place using byte-writes for alignment safety */ | ||
| opts[i + 2] = (uint8_t)(new_mss >> 8); | ||
| opts[i + 3] = (uint8_t)(new_mss & 0xFF); | ||
|
|
||
| /* Recalculate TCP Checksum incrementally */ | ||
| uint16_t old_cs = (tap_pkt[offset + 16] << 8) | tap_pkt[offset + 17]; | ||
| uint16_t new_cs = tcp_csum_update(old_cs, old_mss, new_mss); | ||
|
|
||
| tap_pkt[offset + 16] = (uint8_t)(new_cs >> 8); | ||
| tap_pkt[offset + 17] = (uint8_t)(new_cs & 0xFF); | ||
| } | ||
| break; | ||
| } | ||
|
|
||
| i += olen; | ||
| } | ||
| } | ||
|
|
||
| /* ***************************************************** */ | ||
|
|
||
| /** Send an ecapsulated ethernet PACKET to a destination edge or broadcast MAC | ||
| * address. */ | ||
| static int send_packet (struct n3n_runtime_data * eee, | ||
|
|
@@ -2250,6 +2437,11 @@ size_t edge_encode_packet (struct n3n_runtime_data *eee, | |
| uint8_t *pktbuf, size_t pktbuf_size, | ||
| n2n_mac_t out_destMac) { | ||
|
|
||
| /* Clamp TCP MSS if enabled */ | ||
| if(eee->conf.clamp_mss) { | ||
| clamp_mss(eee, tap_pkt, len); | ||
| } | ||
|
|
||
| ipstr_t ip_buf; | ||
| n2n_common_t cmn; | ||
| n2n_PACKET_t pkt; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,7 @@ output_format=pretty | |
| [tuntap] | ||
| address=0.0.0.0/0 | ||
| address_mode=auto | ||
| clamp_mss=false | ||
| metric=0 | ||
| mtu=0 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| PASS: incremental matches full recalculation | ||
| PASS: carry wrapping correct | ||
| PASS: MSS clamped 1500 -> 1460 | ||
| PASS: new checksum valid (0x02d8) | ||
| PASS: non-SYN packet unchanged | ||
| PASS: MSS=1000 not clamped (<= 1460) | ||
| PASS: clamp_mss=0 ignored by function (check at caller level) | ||
| PASS: IPv6 MSS clamped 1500 -> 1440 | ||
| PASS: VLAN-tagged MSS clamped 1500 -> 1456 | ||
| PASS: QinQ-tagged MSS clamped 1500 -> 1452 | ||
| PASS: IP-in-IP MSS clamped 1500 -> 1440 | ||
| PASS: fragmented packets skipped | ||
| PASS: IPv6 fragment extension header skips clamp | ||
| PASS: TAP-style MSS 1500->1460, checksum valid | ||
| PASS: UDP-edge-style MSS 1500->1460, checksum valid | ||
| PASS: mtu=0 skips clamping | ||
| PASS: custom MTU=1400, MSS clamped to 1360 | ||
| PASS: SYN-ACK also clamped 1500 -> 1460 | ||
| PASS: short packet unchanged (safe early return) | ||
| PASS: non-IP ethertype (ARP) skipped | ||
| PASS: IHL=6 IP options, MSS clamped to 1456 | ||
| PASS: TCP with no options handled without crash | ||
| PASS: 0x9100 VLAN MSS clamped 1500 -> 1456 | ||
| PASS: IPv6 Hop-by-Hop ext, MSS clamped to 1432 | ||
| PASS: IPv6 Auth Header (51) skips clamp | ||
| PASS: IPv6 Routing ext, MSS clamped to 1432 | ||
| PASS: IPv6 Destination Options ext, MSS clamped to 1432 | ||
| PASS: first MSS option processed, packet valid | ||
| PASS: MSS=1460 (== max_mss) not modified | ||
| PASS: csum_update identity (same value) returns original | ||
| PASS: malformed TCP options handled without crash | ||
| PASS: MSS=0 left unchanged (not > max_mss) | ||
| === All MSS tests complete === |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As these are internal functions that we do not wish to export to external users, they should not be declared in the public header files.
If the functions are simply placed in the edge_utils.c file before they are called, then there is no need to have any forward definition at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was because I had prepared unit tests that weren't included in the initial PR, but I have already added them in the subsequent commits.