Add compatibility with docker bridge
This commit is contained in:
parent
a9027729be
commit
a36276922a
3 changed files with 275 additions and 2 deletions
|
|
@ -46,6 +46,8 @@ type autoRedirect struct {
|
||||||
redirectRouteTableIndex int
|
redirectRouteTableIndex int
|
||||||
redirectRouteAccess sync.Mutex
|
redirectRouteAccess sync.Mutex
|
||||||
redirectRoutesActive bool
|
redirectRoutesActive bool
|
||||||
|
dockerFirewallMonitor *nftables.Monitor
|
||||||
|
dockerFirewallDone chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
|
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/sagernet/nftables"
|
"github.com/sagernet/nftables"
|
||||||
"github.com/sagernet/nftables/binaryutil"
|
"github.com/sagernet/nftables/binaryutil"
|
||||||
"github.com/sagernet/nftables/expr"
|
"github.com/sagernet/nftables/expr"
|
||||||
"github.com/sagernet/sing-tun/internal/gtcpip/header"
|
"github.com/sagernet/sing-tun/gtcpip/header"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
@ -295,11 +295,15 @@ func (r *autoRedirect) setupNFTables() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "configure openwrt firewall4")
|
return E.Cause(err, "configure openwrt firewall4")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = nft.Flush()
|
err = nft.Flush()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "flush nftables")
|
return E.Cause(err, "flush nftables")
|
||||||
}
|
}
|
||||||
|
r.startDockerFirewallMonitor()
|
||||||
|
err = r.configureDockerFirewall(false)
|
||||||
|
if err != nil && r.logger != nil {
|
||||||
|
r.logger.Warn("configure docker firewall: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
r.networkListener = r.networkMonitor.RegisterCallback(func() {
|
r.networkListener = r.networkMonitor.RegisterCallback(func() {
|
||||||
err = r.nftablesUpdateLocalAddressSet()
|
err = r.nftablesUpdateLocalAddressSet()
|
||||||
|
|
@ -373,6 +377,7 @@ func (r *autoRedirect) cleanupNFTables() {
|
||||||
if r.networkListener != nil {
|
if r.networkListener != nil {
|
||||||
r.networkMonitor.UnregisterCallback(r.networkListener)
|
r.networkMonitor.UnregisterCallback(r.networkListener)
|
||||||
}
|
}
|
||||||
|
r.stopDockerFirewallMonitor()
|
||||||
nft, err := nftables.New()
|
nft, err := nftables.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -384,6 +389,10 @@ func (r *autoRedirect) cleanupNFTables() {
|
||||||
_ = r.configureOpenWRTFirewall4(nft, true)
|
_ = r.configureOpenWRTFirewall4(nft, true)
|
||||||
_ = nft.Flush()
|
_ = nft.Flush()
|
||||||
_ = nft.CloseLasting()
|
_ = nft.CloseLasting()
|
||||||
|
err = r.configureDockerFirewall(true)
|
||||||
|
if err != nil && r.logger != nil {
|
||||||
|
r.logger.Warn("cleanup docker firewall: ", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *nftables.Table) error {
|
func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *nftables.Table) error {
|
||||||
|
|
|
||||||
262
redirect_nftables_docker.go
Normal file
262
redirect_nftables_docker.go
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package tun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/nftables"
|
||||||
|
"github.com/sagernet/nftables/expr"
|
||||||
|
"github.com/sagernet/nftables/userdata"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nftablesDockerFilterTable = "filter"
|
||||||
|
nftablesDockerUserChain = "DOCKER-USER"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *autoRedirect) startDockerFirewallMonitor() {
|
||||||
|
if r.dockerFirewallMonitor != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
r.dockerFirewallDone = doneCh
|
||||||
|
monitor := nftables.NewMonitor(
|
||||||
|
nftables.WithMonitorAction(nftables.MonitorActionAny),
|
||||||
|
nftables.WithMonitorObject(nftables.MonitorObjectRuleset),
|
||||||
|
nftables.WithMonitorEventBuffer(16),
|
||||||
|
)
|
||||||
|
nft, err := nftables.New()
|
||||||
|
if err != nil {
|
||||||
|
if r.logger != nil {
|
||||||
|
r.logger.Warn("create nftables monitor connection: ", err)
|
||||||
|
}
|
||||||
|
close(doneCh)
|
||||||
|
r.dockerFirewallDone = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
events, err := nft.AddGenerationalMonitor(monitor)
|
||||||
|
_ = nft.CloseLasting()
|
||||||
|
if err != nil {
|
||||||
|
if r.logger != nil {
|
||||||
|
r.logger.Warn("start nftables monitor: ", err)
|
||||||
|
}
|
||||||
|
close(doneCh)
|
||||||
|
r.dockerFirewallDone = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.dockerFirewallMonitor = monitor
|
||||||
|
go r.loopDockerFirewallMonitor(events, doneCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) stopDockerFirewallMonitor() {
|
||||||
|
if r.dockerFirewallMonitor == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = r.dockerFirewallMonitor.Close()
|
||||||
|
<-r.dockerFirewallDone
|
||||||
|
r.dockerFirewallMonitor = nil
|
||||||
|
r.dockerFirewallDone = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) loopDockerFirewallMonitor(events <-chan *nftables.MonitorEvents, doneCh chan<- struct{}) {
|
||||||
|
defer close(doneCh)
|
||||||
|
for monitorEvents := range events {
|
||||||
|
if monitorEvents != nil && monitorEvents.GeneratedBy != nil && monitorEvents.GeneratedBy.Error != nil {
|
||||||
|
if r.logger != nil {
|
||||||
|
r.logger.Warn("nftables monitor closed: ", monitorEvents.GeneratedBy.Error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !nftablesDockerFirewallEventsRelevant(monitorEvents) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := r.configureDockerFirewall(false)
|
||||||
|
if err != nil && r.logger != nil {
|
||||||
|
r.logger.Warn("update docker firewall: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) configureDockerFirewall(cleanup bool) error {
|
||||||
|
nft, err := nftables.New()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "create nftables connection")
|
||||||
|
}
|
||||||
|
defer nft.CloseLasting()
|
||||||
|
|
||||||
|
err = r.configureDockerFirewallWithConn(nft, cleanup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nft.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) configureDockerFirewallWithConn(nft *nftables.Conn, cleanup bool) error {
|
||||||
|
var err error
|
||||||
|
if r.enableIPv4 {
|
||||||
|
err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv4, cleanup))
|
||||||
|
}
|
||||||
|
if r.enableIPv6 {
|
||||||
|
err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv6, cleanup))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) configureDockerFirewallForFamily(nft *nftables.Conn, family nftables.TableFamily, cleanup bool) error {
|
||||||
|
table, chain, loaded, err := nftablesLoadDockerUserChain(nft, family)
|
||||||
|
if err != nil || !loaded {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.configureDockerFirewallRules(nft, table, chain, cleanup)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) configureDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, cleanup bool) error {
|
||||||
|
rules, err := nft.GetRules(table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "list docker user rules")
|
||||||
|
}
|
||||||
|
if cleanup {
|
||||||
|
return r.cleanupDockerFirewallRules(nft, rules)
|
||||||
|
}
|
||||||
|
return r.reconcileDockerFirewallRules(nft, table, chain, rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nftablesLoadDockerUserChain(nft *nftables.Conn, family nftables.TableFamily) (*nftables.Table, *nftables.Chain, bool, error) {
|
||||||
|
table, err := nft.ListTableOfFamily(nftablesDockerFilterTable, family)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, nil
|
||||||
|
}
|
||||||
|
chain, err := nft.ListChain(table, nftablesDockerUserChain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, nil
|
||||||
|
}
|
||||||
|
return table, chain, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nftablesDockerFirewallEventsRelevant(events *nftables.MonitorEvents) bool {
|
||||||
|
if events == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return slices.ContainsFunc(events.Changes, nftablesDockerFirewallEventRelevant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nftablesDockerFirewallEventRelevant(event *nftables.MonitorEvent) bool {
|
||||||
|
if event == nil || event.Error != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch data := event.Data.(type) {
|
||||||
|
case *nftables.Table:
|
||||||
|
return nftablesIsDockerFirewallTable(data)
|
||||||
|
case *nftables.Chain:
|
||||||
|
return data.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table)
|
||||||
|
case *nftables.Rule:
|
||||||
|
return data.Chain != nil && data.Chain.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nftablesIsDockerFirewallTable(table *nftables.Table) bool {
|
||||||
|
return table != nil &&
|
||||||
|
table.Name == nftablesDockerFilterTable &&
|
||||||
|
(table.Family == nftables.TableFamilyIPv4 || table.Family == nftables.TableFamilyIPv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) cleanupDockerFirewallRules(nft *nftables.Conn, rules []*nftables.Rule) error {
|
||||||
|
var deleteErr error
|
||||||
|
for _, rule := range rules {
|
||||||
|
if r.nftablesIsDockerCompatibilityRule(rule) {
|
||||||
|
deleteErr = E.Errors(deleteErr, nft.DelRule(rule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleteErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) reconcileDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, rules []*nftables.Rule) error {
|
||||||
|
outputComment := r.nftablesDockerCompatibilityComment("output to tun")
|
||||||
|
inputComment := r.nftablesDockerCompatibilityComment("input from tun")
|
||||||
|
var hasOutputRule bool
|
||||||
|
var hasInputRule bool
|
||||||
|
var deleteErr error
|
||||||
|
for _, rule := range rules {
|
||||||
|
if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment) && !hasOutputRule {
|
||||||
|
hasOutputRule = true
|
||||||
|
} else if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment) && !hasInputRule {
|
||||||
|
hasInputRule = true
|
||||||
|
} else if r.nftablesIsDockerCompatibilityRule(rule) {
|
||||||
|
deleteErr = E.Errors(deleteErr, nft.DelRule(rule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deleteErr != nil {
|
||||||
|
return deleteErr
|
||||||
|
}
|
||||||
|
if !hasOutputRule {
|
||||||
|
nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment))
|
||||||
|
}
|
||||||
|
if !hasInputRule {
|
||||||
|
nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nftablesDockerCompatibilityRule(table *nftables.Table, chain *nftables.Chain, ifName string, ifNameKey expr.MetaKey, comment string) *nftables.Rule {
|
||||||
|
return &nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: chain,
|
||||||
|
Exprs: []expr.Any{
|
||||||
|
&expr.Meta{
|
||||||
|
Key: ifNameKey,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: nftablesIfname(ifName),
|
||||||
|
},
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserData: userdata.AppendString(nil, userdata.TypeComment, comment),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nftablesDockerCompatibilityRuleMatches(rule *nftables.Rule, ifName string, ifNameKey expr.MetaKey, comment string) bool {
|
||||||
|
ruleComment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment)
|
||||||
|
if !loaded || ruleComment != comment || len(rule.Exprs) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
meta, loaded := rule.Exprs[0].(*expr.Meta)
|
||||||
|
if !loaded || meta.Key != ifNameKey || meta.Register != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cmp, loaded := rule.Exprs[1].(*expr.Cmp)
|
||||||
|
if !loaded || cmp.Op != expr.CmpOpEq || cmp.Register != 1 || !bytes.Equal(cmp.Data, nftablesIfname(ifName)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, loaded = rule.Exprs[2].(*expr.Counter)
|
||||||
|
if !loaded {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
verdict, loaded := rule.Exprs[3].(*expr.Verdict)
|
||||||
|
return loaded && verdict.Kind == expr.VerdictAccept
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) nftablesIsDockerCompatibilityRule(rule *nftables.Rule) bool {
|
||||||
|
comment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment)
|
||||||
|
return loaded && strings.HasPrefix(comment, r.nftablesDockerCompatibilityCommentPrefix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) nftablesDockerCompatibilityComment(direction string) string {
|
||||||
|
return r.nftablesDockerCompatibilityCommentPrefix() + direction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) nftablesDockerCompatibilityCommentPrefix() string {
|
||||||
|
return "!" + r.tableName + ": Docker compatibility "
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue