mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-01 15:12:37 -05:00
Keep the dialed service socket in the credentialed forwarding child and return a private socketpair endpoint instead. Passing the service socket to tailscaled preserves connect-time peer credentials but causes payload SCM_CREDENTIALS to identify the root daemon. Keep dial children alive for bidirectional forwarding. Return a lifetime pipe alongside the data socket so full closure or a discarded dial reply ends the relay independently of data EOF. Preserve half-closes and the monitor's descriptor acknowledgment/teardown ownership for both fds. Listener children retain their existing descriptor handoff behavior. Handle local Unix forwarding channel closure independently of the SSH connection: the channel request stream closes on CHANNEL_CLOSE, not CHANNEL_EOF. Stop waiting for service output while giving queued client payload a bounded drain in each forwarding stage, so a silent or non-reading service cannot retain the relay indefinitely. Test relay exit/reaping with silent services, discarded replies, queued payload and backpressure, including end-to-end SSH channel closure while the transport stays connected. Fix a canary test's unsynchronized marker assertion to wait for the reporting goroutine. Add a root-run cross-UID SO_PASSCRED regression test and lifecycle coverage for both half-close directions and monitor shutdown with an idle relay. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> Change-Id: I54b8ddf02b9d82434e474fea2ec283c0e39fab3e
95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestFwdDialRelayLifecycle checks that the extra socketpair preserves
|
|
// half-closes in both directions, and that monitor teardown closes an idle
|
|
// service connection even while the relay is waiting for traffic.
|
|
func TestFwdDialRelayLifecycle(t *testing.T) {
|
|
for _, mode := range []string{"client-half-close", "service-half-close", "monitor-close"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
mc := startFwdTestMonitor(t, os.Getuid(), os.Getgid(), testGroups(t))
|
|
dir := fwdWritableDir(t, os.Getuid(), os.Getgid())
|
|
path := filepath.Join(dir, "relay.sock")
|
|
ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
if err := ln.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := testCtx()
|
|
defer cancel()
|
|
client, err := mc.Dial(ctx, path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
service, err := ln.AcceptUnix()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer service.Close()
|
|
for _, c := range []net.Conn{client, service} {
|
|
if err := c.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
readEOF := func(c net.Conn, want string) {
|
|
t.Helper()
|
|
got, err := io.ReadAll(c)
|
|
if err != nil || string(got) != want {
|
|
t.Fatalf("read to EOF = %q, %v; want %q, nil", got, err, want)
|
|
}
|
|
}
|
|
if mode == "monitor-close" {
|
|
if err := mc.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
readEOF(service, "")
|
|
readEOF(client, "")
|
|
return
|
|
}
|
|
|
|
var first, second interface {
|
|
net.Conn
|
|
CloseWrite() error
|
|
} = client, service
|
|
if mode == "service-half-close" {
|
|
first, second = second, first
|
|
}
|
|
if _, err := io.WriteString(first, "request"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := first.CloseWrite(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
readEOF(second, "request")
|
|
// A write EOF must not prevent a response in the other direction.
|
|
if _, err := io.WriteString(second, "response"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := second.CloseWrite(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
readEOF(first, "response")
|
|
if err := mc.Ping(ctx); err != nil {
|
|
t.Fatalf("monitor after relay completed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|