mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-28 12:02:23 +00:00
ssh/tailssh: add plan9 incubator
wgengine/router: add missing router file Change-Id: I1b6573ea5e2d3ab23f1d297209f822adee7b10e0
This commit is contained in:
parent
f3011fce04
commit
dd3d4e3fe9
@ -82,7 +82,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
LD github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
|
||||
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled+
|
||||
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
|
||||
|
2
go.mod
2
go.mod
@ -21,6 +21,7 @@ require (
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creachadair/taskgroup v0.13.2
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
||||
github.com/distribution/reference v0.6.0
|
||||
@ -88,6 +89,7 @@ require (
|
||||
github.com/tc-hib/winres v0.2.1
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
github.com/u-root/u-root v0.14.0
|
||||
github.com/vishvananda/netns v0.0.4
|
||||
go.uber.org/zap v1.27.0
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||
|
8
go.sum
8
go.sum
@ -234,6 +234,8 @@ github.com/creachadair/mds v0.17.1/go.mod h1:4b//mUiL8YldH6TImXjmW45myzTLNS1LLjO
|
||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo=
|
||||
github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc=
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
|
||||
@ -543,6 +545,8 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1 h1:jWoR2Yqg8tzM0v6LAiP7i1bikZJu3gxpgvu3g1Lw+a0=
|
||||
github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
||||
@ -946,6 +950,10 @@ github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a h1:eg5FkNoQp76ZsswyGZ+TjYqA/rhKefxK8BW7XOlQsxo=
|
||||
github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a/go.mod h1:e/8TmrdreH0sZOw2DFKBaUV7bvDWRq6SeM9PzkuVM68=
|
||||
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
|
||||
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
|
@ -1032,14 +1032,6 @@ func (ss *sshSession) startWithStdPipes() (err error) {
|
||||
}
|
||||
|
||||
func envForUser(u *userMeta) []string {
|
||||
if runtime.GOOS == "plan9" {
|
||||
return []string{
|
||||
fmt.Sprintf("shell=%s", u.LoginShell()),
|
||||
"service=ssh",
|
||||
fmt.Sprintf("USER=%s", u.Username),
|
||||
fmt.Sprintf("home=%s", u.HomeDir),
|
||||
}
|
||||
}
|
||||
return []string{
|
||||
fmt.Sprintf("SHELL=%s", u.LoginShell()),
|
||||
fmt.Sprintf("USER=%s", u.Username),
|
||||
@ -1116,7 +1108,7 @@ func (ia *incubatorArgs) loginArgs(loginCmdPath string) []string {
|
||||
|
||||
func shellArgs(isShell bool, cmd string) []string {
|
||||
if isShell {
|
||||
if runtime.GOOS == freebsd || runtime.GOOS == openbsd || runtime.GOOS == "plan9" {
|
||||
if runtime.GOOS == freebsd || runtime.GOOS == openbsd {
|
||||
// bsd shells don't support the "-l" option, so we can't run as a login shell
|
||||
return []string{}
|
||||
}
|
||||
|
486
ssh/tailssh/incubator_plan9.go
Normal file
486
ssh/tailssh/incubator_plan9.go
Normal file
@ -0,0 +1,486 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// This file contains the plan9-specific version of the incubator. Tailscaled
|
||||
// launches the incubator as the same user as it was launched as. The
|
||||
// incubator then registers a new session with the OS, sets its UID
|
||||
// and groups to the specified `--uid`, `--gid` and `--groups`, and
|
||||
// then launches the requested `--cmd`.
|
||||
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
childproc.Add("ssh", beIncubator)
|
||||
childproc.Add("sftp", beSFTP)
|
||||
}
|
||||
|
||||
// maybeStartLoginSession informs the system that we are about to log someone
|
||||
// in. On success, it may return a non-nil close func which must be closed to
|
||||
// release the session.
|
||||
// We can only do this if we are running as root.
|
||||
// This is best effort to still allow running on machines where
|
||||
// we don't support starting sessions, e.g. darwin.
|
||||
// See maybeStartLoginSessionLinux.
|
||||
var maybeStartLoginSession = func(dlogf logger.Logf, ia incubatorArgs) (close func() error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// newIncubatorCommand returns a new exec.Cmd configured with
|
||||
// `tailscaled be-child ssh` as the entrypoint.
|
||||
//
|
||||
// If ss.srv.tailscaledPath is empty, this method is equivalent to
|
||||
// exec.CommandContext.
|
||||
//
|
||||
// The returned Cmd.Env is guaranteed to be nil; the caller populates it.
|
||||
func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err error) {
|
||||
defer func() {
|
||||
if cmd.Env != nil {
|
||||
panic("internal error")
|
||||
}
|
||||
}()
|
||||
|
||||
var isSFTP, isShell bool
|
||||
switch ss.Subsystem() {
|
||||
case "sftp":
|
||||
isSFTP = true
|
||||
case "":
|
||||
isShell = ss.RawCommand() == ""
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected subsystem: %v", ss.Subsystem()))
|
||||
}
|
||||
|
||||
if ss.conn.srv.tailscaledPath == "" {
|
||||
if isSFTP {
|
||||
// SFTP relies on the embedded Go-based SFTP server in tailscaled,
|
||||
// so without tailscaled, we can't serve SFTP.
|
||||
return nil, errors.New("no tailscaled found on path, can't serve SFTP")
|
||||
}
|
||||
|
||||
loginShell := ss.conn.localUser.LoginShell()
|
||||
args := shellArgs(isShell, ss.RawCommand())
|
||||
logf("directly running %s %q", loginShell, args)
|
||||
return exec.CommandContext(ss.ctx, loginShell, args...), nil
|
||||
}
|
||||
|
||||
lu := ss.conn.localUser
|
||||
ci := ss.conn.info
|
||||
groups := strings.Join(ss.conn.userGroupIDs, ",")
|
||||
remoteUser := ci.uprof.LoginName
|
||||
if ci.node.IsTagged() {
|
||||
remoteUser = strings.Join(ci.node.Tags().AsSlice(), ",")
|
||||
}
|
||||
|
||||
incubatorArgs := []string{
|
||||
"be-child",
|
||||
"ssh",
|
||||
"--login-shell=" + lu.LoginShell(),
|
||||
"--uid=" + lu.Uid,
|
||||
"--gid=" + lu.Gid,
|
||||
"--groups=" + groups,
|
||||
"--local-user=" + lu.Username,
|
||||
"--home-dir=" + lu.HomeDir,
|
||||
"--remote-user=" + remoteUser,
|
||||
"--remote-ip=" + ci.src.Addr().String(),
|
||||
"--has-tty=false", // updated in-place by startWithPTY
|
||||
"--tty-name=", // updated in-place by startWithPTY
|
||||
}
|
||||
|
||||
nm := ss.conn.srv.lb.NetMap()
|
||||
forceV1Behavior := nm.HasCap(tailcfg.NodeAttrSSHBehaviorV1) && !nm.HasCap(tailcfg.NodeAttrSSHBehaviorV2)
|
||||
if forceV1Behavior {
|
||||
incubatorArgs = append(incubatorArgs, "--force-v1-behavior")
|
||||
}
|
||||
|
||||
if debugTest.Load() {
|
||||
incubatorArgs = append(incubatorArgs, "--debug-test")
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSFTP:
|
||||
// Note that we include both the `--sftp` flag and a command to launch
|
||||
// tailscaled as `be-child sftp`. If login or su is available, and
|
||||
// we're not running with tailcfg.NodeAttrSSHBehaviorV1, this will
|
||||
// result in serving SFTP within a login shell, with full PAM
|
||||
// integration. Otherwise, we'll serve SFTP in the incubator process
|
||||
// with no PAM integration.
|
||||
incubatorArgs = append(incubatorArgs, "--sftp", fmt.Sprintf("--cmd=%s be-child sftp", ss.conn.srv.tailscaledPath))
|
||||
case isShell:
|
||||
incubatorArgs = append(incubatorArgs, "--shell")
|
||||
default:
|
||||
incubatorArgs = append(incubatorArgs, "--cmd="+ss.RawCommand())
|
||||
}
|
||||
|
||||
allowSendEnv := nm.HasCap(tailcfg.NodeAttrSSHEnvironmentVariables)
|
||||
if allowSendEnv {
|
||||
env, err := filterEnv(ss.conn.acceptEnv, ss.Session.Environ())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(env) > 0 {
|
||||
encoded, err := json.Marshal(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode environment: %w", err)
|
||||
}
|
||||
incubatorArgs = append(incubatorArgs, fmt.Sprintf("--encoded-env=%q", encoded))
|
||||
}
|
||||
}
|
||||
|
||||
return exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...), nil
|
||||
}
|
||||
|
||||
var debugTest atomic.Bool
|
||||
|
||||
type stdRWC struct{}
|
||||
|
||||
func (stdRWC) Read(p []byte) (n int, err error) {
|
||||
return os.Stdin.Read(p)
|
||||
}
|
||||
|
||||
func (stdRWC) Write(b []byte) (n int, err error) {
|
||||
return os.Stdout.Write(b)
|
||||
}
|
||||
|
||||
func (stdRWC) Close() error {
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
type incubatorArgs struct {
|
||||
loginShell string
|
||||
uid int
|
||||
gid int
|
||||
gids []int
|
||||
localUser string
|
||||
homeDir string
|
||||
remoteUser string
|
||||
remoteIP string
|
||||
ttyName string
|
||||
hasTTY bool
|
||||
cmd string
|
||||
isSFTP bool
|
||||
isShell bool
|
||||
forceV1Behavior bool
|
||||
debugTest bool
|
||||
isSELinuxEnforcing bool
|
||||
encodedEnv string
|
||||
}
|
||||
|
||||
func parseIncubatorArgs(args []string) (incubatorArgs, error) {
|
||||
var ia incubatorArgs
|
||||
var groups string
|
||||
|
||||
flags := flag.NewFlagSet("", flag.ExitOnError)
|
||||
flags.StringVar(&ia.loginShell, "login-shell", "", "path to the user's preferred login shell")
|
||||
flags.IntVar(&ia.uid, "uid", 0, "the uid of local-user")
|
||||
flags.IntVar(&ia.gid, "gid", 0, "the gid of local-user")
|
||||
flags.StringVar(&groups, "groups", "", "comma-separated list of gids of local-user")
|
||||
flags.StringVar(&ia.localUser, "local-user", "", "the user to run as")
|
||||
flags.StringVar(&ia.homeDir, "home-dir", "/", "the user's home directory")
|
||||
flags.StringVar(&ia.remoteUser, "remote-user", "", "the remote user/tags")
|
||||
flags.StringVar(&ia.remoteIP, "remote-ip", "", "the remote Tailscale IP")
|
||||
flags.StringVar(&ia.ttyName, "tty-name", "", "the tty name (pts/3)")
|
||||
flags.BoolVar(&ia.hasTTY, "has-tty", false, "is the output attached to a tty")
|
||||
flags.StringVar(&ia.cmd, "cmd", "", "the cmd to launch, including all arguments (ignored in sftp mode)")
|
||||
flags.BoolVar(&ia.isShell, "shell", false, "is launching a shell (with no cmds)")
|
||||
flags.BoolVar(&ia.isSFTP, "sftp", false, "run sftp server (cmd is ignored)")
|
||||
flags.BoolVar(&ia.forceV1Behavior, "force-v1-behavior", false, "allow falling back to the su command if login is unavailable")
|
||||
flags.BoolVar(&ia.debugTest, "debug-test", false, "should debug in test mode")
|
||||
flags.BoolVar(&ia.isSELinuxEnforcing, "is-selinux-enforcing", false, "whether SELinux is in enforcing mode")
|
||||
flags.StringVar(&ia.encodedEnv, "encoded-env", "", "JSON encoded array of environment variables in '['key=value']' format")
|
||||
flags.Parse(args)
|
||||
|
||||
for _, g := range strings.Split(groups, ",") {
|
||||
gid, err := strconv.Atoi(g)
|
||||
if err != nil {
|
||||
return ia, fmt.Errorf("unable to parse group id %q: %w", g, err)
|
||||
}
|
||||
ia.gids = append(ia.gids, gid)
|
||||
}
|
||||
|
||||
return ia, nil
|
||||
}
|
||||
|
||||
func (ia incubatorArgs) forwadedEnviron() ([]string, string, error) {
|
||||
environ := os.Environ()
|
||||
// pass through SSH_AUTH_SOCK environment variable to support ssh agent forwarding
|
||||
allowListKeys := "SSH_AUTH_SOCK"
|
||||
|
||||
if ia.encodedEnv != "" {
|
||||
unquoted, err := strconv.Unquote(ia.encodedEnv)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("unable to parse encodedEnv %q: %w", ia.encodedEnv, err)
|
||||
}
|
||||
|
||||
var extraEnviron []string
|
||||
|
||||
err = json.Unmarshal([]byte(unquoted), &extraEnviron)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("unable to parse encodedEnv %q: %w", ia.encodedEnv, err)
|
||||
}
|
||||
|
||||
environ = append(environ, extraEnviron...)
|
||||
|
||||
for _, v := range extraEnviron {
|
||||
allowListKeys = fmt.Sprintf("%s,%s", allowListKeys, strings.Split(v, "=")[0])
|
||||
}
|
||||
}
|
||||
|
||||
return environ, allowListKeys, nil
|
||||
}
|
||||
|
||||
// beIncubator is the entrypoint to the `tailscaled be-child ssh` subcommand.
|
||||
// It is responsible for informing the system of a new login session for the
|
||||
// user. This is sometimes necessary for mounting home directories and
|
||||
// decrypting file systems.
|
||||
//
|
||||
// Tailscaled launches the incubator as the same user as it was launched as.
|
||||
func beIncubator(args []string) error {
|
||||
// To defend against issues like https://golang.org/issue/1435,
|
||||
// defensively lock our current goroutine's thread to the current
|
||||
// system thread before we start making any UID/GID/group changes.
|
||||
//
|
||||
// This shouldn't matter on Linux because syscall.AllThreadsSyscall is
|
||||
// used to invoke syscalls on all OS threads, but (as of 2023-03-23)
|
||||
// that function is not implemented on all platforms.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
ia, err := parseIncubatorArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ia.isSFTP && ia.isShell {
|
||||
return fmt.Errorf("--sftp and --shell are mutually exclusive")
|
||||
}
|
||||
|
||||
dlogf := logger.Discard
|
||||
if ia.debugTest {
|
||||
// In testing, we don't always have syslog, so log to a temp file.
|
||||
if logFile, err := os.OpenFile("/tmp/tailscalessh.log", os.O_APPEND|os.O_WRONLY, 0666); err == nil {
|
||||
lf := log.New(logFile, "", 0)
|
||||
dlogf = func(msg string, args ...any) {
|
||||
lf.Printf(msg, args...)
|
||||
logFile.Sync()
|
||||
}
|
||||
defer logFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return handleInProcess(dlogf, ia)
|
||||
}
|
||||
|
||||
func handleInProcess(dlogf logger.Logf, ia incubatorArgs) error {
|
||||
if ia.isSFTP {
|
||||
return handleSFTPInProcess(dlogf, ia)
|
||||
}
|
||||
return handleSSHInProcess(dlogf, ia)
|
||||
}
|
||||
|
||||
func handleSFTPInProcess(dlogf logger.Logf, ia incubatorArgs) error {
|
||||
dlogf("handling sftp")
|
||||
|
||||
sessionCloser := maybeStartLoginSession(dlogf, ia)
|
||||
if sessionCloser != nil {
|
||||
defer sessionCloser()
|
||||
}
|
||||
|
||||
return serveSFTP()
|
||||
}
|
||||
|
||||
// beSFTP serves SFTP in-process.
|
||||
func beSFTP(args []string) error {
|
||||
return serveSFTP()
|
||||
}
|
||||
|
||||
func serveSFTP() error {
|
||||
server, err := sftp.NewServer(stdRWC{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(https://github.com/pkg/sftp/pull/554): Revert the check for io.EOF,
|
||||
// when sftp is patched to report clean termination.
|
||||
if err := server.Serve(); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSSHInProcess is a last resort if we couldn't use login or su. It
|
||||
// registers a new session with the OS, sets its UID, GID and groups to the
|
||||
// specified values, and then launches the requested `--cmd` in the user's
|
||||
// login shell.
|
||||
func handleSSHInProcess(dlogf logger.Logf, ia incubatorArgs) error {
|
||||
sessionCloser := maybeStartLoginSession(dlogf, ia)
|
||||
if sessionCloser != nil {
|
||||
defer sessionCloser()
|
||||
}
|
||||
|
||||
environ, _, err := ia.forwadedEnviron()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := shellArgs(ia.isShell, ia.cmd)
|
||||
dlogf("running %s %q", ia.loginShell, args)
|
||||
cmd := newCommand(ia.hasTTY, ia.loginShell, environ, args)
|
||||
err = cmd.Run()
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
ps := ee.ProcessState
|
||||
code := ps.ExitCode()
|
||||
if code < 0 {
|
||||
// TODO(bradfitz): do we need to also check the syscall.WaitStatus
|
||||
// and make our process look like it also died by signal/same signal
|
||||
// as our child process? For now we just do the exit code.
|
||||
fmt.Fprintf(os.Stderr, "[tailscale-ssh: process died: %v]\n", ps.String())
|
||||
code = 1 // for now. so we don't exit with negative
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func newCommand(hasTTY bool, cmdPath string, cmdEnviron []string, cmdArgs []string) *exec.Cmd {
|
||||
cmd := exec.Command(cmdPath, cmdArgs...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = cmdEnviron
|
||||
|
||||
if hasTTY {
|
||||
// If we were launched with a tty then we should mark that as the ctty
|
||||
// of the child. However, as the ctty is being passed from the parent
|
||||
// we set the child to foreground instead which also passes the ctty.
|
||||
// However, we can not do this if never had a tty to begin with.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
// XXX TODO
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
const (
|
||||
// This controls whether we assert that our privileges were dropped
|
||||
// using geteuid/getegid; it's a const and not an envknob because the
|
||||
// incubator doesn't see the parent's environment.
|
||||
//
|
||||
// TODO(andrew): remove this const and always do this after sufficient
|
||||
// testing, e.g. the 1.40 release
|
||||
assertPrivilegesWereDropped = true
|
||||
|
||||
// TODO(andrew-d): verify that this works in more configurations before
|
||||
// enabling by default.
|
||||
assertPrivilegesWereDroppedByAttemptingToUnDrop = false
|
||||
)
|
||||
|
||||
// launchProcess launches an incubator process for the provided session.
|
||||
// It is responsible for configuring the process execution environment.
|
||||
// The caller can wait for the process to exit by calling cmd.Wait().
|
||||
//
|
||||
// It sets ss.cmd, stdin, stdout, and stderr.
|
||||
func (ss *sshSession) launchProcess() error {
|
||||
var err error
|
||||
ss.cmd, err = ss.newIncubatorCommand(ss.logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := ss.cmd
|
||||
cmd.Dir = "/"
|
||||
cmd.Env = envForUser(ss.conn.localUser)
|
||||
for _, kv := range ss.Environ() {
|
||||
if acceptEnvPair(kv) {
|
||||
cmd.Env = append(cmd.Env, kv)
|
||||
}
|
||||
}
|
||||
|
||||
ci := ss.conn.info
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.Addr(), ci.src.Port(), ci.dst.Port()),
|
||||
fmt.Sprintf("SSH_CONNECTION=%s %d %s %d", ci.src.Addr(), ci.src.Port(), ci.dst.Addr(), ci.dst.Port()),
|
||||
)
|
||||
|
||||
if ss.agentListener != nil {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_AUTH_SOCK=%s", ss.agentListener.Addr()))
|
||||
}
|
||||
|
||||
return ss.startWithStdPipes()
|
||||
}
|
||||
|
||||
// startWithStdPipes starts cmd with os.Pipe for Stdin, Stdout and Stderr.
|
||||
func (ss *sshSession) startWithStdPipes() (err error) {
|
||||
var rdStdin, wrStdout, wrStderr io.ReadWriteCloser
|
||||
defer func() {
|
||||
if err != nil {
|
||||
closeAll(rdStdin, ss.wrStdin, ss.rdStdout, wrStdout, ss.rdStderr, wrStderr)
|
||||
}
|
||||
}()
|
||||
if ss.cmd == nil {
|
||||
return errors.New("nil cmd")
|
||||
}
|
||||
if rdStdin, ss.wrStdin, err = os.Pipe(); err != nil {
|
||||
return err
|
||||
}
|
||||
if ss.rdStdout, wrStdout, err = os.Pipe(); err != nil {
|
||||
return err
|
||||
}
|
||||
if ss.rdStderr, wrStderr, err = os.Pipe(); err != nil {
|
||||
return err
|
||||
}
|
||||
ss.cmd.Stdin = rdStdin
|
||||
ss.cmd.Stdout = wrStdout
|
||||
ss.cmd.Stderr = wrStderr
|
||||
ss.childPipes = []io.Closer{rdStdin, wrStdout, wrStderr}
|
||||
return ss.cmd.Start()
|
||||
}
|
||||
|
||||
func envForUser(u *userMeta) []string {
|
||||
// XXX TODO(bradfitz): fix this for plan9
|
||||
return []string{
|
||||
fmt.Sprintf("SHELL=%s", u.LoginShell()),
|
||||
fmt.Sprintf("USER=%s", u.Username),
|
||||
fmt.Sprintf("HOME=%s", u.HomeDir),
|
||||
fmt.Sprintf("PATH=%s", defaultPathForUser(&u.User)),
|
||||
}
|
||||
}
|
||||
|
||||
// acceptEnvPair reports whether the environment variable key=value pair
|
||||
// should be accepted from the client. It uses the same default as OpenSSH
|
||||
// AcceptEnv.
|
||||
func acceptEnvPair(kv string) bool {
|
||||
k, _, ok := strings.Cut(kv, "=")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return k == "TERM" || k == "LANG" || strings.HasPrefix(k, "LC_")
|
||||
}
|
||||
|
||||
func shellArgs(isShell bool, cmd string) []string {
|
||||
if isShell {
|
||||
return []string{"-l"}
|
||||
} else {
|
||||
return []string{"-c", cmd}
|
||||
}
|
||||
}
|
155
wgengine/router/router_plan9.go
Normal file
155
wgengine/router/router_plan9.go
Normal file
@ -0,0 +1,155 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
r := &plan9Router{
|
||||
logf: logf,
|
||||
tundev: tundev,
|
||||
netMon: netMon,
|
||||
}
|
||||
cleanAllTailscaleRoutes(logf)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type plan9Router struct {
|
||||
logf logger.Logf
|
||||
tundev tun.Device
|
||||
netMon *netmon.Monitor
|
||||
health *health.Tracker
|
||||
}
|
||||
|
||||
func (r *plan9Router) Up() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *plan9Router) Set(cfg *Config) error {
|
||||
if cfg == nil {
|
||||
cleanAllTailscaleRoutes(r.logf)
|
||||
return nil
|
||||
}
|
||||
|
||||
var self4, self6 netip.Addr
|
||||
for _, addr := range cfg.LocalAddrs {
|
||||
ctl := r.tundev.File()
|
||||
maskBits := addr.Bits()
|
||||
if addr.Addr().Is4() {
|
||||
// The mask sizes in Plan9 are in IPv6 bits, even for IPv4.
|
||||
maskBits += (128 - 32)
|
||||
self4 = addr.Addr()
|
||||
}
|
||||
if addr.Addr().Is6() {
|
||||
self6 = addr.Addr()
|
||||
}
|
||||
_, err := fmt.Fprintf(ctl, "add %s /%d\n", addr.Addr().String(), maskBits)
|
||||
r.logf("XXX add %s /%d = %v", addr.Addr().String(), maskBits, err)
|
||||
}
|
||||
|
||||
r.logf("XXX TODO: Set Routes %v", cfg.Routes)
|
||||
|
||||
ipr, err := os.OpenFile("/net/iproute", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open /net/iproute: %w", err)
|
||||
}
|
||||
defer ipr.Close()
|
||||
|
||||
// TODO(bradfitz): read existing routes, delete ones tagged "tail"
|
||||
// that aren't in cfg.LocalRoutes.
|
||||
|
||||
if _, err := fmt.Fprintf(ipr, "tag tail\n"); err != nil {
|
||||
return fmt.Errorf("tag tail: %w", err)
|
||||
}
|
||||
|
||||
for _, route := range cfg.Routes {
|
||||
maskBits := route.Bits()
|
||||
if route.Addr().Is4() {
|
||||
// The mask sizes in Plan9 are in IPv6 bits, even for IPv4.
|
||||
maskBits += (128 - 32)
|
||||
}
|
||||
var nextHop netip.Addr
|
||||
if route.Addr().Is4() {
|
||||
nextHop = self4
|
||||
} else if route.Addr().Is6() {
|
||||
nextHop = self6
|
||||
}
|
||||
if !nextHop.IsValid() {
|
||||
r.logf("XXX skipping route %s: no next hop (no self addr)", route.String())
|
||||
continue
|
||||
}
|
||||
r.logf("XXX plan9.router: add %s /%d %s", route.Addr(), maskBits, nextHop)
|
||||
if _, err := fmt.Fprintf(ipr, "add %s /%d %s\n", route.Addr(), maskBits, nextHop); err != nil {
|
||||
return fmt.Errorf("add %s: %w", route.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
r.logf("XXX TODO: Set LocalRoutes %v", cfg.LocalRoutes)
|
||||
// TODO(bradfitz): implement this.
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateMagicsockPort implements the Router interface. This implementation
|
||||
// does nothing and returns nil because this router does not currently need
|
||||
// to know what the magicsock UDP port is.
|
||||
func (r *plan9Router) UpdateMagicsockPort(_ uint16, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *plan9Router) Close() error {
|
||||
// TODO(bradfitz): unbind
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanUp(logf logger.Logf, _ string) {
|
||||
cleanAllTailscaleRoutes(logf)
|
||||
}
|
||||
|
||||
func cleanAllTailscaleRoutes(logf logger.Logf) {
|
||||
routes, err := os.OpenFile("/net/iproute", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
logf("cleaning routes: %v", err)
|
||||
return
|
||||
}
|
||||
defer routes.Close()
|
||||
|
||||
// Using io.ReadAll or os.ReadFile on /net/iproute fails; it results in a
|
||||
// 511 byte result when the actual /net/iproute contents are over 1k.
|
||||
// So do it in one big read instead. Who knows.
|
||||
routeBuf := make([]byte, 1<<20)
|
||||
n, err := routes.Read(routeBuf)
|
||||
if err != nil {
|
||||
logf("cleaning routes: %v", err)
|
||||
return
|
||||
}
|
||||
routeBuf = routeBuf[:n]
|
||||
|
||||
//logf("cleaning routes: %d bytes: %q", len(routeBuf), routeBuf)
|
||||
|
||||
bs := bufio.NewScanner(bytes.NewReader(routeBuf))
|
||||
for bs.Scan() {
|
||||
f := strings.Fields(bs.Text())
|
||||
if len(f) < 6 {
|
||||
continue
|
||||
}
|
||||
tag := f[4]
|
||||
if tag != "tail" {
|
||||
continue
|
||||
}
|
||||
_, err := fmt.Fprintf(routes, "remove %s %s\n", f[0], f[1])
|
||||
logf("router: cleaning route %s %s: %v", f[0], f[1], err)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user