ssh/tailssh: add integration tests for ssh
Adds basic integration tests for beIncubator that run on - MacOS - Ubuntu - Fedora Updates #11854 Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
parent
fee3aeb7f2
commit
793170fc36
|
@ -575,6 +575,16 @@ jobs:
|
|||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
ssh:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: run ssh tests
|
||||
run: |
|
||||
export PATH=$(./tool/go env GOROOT)/bin:$PATH
|
||||
make sshintegrationtest
|
||||
|
||||
check_mergeability:
|
||||
if: always()
|
||||
runs-on: ubuntu-22.04
|
||||
|
|
11
Makefile
11
Makefile
|
@ -100,6 +100,17 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
|
|||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
|
||||
|
||||
.PHONY: sshintegrationtest
|
||||
sshintegrationtest:
|
||||
GOOS=linux GOARCH=amd64 go test -tags integrationtest -c ./ssh/tailssh -o ssh/tailssh/testcontainers/tailssh.test && \
|
||||
echo "Testing on ubuntu:focal" && docker build --build-arg="BASE=ubuntu:focal" -t ssh-ubuntu-focal ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:jammy" && docker build --build-arg="BASE=ubuntu:jammy" -t ssh-ubuntu-jammy ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:mantic" && docker build --build-arg="BASE=ubuntu:mantic" -t ssh-ubuntu-mantic ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:noble" && docker build --build-arg="BASE=ubuntu:noble" -t ssh-ubuntu-noble ssh/tailssh/testcontainers && \
|
||||
echo "Testing on fedora:38" && docker build --build-arg="BASE=dokken/fedora-38" -t ssh-fedora-38 ssh/tailssh/testcontainers && \
|
||||
echo "Testing on fedora:39" && docker build --build-arg="BASE=dokken/fedora-39" -t ssh-fedora-39 ssh/tailssh/testcontainers && \
|
||||
echo "Testing on fedora:40" && docker build --build-arg="BASE=dokken/fedora-40" -t ssh-fedora-40 ssh/tailssh/testcontainers
|
||||
|
||||
help: ## Show this help
|
||||
@echo "\nSpecify a command. The choices are:\n"
|
||||
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
|
||||
|
|
|
@ -134,6 +134,8 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
|
|||
|
||||
const debugIncubator = false
|
||||
|
||||
var runningInTest = false
|
||||
|
||||
type stdRWC struct{}
|
||||
|
||||
func (stdRWC) Read(p []byte) (n int, err error) {
|
||||
|
@ -162,6 +164,10 @@ type incubatorArgs struct {
|
|||
isSFTP bool
|
||||
isShell bool
|
||||
cmdArgs []string
|
||||
env []string
|
||||
stdin io.ReadCloser
|
||||
stdout io.WriteCloser
|
||||
stderr io.WriteCloser
|
||||
}
|
||||
|
||||
func parseIncubatorArgs(args []string) (a incubatorArgs) {
|
||||
|
@ -192,6 +198,10 @@ func parseIncubatorArgs(args []string) (a incubatorArgs) {
|
|||
// OS, sets its UID and groups to the specified `--uid`, `--gid` and
|
||||
// `--groups` and then launches the requested `--cmd`.
|
||||
func beIncubator(args []string) error {
|
||||
return doBeIncubator(args, os.Environ(), os.Stdin, os.Stdout, os.Stderr)
|
||||
}
|
||||
|
||||
func doBeIncubator(args []string, env []string, stdin io.ReadCloser, stdout, stderr io.WriteCloser) 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.
|
||||
|
@ -202,17 +212,25 @@ func beIncubator(args []string) error {
|
|||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
ia := parseIncubatorArgs(args)
|
||||
if ia.isSFTP && ia.isShell {
|
||||
return fmt.Errorf("--sftp and --shell are mutually exclusive")
|
||||
}
|
||||
|
||||
logf := logger.Discard
|
||||
if debugIncubator {
|
||||
// We don't own stdout or stderr, so the only place we can log is syslog.
|
||||
if sl, err := syslog.New(syslog.LOG_INFO|syslog.LOG_DAEMON, "tailscaled-ssh"); err == nil {
|
||||
log.Println("Logging to syslog")
|
||||
logf = log.New(sl, "", 0).Printf
|
||||
}
|
||||
} else if runningInTest {
|
||||
// We can log to stdout during testing
|
||||
logf = log.Printf
|
||||
}
|
||||
|
||||
ia := parseIncubatorArgs(args)
|
||||
ia.env = env
|
||||
ia.stdin = stdin
|
||||
ia.stdout = stdout
|
||||
ia.stderr = stderr
|
||||
if ia.isSFTP && ia.isShell {
|
||||
return fmt.Errorf("--sftp and --shell are mutually exclusive")
|
||||
}
|
||||
|
||||
if handled, err := tryLoginShell(logf, ia); handled {
|
||||
|
@ -228,6 +246,16 @@ func beIncubator(args []string) error {
|
|||
defer sessionCloser()
|
||||
}
|
||||
|
||||
// We weren't able to use login, maybe we can use su.
|
||||
// Currently, we only support falling back to su on Linux. This
|
||||
// potentially could work on BSDs as well, but requires testing.
|
||||
canUseSU := runtime.GOOS == "linux"
|
||||
if canUseSU {
|
||||
if handled, err := tryLoginWithSU(logf, ia); handled {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var groupIDs []int
|
||||
for _, g := range strings.Split(ia.groups, ",") {
|
||||
gid, err := strconv.ParseInt(g, 10, 32)
|
||||
|
@ -257,10 +285,10 @@ func beIncubator(args []string) error {
|
|||
}
|
||||
|
||||
cmd := exec.Command(ia.cmdName, ia.cmdArgs...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Stdin = ia.stdin
|
||||
cmd.Stdout = ia.stdout
|
||||
cmd.Stderr = ia.stderr
|
||||
cmd.Env = ia.env
|
||||
|
||||
if ia.hasTTY {
|
||||
// If we were launched with a tty then we should
|
||||
|
@ -355,20 +383,13 @@ func tryLoginShell(logf logger.Logf, ia incubatorArgs) (bool, error) {
|
|||
|
||||
if shouldUseLoginCmd {
|
||||
if loginCmdPath, err := exec.LookPath("login"); err == nil {
|
||||
logf("using %s command", loginCmdPath)
|
||||
return true, unix.Exec(loginCmdPath, ia.loginArgs(loginCmdPath), os.Environ())
|
||||
loginArgs := ia.loginArgs(loginCmdPath)
|
||||
logf("running %s %+v", loginCmdPath, loginArgs)
|
||||
return true, unix.Exec(loginCmdPath, loginArgs, os.Environ())
|
||||
}
|
||||
}
|
||||
|
||||
// We weren't able to use login, maybe we can use su.
|
||||
// Currently, we only support falling back to su on Linux. This
|
||||
// potentially could work on BSDs as well, but requires testing.
|
||||
canUseSU := runtime.GOOS == "linux"
|
||||
if !canUseSU {
|
||||
logf("not attempting su")
|
||||
return false, nil
|
||||
}
|
||||
return tryLoginWithSU(logf, ia)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// tryLoginWithSU attempts to start a login shell using su instead of login. If
|
||||
|
@ -424,7 +445,7 @@ func tryLoginWithSU(logf logger.Logf, ia incubatorArgs) (bool, error) {
|
|||
}
|
||||
|
||||
logf("logging in with su %+v", loginArgs)
|
||||
return true, unix.Exec("/usr/bin/su", loginArgs, os.Environ())
|
||||
return true, unix.Exec(su, loginArgs, os.Environ())
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build integrationtest
|
||||
// +build integrationtest
|
||||
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// TestBeIncubator runs an integration test of the beIncubator function. It
|
||||
// expects an execution environment that meets the following requirements:
|
||||
//
|
||||
// - OS is one of MacOS, Linux, FreeBSD or OpenBSD
|
||||
// - User "testuser" exists
|
||||
// - "theuser" is in groups "groupone" and "grouptwo"
|
||||
func TestIntegrationTestBeIncubator(t *testing.T) {
|
||||
runningInTest = true
|
||||
|
||||
testuser, err := user.Lookup("testuser")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
groupone, err := user.LookupGroup("groupone")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grouptwo, err := user.LookupGroup("grouptwo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
runCmd := func(cmd string) string {
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
args := []string{
|
||||
"--uid", testuser.Uid,
|
||||
"--gid", testuser.Gid,
|
||||
"--groups", groupone.Gid + "," + grouptwo.Gid,
|
||||
"--local-user", "testuser",
|
||||
"--remote-user", "remoteuser",
|
||||
"--remote-ip", "192.168.1.180",
|
||||
"--cmd", cmd,
|
||||
}
|
||||
|
||||
log.Printf("Testing with args %+v", args)
|
||||
|
||||
stdinReader, stdin := io.Pipe()
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
stderrReader, stderrWriter := io.Pipe()
|
||||
defer stdin.Close()
|
||||
defer stdoutReader.Close()
|
||||
defer stderrReader.Close()
|
||||
|
||||
go func() {
|
||||
errCh <- doBeIncubator(args, os.Environ(), stdinReader, stdoutWriter, stderrWriter)
|
||||
}()
|
||||
|
||||
stdout := bufio.NewReader(stdoutReader)
|
||||
go io.Copy(os.Stderr, stderrReader)
|
||||
result, err := stdout.ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return strings.TrimSpace(result)
|
||||
}
|
||||
|
||||
gotPwd := runCmd("pwd")
|
||||
wantPwd := "/home/testuserd"
|
||||
if runtime.GOOS == "darwin" {
|
||||
wantPwd = "/Users/testuser"
|
||||
}
|
||||
if diff := cmp.Diff(gotPwd, wantPwd); diff != "" {
|
||||
t.Fatalf("unexpected pwd output (-got +want):\n%s", diff)
|
||||
}
|
||||
|
||||
gotId := runCmd("id")
|
||||
if !strings.Contains(gotId, "testuserd") {
|
||||
t.Logf("id output %q missing testuser", gotId)
|
||||
}
|
||||
if !strings.Contains(gotId, "groupone") {
|
||||
t.Logf("id output %q missing groupone", gotId)
|
||||
}
|
||||
if !strings.Contains(gotId, "grouptwo") {
|
||||
t.Logf("id output %q missing grouptwo", gotId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
ARG BASE
|
||||
FROM ${BASE}
|
||||
|
||||
RUN groupadd -g 10000 groupone
|
||||
RUN groupadd -g 10001 grouptwo
|
||||
RUN useradd -g 10000 -G 10001 -u 10002 -m testuser
|
||||
# RUN dnf install -y util-linux || echo "not fedora"
|
||||
COPY . .
|
||||
RUN ./tailssh.test -test.run TestIntegrationTest
|
Loading…
Reference in New Issue