Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/n2n_typedefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ typedef struct n2n_edge_conf {
devstr_t tuntap_dev_name;
struct n2n_ip_subnet tuntap_v4;
uint8_t tuntap_ip_mode; /**< Interface IP address allocated mode, eg. DHCP. */

bool clamp_mss; /**< Clamp TCP MSS to MTU. */
uint32_t test_benchmark_seconds;
int test_output_format;

Expand Down
4 changes: 4 additions & 0 deletions include/n3n/edge.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ void send_register_super (struct n3n_runtime_data *eee);
void send_query_peer (struct n3n_runtime_data *eee, const n2n_mac_t dst_mac);
void supernode_connect (struct n3n_runtime_data *eee);

/* TCP MSS clamping */
uint16_t tcp_csum_update (uint16_t old_csum, uint16_t old_val, uint16_t new_val);
void clamp_mss (struct n3n_runtime_data *eee, uint8_t *tap_pkt, size_t len);
Comment on lines +32 to +34

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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.


#endif
9 changes: 9 additions & 0 deletions src/conffile_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,15 @@ static struct n3n_conf_option section_tuntap[] = {
"address is set by the edge and an external process is "
"expected to set it.",
},
{
.name = "clamp_mss",
.type = n3n_conf_bool,
.offset = offsetof(n2n_edge_conf_t, clamp_mss),
.desc = "Clamp TCP MSS to MTU",
.help = "When enabled, the MSS value in TCP SYN packets is adjusted "
"to prevent fragmentation issues when the underlying MTU is "
"smaller than the standard 1500 bytes.",
},
{
.name = "macaddr",
.type = n3n_conf_strncpy,
Expand Down
192 changes: 192 additions & 0 deletions src/edge_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions tests/test_builtin_edge.sh.expected
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ output_format=pretty
[tuntap]
address=0.0.0.0/0
address_mode=auto
clamp_mss=false
metric=0
mtu=0

Expand Down
33 changes: 33 additions & 0 deletions tests/tests-mss.expected
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 ===
1 change: 1 addition & 0 deletions tools/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ TESTS+=tests-elliptic
TESTS+=tests-transform
TESTS+=tests-wire
TESTS+=tests-auth
TESTS+=tests-mss

.PHONY: all clean install
all: $(TOOLS) $(TESTS)
Expand Down
Loading
Loading