Skip to content

Commit 18ab8bd

Browse files
authored
Merge pull request #232 from NLipatov/fix/mss-clamping
Fix/mss clamping
2 parents c4a630f + f556c4e commit 18ab8bd

File tree

11 files changed

+584
-226
lines changed

11 files changed

+584
-226
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/.idea/inspectionProfiles/Project_Default.xml
66
.vscode/
77
src/tungo
8+
.tool-versions

src/infrastructure/PAL/linux/network_tools/iptables/contract.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,4 @@ type Contract interface {
99
DisableForwardingFromDevToTun(tunName, devName string) error
1010
EnableForwardingTunToTun(tunName string) error
1111
DisableForwardingTunToTun(tunName string) error
12-
ConfigureMssClamping() error
1312
}

src/infrastructure/PAL/linux/network_tools/iptables/wrapper.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -97,41 +97,3 @@ func (w *Wrapper) DisableForwardingTunToTun(tunName string) error {
9797

9898
return nil
9999
}
100-
101-
func (w *Wrapper) ConfigureMssClamping() error {
102-
// Configuration for IPv4, chain FORWARD
103-
outputForward, errForward := w.commander.CombinedOutput("iptables", "-t", "mangle",
104-
"-A", "FORWARD", "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
105-
if errForward != nil {
106-
return fmt.Errorf("failed to configure MSS clamping on FORWARD chain: %s, output: %s",
107-
errForward, outputForward)
108-
}
109-
110-
// Configuration for IPv4, chain OUTPUT
111-
outputOutput, errOutput := w.commander.
112-
CombinedOutput("iptables", "-t", "mangle",
113-
"-A", "OUTPUT", "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
114-
if errOutput != nil {
115-
return fmt.Errorf("failed to configure MSS clamping on OUTPUT chain: %s, output: %s",
116-
errOutput, outputOutput)
117-
}
118-
119-
// Configuration for IPv6, chain FORWARD
120-
outputForward6, errForward6 := w.commander.
121-
CombinedOutput("ip6tables", "-t", "mangle",
122-
"-A", "FORWARD", "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
123-
if errForward6 != nil {
124-
return fmt.Errorf("failed to configure IPv6 MSS clamping on FORWARD chain: %s, output: %s",
125-
errForward6, outputForward6)
126-
}
127-
128-
// Configuration for IPv6, chain OUTPUT
129-
outputOutput6, errOutput6 := w.commander.CombinedOutput("ip6tables", "-t", "mangle", "-A",
130-
"OUTPUT", "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
131-
if errOutput6 != nil {
132-
return fmt.Errorf("failed to configure IPv6 MSS clamping on OUTPUT chain: %s, output: %s",
133-
errOutput6, outputOutput6)
134-
}
135-
136-
return nil
137-
}
Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package iptables
22

33
import (
4-
"errors"
54
"strings"
65
"testing"
76
)
@@ -53,17 +52,6 @@ func TestWrapper_AllCommands(t *testing.T) {
5352
{"DisableForwardingFromDevToTun", func() error { return w.DisableForwardingFromDevToTun(tun, dev) }},
5453
{"EnableForwardingTunToTun", func() error { return w.EnableForwardingTunToTun(tun) }},
5554
{"DisableForwardingTunToTun", func() error { return w.DisableForwardingTunToTun(tun) }},
56-
{"ConfigureMssClamping", func() error {
57-
return NewWrapper(&mockCommander{
58-
outputMap: map[string][]byte{
59-
"iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": {},
60-
"iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": {},
61-
"ip6tables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": {},
62-
"ip6tables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": {},
63-
},
64-
errMap: map[string]error{},
65-
}).ConfigureMssClamping()
66-
}},
6755
}
6856

6957
for _, tt := range tests {
@@ -74,55 +62,3 @@ func TestWrapper_AllCommands(t *testing.T) {
7462
})
7563
}
7664
}
77-
78-
func TestWrapper_ConfigureMssClamping_ErrorCases(t *testing.T) {
79-
errorCases := []struct {
80-
name string
81-
errMap map[string]error
82-
errMsg string
83-
}{
84-
{
85-
name: "FORWARD IPv4 error",
86-
errMap: map[string]error{
87-
"iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": errors.New("fail1"),
88-
},
89-
errMsg: "FORWARD chain",
90-
},
91-
{
92-
name: "OUTPUT IPv4 error",
93-
errMap: map[string]error{
94-
"iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": errors.New("fail2"),
95-
},
96-
errMsg: "OUTPUT chain",
97-
},
98-
{
99-
name: "FORWARD IPv6 error",
100-
errMap: map[string]error{
101-
"ip6tables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": errors.New("fail3"),
102-
},
103-
errMsg: "IPv6 MSS clamping on FORWARD",
104-
},
105-
{
106-
name: "OUTPUT IPv6 error",
107-
errMap: map[string]error{
108-
"ip6tables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu": errors.New("fail4"),
109-
},
110-
errMsg: "IPv6 MSS clamping on OUTPUT",
111-
},
112-
}
113-
114-
for _, tc := range errorCases {
115-
t.Run(tc.name, func(t *testing.T) {
116-
out := map[string][]byte{}
117-
for cmd := range tc.errMap {
118-
out[cmd] = []byte{}
119-
}
120-
w := newWrapperWithMocks(out, tc.errMap)
121-
122-
err := w.ConfigureMssClamping()
123-
if err == nil || !strings.Contains(err.Error(), tc.errMsg) {
124-
t.Errorf("expected error containing '%s', got: %v", tc.errMsg, err)
125-
}
126-
})
127-
}
128-
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package mssclamp
2+
3+
// Contract defines MSS clamping management for TCP SYN packets
4+
// routed through the TunGo TUN interface.
5+
type Contract interface {
6+
Install(tunName string) error
7+
Remove(tunName string) error
8+
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package mssclamp
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"tungo/infrastructure/PAL/exec_commander"
7+
)
8+
9+
type backend int
10+
11+
const (
12+
backendUnknown backend = iota
13+
backendIptables
14+
backendNft
15+
16+
nftTable = "tungo_mss"
17+
nftOutputChain = "tungo_mss_output"
18+
nftForwardChain = "tungo_mss_forward"
19+
)
20+
21+
// Manager installs and removes TCP MSS clamping rules bound to a TUN device.
22+
// TCPMSS mangle rules keep ClientHello-sized packets from blackholing inside
23+
// the UDP tunnel by advertising an MSS that fits the effective tunnel PMTU.
24+
type Manager struct {
25+
commander exec_commander.Commander
26+
backend backend
27+
}
28+
29+
func NewManager(commander exec_commander.Commander) *Manager {
30+
return &Manager{commander: commander}
31+
}
32+
33+
// Install applies MSS clamping for IPv4 and IPv6 TCP SYN packets entering or
34+
// leaving the given TUN interface. The rules are added before packets are
35+
// encapsulated into the UDP tunnel to avoid PMTU blackholes for TLS traffic.
36+
func (m *Manager) Install(tunName string) error {
37+
backend, err := m.detectBackend()
38+
if err != nil {
39+
return err
40+
}
41+
42+
switch backend {
43+
case backendIptables:
44+
return m.installIptables(tunName)
45+
case backendNft:
46+
return m.installNft(tunName)
47+
default:
48+
return fmt.Errorf("unsupported MSS clamping backend")
49+
}
50+
}
51+
52+
// Remove tears down MSS clamping rules bound to the given TUN interface.
53+
func (m *Manager) Remove(tunName string) error {
54+
backend, err := m.detectBackend()
55+
if err != nil {
56+
return err
57+
}
58+
59+
switch backend {
60+
case backendIptables:
61+
return m.removeIptables(tunName)
62+
case backendNft:
63+
return m.removeNft()
64+
default:
65+
return fmt.Errorf("unsupported MSS clamping backend")
66+
}
67+
}
68+
69+
func (m *Manager) detectBackend() (backend, error) {
70+
if m.backend != backendUnknown {
71+
return m.backend, nil
72+
}
73+
74+
if _, err := m.commander.Output("iptables", "--version"); err == nil {
75+
m.backend = backendIptables
76+
return m.backend, nil
77+
}
78+
79+
if _, err := m.commander.Output("nft", "--version"); err == nil {
80+
m.backend = backendNft
81+
return m.backend, nil
82+
}
83+
84+
return backendUnknown, fmt.Errorf("neither iptables nor nftables is available for TCP MSS clamping")
85+
}
86+
87+
type describedCommand struct {
88+
name string
89+
args []string
90+
desc string
91+
}
92+
93+
func (m *Manager) run(commands []describedCommand) error {
94+
for _, cmd := range commands {
95+
output, err := m.commander.CombinedOutput(cmd.name, cmd.args...)
96+
if err != nil {
97+
return fmt.Errorf("failed to %s: %v, output: %s", cmd.desc, err, output)
98+
}
99+
}
100+
return nil
101+
}
102+
103+
func (m *Manager) installIptables(tunName string) error {
104+
rules := []describedCommand{
105+
{
106+
name: "iptables",
107+
args: []string{"-t", "mangle", "-A", "OUTPUT", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
108+
desc: "add IPv4 OUTPUT TCPMSS clamp for " + tunName,
109+
},
110+
{
111+
name: "iptables",
112+
args: []string{"-t", "mangle", "-A", "FORWARD", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
113+
desc: "add IPv4 FORWARD oif TCPMSS clamp for " + tunName,
114+
},
115+
{
116+
name: "iptables",
117+
args: []string{"-t", "mangle", "-A", "FORWARD", "-i", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
118+
desc: "add IPv4 FORWARD iif TCPMSS clamp for " + tunName,
119+
},
120+
{
121+
name: "ip6tables",
122+
args: []string{"-t", "mangle", "-A", "OUTPUT", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
123+
desc: "add IPv6 OUTPUT TCPMSS clamp for " + tunName,
124+
},
125+
{
126+
name: "ip6tables",
127+
args: []string{"-t", "mangle", "-A", "FORWARD", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
128+
desc: "add IPv6 FORWARD oif TCPMSS clamp for " + tunName,
129+
},
130+
{
131+
name: "ip6tables",
132+
args: []string{"-t", "mangle", "-A", "FORWARD", "-i", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
133+
desc: "add IPv6 FORWARD iif TCPMSS clamp for " + tunName,
134+
},
135+
}
136+
137+
return m.run(rules)
138+
}
139+
140+
func (m *Manager) removeIptables(tunName string) error {
141+
rules := []describedCommand{
142+
{
143+
name: "iptables",
144+
args: []string{"-t", "mangle", "-D", "OUTPUT", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
145+
desc: "delete IPv4 OUTPUT TCPMSS clamp for " + tunName,
146+
},
147+
{
148+
name: "iptables",
149+
args: []string{"-t", "mangle", "-D", "FORWARD", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
150+
desc: "delete IPv4 FORWARD oif TCPMSS clamp for " + tunName,
151+
},
152+
{
153+
name: "iptables",
154+
args: []string{"-t", "mangle", "-D", "FORWARD", "-i", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
155+
desc: "delete IPv4 FORWARD iif TCPMSS clamp for " + tunName,
156+
},
157+
{
158+
name: "ip6tables",
159+
args: []string{"-t", "mangle", "-D", "OUTPUT", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
160+
desc: "delete IPv6 OUTPUT TCPMSS clamp for " + tunName,
161+
},
162+
{
163+
name: "ip6tables",
164+
args: []string{"-t", "mangle", "-D", "FORWARD", "-o", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
165+
desc: "delete IPv6 FORWARD oif TCPMSS clamp for " + tunName,
166+
},
167+
{
168+
name: "ip6tables",
169+
args: []string{"-t", "mangle", "-D", "FORWARD", "-i", tunName, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu"},
170+
desc: "delete IPv6 FORWARD iif TCPMSS clamp for " + tunName,
171+
},
172+
}
173+
174+
return m.run(rules)
175+
}
176+
177+
func (m *Manager) installNft(tunName string) error {
178+
// Clean up any stale table from previous runs so we can install a fresh set.
179+
_, _ = m.commander.CombinedOutput("nft", "delete", "table", "inet", nftTable)
180+
181+
commands := []describedCommand{
182+
{
183+
name: "nft",
184+
args: []string{"add", "table", "inet", nftTable},
185+
desc: "create nftable table for MSS clamping",
186+
},
187+
{
188+
name: "nft",
189+
args: []string{"add", "chain", "inet", nftTable, nftOutputChain, "{", "type", "route", "hook", "output", "priority", "mangle", ";", "policy", "accept", ";", "}"},
190+
desc: "create nftable output chain",
191+
},
192+
{
193+
name: "nft",
194+
args: []string{"add", "chain", "inet", nftTable, nftForwardChain, "{", "type", "filter", "hook", "forward", "priority", "mangle", ";", "policy", "accept", ";", "}"},
195+
desc: "create nftable forward chain",
196+
},
197+
{
198+
name: "nft",
199+
args: append([]string{"add", "rule", "inet", nftTable, nftOutputChain, "oifname", tunName}, nftClampRule()...),
200+
desc: "add nft OUTPUT TCPMSS clamp for " + tunName,
201+
},
202+
{
203+
name: "nft",
204+
args: append([]string{"add", "rule", "inet", nftTable, nftForwardChain, "oifname", tunName}, nftClampRule()...),
205+
desc: "add nft FORWARD oif TCPMSS clamp for " + tunName,
206+
},
207+
{
208+
name: "nft",
209+
args: append([]string{"add", "rule", "inet", nftTable, nftForwardChain, "iifname", tunName}, nftClampRule()...),
210+
desc: "add nft FORWARD iif TCPMSS clamp for " + tunName,
211+
},
212+
}
213+
214+
return m.run(commands)
215+
}
216+
217+
func (m *Manager) removeNft() error {
218+
output, err := m.commander.CombinedOutput("nft", "delete", "table", "inet", nftTable)
219+
if err != nil {
220+
// Treat missing tables as benign; they mean nothing is left to clean up.
221+
msg := strings.ToLower(err.Error() + string(output))
222+
if strings.Contains(msg, "no such file or directory") ||
223+
strings.Contains(msg, "does not exist") ||
224+
strings.Contains(msg, "no such table") {
225+
return nil
226+
}
227+
return fmt.Errorf("failed to delete nft MSS clamp table: %v, output: %s", err, output)
228+
}
229+
return nil
230+
}
231+
232+
func nftClampRule() []string {
233+
return []string{"tcp", "flags", "syn|rst", "==", "syn", "tcp", "option", "maxseg", "size", "set", "clamp", "to", "pmtu"}
234+
}

0 commit comments

Comments
 (0)