Add netns support
This commit is contained in:
parent
d0d4ebd8db
commit
994d6ccdbf
7 changed files with 220 additions and 80 deletions
73
netns_linux.go
Normal file
73
netns_linux.go
Normal 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
12
netns_other.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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,25 +159,44 @@ 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 r.tunOptions.NetNs != "" {
|
||||
var monitor NetworkUpdateMonitor
|
||||
monitor, err = NewNetworkUpdateMonitor(r.logger)
|
||||
if err != nil {
|
||||
return E.Cause(err, "setup nftables")
|
||||
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 {
|
||||
err = r.setupRedirectRoutes()
|
||||
if err != nil {
|
||||
setupErr = r.setupRedirectRoutes()
|
||||
if setupErr != nil {
|
||||
r.cleanupNFTables()
|
||||
return E.Cause(err, "setup redirect routes")
|
||||
return E.Cause(setupErr, "setup redirect routes")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.cleanupIPTables()
|
||||
err = r.setupIPTables()
|
||||
|
|
@ -185,8 +212,14 @@ func (r *autoRedirect) Close() error {
|
|||
r.nfqueueHandler.Close()
|
||||
}
|
||||
if r.useNFTables {
|
||||
_ = 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,27 +299,38 @@ func (r *autoRedirect) setupNFTables() error {
|
|||
if err != nil {
|
||||
return E.Cause(err, "flush nftables")
|
||||
}
|
||||
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,10 +401,12 @@ func (r *autoRedirect) cleanupNFTables() {
|
|||
_ = r.configureOpenWRTFirewall4(nft, true)
|
||||
_ = nft.Flush()
|
||||
_ = nft.CloseLasting()
|
||||
if r.tunOptions.NetNs == "" {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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
1
tun.go
|
|
@ -66,6 +66,7 @@ const (
|
|||
|
||||
type Options struct {
|
||||
Name string
|
||||
NetNs string
|
||||
Inet4Address []netip.Prefix
|
||||
Inet6Address []netip.Prefix
|
||||
MTU uint32
|
||||
|
|
|
|||
25
tun_linux.go
25
tun_linux.go
|
|
@ -51,8 +51,8 @@ type NativeTun struct {
|
|||
}
|
||||
|
||||
func New(options Options) (Tun, error) {
|
||||
var nativeTun *NativeTun
|
||||
if options.FileDescriptor == 0 {
|
||||
return execInNetworkNamespace(options.NetNs, func() (Tun, error) {
|
||||
tunFd, err := open(options.Name, options.GSO)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "open tun")
|
||||
|
|
@ -61,7 +61,7 @@ func New(options Options) (Tun, error) {
|
|||
if err != nil {
|
||||
return nil, E.Errors(err, unix.Close(tunFd))
|
||||
}
|
||||
nativeTun = &NativeTun{
|
||||
nativeTun := &NativeTun{
|
||||
tunFd: tunFd,
|
||||
tunFile: os.NewFile(uintptr(tunFd), "tun"),
|
||||
options: options,
|
||||
|
|
@ -70,8 +70,10 @@ func New(options Options) (Tun, error) {
|
|||
if err != nil {
|
||||
return nil, E.Errors(err, unix.Close(tunFd))
|
||||
}
|
||||
} else {
|
||||
nativeTun = &NativeTun{
|
||||
return nativeTun, nil
|
||||
})
|
||||
}
|
||||
nativeTun := &NativeTun{
|
||||
tunFd: options.FileDescriptor,
|
||||
tunFile: os.NewFile(uintptr(options.FileDescriptor), "tun"),
|
||||
options: options,
|
||||
|
|
@ -84,7 +86,6 @@ func New(options Options) (Tun, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nativeTun, nil
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
return E.Errors(runInNetworkNamespace(t.options.NetNs, func() error {
|
||||
t.unsetAddresses()
|
||||
return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile)))
|
||||
return E.Errors(t.unsetRoute(), t.unsetRules())
|
||||
}), common.Close(common.PtrOrNil(t.tunFile)))
|
||||
}
|
||||
|
||||
func (t *NativeTun) Read(p []byte) (n int, err error) {
|
||||
|
|
@ -625,6 +628,7 @@ func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error {
|
|||
t.options = tunOptions
|
||||
return nil
|
||||
}
|
||||
return runInNetworkNamespace(t.options.NetNs, func() error {
|
||||
tunLink, err := netlink.LinkByName(t.options.Name)
|
||||
if err != nil {
|
||||
return E.Cause(err, "find tun interface")
|
||||
|
|
@ -635,6 +639,7 @@ func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error {
|
|||
}
|
||||
t.options = tunOptions
|
||||
return t.setRoute(tunLink)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *NativeTun) routes(tunLink netlink.Link) ([]netlink.Route, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue