Skip to content

Commit 43b6b84

Browse files
committed
Add compatibility with docker bridge
1 parent 2ab6a0f commit 43b6b84

3 files changed

Lines changed: 278 additions & 1 deletion

File tree

redirect_linux.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ type autoRedirect struct {
4444
nfqueueEnabled bool
4545
redirectRouteTableIndex int
4646
redirectInterfaces []control.Interface
47+
dockerFirewallMonitor *nftables.Monitor
48+
dockerFirewallDone chan struct{}
4749
}
4850

4951
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {

redirect_nftables.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,15 @@ func (r *autoRedirect) setupNFTables() error {
283283
if err != nil {
284284
return E.Cause(err, "configure openwrt firewall4")
285285
}
286-
287286
err = nft.Flush()
288287
if err != nil {
289288
return E.Cause(err, "flush nftables")
290289
}
290+
r.startDockerFirewallMonitor()
291+
err = r.configureDockerFirewall(false)
292+
if err != nil && r.logger != nil {
293+
r.logger.Warn("configure docker firewall: ", err)
294+
}
291295

292296
r.networkListener = r.networkMonitor.RegisterCallback(func() {
293297
err = r.nftablesUpdateLocalAddressSet()
@@ -361,6 +365,7 @@ func (r *autoRedirect) cleanupNFTables() {
361365
if r.networkListener != nil {
362366
r.networkMonitor.UnregisterCallback(r.networkListener)
363367
}
368+
r.stopDockerFirewallMonitor()
364369
nft, err := nftables.New()
365370
if err != nil {
366371
return
@@ -372,6 +377,10 @@ func (r *autoRedirect) cleanupNFTables() {
372377
_ = r.configureOpenWRTFirewall4(nft, true)
373378
_ = nft.Flush()
374379
_ = nft.CloseLasting()
380+
err = r.configureDockerFirewall(true)
381+
if err != nil && r.logger != nil {
382+
r.logger.Warn("cleanup docker firewall: ", err)
383+
}
375384
}
376385

377386
func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *nftables.Table) error {

redirect_nftables_docker.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
//go:build linux
2+
3+
package tun
4+
5+
import (
6+
"bytes"
7+
"strings"
8+
9+
"github.com/sagernet/nftables"
10+
"github.com/sagernet/nftables/expr"
11+
"github.com/sagernet/nftables/userdata"
12+
E "github.com/sagernet/sing/common/exceptions"
13+
)
14+
15+
const (
16+
nftablesDockerFilterTable = "filter"
17+
nftablesDockerUserChain = "DOCKER-USER"
18+
)
19+
20+
func (r *autoRedirect) startDockerFirewallMonitor() {
21+
if r.dockerFirewallMonitor != nil {
22+
return
23+
}
24+
doneCh := make(chan struct{})
25+
r.dockerFirewallDone = doneCh
26+
monitor := nftables.NewMonitor(
27+
nftables.WithMonitorAction(nftables.MonitorActionAny),
28+
nftables.WithMonitorObject(nftables.MonitorObjectRuleset),
29+
nftables.WithMonitorEventBuffer(16),
30+
)
31+
nft, err := nftables.New()
32+
if err != nil {
33+
if r.logger != nil {
34+
r.logger.Warn("create nftables monitor connection: ", err)
35+
}
36+
close(doneCh)
37+
r.dockerFirewallDone = nil
38+
return
39+
}
40+
events, err := nft.AddGenerationalMonitor(monitor)
41+
_ = nft.CloseLasting()
42+
if err != nil {
43+
if r.logger != nil {
44+
r.logger.Warn("start nftables monitor: ", err)
45+
}
46+
close(doneCh)
47+
r.dockerFirewallDone = nil
48+
return
49+
}
50+
r.dockerFirewallMonitor = monitor
51+
go r.loopDockerFirewallMonitor(events, doneCh)
52+
}
53+
54+
func (r *autoRedirect) stopDockerFirewallMonitor() {
55+
if r.dockerFirewallMonitor == nil {
56+
return
57+
}
58+
_ = r.dockerFirewallMonitor.Close()
59+
<-r.dockerFirewallDone
60+
r.dockerFirewallMonitor = nil
61+
r.dockerFirewallDone = nil
62+
}
63+
64+
func (r *autoRedirect) loopDockerFirewallMonitor(events <-chan *nftables.MonitorEvents, doneCh chan<- struct{}) {
65+
defer close(doneCh)
66+
for monitorEvents := range events {
67+
if monitorEvents != nil && monitorEvents.GeneratedBy != nil && monitorEvents.GeneratedBy.Error != nil {
68+
if r.logger != nil {
69+
r.logger.Warn("nftables monitor closed: ", monitorEvents.GeneratedBy.Error)
70+
}
71+
return
72+
}
73+
if !nftablesDockerFirewallEventsRelevant(monitorEvents) {
74+
continue
75+
}
76+
err := r.configureDockerFirewall(false)
77+
if err != nil && r.logger != nil {
78+
r.logger.Warn("update docker firewall: ", err)
79+
}
80+
}
81+
}
82+
83+
func (r *autoRedirect) configureDockerFirewall(cleanup bool) error {
84+
nft, err := nftables.New()
85+
if err != nil {
86+
return E.Cause(err, "create nftables connection")
87+
}
88+
defer nft.CloseLasting()
89+
90+
err = r.configureDockerFirewallWithConn(nft, cleanup)
91+
if err != nil {
92+
return err
93+
}
94+
return nft.Flush()
95+
}
96+
97+
func (r *autoRedirect) configureDockerFirewallWithConn(nft *nftables.Conn, cleanup bool) error {
98+
var err error
99+
if r.enableIPv4 {
100+
err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv4, cleanup))
101+
}
102+
if r.enableIPv6 {
103+
err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv6, cleanup))
104+
}
105+
return err
106+
}
107+
108+
func (r *autoRedirect) configureDockerFirewallForFamily(nft *nftables.Conn, family nftables.TableFamily, cleanup bool) error {
109+
table, chain, loaded, err := nftablesLoadDockerUserChain(nft, family)
110+
if err != nil || !loaded {
111+
return err
112+
}
113+
err = r.configureDockerFirewallRules(nft, table, chain, cleanup)
114+
return err
115+
}
116+
117+
func (r *autoRedirect) configureDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, cleanup bool) error {
118+
rules, err := nft.GetRules(table, chain)
119+
if err != nil {
120+
return E.Cause(err, "list docker user rules")
121+
}
122+
if cleanup {
123+
return r.cleanupDockerFirewallRules(nft, rules)
124+
}
125+
return r.reconcileDockerFirewallRules(nft, table, chain, rules)
126+
}
127+
128+
func nftablesLoadDockerUserChain(nft *nftables.Conn, family nftables.TableFamily) (*nftables.Table, *nftables.Chain, bool, error) {
129+
table, err := nft.ListTableOfFamily(nftablesDockerFilterTable, family)
130+
if err != nil {
131+
return nil, nil, false, nil
132+
}
133+
chain, err := nft.ListChain(table, nftablesDockerUserChain)
134+
if err != nil {
135+
return nil, nil, false, nil
136+
}
137+
return table, chain, true, nil
138+
}
139+
140+
func nftablesDockerFirewallEventsRelevant(events *nftables.MonitorEvents) bool {
141+
if events == nil {
142+
return false
143+
}
144+
for _, event := range events.Changes {
145+
if nftablesDockerFirewallEventRelevant(event) {
146+
return true
147+
}
148+
}
149+
return false
150+
}
151+
152+
func nftablesDockerFirewallEventRelevant(event *nftables.MonitorEvent) bool {
153+
if event == nil || event.Error != nil {
154+
return false
155+
}
156+
switch data := event.Data.(type) {
157+
case *nftables.Table:
158+
return nftablesIsDockerFirewallTable(data)
159+
case *nftables.Chain:
160+
return data.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table)
161+
case *nftables.Rule:
162+
return data.Chain != nil && data.Chain.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table)
163+
default:
164+
return false
165+
}
166+
}
167+
168+
func nftablesIsDockerFirewallTable(table *nftables.Table) bool {
169+
return table != nil &&
170+
table.Name == nftablesDockerFilterTable &&
171+
(table.Family == nftables.TableFamilyIPv4 || table.Family == nftables.TableFamilyIPv6)
172+
}
173+
174+
func (r *autoRedirect) cleanupDockerFirewallRules(nft *nftables.Conn, rules []*nftables.Rule) error {
175+
var deleteErr error
176+
for _, rule := range rules {
177+
if r.nftablesIsDockerCompatibilityRule(rule) {
178+
deleteErr = E.Errors(deleteErr, nft.DelRule(rule))
179+
}
180+
}
181+
return deleteErr
182+
}
183+
184+
func (r *autoRedirect) reconcileDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, rules []*nftables.Rule) error {
185+
outputComment := r.nftablesDockerCompatibilityComment("output to tun")
186+
inputComment := r.nftablesDockerCompatibilityComment("input from tun")
187+
var hasOutputRule bool
188+
var hasInputRule bool
189+
var deleteErr error
190+
for _, rule := range rules {
191+
if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment) && !hasOutputRule {
192+
hasOutputRule = true
193+
} else if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment) && !hasInputRule {
194+
hasInputRule = true
195+
} else if r.nftablesIsDockerCompatibilityRule(rule) {
196+
deleteErr = E.Errors(deleteErr, nft.DelRule(rule))
197+
}
198+
}
199+
if deleteErr != nil {
200+
return deleteErr
201+
}
202+
if !hasOutputRule {
203+
nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment))
204+
}
205+
if !hasInputRule {
206+
nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment))
207+
}
208+
return nil
209+
}
210+
211+
func nftablesDockerCompatibilityRule(table *nftables.Table, chain *nftables.Chain, ifName string, ifNameKey expr.MetaKey, comment string) *nftables.Rule {
212+
return &nftables.Rule{
213+
Table: table,
214+
Chain: chain,
215+
Exprs: []expr.Any{
216+
&expr.Meta{
217+
Key: ifNameKey,
218+
Register: 1,
219+
},
220+
&expr.Cmp{
221+
Op: expr.CmpOpEq,
222+
Register: 1,
223+
Data: nftablesIfname(ifName),
224+
},
225+
&expr.Counter{},
226+
&expr.Verdict{
227+
Kind: expr.VerdictAccept,
228+
},
229+
},
230+
UserData: userdata.AppendString(nil, userdata.TypeComment, comment),
231+
}
232+
}
233+
234+
func nftablesDockerCompatibilityRuleMatches(rule *nftables.Rule, ifName string, ifNameKey expr.MetaKey, comment string) bool {
235+
ruleComment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment)
236+
if !loaded || ruleComment != comment || len(rule.Exprs) != 4 {
237+
return false
238+
}
239+
meta, loaded := rule.Exprs[0].(*expr.Meta)
240+
if !loaded || meta.Key != ifNameKey || meta.Register != 1 {
241+
return false
242+
}
243+
cmp, loaded := rule.Exprs[1].(*expr.Cmp)
244+
if !loaded || cmp.Op != expr.CmpOpEq || cmp.Register != 1 || !bytes.Equal(cmp.Data, nftablesIfname(ifName)) {
245+
return false
246+
}
247+
_, loaded = rule.Exprs[2].(*expr.Counter)
248+
if !loaded {
249+
return false
250+
}
251+
verdict, loaded := rule.Exprs[3].(*expr.Verdict)
252+
return loaded && verdict.Kind == expr.VerdictAccept
253+
}
254+
255+
func (r *autoRedirect) nftablesIsDockerCompatibilityRule(rule *nftables.Rule) bool {
256+
comment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment)
257+
return loaded && strings.HasPrefix(comment, r.nftablesDockerCompatibilityCommentPrefix())
258+
}
259+
260+
func (r *autoRedirect) nftablesDockerCompatibilityComment(direction string) string {
261+
return r.nftablesDockerCompatibilityCommentPrefix() + direction
262+
}
263+
264+
func (r *autoRedirect) nftablesDockerCompatibilityCommentPrefix() string {
265+
return "!" + r.tableName + ": Docker compatibility "
266+
}

0 commit comments

Comments
 (0)