diff --git a/protocol/tailscale/tailssh/server_helper_unix.go b/protocol/tailscale/tailssh/server_helper_unix.go index 880759b03..d595d0240 100644 --- a/protocol/tailscale/tailssh/server_helper_unix.go +++ b/protocol/tailscale/tailssh/server_helper_unix.go @@ -11,6 +11,7 @@ import ( gliderssh "github.com/sagernet/gliderssh" "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" ) func isPrivilegedUser() bool { @@ -21,10 +22,21 @@ func requestedUserMatchesProcess(localUser *adapter.PlatformUser) (bool, error) return localUser.Uid == os.Getuid() && localUser.Gid == os.Getgid(), nil } -// verifyShellIdentity is a no-op on Unix: spawned shells and sftp-server drop to the -// requested user via setCredential, so the child already runs as that user. -func verifyShellIdentity(_ adapter.PlatformInterface, _ *adapter.PlatformUser) error { - return nil +func verifyShellIdentity(platformInterface adapter.PlatformInterface, localUser *adapter.PlatformUser) error { + if platformInterface != nil && platformInterface.UsePlatformShell() { + return nil + } + match, err := requestedUserMatchesProcess(localUser) + if err != nil { + return err + } + if match { + return nil + } + if !isPrivilegedUser() { + return E.New("switching to local user ", localUser.Username, " requires sing-box to run as root") + } + return checkSwitchUserPrivilege(localUser) } func systemHostKeyPath() string { diff --git a/protocol/tailscale/tailssh/switch_user_linux.go b/protocol/tailscale/tailssh/switch_user_linux.go new file mode 100644 index 000000000..75441d9da --- /dev/null +++ b/protocol/tailscale/tailssh/switch_user_linux.go @@ -0,0 +1,24 @@ +//go:build with_gvisor && linux + +package tailssh + +import ( + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/unix" +) + +func checkSwitchUserPrivilege(localUser *adapter.PlatformUser) error { + header := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3} + var data [2]unix.CapUserData + err := unix.Capget(&header, &data[0]) + if err != nil { + return E.Cause(err, "query process capabilities") + } + const required = 1<