mirror of
https://github.com/SagerNet/sing-tun.git
synced 2025-09-27 04:55:53 +08:00
254 lines
5.7 KiB
Go
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
|
|
}
|