mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
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/certificate"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/httpclient"
|
"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/taskmonitor"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||||
@@ -61,8 +62,9 @@ type Box struct {
|
|||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
option.Options
|
option.Options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
PlatformLogWriter log.PlatformWriter
|
PlatformLogWriter log.PlatformWriter
|
||||||
|
NetworkNamespaceHolderArgs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Context(
|
func Context(
|
||||||
@@ -194,6 +196,12 @@ func New(options Options) (*Box, error) {
|
|||||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
internalServices = append(internalServices, 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)
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
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 {
|
if err != nil {
|
||||||
return option.Options{}, err
|
return option.Options{}, err
|
||||||
}
|
}
|
||||||
|
return mergeOptionsList(optionsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeOptionsList(optionsList []*OptionsEntry) (option.Options, error) {
|
||||||
if len(optionsList) == 1 {
|
if len(optionsList) == 1 {
|
||||||
return optionsList[0].options, nil
|
return optionsList[0].options, nil
|
||||||
}
|
}
|
||||||
var mergedMessage json.RawMessage
|
var (
|
||||||
|
mergedMessage json.RawMessage
|
||||||
|
err error
|
||||||
|
)
|
||||||
for _, options := range optionsList {
|
for _, options := range optionsList {
|
||||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,11 +129,7 @@ func readConfigAndMerge() (option.Options, error) {
|
|||||||
return mergedOptions, nil
|
return mergedOptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func create() (*box.Box, context.CancelFunc, error) {
|
func create(options option.Options) (*box.Box, context.CancelFunc, error) {
|
||||||
options, err := readConfigAndMerge()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if disableColor {
|
if disableColor {
|
||||||
if options.Log == nil {
|
if options.Log == nil {
|
||||||
options.Log = &option.LogOptions{}
|
options.Log = &option.LogOptions{}
|
||||||
@@ -135,8 +138,9 @@ func create() (*box.Box, context.CancelFunc, error) {
|
|||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(globalCtx)
|
ctx, cancel := context.WithCancel(globalCtx)
|
||||||
instance, err := box.New(box.Options{
|
instance, err := box.New(box.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Options: options,
|
Options: options,
|
||||||
|
NetworkNamespaceHolderArgs: []string{"/proc/self/exe", commandNetnsHolder.Use},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
@@ -167,13 +171,25 @@ func create() (*box.Box, context.CancelFunc, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run() 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)
|
osSignals := make(chan os.Signal, 1)
|
||||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
defer signal.Stop(osSignals)
|
defer signal.Stop(osSignals)
|
||||||
for {
|
for {
|
||||||
instance, cancel, err := create()
|
instance, cancel, createErr := create(options)
|
||||||
if err != nil {
|
if createErr != nil {
|
||||||
return err
|
return createErr
|
||||||
}
|
}
|
||||||
runtimeDebug.FreeOSMemory()
|
runtimeDebug.FreeOSMemory()
|
||||||
for {
|
for {
|
||||||
@@ -198,6 +214,10 @@ func run() error {
|
|||||||
}
|
}
|
||||||
break
|
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")
|
return nil, E.New("domain not resolved")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
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) {
|
switch N.NetworkName(network) {
|
||||||
case N.NetworkUDP:
|
case N.NetworkUDP:
|
||||||
if !address.IsIPv6() {
|
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) {
|
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
if d.networkStrategy == nil {
|
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
|
listenConfig := d.udpListener
|
||||||
if d.autoDetectBindFunc != nil && destination.Addr.IsValid() {
|
if d.autoDetectBindFunc != nil && destination.Addr.IsValid() {
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
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"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
"github.com/vishvananda/netns"
|
"github.com/vishvananda/netns"
|
||||||
)
|
)
|
||||||
@@ -143,30 +144,45 @@ func (l *Listener) ListenOptions() option.ListenOptions {
|
|||||||
return l.listenOptions
|
return l.listenOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
|
func ListenNetworkNamespace[T any](ctx context.Context, nameOrPath string, block func() (T, error)) (T, error) {
|
||||||
if nameOrPath != "" {
|
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()
|
runtime.LockOSThread()
|
||||||
defer runtime.UnlockOSThread()
|
value, err := listenNetworkNamespaceThread(nameOrPath, block)
|
||||||
currentNs, err := netns.Get()
|
resultChannel <- blockResult{value, err}
|
||||||
if err != nil {
|
}()
|
||||||
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
result := <-resultChannel
|
||||||
}
|
return result.value, result.err
|
||||||
defer currentNs.Close()
|
}
|
||||||
defer netns.Set(currentNs)
|
|
||||||
var targetNs netns.NsHandle
|
func listenNetworkNamespaceThread[T any](nameOrPath string, block func() (T, error)) (T, error) {
|
||||||
if strings.HasPrefix(nameOrPath, "/") {
|
var (
|
||||||
targetNs, err = netns.GetFromPath(nameOrPath)
|
targetNs netns.NsHandle
|
||||||
} else {
|
err error
|
||||||
targetNs, err = netns.GetFromName(nameOrPath)
|
)
|
||||||
}
|
if strings.HasPrefix(nameOrPath, "/") {
|
||||||
if err != nil {
|
targetNs, err = netns.GetFromPath(nameOrPath)
|
||||||
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
|
} else {
|
||||||
}
|
targetNs, err = netns.GetFromName(nameOrPath)
|
||||||
defer targetNs.Close()
|
}
|
||||||
err = netns.Set(targetNs)
|
if err != nil {
|
||||||
if err != nil {
|
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
|
||||||
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
|
}
|
||||||
}
|
defer targetNs.Close()
|
||||||
|
err = netns.Set(targetNs)
|
||||||
|
if err != nil {
|
||||||
|
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
|
||||||
}
|
}
|
||||||
return block()
|
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 {
|
if l.listenOptions.TCPFastOpen {
|
||||||
var tfoConfig tfo.ListenConfig
|
var tfoConfig tfo.ListenConfig
|
||||||
tfoConfig.ListenConfig = 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())
|
return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
|
||||||
})
|
})
|
||||||
if err != nil {
|
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) {
|
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 != "" {
|
if l.listenOptions.BindInterface != "" {
|
||||||
dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
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) {
|
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 != "" {
|
if l.listenOptions.BindInterface != "" {
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
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: [include_mac_address](#include_mac_address)
|
||||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||||
:material-plus: [dns_mode](#dns_mode)
|
: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"
|
!!! quote "Changes in sing-box 1.13.3"
|
||||||
|
|
||||||
@@ -197,6 +198,22 @@ icon: material/new-box
|
|||||||
|
|
||||||
Virtual device name, automatically selected if empty.
|
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
|
#### address
|
||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! 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: [include_mac_address](#include_mac_address)
|
||||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||||
:material-plus: [dns_mode](#dns_mode)
|
: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 中的更改"
|
!!! 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
|
#### address
|
||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ sing-box uses JSON for configuration files.
|
|||||||
"certificate": {},
|
"certificate": {},
|
||||||
"certificate_providers": [],
|
"certificate_providers": [],
|
||||||
"http_clients": [],
|
"http_clients": [],
|
||||||
|
"network_namespaces": [],
|
||||||
"endpoints": [],
|
"endpoints": [],
|
||||||
"inbounds": [],
|
"inbounds": [],
|
||||||
"outbounds": [],
|
"outbounds": [],
|
||||||
@@ -30,6 +31,7 @@ sing-box uses JSON for configuration files.
|
|||||||
| `certificate` | [Certificate](./certificate/) |
|
| `certificate` | [Certificate](./certificate/) |
|
||||||
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
||||||
| `http_clients` | [HTTP Client](./shared/http-client/) |
|
| `http_clients` | [HTTP Client](./shared/http-client/) |
|
||||||
|
| `network_namespaces` | [Network Namespace](./network-namespace/) |
|
||||||
| `endpoints` | [Endpoint](./endpoint/) |
|
| `endpoints` | [Endpoint](./endpoint/) |
|
||||||
| `inbounds` | [Inbound](./inbound/) |
|
| `inbounds` | [Inbound](./inbound/) |
|
||||||
| `outbounds` | [Outbound](./outbound/) |
|
| `outbounds` | [Outbound](./outbound/) |
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
|||||||
"certificate": {},
|
"certificate": {},
|
||||||
"certificate_providers": [],
|
"certificate_providers": [],
|
||||||
"http_clients": [],
|
"http_clients": [],
|
||||||
|
"network_namespaces": [],
|
||||||
"endpoints": [],
|
"endpoints": [],
|
||||||
"inbounds": [],
|
"inbounds": [],
|
||||||
"outbounds": [],
|
"outbounds": [],
|
||||||
@@ -30,6 +31,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
|||||||
| `certificate` | [证书](./certificate/) |
|
| `certificate` | [证书](./certificate/) |
|
||||||
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
||||||
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
|
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
|
||||||
|
| `network_namespaces` | [网络命名空间](./network-namespace/) |
|
||||||
| `endpoints` | [端点](./endpoint/) |
|
| `endpoints` | [端点](./endpoint/) |
|
||||||
| `inbounds` | [入站](./inbound/) |
|
| `inbounds` | [入站](./inbound/) |
|
||||||
| `outbounds` | [出站](./outbound/) |
|
| `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"
|
!!! 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"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
@@ -118,6 +119,9 @@ Reuse listener address.
|
|||||||
|
|
||||||
Set network namespace, name or path.
|
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
|
||||||
|
|
||||||
Connect timeout, in golang's Duration format.
|
Connect timeout, in golang's Duration format.
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ icon: material/new-box
|
|||||||
|
|
||||||
!!! quote "sing-box 1.14.0 中的更改"
|
!!! 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 中的更改"
|
!!! 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
|
#### connect_timeout
|
||||||
|
|
||||||
连接超时,采用 golang 的 Duration 格式。
|
连接超时,采用 golang 的 Duration 格式。
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.14.0"
|
||||||
|
|
||||||
|
:material-alert: [netns](#netns)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.13.0"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
|
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
|
||||||
@@ -97,6 +101,8 @@ Reuse listener address.
|
|||||||
|
|
||||||
Set network namespace, name or path.
|
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
|
#### tcp_fast_open
|
||||||
|
|
||||||
Enable TCP Fast Open.
|
Enable TCP Fast Open.
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.14.0 中的更改"
|
||||||
|
|
||||||
|
:material-alert: [netns](#netns)
|
||||||
|
|
||||||
!!! quote "sing-box 1.13.0 中的更改"
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
|
: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
|
||||||
|
|
||||||
启用 TCP Fast Open。
|
启用 TCP Fast Open。
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ nav:
|
|||||||
- TCP Brutal: configuration/shared/tcp-brutal.md
|
- TCP Brutal: configuration/shared/tcp-brutal.md
|
||||||
- Wi-Fi State: configuration/shared/wifi-state.md
|
- Wi-Fi State: configuration/shared/wifi-state.md
|
||||||
- Neighbor Resolution: configuration/shared/neighbor.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:
|
- Endpoint:
|
||||||
- configuration/endpoint/index.md
|
- configuration/endpoint/index.md
|
||||||
- WireGuard: configuration/endpoint/wireguard.md
|
- WireGuard: configuration/endpoint/wireguard.md
|
||||||
@@ -297,6 +301,7 @@ plugins:
|
|||||||
V2Ray Transport: V2Ray 传输层
|
V2Ray Transport: V2Ray 传输层
|
||||||
Wi-Fi State: Wi-Fi 状态
|
Wi-Fi State: Wi-Fi 状态
|
||||||
|
|
||||||
|
Network Namespace: 网络命名空间
|
||||||
Endpoint: 端点
|
Endpoint: 端点
|
||||||
Inbound: 入站
|
Inbound: 入站
|
||||||
Outbound: 出站
|
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"`
|
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||||
CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
|
CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
|
||||||
HTTPClients []HTTPClient `json:"http_clients,omitempty"`
|
HTTPClients []HTTPClient `json:"http_clients,omitempty"`
|
||||||
|
NetworkNamespaces []NetworkNamespace `json:"network_namespaces,omitempty"`
|
||||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
type TunInboundOptions struct {
|
type TunInboundOptions struct {
|
||||||
InterfaceName string `json:"interface_name,omitempty"`
|
InterfaceName string `json:"interface_name,omitempty"`
|
||||||
|
NetNs string `json:"netns,omitempty"`
|
||||||
MTU uint32 `json:"mtu,omitempty"`
|
MTU uint32 `json:"mtu,omitempty"`
|
||||||
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
||||||
DNSMode string `json:"dns_mode,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)
|
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||||
|
if options.NetNs != "" && !C.IsLinux {
|
||||||
|
return nil, E.New("`netns` is only supported on Linux")
|
||||||
|
}
|
||||||
tunMTU := options.MTU
|
tunMTU := options.MTU
|
||||||
if tunMTU == 0 {
|
if tunMTU == 0 {
|
||||||
if platformInterface != nil && platformInterface.UnderNetworkExtension() {
|
if platformInterface != nil && platformInterface.UnderNetworkExtension() {
|
||||||
@@ -190,6 +193,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
tunOptions: tun.Options{
|
tunOptions: tun.Options{
|
||||||
Name: options.InterfaceName,
|
Name: options.InterfaceName,
|
||||||
|
NetNs: options.NetNs,
|
||||||
MTU: tunMTU,
|
MTU: tunMTU,
|
||||||
GSO: enableGSO,
|
GSO: enableGSO,
|
||||||
Inet4Address: inet4Address,
|
Inet4Address: inet4Address,
|
||||||
@@ -266,9 +270,11 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
}
|
}
|
||||||
if !C.IsAndroid {
|
if !C.IsAndroid {
|
||||||
inbound.tunOptions.AutoRedirectMarkMode = true
|
inbound.tunOptions.AutoRedirectMarkMode = true
|
||||||
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
if options.NetNs == "" {
|
||||||
if err != nil {
|
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,6 +361,12 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
if t.tunOptions.Name == "" {
|
if t.tunOptions.Name == "" {
|
||||||
t.tunOptions.Name = tun.CalculateInterfaceName("")
|
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 {
|
if t.platformInterface == nil {
|
||||||
for _, routeRuleSet := range t.routeRuleSet {
|
for _, routeRuleSet := range t.routeRuleSet {
|
||||||
ipSets := routeRuleSet.ExtractIPSet()
|
ipSets := routeRuleSet.ExtractIPSet()
|
||||||
|
|||||||
Reference in New Issue
Block a user