sing-box/protocol/tailscale/tailssh/subprocess_unix.go
2026-06-25 17:38:52 +08:00

99 lines
2.8 KiB
Go

//go:build unix
package tailssh
import (
"os"
"os/exec"
"syscall"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
"github.com/creack/pty"
"golang.org/x/sys/unix"
)
func StartPtyProcess(shell string, args, env []string, dir string, uid, gid int, groups []int, rows, cols uint16) (*os.File, *os.Process, error) {
cmd := exec.Command(shell)
cmd.Args = args
cmd.Dir = dir
cmd.Env = env
attrs := &syscall.SysProcAttr{
Setsid: true,
Setctty: true,
Ctty: 0,
}
setCredential(attrs, uid, gid, groups)
var size *pty.Winsize
if rows > 0 && cols > 0 {
size = &pty.Winsize{Rows: rows, Cols: cols}
}
master, err := pty.StartWithAttrs(cmd, size, attrs)
if err != nil {
return nil, nil, err
}
return master, cmd.Process, nil
}
func StartSocketpairProcess(shell string, args, env []string, dir string, uid, gid int, groups []int) (*os.File, *os.Process, error) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
return nil, nil, E.Cause(err, "socketpair")
}
syscall.CloseOnExec(fds[0])
syscall.CloseOnExec(fds[1])
childFile := os.NewFile(uintptr(fds[1]), "socketpair-child")
cmd := exec.Command(shell)
cmd.Args = args
cmd.Dir = dir
cmd.Env = env
cmd.Stdin = childFile
cmd.Stdout = childFile
cmd.Stderr = childFile
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
setCredential(cmd.SysProcAttr, uid, gid, groups)
err = cmd.Start()
childFile.Close()
if err != nil {
syscall.Close(fds[0])
return nil, nil, err
}
return os.NewFile(uintptr(fds[0]), "socketpair-parent"), cmd.Process, nil
}
func setCredential(attr *syscall.SysProcAttr, uid, gid int, groups []int) {
if uid < 0 {
return
}
// Skip only when the target identity already matches the server: a non-root
// server cannot setgroups/setgid, so attempting it would only fail the exec.
// When the gid differs (a privileged server dropping to another group) we
// still apply the credential so supplementary groups are reset.
if uid == os.Getuid() && gid == os.Getgid() {
return
}
// macOS and iOS reject setgroups with more than 16 groups (EINVAL), which
// fails the exec; cap to the first 16.
if C.IsDarwin && len(groups) > 16 {
groups = groups[:16]
}
cred := &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
}
// Always call setgroups when dropping privileges: an empty slice clears the
// parent's supplementary groups. Leaving NoSetGroups set here would make a
// child dropped from root retain root's supplementary groups (wheel/sudo/...).
cred.Groups = make([]uint32, len(groups))
for i, g := range groups {
cred.Groups[i] = uint32(g)
}
attr.Credential = cred
}
func SetWinsize(fd int, rows, cols uint16) error {
return unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{Row: rows, Col: cols})
}