Files
sing-tun/redirect_nftables_rules_firewalld.go
2025-07-07 17:48:43 +08:00

254 lines
5.7 KiB
Go

//go:build linux
package tun
import (
"time"
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/expr"
E "github.com/sagernet/sing/common/exceptions"
"github.com/godbus/dbus/v5"
"golang.org/x/exp/slices"
)
const (
NFT_TABLE_F_DORMANT = 0x1
NFT_TABLE_F_OWNER = 0x2
NFT_TABLE_F_PERSIST = 0x4
)
const (
firewalldInterface = "org.fedoraproject.FirewallD1"
firewalldPath = "/org/fedoraproject/FirewallD1"
)
func (r *autoRedirect) configureFirewalld(nft *nftables.Conn, cleanup bool) error {
if cleanup {
if r.firewalldListener == nil {
return nil
}
r.firewalldListener.Close()
return r.configureFirewalldRules(nft, false)
}
conn, err := dbus.SystemBus()
if err != nil {
return nil
}
err = conn.Object(firewalldInterface, firewalldPath).Call("org.freedesktop.DBus.Peer.Ping", 0).Err
if err != nil {
return nil
}
err = r.configureFirewalldRules(nft, false)
if err != nil {
r.logger.Warn(E.Cause(err, "configure firewalld rules"))
return nil
}
err = conn.BusObject().AddMatchSignal(firewalldInterface, "Reloaded").Err
if err != nil {
return E.Cause(err, "configure firewalld reload listener")
}
err = conn.BusObject().AddMatchSignal(
"org.freedesktop.DBus",
"NameOwnerChanged",
dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchArg(0, firewalldInterface),
).Err
if err != nil {
return E.Cause(err, "configure firewalld restart listener")
}
signal := make(chan *dbus.Signal, 1)
conn.Signal(signal)
listener := &firewalldListener{
autoRedirect: r,
conn: conn,
signal: signal,
done: make(chan struct{}),
}
go listener.loopReload()
r.firewalldListener = listener
return nil
}
type firewalldListener struct {
*autoRedirect
conn *dbus.Conn
signal chan *dbus.Signal
done chan struct{}
}
func (l *firewalldListener) loopReload() {
for {
select {
case <-l.done:
return
case signal := <-l.signal:
var restarted bool
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
continue
} else {
restarted = true
}
}
err := l.configureFirewalldRulesOnce(restarted)
if err != nil {
l.logger.Error(E.Cause(err, "reconfigure firewalld rules"))
}
}
}
}
func (l *firewalldListener) configureFirewalldRulesOnce(restarted bool) error {
nft, err := nftables.New()
if err != nil {
return err
}
defer nft.CloseLasting()
if restarted {
for i := 0; i < 10; i++ {
_, err = nft.ListTableOfFamily("firewalld", nftables.TableFamilyINet)
if err == nil {
break
}
time.Sleep(10 * time.Millisecond)
}
}
err = l.configureFirewalldRules(nft, false)
if err != nil {
return err
}
return nft.Flush()
}
func (l *firewalldListener) Close() {
select {
case <-l.done:
return
default:
close(l.done)
l.conn.Close()
}
}
func (r *autoRedirect) configureFirewalldRules(nft *nftables.Conn, cleanup bool) error {
tableFirewalld, err := nft.ListTableOfFamily("firewalld", nftables.TableFamilyINet)
if err != nil {
return err
}
if tableFirewalld.Flags&NFT_TABLE_F_OWNER != 0 {
var conn *dbus.Conn
conn, err = dbus.SystemBus()
if err != nil {
return E.Cause(err, "connect to system bus")
}
err = conn.Object(firewalldInterface, firewalldPath+"/config").SetProperty(firewalldInterface+".config.NftablesTableOwner", dbus.MakeVariant("no"))
if err != nil {
return E.Cause(err, "take owner of firewalld table")
}
err = conn.Object(firewalldInterface, firewalldPath).Call(
firewalldInterface+".reload", 0).Err
if err != nil {
return E.Cause(err, "reload firewalld")
}
tableFirewalld, err = nft.ListTableOfFamily("firewalld", nftables.TableFamilyINet)
if err != nil {
return E.Cause(err, "check reloaded firewalld table")
}
if tableFirewalld.Flags&NFT_TABLE_F_OWNER != 0 {
return E.New("unable to take owner of firewalld table")
}
}
for _, chainName := range []string{"filter_INPUT", "filter_FORWARD"} {
var rules []*nftables.Rule
rules, err = nft.GetRules(tableFirewalld, &nftables.Chain{
Name: chainName,
})
if err != nil {
return err
}
for _, rule := range rules {
if len(rule.Exprs) != 4 {
continue
}
exprMeta, isMeta := rule.Exprs[0].(*expr.Meta)
if !isMeta {
continue
}
if exprMeta.Key != expr.MetaKeyIIFNAME && exprMeta.Key != expr.MetaKeyOIFNAME {
continue
}
exprCmp, isCmp := rule.Exprs[1].(*expr.Cmp)
if !isCmp {
continue
}
if !slices.Equal(exprCmp.Data, nftablesIfname(r.tunOptions.Name)) {
continue
}
if cleanup {
err = nft.DelRule(rule)
if err != nil {
return err
}
} else {
return nil
}
}
}
if !cleanup {
ruleIif := &nftables.Rule{
Table: tableFirewalld,
Exprs: []expr.Any{
&expr.Meta{
Key: expr.MetaKeyIIFNAME,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: nftablesIfname(r.tunOptions.Name),
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
},
}
ruleOif := &nftables.Rule{
Table: tableFirewalld,
Exprs: []expr.Any{
&expr.Meta{
Key: expr.MetaKeyOIFNAME,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: nftablesIfname(r.tunOptions.Name),
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
},
}
chainForward := &nftables.Chain{
Name: "filter_FORWARD",
}
ruleIif.Chain = chainForward
ruleOif.Chain = chainForward
nft.InsertRule(ruleOif)
nft.InsertRule(ruleIif)
chainInput := &nftables.Chain{
Name: "filter_INPUT",
}
ruleIif.Chain = chainInput
ruleOif.Chain = chainInput
nft.InsertRule(ruleOif)
nft.InsertRule(ruleIif)
return nil
}
return nil
}