Add netns support

This commit is contained in:
世界 2026-07-11 00:57:57 +08:00
parent d0d4ebd8db
commit 994d6ccdbf
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
7 changed files with 220 additions and 80 deletions

73
netns_linux.go Normal file
View file

@ -0,0 +1,73 @@
package tun
import (
"context"
"net"
"runtime"
"strings"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/unix"
)
func listenNetworkNamespace(ctx context.Context, nameOrPath string, config net.ListenConfig, network, address string) (net.Listener, error) {
return execInNetworkNamespace(nameOrPath, func() (net.Listener, error) {
return config.Listen(ctx, network, address)
})
}
type networkNamespaceInterfaceFinder struct {
control.InterfaceFinder
options *Options
}
func (f *networkNamespaceInterfaceFinder) Update() error {
return runInNetworkNamespace(f.options.NetNs, f.InterfaceFinder.Update)
}
func execInNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
if nameOrPath == "" {
return block()
}
type blockResult struct {
value T
err error
}
resultChannel := make(chan blockResult, 1)
go func() {
runtime.LockOSThread()
value, err := execInNetworkNamespaceThread(nameOrPath, block)
resultChannel <- blockResult{value, err}
}()
result := <-resultChannel
return result.value, result.err
}
func execInNetworkNamespaceThread[T any](nameOrPath string, block func() (T, error)) (T, error) {
var defaultValue T
var path string
if strings.HasPrefix(nameOrPath, "/") {
path = nameOrPath
} else {
path = "/run/netns/" + nameOrPath
}
targetFd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC, 0)
if err != nil {
return defaultValue, E.Cause(err, "open netns ", nameOrPath)
}
defer unix.Close(targetFd)
err = unix.Setns(targetFd, unix.CLONE_NEWNET)
if err != nil {
return defaultValue, E.Cause(err, "set netns to ", nameOrPath)
}
return block()
}
func runInNetworkNamespace(nameOrPath string, block func() error) error {
_, err := execInNetworkNamespace(nameOrPath, func() (struct{}, error) {
return struct{}{}, block()
})
return err
}

12
netns_other.go Normal file
View file

@ -0,0 +1,12 @@
//go:build !linux
package tun
import (
"context"
"net"
)
func listenNetworkNamespace(ctx context.Context, nameOrPath string, config net.ListenConfig, network, address string) (net.Listener, error) {
return config.Listen(ctx, network, address)
}

View file

@ -26,6 +26,7 @@ type autoRedirect struct {
logger logger.Logger
tableName string
networkMonitor NetworkUpdateMonitor
ownedNetworkMonitor bool
networkListener *list.Element[NetworkUpdateCallback]
interfaceFinder control.InterfaceFinder
localAddresses []netip.Prefix
@ -51,7 +52,7 @@ type autoRedirect struct {
}
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
return &autoRedirect{
r := &autoRedirect{
tunOptions: options.TunOptions,
ctx: options.Context,
handler: options.Handler,
@ -63,7 +64,11 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
customRedirectPortFunc: options.CustomRedirectPort,
routeAddressSet: options.RouteAddressSet,
routeExcludeAddressSet: options.RouteExcludeAddressSet,
}, nil
}
if options.TunOptions.NetNs != "" {
r.interfaceFinder = &networkNamespaceInterfaceFinder{control.NewDefaultInterfaceFinder(), options.TunOptions}
}
return r, nil
}
func (r *autoRedirect) Start() error {
@ -89,8 +94,11 @@ func (r *autoRedirect) Start() error {
}
}
} else {
if r.tunOptions.NetNs != "" && !r.useNFTables {
return E.New("auto_redirect in network namespace requires nftables")
}
if r.useNFTables {
err = r.initializeNFTables()
err = runInNetworkNamespace(r.tunOptions.NetNs, r.initializeNFTables)
if err != nil {
return E.Cause(err, "missing nftables support")
}
@ -132,7 +140,7 @@ func (r *autoRedirect) Start() error {
listenAddr = netip.IPv4Unspecified()
}
server := newRedirectServer(r.ctx, r.handler, r.logger, listenAddr)
err = server.Start()
err = runInNetworkNamespace(r.tunOptions.NetNs, server.Start)
if err != nil {
return E.Cause(err, "start redirect server")
}
@ -151,24 +159,43 @@ func (r *autoRedirect) Start() error {
})
if err != nil {
r.logger.Warn("nfqueue not available, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err)
} else if err = handler.Start(); err != nil {
} else if err = runInNetworkNamespace(r.tunOptions.NetNs, handler.Start); err != nil {
r.logger.Warn("nfqueue start failed, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err)
} else {
r.nfqueueHandler = handler
r.nfqueueEnabled = true
}
}
r.cleanupNFTables()
err = r.setupNFTables()
if err != nil {
return E.Cause(err, "setup nftables")
}
if r.tunOptions.AutoRedirectMarkMode {
err = r.setupRedirectRoutes()
if r.tunOptions.NetNs != "" {
var monitor NetworkUpdateMonitor
monitor, err = NewNetworkUpdateMonitor(r.logger)
if err != nil {
r.cleanupNFTables()
return E.Cause(err, "setup redirect routes")
return E.Cause(err, "create netns network monitor")
}
err = runInNetworkNamespace(r.tunOptions.NetNs, monitor.Start)
if err != nil {
return E.Cause(err, "start netns network monitor")
}
r.networkMonitor = monitor
r.ownedNetworkMonitor = true
}
err = runInNetworkNamespace(r.tunOptions.NetNs, func() error {
r.cleanupNFTables()
setupErr := r.setupNFTables()
if setupErr != nil {
return E.Cause(setupErr, "setup nftables")
}
if r.tunOptions.AutoRedirectMarkMode {
setupErr = r.setupRedirectRoutes()
if setupErr != nil {
r.cleanupNFTables()
return E.Cause(setupErr, "setup redirect routes")
}
}
return nil
})
if err != nil {
return err
}
} else {
r.cleanupIPTables()
@ -185,8 +212,14 @@ func (r *autoRedirect) Close() error {
r.nfqueueHandler.Close()
}
if r.useNFTables {
r.cleanupNFTables()
r.cleanupRedirectRoutes()
_ = runInNetworkNamespace(r.tunOptions.NetNs, func() error {
r.cleanupNFTables()
r.cleanupRedirectRoutes()
return nil
})
if r.ownedNetworkMonitor {
_ = r.networkMonitor.Close()
}
} else {
r.cleanupIPTables()
}
@ -197,7 +230,7 @@ func (r *autoRedirect) Close() error {
func (r *autoRedirect) UpdateRouteAddressSet() {
if r.useNFTables {
err := r.nftablesUpdateRouteAddressSet()
err := runInNetworkNamespace(r.tunOptions.NetNs, r.nftablesUpdateRouteAddressSet)
if err != nil {
r.logger.Error("update route address set: ", err)
}

View file

@ -299,27 +299,38 @@ func (r *autoRedirect) setupNFTables() error {
if err != nil {
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)
if r.tunOptions.NetNs == "" {
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() {
err = r.nftablesUpdateLocalAddressSet()
if err != nil {
r.logger.Error("update local address set: ", err)
}
if r.tunOptions.AutoRedirectMarkMode {
err = r.updateRedirectRoutes()
if err != nil {
r.logger.Error("update redirect routes: ", err)
}
updateErr := runInNetworkNamespace(r.tunOptions.NetNs, r.updateNetworkAddresses)
if updateErr != nil {
r.logger.Error(updateErr)
}
})
return nil
}
func (r *autoRedirect) updateNetworkAddresses() error {
err := r.nftablesUpdateLocalAddressSet()
if err != nil {
err = E.Cause(err, "update local address set")
}
if r.tunOptions.AutoRedirectMarkMode {
routeErr := r.updateRedirectRoutes()
if routeErr != nil {
routeErr = E.Cause(routeErr, "update redirect routes")
}
err = E.Errors(err, routeErr)
}
return err
}
// TODO: test if this works
func (r *autoRedirect) nftablesUpdateLocalAddressSet() error {
err := r.interfaceFinder.Update()
@ -376,6 +387,7 @@ func (r *autoRedirect) nftablesUpdateRouteAddressSet() error {
func (r *autoRedirect) cleanupNFTables() {
if r.networkListener != nil {
r.networkMonitor.UnregisterCallback(r.networkListener)
r.networkListener = nil
}
r.stopDockerFirewallMonitor()
nft, err := nftables.New()
@ -389,9 +401,11 @@ func (r *autoRedirect) cleanupNFTables() {
_ = r.configureOpenWRTFirewall4(nft, true)
_ = nft.Flush()
_ = nft.CloseLasting()
err = r.configureDockerFirewall(true)
if err != nil && r.logger != nil {
r.logger.Warn("cleanup docker firewall: ", err)
if r.tunOptions.NetNs == "" {
err = r.configureDockerFirewall(true)
if err != nil && r.logger != nil {
r.logger.Warn("cleanup docker firewall: ", err)
}
}
}

View file

@ -28,6 +28,7 @@ type System struct {
ctx context.Context
tun Tun
tunName string
netNs string
mtu int
handler Handler
logger logger.Logger
@ -68,6 +69,7 @@ func NewSystem(options StackOptions) (Stack, error) {
ctx: options.Context,
tun: options.Tun,
tunName: options.TunOptions.Name,
netNs: options.TunOptions.NetNs,
mtu: int(options.TunOptions.MTU),
inet4LoopbackAddress: options.TunOptions.Inet4LoopbackAddress,
inet6LoopbackAddress: options.TunOptions.Inet6LoopbackAddress,
@ -135,7 +137,7 @@ func (s *System) start() error {
var err error
if s.inet4NextAddress.IsValid() {
for range 3 {
tcpListener, err = listener.Listen(s.ctx, "tcp4", net.JoinHostPort(s.inet4Address.String(), "0"))
tcpListener, err = listenNetworkNamespace(s.ctx, s.netNs, listener, "tcp4", net.JoinHostPort(s.inet4Address.String(), "0"))
if !retryableListenError(err) {
break
}
@ -150,7 +152,7 @@ func (s *System) start() error {
}
if s.inet6NextAddress.IsValid() {
for range 3 {
tcpListener, err = listener.Listen(s.ctx, "tcp6", net.JoinHostPort(s.inet6Address.String(), "0"))
tcpListener, err = listenNetworkNamespace(s.ctx, s.netNs, listener, "tcp6", net.JoinHostPort(s.inet6Address.String(), "0"))
if !retryableListenError(err) {
break
}

1
tun.go
View file

@ -66,6 +66,7 @@ const (
type Options struct {
Name string
NetNs string
Inet4Address []netip.Prefix
Inet6Address []netip.Prefix
MTU uint32

View file

@ -51,37 +51,38 @@ type NativeTun struct {
}
func New(options Options) (Tun, error) {
var nativeTun *NativeTun
if options.FileDescriptor == 0 {
tunFd, err := open(options.Name, options.GSO)
if err != nil {
return nil, E.Cause(err, "open tun")
}
tunLink, err := netlink.LinkByName(options.Name)
if err != nil {
return nil, E.Errors(err, unix.Close(tunFd))
}
nativeTun = &NativeTun{
tunFd: tunFd,
tunFile: os.NewFile(uintptr(tunFd), "tun"),
options: options,
}
err = nativeTun.configure(tunLink)
if err != nil {
return nil, E.Errors(err, unix.Close(tunFd))
}
} else {
nativeTun = &NativeTun{
tunFd: options.FileDescriptor,
tunFile: os.NewFile(uintptr(options.FileDescriptor), "tun"),
options: options,
}
if options.GSO {
err := nativeTun.enableGSO()
return execInNetworkNamespace(options.NetNs, func() (Tun, error) {
tunFd, err := open(options.Name, options.GSO)
if err != nil {
if options.Logger != nil {
options.Logger.Warn(err)
}
return nil, E.Cause(err, "open tun")
}
tunLink, err := netlink.LinkByName(options.Name)
if err != nil {
return nil, E.Errors(err, unix.Close(tunFd))
}
nativeTun := &NativeTun{
tunFd: tunFd,
tunFile: os.NewFile(uintptr(tunFd), "tun"),
options: options,
}
err = nativeTun.configure(tunLink)
if err != nil {
return nil, E.Errors(err, unix.Close(tunFd))
}
return nativeTun, nil
})
}
nativeTun := &NativeTun{
tunFd: options.FileDescriptor,
tunFile: os.NewFile(uintptr(options.FileDescriptor), "tun"),
options: options,
}
if options.GSO {
err := nativeTun.enableGSO()
if err != nil {
if options.Logger != nil {
options.Logger.Warn(err)
}
}
}
@ -290,10 +291,10 @@ func (t *NativeTun) Name() (string, error) {
func (t *NativeTun) Start() error {
if t.options.FileDescriptor == 0 {
if !t.options.EXP_ExternalConfiguration {
if !t.options.EXP_ExternalConfiguration && t.options.NetNs == "" {
t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name)
}
err := t.start()
err := runInNetworkNamespace(t.options.NetNs, t.start)
if err != nil {
return err
}
@ -354,7 +355,7 @@ func (t *NativeTun) start() error {
return E.Cause(err, "set rules")
}
if t.options.DNSMode != DNSModeDisabled {
if t.options.DNSMode != DNSModeDisabled && t.options.NetNs == "" {
err = t.setSearchDomainForSystemdResolved()
if err != nil {
return E.Cause(err, "set search domain")
@ -374,11 +375,13 @@ func (t *NativeTun) Close() error {
if t.options.EXP_ExternalConfiguration {
return common.Close(common.PtrOrNil(t.tunFile))
}
if t.options.DNSMode != DNSModeDisabled {
if t.options.DNSMode != DNSModeDisabled && t.options.NetNs == "" {
t.unsetSearchDomainForSystemdResolved()
}
t.unsetAddresses()
return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile)))
return E.Errors(runInNetworkNamespace(t.options.NetNs, func() error {
t.unsetAddresses()
return E.Errors(t.unsetRoute(), t.unsetRules())
}), common.Close(common.PtrOrNil(t.tunFile)))
}
func (t *NativeTun) Read(p []byte) (n int, err error) {
@ -625,16 +628,18 @@ func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error {
t.options = tunOptions
return nil
}
tunLink, err := netlink.LinkByName(t.options.Name)
if err != nil {
return E.Cause(err, "find tun interface")
}
err = t.unsetRoute0(tunLink)
if err != nil {
return E.Cause(err, "unset old routes")
}
t.options = tunOptions
return t.setRoute(tunLink)
return runInNetworkNamespace(t.options.NetNs, func() error {
tunLink, err := netlink.LinkByName(t.options.Name)
if err != nil {
return E.Cause(err, "find tun interface")
}
err = t.unsetRoute0(tunLink)
if err != nil {
return E.Cause(err, "unset old routes")
}
t.options = tunOptions
return t.setRoute(tunLink)
})
}
func (t *NativeTun) routes(tunLink netlink.Link) ([]netlink.Route, error) {