mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-07-19 21:08:40 +08:00
195 lines
4.7 KiB
Go
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
|
|
}
|