Add netns and unshare support
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package adapter
|
||||
|
||||
type NetworkNamespaceManager interface {
|
||||
ResolvePath(nameOrPath string) string
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/netns"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||
@@ -61,8 +62,9 @@ type Box struct {
|
||||
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
NetworkNamespaceHolderArgs []string
|
||||
}
|
||||
|
||||
func Context(
|
||||
@@ -194,6 +196,12 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
netnsManager, err := netns.NewManager(logFactory.NewLogger("netns"), options.NetworkNamespaces, options.NetworkNamespaceHolderArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.NetworkNamespaceManager](ctx, netnsManager)
|
||||
internalServices = append(internalServices, netnsManager)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/common/netns"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandNetnsHolder = &cobra.Command{
|
||||
Use: "netns-holder",
|
||||
Args: cobra.NoArgs,
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
netns.Hold()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandNetnsHolder)
|
||||
}
|
||||
+31
-11
@@ -104,10 +104,17 @@ func readConfigAndMerge() (option.Options, error) {
|
||||
if err != nil {
|
||||
return option.Options{}, err
|
||||
}
|
||||
return mergeOptionsList(optionsList)
|
||||
}
|
||||
|
||||
func mergeOptionsList(optionsList []*OptionsEntry) (option.Options, error) {
|
||||
if len(optionsList) == 1 {
|
||||
return optionsList[0].options, nil
|
||||
}
|
||||
var mergedMessage json.RawMessage
|
||||
var (
|
||||
mergedMessage json.RawMessage
|
||||
err error
|
||||
)
|
||||
for _, options := range optionsList {
|
||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||
if err != nil {
|
||||
@@ -122,11 +129,7 @@ func readConfigAndMerge() (option.Options, error) {
|
||||
return mergedOptions, nil
|
||||
}
|
||||
|
||||
func create() (*box.Box, context.CancelFunc, error) {
|
||||
options, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
func create(options option.Options) (*box.Box, context.CancelFunc, error) {
|
||||
if disableColor {
|
||||
if options.Log == nil {
|
||||
options.Log = &option.LogOptions{}
|
||||
@@ -135,8 +138,9 @@ func create() (*box.Box, context.CancelFunc, error) {
|
||||
}
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
NetworkNamespaceHolderArgs: []string{"/proc/self/exe", commandNetnsHolder.Use},
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
@@ -167,13 +171,25 @@ func create() (*box.Box, context.CancelFunc, error) {
|
||||
}
|
||||
|
||||
func run() error {
|
||||
optionsList, err := readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options, err := mergeOptionsList(optionsList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = runInUserNamespaceIfNeeded(options, optionsList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer signal.Stop(osSignals)
|
||||
for {
|
||||
instance, cancel, err := create()
|
||||
if err != nil {
|
||||
return err
|
||||
instance, cancel, createErr := create(options)
|
||||
if createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
runtimeDebug.FreeOSMemory()
|
||||
for {
|
||||
@@ -198,6 +214,10 @@ func run() error {
|
||||
}
|
||||
break
|
||||
}
|
||||
options, err = readConfigAndMerge()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func runInUserNamespaceIfNeeded(options option.Options, optionsList []*OptionsEntry) error {
|
||||
if !common.Any(options.NetworkNamespaces, func(namespace option.NetworkNamespace) bool {
|
||||
return namespace.Type == C.NetNsTypeUnshare
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
var header unix.CapUserHeader
|
||||
header.Version = unix.LINUX_CAPABILITY_VERSION_3
|
||||
var data [2]unix.CapUserData
|
||||
err := unix.Capget(&header, &data[0])
|
||||
if err != nil {
|
||||
return E.Cause(err, "get capabilities")
|
||||
}
|
||||
if data[0].Effective&(1<<unix.CAP_SYS_ADMIN) != 0 {
|
||||
return nil
|
||||
}
|
||||
command := exec.Command("/proc/self/exe", os.Args[1:]...)
|
||||
command.Args = os.Args
|
||||
command.Stdin = os.Stdin
|
||||
for _, entry := range optionsList {
|
||||
if entry.path == "stdin" {
|
||||
command.Stdin = bytes.NewReader(entry.content)
|
||||
}
|
||||
}
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
command.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWUSER,
|
||||
UidMappings: []syscall.SysProcIDMap{
|
||||
{ContainerID: os.Geteuid(), HostID: os.Geteuid(), Size: 1},
|
||||
},
|
||||
GidMappings: []syscall.SysProcIDMap{
|
||||
{ContainerID: os.Getegid(), HostID: os.Getegid(), Size: 1},
|
||||
},
|
||||
GidMappingsEnableSetgroups: false,
|
||||
AmbientCaps: []uintptr{unix.CAP_SYS_ADMIN, unix.CAP_NET_ADMIN, unix.CAP_NET_RAW},
|
||||
Setpgid: true,
|
||||
Pdeathsig: syscall.SIGKILL,
|
||||
}
|
||||
err = command.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create user namespace for unshare network namespace (is unprivileged user namespace creation allowed by the kernel?)")
|
||||
}
|
||||
signalChannel := make(chan os.Signal, 4)
|
||||
signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
go func() {
|
||||
for receivedSignal := range signalChannel {
|
||||
command.Process.Signal(receivedSignal)
|
||||
}
|
||||
}()
|
||||
err = command.Wait()
|
||||
exitError, isExitError := E.Cast[*exec.ExitError](err)
|
||||
if isExitError {
|
||||
os.Exit(exitError.ExitCode())
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause(err, "wait user namespace child")
|
||||
}
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/sagernet/sing-box/option"
|
||||
|
||||
func runInUserNamespaceIfNeeded(options option.Options, optionsList []*OptionsEntry) error {
|
||||
return nil
|
||||
}
|
||||
@@ -250,7 +250,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
||||
return nil, E.New("domain not resolved")
|
||||
}
|
||||
if d.networkStrategy == nil {
|
||||
return d.trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
||||
return d.trackConn(listener.ListenNetworkNamespace[net.Conn](ctx, d.netns, func() (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkUDP:
|
||||
if !address.IsIPv6() {
|
||||
@@ -320,7 +320,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
||||
|
||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if d.networkStrategy == nil {
|
||||
return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
||||
return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](ctx, d.netns, func() (net.PacketConn, error) {
|
||||
listenConfig := d.udpListener
|
||||
if d.autoDetectBindFunc != nil && destination.Addr.IsValid() {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
||||
|
||||
+39
-23
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/vishvananda/netns"
|
||||
)
|
||||
@@ -143,30 +144,45 @@ func (l *Listener) ListenOptions() option.ListenOptions {
|
||||
return l.listenOptions
|
||||
}
|
||||
|
||||
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
|
||||
if nameOrPath != "" {
|
||||
func ListenNetworkNamespace[T any](ctx context.Context, nameOrPath string, block func() (T, error)) (T, error) {
|
||||
if nameOrPath == "" {
|
||||
return block()
|
||||
}
|
||||
manager := service.FromContext[adapter.NetworkNamespaceManager](ctx)
|
||||
if manager != nil {
|
||||
nameOrPath = manager.ResolvePath(nameOrPath)
|
||||
}
|
||||
type blockResult struct {
|
||||
value T
|
||||
err error
|
||||
}
|
||||
resultChannel := make(chan blockResult, 1)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
currentNs, err := netns.Get()
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
||||
}
|
||||
defer currentNs.Close()
|
||||
defer netns.Set(currentNs)
|
||||
var targetNs netns.NsHandle
|
||||
if strings.HasPrefix(nameOrPath, "/") {
|
||||
targetNs, err = netns.GetFromPath(nameOrPath)
|
||||
} else {
|
||||
targetNs, err = netns.GetFromName(nameOrPath)
|
||||
}
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
|
||||
}
|
||||
defer targetNs.Close()
|
||||
err = netns.Set(targetNs)
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
|
||||
}
|
||||
value, err := listenNetworkNamespaceThread(nameOrPath, block)
|
||||
resultChannel <- blockResult{value, err}
|
||||
}()
|
||||
result := <-resultChannel
|
||||
return result.value, result.err
|
||||
}
|
||||
|
||||
func listenNetworkNamespaceThread[T any](nameOrPath string, block func() (T, error)) (T, error) {
|
||||
var (
|
||||
targetNs netns.NsHandle
|
||||
err error
|
||||
)
|
||||
if strings.HasPrefix(nameOrPath, "/") {
|
||||
targetNs, err = netns.GetFromPath(nameOrPath)
|
||||
} else {
|
||||
targetNs, err = netns.GetFromName(nameOrPath)
|
||||
}
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
|
||||
}
|
||||
defer targetNs.Close()
|
||||
err = netns.Set(targetNs)
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
})
|
||||
})
|
||||
}
|
||||
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
|
||||
tcpListener, err := ListenNetworkNamespace[net.Listener](l.ctx, l.listenOptions.NetNs, func() (net.Listener, error) {
|
||||
if l.listenOptions.TCPFastOpen {
|
||||
var tfoConfig tfo.ListenConfig
|
||||
tfoConfig.ListenConfig = listenConfig
|
||||
|
||||
@@ -49,7 +49,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
})
|
||||
})
|
||||
}
|
||||
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.ctx, l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
|
||||
})
|
||||
if err != nil {
|
||||
@@ -62,7 +62,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
}
|
||||
|
||||
func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) {
|
||||
return ListenNetworkNamespace[net.Conn](l.ctx, l.listenOptions.NetNs, func() (net.Conn, error) {
|
||||
if l.listenOptions.BindInterface != "" {
|
||||
dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network s
|
||||
}
|
||||
|
||||
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
|
||||
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
return ListenNetworkNamespace[net.PacketConn](l.ctx, l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
if l.listenOptions.BindInterface != "" {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package netns
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/netlink"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
vnetns "github.com/vishvananda/netns"
|
||||
)
|
||||
|
||||
type holder struct {
|
||||
command *exec.Cmd
|
||||
pipeWriter *os.File
|
||||
pidFile string
|
||||
}
|
||||
|
||||
func (m *Manager) start() error {
|
||||
for _, namespace := range m.namespaces {
|
||||
switch namespace.Type {
|
||||
case C.NetNsTypeDefault:
|
||||
path := namespace.DefaultOptions.Path
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/run/netns/" + path
|
||||
}
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return E.Cause(err, "network namespace[", namespace.Tag, "]")
|
||||
}
|
||||
case C.NetNsTypeUnshare:
|
||||
err := m.startNamespace(namespace)
|
||||
if err != nil {
|
||||
return E.Cause(err, "network namespace[", namespace.Tag, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
|
||||
if len(m.holderArgs) == 0 {
|
||||
return E.New("unshare network namespace is only supported in `sing-box run`")
|
||||
}
|
||||
created, err := m.startHolder(namespace.UnshareOptions.PidFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.holders = append(m.holders, created)
|
||||
pid := created.command.Process.Pid
|
||||
if created.pidFile != "" {
|
||||
err = os.WriteFile(created.pidFile, []byte(strconv.Itoa(pid)+"\n"), 0o644)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write pid file")
|
||||
}
|
||||
}
|
||||
m.paths[namespace.Tag] = netnsPath(pid)
|
||||
m.logger.Info("created network namespace[", namespace.Tag, "], holder pid: ", pid)
|
||||
if os.Geteuid() == 0 {
|
||||
m.logger.Info("enter network namespace[", namespace.Tag, "] with: nsenter -n -t ", pid)
|
||||
} else {
|
||||
m.logger.Info("enter network namespace[", namespace.Tag, "] with: nsenter -U --preserve-credentials -n -t ", pid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) close() error {
|
||||
for _, created := range m.holders {
|
||||
created.pipeWriter.Close()
|
||||
if created.pidFile != "" {
|
||||
os.Remove(created.pidFile)
|
||||
}
|
||||
}
|
||||
m.holders = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func netnsPath(pid int) string {
|
||||
return "/proc/" + strconv.Itoa(pid) + "/ns/net"
|
||||
}
|
||||
|
||||
func (m *Manager) startHolder(pidFile string) (*holder, error) {
|
||||
pipeReader, pipeWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
command := exec.Command(m.holderArgs[0], m.holderArgs[1:]...)
|
||||
command.Stdin = pipeReader
|
||||
command.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWNET,
|
||||
}
|
||||
err = command.Start()
|
||||
pipeReader.Close()
|
||||
if err != nil {
|
||||
pipeWriter.Close()
|
||||
return nil, E.Cause(err, "start holder process")
|
||||
}
|
||||
go command.Wait()
|
||||
err = setupNamespace(command.Process.Pid)
|
||||
if err != nil {
|
||||
pipeWriter.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &holder{command: command, pipeWriter: pipeWriter, pidFile: pidFile}, nil
|
||||
}
|
||||
|
||||
func setupNamespace(pid int) error {
|
||||
resultChannel := make(chan error, 1)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
resultChannel <- setupNamespaceThread(pid)
|
||||
}()
|
||||
return <-resultChannel
|
||||
}
|
||||
|
||||
func setupNamespaceThread(pid int) error {
|
||||
targetNs, err := vnetns.GetFromPath(netnsPath(pid))
|
||||
if err != nil {
|
||||
return E.Cause(err, "open created netns")
|
||||
}
|
||||
defer targetNs.Close()
|
||||
err = vnetns.Set(targetNs)
|
||||
if err != nil {
|
||||
return E.Cause(err, "enter created netns")
|
||||
}
|
||||
loopbackLink, err := netlink.LinkByName("lo")
|
||||
if err != nil {
|
||||
return E.Cause(err, "find lo")
|
||||
}
|
||||
err = netlink.LinkSetUp(loopbackLink)
|
||||
if err != nil {
|
||||
return E.Cause(err, "set lo up")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build !linux
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type holder struct{}
|
||||
|
||||
func (m *Manager) start() error {
|
||||
if len(m.namespaces) > 0 {
|
||||
return E.New("network namespaces are only supported on Linux")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//go:build linux
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func TestUnshareNamespace(t *testing.T) {
|
||||
if os.Getenv("NETNS_TEST_HOLDER") == "1" {
|
||||
Hold()
|
||||
}
|
||||
pipeReader, pipeWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pipeReader.Close()
|
||||
defer pipeWriter.Close()
|
||||
os.Setenv("NETNS_TEST_HOLDER", "1")
|
||||
defer os.Unsetenv("NETNS_TEST_HOLDER")
|
||||
manager, err := NewManager(logger.NOP(), []option.NetworkNamespace{{
|
||||
Type: C.NetNsTypeUnshare,
|
||||
Tag: "test",
|
||||
UnshareOptions: option.UnshareNetworkNamespaceOptions{
|
||||
PidFile: "/proc/self/fd/" + F.ToString(pipeWriter.Fd()),
|
||||
},
|
||||
}}, []string{"/proc/self/exe", "-test.run=^TestUnshareNamespace$"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = manager.Start(adapter.StartStateInitialize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
pipeReader.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
pidLine, err := bufio.NewReader(pipeReader).ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatal("read pid from pipe: ", err)
|
||||
}
|
||||
pid, err := strconv.Atoi(strings.TrimSuffix(pidLine, "\n"))
|
||||
if err != nil {
|
||||
t.Fatal("parse pid: ", err)
|
||||
}
|
||||
|
||||
resolvedPath := manager.ResolvePath("test")
|
||||
if resolvedPath != netnsPath(pid) {
|
||||
t.Fatal("resolved path ", resolvedPath, " does not match pid ", pid)
|
||||
}
|
||||
currentNs, err := os.Readlink("/proc/thread-self/ns/net")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
holderNs, err := os.Readlink(resolvedPath)
|
||||
if err != nil {
|
||||
t.Fatal("holder netns not accessible: ", err)
|
||||
}
|
||||
if currentNs == holderNs {
|
||||
t.Fatal("holder is in the current netns")
|
||||
}
|
||||
|
||||
err = manager.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
|
||||
_, err = os.Stat("/proc/" + strconv.Itoa(pid))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("holder process did not exit after close")
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package netns
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var _ adapter.NetworkNamespaceManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger logger.ContextLogger
|
||||
namespaces []option.NetworkNamespace
|
||||
holderArgs []string
|
||||
paths map[string]string
|
||||
holders []*holder
|
||||
}
|
||||
|
||||
func NewManager(logger logger.ContextLogger, namespaces []option.NetworkNamespace, holderArgs []string) (*Manager, error) {
|
||||
paths := make(map[string]string)
|
||||
for _, namespace := range namespaces {
|
||||
if namespace.Tag == "" {
|
||||
return nil, E.New("network namespace: missing tag")
|
||||
}
|
||||
_, duplicated := paths[namespace.Tag]
|
||||
if duplicated {
|
||||
return nil, E.New("network namespace: duplicated tag: ", namespace.Tag)
|
||||
}
|
||||
switch namespace.Type {
|
||||
case C.NetNsTypeDefault:
|
||||
if namespace.DefaultOptions.Path == "" {
|
||||
return nil, E.New("network namespace[", namespace.Tag, "]: missing path")
|
||||
}
|
||||
paths[namespace.Tag] = namespace.DefaultOptions.Path
|
||||
case C.NetNsTypeUnshare:
|
||||
paths[namespace.Tag] = ""
|
||||
}
|
||||
}
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
namespaces: namespaces,
|
||||
holderArgs: holderArgs,
|
||||
paths: paths,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Name() string {
|
||||
return "netns"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateInitialize {
|
||||
return nil
|
||||
}
|
||||
return m.start()
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
return m.close()
|
||||
}
|
||||
|
||||
func (m *Manager) ResolvePath(nameOrPath string) string {
|
||||
path, loaded := m.paths[nameOrPath]
|
||||
if loaded && path != "" {
|
||||
return path
|
||||
}
|
||||
return nameOrPath
|
||||
}
|
||||
|
||||
func Hold() {
|
||||
buffer := make([]byte, 1)
|
||||
for {
|
||||
_, err := os.Stdin.Read(buffer)
|
||||
if err != nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
NetNsTypeDefault = "default"
|
||||
NetNsTypeUnshare = "unshare"
|
||||
)
|
||||
@@ -7,7 +7,8 @@ icon: material/new-box
|
||||
:material-plus: [include_mac_address](#include_mac_address)
|
||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||
:material-plus: [dns_mode](#dns_mode)
|
||||
:material-plus: [dns_address](#dns_address)
|
||||
:material-plus: [dns_address](#dns_address)
|
||||
:material-plus: [netns](#netns)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.3"
|
||||
|
||||
@@ -197,6 +198,22 @@ icon: material/new-box
|
||||
|
||||
Virtual device name, automatically selected if empty.
|
||||
|
||||
#### netns
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux.
|
||||
|
||||
Create the tun interface in the specified network namespace, name, path, or the tag of a
|
||||
[network namespace](/configuration/network-namespace/).
|
||||
|
||||
When set, `auto_route` and `auto_redirect` operate inside the namespace, and no root privilege is
|
||||
required if the namespace is owned by the current user.
|
||||
|
||||
Conflict with `platform`.
|
||||
|
||||
#### address
|
||||
|
||||
!!! question "Since sing-box 1.10.0"
|
||||
|
||||
@@ -7,7 +7,8 @@ icon: material/new-box
|
||||
:material-plus: [include_mac_address](#include_mac_address)
|
||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||
:material-plus: [dns_mode](#dns_mode)
|
||||
:material-plus: [dns_address](#dns_address)
|
||||
:material-plus: [dns_address](#dns_address)
|
||||
:material-plus: [netns](#netns)
|
||||
|
||||
!!! quote "sing-box 1.13.3 中的更改"
|
||||
|
||||
@@ -199,6 +200,20 @@ icon: material/new-box
|
||||
|
||||
虚拟设备名称,默认自动选择。
|
||||
|
||||
#### netns
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux。
|
||||
|
||||
在指定的网络命名空间中创建 tun 接口,可以是名称、路径,或[网络命名空间](/zh/configuration/network-namespace/)的标签。
|
||||
|
||||
设置后,`auto_route` 和 `auto_redirect` 在该命名空间内生效;若命名空间归当前用户所有,则无需 root 权限。
|
||||
|
||||
与 `platform` 冲突。
|
||||
|
||||
#### address
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
@@ -11,6 +11,7 @@ sing-box uses JSON for configuration files.
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"network_namespaces": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -30,6 +31,7 @@ sing-box uses JSON for configuration files.
|
||||
| `certificate` | [Certificate](./certificate/) |
|
||||
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP Client](./shared/http-client/) |
|
||||
| `network_namespaces` | [Network Namespace](./network-namespace/) |
|
||||
| `endpoints` | [Endpoint](./endpoint/) |
|
||||
| `inbounds` | [Inbound](./inbound/) |
|
||||
| `outbounds` | [Outbound](./outbound/) |
|
||||
|
||||
@@ -11,6 +11,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"network_namespaces": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -30,6 +31,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
| `certificate` | [证书](./certificate/) |
|
||||
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
|
||||
| `network_namespaces` | [网络命名空间](./network-namespace/) |
|
||||
| `endpoints` | [端点](./endpoint/) |
|
||||
| `inbounds` | [入站](./inbound/) |
|
||||
| `outbounds` | [出站](./outbound/) |
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
# Default
|
||||
|
||||
Attach to an existing network namespace.
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"network_namespaces": [
|
||||
{
|
||||
"type": "default", // optional
|
||||
"tag": "",
|
||||
"path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### path
|
||||
|
||||
==Required==
|
||||
|
||||
Name or path of the network namespace, for example `sing` or `/run/netns/sing`.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
# Default
|
||||
|
||||
附加到已存在的网络命名空间。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"network_namespaces": [
|
||||
{
|
||||
"type": "default", // 可选
|
||||
"tag": "",
|
||||
"path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### path
|
||||
|
||||
==必填==
|
||||
|
||||
网络命名空间的名称或路径,例如 `sing` 或 `/run/netns/sing`。
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux.
|
||||
|
||||
# Network Namespace
|
||||
|
||||
Network namespaces let inbounds and outbounds run inside a separate Linux network namespace,
|
||||
referenced by tag from the [tun](/configuration/inbound/tun/#netns),
|
||||
[Listen Fields](/configuration/shared/listen/#netns) and [Dial Fields](/configuration/shared/dial/#netns).
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"network_namespaces": [
|
||||
{
|
||||
"type": "",
|
||||
"tag": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### type
|
||||
|
||||
The type of the network namespace, `default` is used by default.
|
||||
|
||||
| Type | Format |
|
||||
|-----------|------------------------|
|
||||
| `default` | [Default](./default/) |
|
||||
| `unshare` | [Unshare](./unshare/) |
|
||||
|
||||
#### tag
|
||||
|
||||
==Required==
|
||||
|
||||
The tag of the network namespace.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux。
|
||||
|
||||
# 网络命名空间
|
||||
|
||||
网络命名空间使入站和出站可以运行在独立的 Linux 网络命名空间中,
|
||||
通过标签从 [tun](/zh/configuration/inbound/tun/#netns)、
|
||||
[监听字段](/zh/configuration/shared/listen/#netns) 和 [拨号字段](/zh/configuration/shared/dial/#netns) 引用。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"network_namespaces": [
|
||||
{
|
||||
"type": "",
|
||||
"tag": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### type
|
||||
|
||||
网络命名空间的类型,默认使用 `default`。
|
||||
|
||||
| 类型 | 格式 |
|
||||
|-----------|------------------------|
|
||||
| `default` | [Default](./default/) |
|
||||
| `unshare` | [Unshare](./unshare/) |
|
||||
|
||||
#### tag
|
||||
|
||||
==必填==
|
||||
|
||||
网络命名空间的标签。
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
# Unshare
|
||||
|
||||
Create a new network namespace, without root privilege.
|
||||
|
||||
!!! info ""
|
||||
|
||||
Rootless operation requires the kernel to allow unprivileged user namespace creation.
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"network_namespaces": [
|
||||
{
|
||||
"type": "unshare",
|
||||
"tag": "",
|
||||
"pid_file": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### pid_file
|
||||
|
||||
If set, the PID of the process holding the namespace open is written to this path.
|
||||
|
||||
The namespace can be entered with `nsenter -t <pid> -n` when sing-box is run as root,
|
||||
or `nsenter -t <pid> -U --preserve-credentials -n` otherwise.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
# Unshare
|
||||
|
||||
创建一个新的网络命名空间,无需 root 权限。
|
||||
|
||||
!!! info ""
|
||||
|
||||
无 root 运行需要内核允许非特权用户创建 user namespace。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"network_namespaces": [
|
||||
{
|
||||
"type": "unshare",
|
||||
"tag": "",
|
||||
"pid_file": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### pid_file
|
||||
|
||||
如果设置,持有该命名空间的进程 PID 将写入此路径。
|
||||
|
||||
当 sing-box 以 root 运行时,可通过 `nsenter -t <pid> -n` 进入该命名空间,
|
||||
否则使用 `nsenter -t <pid> -U --preserve-credentials -n`。
|
||||
@@ -4,7 +4,8 @@ icon: material/new-box
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: [domain_resolver](#domain_resolver)
|
||||
:material-alert: [domain_resolver](#domain_resolver)
|
||||
:material-alert: [netns](#netns)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
@@ -118,6 +119,9 @@ Reuse listener address.
|
||||
|
||||
Set network namespace, name or path.
|
||||
|
||||
Since sing-box 1.14.0, the tag of a [network namespace](/configuration/network-namespace/) can also be used.
|
||||
Referencing an `unshare` network namespace should be avoided, since its only route out is the tun interface managed by sing-box itself.
|
||||
|
||||
#### connect_timeout
|
||||
|
||||
Connect timeout, in golang's Duration format.
|
||||
|
||||
@@ -4,7 +4,8 @@ icon: material/new-box
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: [domain_resolver](#domain_resolver)
|
||||
:material-alert: [domain_resolver](#domain_resolver)
|
||||
:material-alert: [netns](#netns)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
@@ -118,6 +119,9 @@ icon: material/new-box
|
||||
|
||||
设置网络命名空间,名称或路径。
|
||||
|
||||
自 sing-box 1.14.0 起,也可以使用[网络命名空间](/zh/configuration/network-namespace/)的标签。
|
||||
应避免引用 `unshare` 类型的网络命名空间,因为其唯一出口是由 sing-box 自身管理的 tun 接口。
|
||||
|
||||
#### connect_timeout
|
||||
|
||||
连接超时,采用 golang 的 Duration 格式。
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: [netns](#netns)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
|
||||
@@ -97,6 +101,8 @@ Reuse listener address.
|
||||
|
||||
Set network namespace, name or path.
|
||||
|
||||
Since sing-box 1.14.0, the tag of a [network namespace](/configuration/network-namespace/) can also be used.
|
||||
|
||||
#### tcp_fast_open
|
||||
|
||||
Enable TCP Fast Open.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: [netns](#netns)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
|
||||
@@ -97,6 +101,8 @@ icon: material/new-box
|
||||
|
||||
设置网络命名空间,名称或路径。
|
||||
|
||||
自 sing-box 1.14.0 起,也可以使用[网络命名空间](/zh/configuration/network-namespace/)的标签。
|
||||
|
||||
#### tcp_fast_open
|
||||
|
||||
启用 TCP Fast Open。
|
||||
|
||||
@@ -51,7 +51,7 @@ require (
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1
|
||||
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260710042924-375e9ae639c5
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260710165757-8c8594272daa
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1
|
||||
|
||||
@@ -286,8 +286,8 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG
|
||||
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg=
|
||||
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260710042924-375e9ae639c5 h1:kL9E3UR9BRTH3ESiI519idZ2FZtObbFs8vlLwfJCt/g=
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260710042924-375e9ae639c5/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260710165757-8c8594272daa h1:NnzWGTMB9OcGctzeJRSSjWCOBx/x7kQPWaNsqNe5q5k=
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260710165757-8c8594272daa/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
|
||||
|
||||
@@ -139,6 +139,10 @@ nav:
|
||||
- TCP Brutal: configuration/shared/tcp-brutal.md
|
||||
- Wi-Fi State: configuration/shared/wifi-state.md
|
||||
- Neighbor Resolution: configuration/shared/neighbor.md
|
||||
- Network Namespace:
|
||||
- configuration/network-namespace/index.md
|
||||
- Default: configuration/network-namespace/default.md
|
||||
- Unshare: configuration/network-namespace/unshare.md
|
||||
- Endpoint:
|
||||
- configuration/endpoint/index.md
|
||||
- WireGuard: configuration/endpoint/wireguard.md
|
||||
@@ -296,6 +300,7 @@ plugins:
|
||||
V2Ray Transport: V2Ray 传输层
|
||||
Wi-Fi State: Wi-Fi 状态
|
||||
|
||||
Network Namespace: 网络命名空间
|
||||
Endpoint: 端点
|
||||
Inbound: 入站
|
||||
Outbound: 出站
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type _NetworkNamespace struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Tag string `json:"tag"`
|
||||
DefaultOptions DefaultNetworkNamespaceOptions `json:"-"`
|
||||
UnshareOptions UnshareNetworkNamespaceOptions `json:"-"`
|
||||
}
|
||||
|
||||
type NetworkNamespace _NetworkNamespace
|
||||
|
||||
func (o NetworkNamespace) MarshalJSON() ([]byte, error) {
|
||||
var v any
|
||||
switch o.Type {
|
||||
case C.NetNsTypeDefault:
|
||||
o.Type = ""
|
||||
v = o.DefaultOptions
|
||||
case C.NetNsTypeUnshare:
|
||||
v = o.UnshareOptions
|
||||
default:
|
||||
return nil, E.New("unknown network namespace type: ", o.Type)
|
||||
}
|
||||
return badjson.MarshallObjects((_NetworkNamespace)(o), v)
|
||||
}
|
||||
|
||||
func (o *NetworkNamespace) UnmarshalJSON(content []byte) error {
|
||||
err := json.Unmarshal(content, (*_NetworkNamespace)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v any
|
||||
switch o.Type {
|
||||
case "", C.NetNsTypeDefault:
|
||||
o.Type = C.NetNsTypeDefault
|
||||
v = &o.DefaultOptions
|
||||
case C.NetNsTypeUnshare:
|
||||
v = &o.UnshareOptions
|
||||
default:
|
||||
return E.New("unknown network namespace type: ", o.Type)
|
||||
}
|
||||
return badjson.UnmarshallExcluded(content, (*_NetworkNamespace)(o), v)
|
||||
}
|
||||
|
||||
type DefaultNetworkNamespaceOptions struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type UnshareNetworkNamespaceOptions struct {
|
||||
PidFile string `json:"pid_file,omitempty"`
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type _Options struct {
|
||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||
CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
|
||||
HTTPClients []HTTPClient `json:"http_clients,omitempty"`
|
||||
NetworkNamespaces []NetworkNamespace `json:"network_namespaces,omitempty"`
|
||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
type TunInboundOptions struct {
|
||||
InterfaceName string `json:"interface_name,omitempty"`
|
||||
NetNs string `json:"netns,omitempty"`
|
||||
MTU uint32 `json:"mtu,omitempty"`
|
||||
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
||||
DNSMode string `json:"dns_mode,omitempty"`
|
||||
|
||||
+15
-3
@@ -98,6 +98,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
})
|
||||
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
if options.NetNs != "" && !C.IsLinux {
|
||||
return nil, E.New("`netns` is only supported on Linux")
|
||||
}
|
||||
tunMTU := options.MTU
|
||||
if tunMTU == 0 {
|
||||
if platformInterface != nil && platformInterface.UnderNetworkExtension() {
|
||||
@@ -190,6 +193,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
logger: logger,
|
||||
tunOptions: tun.Options{
|
||||
Name: options.InterfaceName,
|
||||
NetNs: options.NetNs,
|
||||
MTU: tunMTU,
|
||||
GSO: enableGSO,
|
||||
Inet4Address: inet4Address,
|
||||
@@ -266,9 +270,11 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
}
|
||||
if !C.IsAndroid {
|
||||
inbound.tunOptions.AutoRedirectMarkMode = true
|
||||
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if options.NetNs == "" {
|
||||
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +361,12 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
||||
if t.tunOptions.Name == "" {
|
||||
t.tunOptions.Name = tun.CalculateInterfaceName("")
|
||||
}
|
||||
if t.tunOptions.NetNs != "" {
|
||||
manager := service.FromContext[adapter.NetworkNamespaceManager](t.ctx)
|
||||
if manager != nil {
|
||||
t.tunOptions.NetNs = manager.ResolvePath(t.tunOptions.NetNs)
|
||||
}
|
||||
}
|
||||
if t.platformInterface == nil {
|
||||
t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
|
||||
for _, routeRuleSet := range t.routeRuleSet {
|
||||
|
||||
Reference in New Issue
Block a user