From 994d6ccdbf3fd9c0fcbad4661b54050f077ca9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 11 Jul 2026 00:57:57 +0800 Subject: [PATCH] Add netns support --- netns_linux.go | 73 ++++++++++++++++++++++++++++++++++ netns_other.go | 12 ++++++ redirect_linux.go | 67 +++++++++++++++++++++++-------- redirect_nftables.go | 46 +++++++++++++-------- stack_system.go | 6 ++- tun.go | 1 + tun_linux.go | 95 +++++++++++++++++++++++--------------------- 7 files changed, 220 insertions(+), 80 deletions(-) create mode 100644 netns_linux.go create mode 100644 netns_other.go diff --git a/netns_linux.go b/netns_linux.go new file mode 100644 index 0000000..e2b5706 --- /dev/null +++ b/netns_linux.go @@ -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 +} diff --git a/netns_other.go b/netns_other.go new file mode 100644 index 0000000..ab5a3a2 --- /dev/null +++ b/netns_other.go @@ -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) +} diff --git a/redirect_linux.go b/redirect_linux.go index 04a1fee..f08d0f7 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -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) } diff --git a/redirect_nftables.go b/redirect_nftables.go index 5944e4e..c71a770 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -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) + } } } diff --git a/stack_system.go b/stack_system.go index 3cb0cb0..f2e2edc 100644 --- a/stack_system.go +++ b/stack_system.go @@ -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 } diff --git a/tun.go b/tun.go index c6518f4..14344b6 100644 --- a/tun.go +++ b/tun.go @@ -66,6 +66,7 @@ const ( type Options struct { Name string + NetNs string Inet4Address []netip.Prefix Inet6Address []netip.Prefix MTU uint32 diff --git a/tun_linux.go b/tun_linux.go index 487051d..41d4dc4 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -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) {