sing-box/common/netns/manager_linux.go
2026-07-11 15:52:42 +08:00

195 lines
4.7 KiB
Go

package netns
import (
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/sagernet/netlink"
"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"
vnetns "github.com/vishvananda/netns"
)
type Manager struct {
logger logger.ContextLogger
namespaces []option.NetworkNamespace
holderArgs []string
paths map[string]string
holders []*holder
}
type holder struct {
command *exec.Cmd
pipeWriter *os.File
pidFile string
}
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
}
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 (m *Manager) ResolvePath(nameOrPath string) string {
path, loaded := m.paths[nameOrPath]
if loaded && path != "" {
return path
}
return nameOrPath
}
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
}