detached e312f75

refactor(sshstdio): delete the superseded direct-dial and remote-launch stack

BuildCommand/BuildCommandForMode/BuildCommandForObservation/
BuildCommandForRemoteLaunch(WithRoot)/BuildCommandForRemoteCleanup,
isolatedLaunchScript/quotedWords/RunRemoteCommand, and the whole
Dial/DialContext(WithLaunch|WithIsolatedLaunch|WithRuntimeObserver)/
dialContext(WithLaunch) family are all unreachable from production: the
daemonmux SSH carriage (mux.go's BuildCommandForMux/DialMuxContext) replaced
them. Dial/DialContext/DialContextWithLaunch were already dead on main; they
are removed here too rather than left as orphaned callers of the functions
this change deletes.

BuildCommandForRemoteCommand and shellQuote stay: BuildCommandForMux still
calls BuildCommandForRemoteCommand in production, so its own test
(TestBuildCommandForRemoteCommandQuotesEveryWord) is kept. launch_test.go
tested only the deleted isolated-launch cluster and is removed entirely.
commit
e312f754cde9f2b64fa47c92c43831c395fbedaa
author
brice · · 2026-09-23 01:55 UTC
parent
e374227
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +0 -467
D internal/adapters/sshstdio/launch_test.go +0 -98
@@ -1,98 +0,0 @@
1-package sshstdio
2-
3-import (
4- "os"
5- "os/exec"
6- "path/filepath"
7- "testing"
8-
9- "github.com/stretchr/testify/require"
10-)
11-
12-func lookupCommand(t *testing.T, name string) string {
13- t.Helper()
14- path, err := exec.LookPath(name)
15- require.NoError(t, err)
16- path, err = filepath.Abs(path)
17- require.NoError(t, err)
18- return path
19-}
20-
21-func TestBuildCommandForRemoteLaunchQuotesBinaryAndEnvironment(t *testing.T) {
22- spec := BuildCommandForRemoteLaunch("test@example.com", "/opt/vev with-space", []string{"HOME=/tmp/home", "VALUE=a;b'c"}, "_stdio")
23- require.Equal(t, "ssh", spec.Path)
24- require.Equal(t, []string{
25- "--",
26- "test@example.com",
27- "'env' '-i' 'HOME=/tmp/home' 'VALUE=a;b'\\''c' '/opt/vev with-space' '_stdio'",
28- }, spec.Args)
29-}
30-
31-func TestBuildCommandForIsolatedRemoteLaunchQuotesRootAndOwner(t *testing.T) {
32- spec := BuildCommandForRemoteLaunchWithRoot("test@example.com", "/tmp/root with-space", "owner;token", "/opt/vev with-space", []string{"HOME=/tmp/home", "VALUE=a;b'c"}, "_stdio")
33- require.Equal(t, "ssh", spec.Path)
34- require.Equal(t, "test@example.com", spec.Args[1])
35- require.Contains(t, spec.Args[2], "'sh' '-c'")
36- require.Contains(t, spec.Args[2], "'/tmp/root with-space'")
37- require.Contains(t, spec.Args[2], "'owner;token'")
38- require.NotContains(t, spec.Args[2], "owner;token' &&")
39- require.Contains(t, spec.Args[2], "VALUE=a;b")
40-}
41-
42-func TestBuildCommandForRemoteCleanupVerifiesOwnerBeforeRemoval(t *testing.T) {
43- spec := BuildCommandForRemoteCleanup("test@example.com", "/tmp/root", "owner", "/opt/vev", []string{"HOME=/tmp/home"}, "_ui-cleanup")
44- require.Contains(t, spec.Args[2], "/tmp/root/.vev-ui-driver-owner")
45- require.Contains(t, spec.Args[2], "'_ui-cleanup'")
46- require.Contains(t, spec.Args[2], "rm -rf \"$root\"")
47-}
48-
49-func TestIsolatedLaunchScriptRemovesRootWhenCleanupCommandFails(t *testing.T) {
50- truePath := lookupCommand(t, "true")
51- falsePath := lookupCommand(t, "false")
52- tests := []struct {
53- name string
54- mode string
55- cleanup bool
56- wantExists bool
57- }{
58- {name: "launch", mode: "_stdio", wantExists: true},
59- {name: "cleanup", mode: "_ui-cleanup", cleanup: true, wantExists: false},
60- }
61- for _, test := range tests {
62- t.Run(test.name, func(t *testing.T) {
63- root := filepath.Join(t.TempDir(), "root")
64- if test.cleanup {
65- require.NoError(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", truePath, nil, "_stdio", false)).Run())
66- }
67- err := exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", falsePath, nil, test.mode, test.cleanup)).Run()
68- var exitErr *exec.ExitError
69- require.ErrorAs(t, err, &exitErr)
70- require.Equal(t, 1, exitErr.ExitCode())
71- _, statErr := os.Stat(root)
72- if test.wantExists {
73- require.NoError(t, statErr)
74- } else {
75- require.ErrorIs(t, statErr, os.ErrNotExist)
76- }
77- })
78- }
79-}
80-
81-func TestIsolatedLaunchScriptOwnsAndCleansFreshRoot(t *testing.T) {
82- truePath := lookupCommand(t, "true")
83- root := filepath.Join(t.TempDir(), "root")
84- launch := isolatedLaunchScript(root, "owner-token", truePath, nil, "_stdio", false)
85- require.NoError(t, exec.Command("sh", "-c", launch).Run())
86- marker := filepath.Join(root, ".vev-ui-driver-owner")
87- data, err := os.ReadFile(marker)
88- require.NoError(t, err)
89- require.Equal(t, []byte("owner-token"), data)
90-
91- // The same owner can reconnect to the endpoint, but another token cannot
92- // reuse the root created by this invocation.
93- require.NoError(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", truePath, nil, "_stdio", false)).Run())
94- require.Error(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "other-owner", truePath, nil, "_stdio", false)).Run())
95- require.NoError(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", truePath, nil, "_ui-cleanup", true)).Run())
96- _, err = os.Stat(root)
97- require.ErrorIs(t, err, os.ErrNotExist)
98-}
@@ -1,98 +0,0 @@
1-package sshstdio
2-
3-import (
4- "os"
5- "os/exec"
6- "path/filepath"
7- "testing"
8-
9- "github.com/stretchr/testify/require"
10-)
11-
12-func lookupCommand(t *testing.T, name string) string {
13- t.Helper()
14- path, err := exec.LookPath(name)
15- require.NoError(t, err)
16- path, err = filepath.Abs(path)
17- require.NoError(t, err)
18- return path
19-}
20-
21-func TestBuildCommandForRemoteLaunchQuotesBinaryAndEnvironment(t *testing.T) {
22- spec := BuildCommandForRemoteLaunch("test@example.com", "/opt/vev with-space", []string{"HOME=/tmp/home", "VALUE=a;b'c"}, "_stdio")
23- require.Equal(t, "ssh", spec.Path)
24- require.Equal(t, []string{
25- "--",
26- "test@example.com",
27- "'env' '-i' 'HOME=/tmp/home' 'VALUE=a;b'\\''c' '/opt/vev with-space' '_stdio'",
28- }, spec.Args)
29-}
30-
31-func TestBuildCommandForIsolatedRemoteLaunchQuotesRootAndOwner(t *testing.T) {
32- spec := BuildCommandForRemoteLaunchWithRoot("test@example.com", "/tmp/root with-space", "owner;token", "/opt/vev with-space", []string{"HOME=/tmp/home", "VALUE=a;b'c"}, "_stdio")
33- require.Equal(t, "ssh", spec.Path)
34- require.Equal(t, "test@example.com", spec.Args[1])
35- require.Contains(t, spec.Args[2], "'sh' '-c'")
36- require.Contains(t, spec.Args[2], "'/tmp/root with-space'")
37- require.Contains(t, spec.Args[2], "'owner;token'")
38- require.NotContains(t, spec.Args[2], "owner;token' &&")
39- require.Contains(t, spec.Args[2], "VALUE=a;b")
40-}
41-
42-func TestBuildCommandForRemoteCleanupVerifiesOwnerBeforeRemoval(t *testing.T) {
43- spec := BuildCommandForRemoteCleanup("test@example.com", "/tmp/root", "owner", "/opt/vev", []string{"HOME=/tmp/home"}, "_ui-cleanup")
44- require.Contains(t, spec.Args[2], "/tmp/root/.vev-ui-driver-owner")
45- require.Contains(t, spec.Args[2], "'_ui-cleanup'")
46- require.Contains(t, spec.Args[2], "rm -rf \"$root\"")
47-}
48-
49-func TestIsolatedLaunchScriptRemovesRootWhenCleanupCommandFails(t *testing.T) {
50- truePath := lookupCommand(t, "true")
51- falsePath := lookupCommand(t, "false")
52- tests := []struct {
53- name string
54- mode string
55- cleanup bool
56- wantExists bool
57- }{
58- {name: "launch", mode: "_stdio", wantExists: true},
59- {name: "cleanup", mode: "_ui-cleanup", cleanup: true, wantExists: false},
60- }
61- for _, test := range tests {
62- t.Run(test.name, func(t *testing.T) {
63- root := filepath.Join(t.TempDir(), "root")
64- if test.cleanup {
65- require.NoError(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", truePath, nil, "_stdio", false)).Run())
66- }
67- err := exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", falsePath, nil, test.mode, test.cleanup)).Run()
68- var exitErr *exec.ExitError
69- require.ErrorAs(t, err, &exitErr)
70- require.Equal(t, 1, exitErr.ExitCode())
71- _, statErr := os.Stat(root)
72- if test.wantExists {
73- require.NoError(t, statErr)
74- } else {
75- require.ErrorIs(t, statErr, os.ErrNotExist)
76- }
77- })
78- }
79-}
80-
81-func TestIsolatedLaunchScriptOwnsAndCleansFreshRoot(t *testing.T) {
82- truePath := lookupCommand(t, "true")
83- root := filepath.Join(t.TempDir(), "root")
84- launch := isolatedLaunchScript(root, "owner-token", truePath, nil, "_stdio", false)
85- require.NoError(t, exec.Command("sh", "-c", launch).Run())
86- marker := filepath.Join(root, ".vev-ui-driver-owner")
87- data, err := os.ReadFile(marker)
88- require.NoError(t, err)
89- require.Equal(t, []byte("owner-token"), data)
90-
91- // The same owner can reconnect to the endpoint, but another token cannot
92- // reuse the root created by this invocation.
93- require.NoError(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", truePath, nil, "_stdio", false)).Run())
94- require.Error(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "other-owner", truePath, nil, "_stdio", false)).Run())
95- require.NoError(t, exec.Command("sh", "-c", isolatedLaunchScript(root, "owner-token", truePath, nil, "_ui-cleanup", true)).Run())
96- _, err = os.Stat(root)
97- require.ErrorIs(t, err, os.ErrNotExist)
98-}
M internal/adapters/sshstdio/transport.go +0 -227
@@ -5,8 +5,6 @@
55 package sshstdio
66
77 import (
8- "bytes"
9- "context"
108 "errors"
119 "fmt"
1210 "io"
@@ -529,19 +527,6 @@ type CommandSpec struct {
529527 Args []string
530528 }
531529
532-// BuildCommand constructs the local ssh subprocess argv without invoking a
533-// local shell. OpenSSH sends the remote command as one string for the remote
534-// user's shell to interpret, so every remote argv word is POSIX single-quoted.
535-func BuildCommand(target, _ string) CommandSpec {
536- return BuildCommandForMode(target, "_stdio", "")
537-}
538-
539-// BuildCommandForMode constructs the local ssh subprocess argv for a hidden vev
540-// remote mode such as _stdio or _quic-bootstrap.
541-func BuildCommandForMode(target, mode, _ string) CommandSpec {
542- return BuildCommandForRemoteCommand(target, "vev", mode)
543-}
544-
545530 // BuildCommandForRemoteCommand constructs ssh argv for an arbitrary remote
546531 // command. Every remote word is POSIX single-quoted; the target remains one
547532 // local argv word after the option terminator.
@@ -554,218 +539,6 @@ func BuildCommandForRemoteCommand(target string, command ...string) CommandSpec
554539 return CommandSpec{Path: "ssh", Args: args}
555540 }
556541
557-// BuildCommandForObservation constructs unattended ssh argv for remote
558-// observation (catalogue checks). Authentication, proxying, and host-key trust
559-// remain owned by the target's effective OpenSSH configuration so managed
560-// transports such as NetBird keep their coherent policy. Vev only disables
561-// TTY allocation and advertised host-key updates, and bounds connection time
562-// and attempts. The caller leaves stdin detached and owns a whole-command
563-// deadline, so background observation cannot wait indefinitely for input.
564-func BuildCommandForObservation(target string, connectTimeout time.Duration, command ...string) CommandSpec {
565- secs := int(connectTimeout / time.Second)
566- if secs < 1 {
567- secs = 1
568- }
569- spec := BuildCommandForRemoteCommand(target, command...)
570- opts := []string{
571- "-T",
572- "-o", "UpdateHostKeys=no",
573- "-o", fmt.Sprintf("ConnectTimeout=%d", secs),
574- "-o", "ConnectionAttempts=1",
575- }
576- return CommandSpec{Path: spec.Path, Args: append(opts, spec.Args...)}
577-}
578-
579-// BuildCommandForRemoteLaunch prefixes an explicit complete environment and
580-// absolute executable to a hidden remote mode. Each value remains one quoted
581-// remote-shell word; no caller input is concatenated into an unquoted command.
582-func BuildCommandForRemoteLaunch(target, executable string, environment []string, mode string) CommandSpec {
583- command := make([]string, 0, len(environment)+4)
584- command = append(command, "env", "-i")
585- command = append(command, environment...)
586- command = append(command, executable, mode)
587- return BuildCommandForRemoteCommand(target, command...)
588-}
589-
590-const isolatedLaunchMarker = ".vev-ui-driver-owner"
591-
592-// BuildCommandForRemoteLaunchWithRoot creates a fresh private endpoint root on
593-// the remote host before starting the selected binary. A stable owner token
594-// lets later connections from the same driver reuse that root while rejecting
595-// a root created by another invocation.
596-func BuildCommandForRemoteLaunchWithRoot(target, root, ownerToken, executable string, environment []string, mode string) CommandSpec {
597- if root == "" || ownerToken == "" {
598- return BuildCommandForRemoteCommand(target, "sh", "-c", "exit 1")
599- }
600- script := isolatedLaunchScript(root, ownerToken, executable, environment, mode, false)
601- return BuildCommandForRemoteCommand(target, "sh", "-c", script)
602-}
603-
604-// BuildCommandForRemoteCleanup stops the daemon bound to an owned endpoint and
605-// removes its root only after the owner token has been verified remotely.
606-func BuildCommandForRemoteCleanup(target, root, ownerToken, executable string, environment []string, mode string) CommandSpec {
607- if root == "" || ownerToken == "" {
608- return BuildCommandForRemoteCommand(target, "sh", "-c", "exit 1")
609- }
610- script := isolatedLaunchScript(root, ownerToken, executable, environment, mode, true)
611- return BuildCommandForRemoteCommand(target, "sh", "-c", script)
612-}
613-
614-func isolatedLaunchScript(root, ownerToken, executable string, environment []string, mode string, cleanup bool) string {
615- rootRef := shellQuote(root)
616- markerRef := shellQuote(root + "/" + isolatedLaunchMarker)
617- tokenRef := shellQuote(ownerToken)
618- var script strings.Builder
619- script.WriteString("set -eu; root=")
620- script.WriteString(rootRef)
621- script.WriteString("; marker=")
622- script.WriteString(markerRef)
623- script.WriteString("; token=")
624- script.WriteString(tokenRef)
625- script.WriteString("; if [ -L \"$root\" ]; then exit 1; fi; ")
626- if cleanup {
627- script.WriteString("if [ ! -e \"$root\" ]; then exit 0; fi; ")
628- }
629- script.WriteString("if [ -e \"$root\" ]; then ")
630- script.WriteString("[ -d \"$root\" ] && [ ! -L \"$marker\" ] && [ -f \"$marker\" ] && [ \"$(cat \"$marker\")\" = \"$token\" ] || exit 1; ")
631- if !cleanup {
632- script.WriteString("else umask 077; mkdir -m 700 \"$root\"; created=1; ")
633- script.WriteString("trap 'if [ \"$created\" = 1 ]; then rm -rf \"$root\"; fi' EXIT; ")
634- for _, child := range []string{"config", "state", "runtime", "tmp"} {
635- script.WriteString("mkdir -m 700 \"$root/")
636- script.WriteString(child)
637- script.WriteString("\"; ")
638- }
639- script.WriteString("printf '%s' \"$token\" > \"$marker\"; chmod 600 \"$marker\"; ")
640- }
641- script.WriteString("fi; ")
642- command := make([]string, 0, len(environment)+4)
643- command = append(command, "env", "-i")
644- command = append(command, environment...)
645- command = append(command, executable, mode)
646- if cleanup {
647- script.WriteString("command_status=0; if ")
648- script.WriteString(quotedWords(command))
649- script.WriteString("; then :; else command_status=$?; fi; rm_status=0; rm -rf \"$root\" || rm_status=$?; if [ \"$command_status\" -ne 0 ]; then exit \"$command_status\"; fi; exit \"$rm_status\"")
650- } else {
651- script.WriteString("trap - EXIT; exec ")
652- script.WriteString(quotedWords(command))
653- }
654- return script.String()
655-}
656-
657-func quotedWords(words []string) string {
658- quoted := make([]string, 0, len(words))
659- for _, word := range words {
660- quoted = append(quoted, shellQuote(word))
661- }
662- return strings.Join(quoted, " ")
663-}
664-
665-// RunRemoteCommand executes a command through the local ssh binary without
666-// exposing its output, which may contain configured environment or endpoint
667-// values.
668-func RunRemoteCommand(ctx context.Context, spec CommandSpec) error {
669- if err := ctx.Err(); err != nil {
670- return err
671- }
672- cmd := exec.CommandContext(ctx, spec.Path, spec.Args...)
673- cmd.Stdout = io.Discard
674- cmd.Stderr = io.Discard
675- return cmd.Run()
676-}
677-
678542 func shellQuote(s string) string {
679543 return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
680544 }
681-
682-// Dial starts ssh target vev _stdio and returns a Transport over the child
683-// process' stdio. Session selection is carried in Hello. The subprocess is
684-// started with exec.Command argv, never through a shell.
685-func Dial(target, session string) (wire.Transport, error) {
686- return DialContext(context.Background(), target, session)
687-}
688-
689-// DialContextWithRuntimeObserver is the option-bearing production constructor.
690-func DialContextWithRuntimeObserver(ctx context.Context, target, session string, logger *slog.Logger, opts ...Option) (wire.Transport, error) {
691- return dialContext(ctx, target, session, logger, opts...)
692-}
693-
694-// DialContextWithLaunch is the explicit-environment variant used by isolated
695-// driver endpoints. An empty executable preserves the normal `vev _stdio`
696-// command.
697-func DialContextWithLaunch(ctx context.Context, target, executable string, environment []string, logger *slog.Logger, opts ...Option) (wire.Transport, error) {
698- return dialContextWithLaunch(ctx, target, "", "", executable, environment, logger, opts...)
699-}
700-
701-// DialContextWithIsolatedLaunch starts a remote child after it has exclusively
702-// created or revalidated its private endpoint root.
703-func DialContextWithIsolatedLaunch(ctx context.Context, target, root, ownerToken, executable string, environment []string, logger *slog.Logger, opts ...Option) (wire.Transport, error) {
704- return dialContextWithLaunch(ctx, target, root, ownerToken, executable, environment, logger, opts...)
705-}
706-
707-// DialContext is like Dial, but the context gates ssh startup. Once the
708-// transport is returned, its Close method owns the subprocess lifetime; the
709-// handshake context must not kill an already-established carriage. Callers may
710-// pass a logger to record ssh start failures and non-clean exits without logging
711-// the generated command line.
712-func DialContext(ctx context.Context, target, session string, logger ...*slog.Logger) (wire.Transport, error) {
713- var log *slog.Logger
714- if len(logger) > 0 {
715- log = logger[0]
716- }
717- return dialContext(ctx, target, session, log)
718-}
719-
720-func dialContext(ctx context.Context, target, session string, log *slog.Logger, opts ...Option) (wire.Transport, error) {
721- return dialContextWithLaunch(ctx, target, "", "", "", nil, log, opts...)
722-}
723-
724-func dialContextWithLaunch(ctx context.Context, target, root, ownerToken, executable string, environment []string, log *slog.Logger, opts ...Option) (wire.Transport, error) {
725- if err := ctx.Err(); err != nil {
726- return nil, err
727- }
728- spec := BuildCommand(target, "")
729- if executable != "" {
730- if root != "" && ownerToken != "" {
731- spec = BuildCommandForRemoteLaunchWithRoot(target, root, ownerToken, executable, environment, "_stdio")
732- } else {
733- spec = BuildCommandForRemoteLaunch(target, executable, environment, "_stdio")
734- }
735- }
736- // The caller's context is also the bounded protocol-handshake context and is
737- // canceled after the first committed publication. Binding it to the command
738- // would kill a healthy long-lived carriage at that boundary. Transport.Close
739- // owns cancellation after Start; the preflight check above keeps canceled
740- // attempts from starting a process.
741- cmd := exec.Command(spec.Path, spec.Args...)
742- stdin, err := cmd.StdinPipe()
743- if err != nil {
744- return nil, fmt.Errorf("sshstdio: stdin pipe: %w", err)
745- }
746- stdout, err := cmd.StdoutPipe()
747- if err != nil {
748- return nil, fmt.Errorf("sshstdio: stdout pipe: %w", err)
749- }
750- var stderr bytes.Buffer
751- cmd.Stderr = &stderr
752- if err := cmd.Start(); err != nil {
753- if log != nil {
754- log.Error("ssh start failed", "target", target, "session", "", "err", err)
755- }
756- return nil, fmt.Errorf("sshstdio: start ssh: %w", err)
757- }
758-
759- waiter := newProcessWaiter(cmd, stdin, &stderr, sshCloseTimeout, log, target, "")
760- transport := newTransport(stdout, stdin, waiter.close, waiter.eofErr)
761- if err := ctx.Err(); err != nil {
762- _ = transport.Close()
763- return nil, err
764- }
765- for _, opt := range opts {
766- if opt != nil {
767- opt(transport)
768- }
769- }
770- return transport, nil
771-}
@@ -5,8 +5,6 @@
5 package sshstdio5 package sshstdio
6 6
7 import (7 import (
8- "bytes"
9- "context"
10 "errors"8 "errors"
11 "fmt"9 "fmt"
12 "io"10 "io"
@@ -529,19 +527,6 @@ type CommandSpec struct {
529 Args []string527 Args []string
530 }528 }
531 529
532-// BuildCommand constructs the local ssh subprocess argv without invoking a
533-// local shell. OpenSSH sends the remote command as one string for the remote
534-// user's shell to interpret, so every remote argv word is POSIX single-quoted.
535-func BuildCommand(target, _ string) CommandSpec {
536- return BuildCommandForMode(target, "_stdio", "")
537-}
538-
539-// BuildCommandForMode constructs the local ssh subprocess argv for a hidden vev
540-// remote mode such as _stdio or _quic-bootstrap.
541-func BuildCommandForMode(target, mode, _ string) CommandSpec {
542- return BuildCommandForRemoteCommand(target, "vev", mode)
543-}
544-
545 // BuildCommandForRemoteCommand constructs ssh argv for an arbitrary remote530 // BuildCommandForRemoteCommand constructs ssh argv for an arbitrary remote
546 // command. Every remote word is POSIX single-quoted; the target remains one531 // command. Every remote word is POSIX single-quoted; the target remains one
547 // local argv word after the option terminator.532 // local argv word after the option terminator.
@@ -554,218 +539,6 @@ func BuildCommandForRemoteCommand(target string, command ...string) CommandSpec
554 return CommandSpec{Path: "ssh", Args: args}539 return CommandSpec{Path: "ssh", Args: args}
555 }540 }
556 541
557-// BuildCommandForObservation constructs unattended ssh argv for remote
558-// observation (catalogue checks). Authentication, proxying, and host-key trust
559-// remain owned by the target's effective OpenSSH configuration so managed
560-// transports such as NetBird keep their coherent policy. Vev only disables
561-// TTY allocation and advertised host-key updates, and bounds connection time
562-// and attempts. The caller leaves stdin detached and owns a whole-command
563-// deadline, so background observation cannot wait indefinitely for input.
564-func BuildCommandForObservation(target string, connectTimeout time.Duration, command ...string) CommandSpec {
565- secs := int(connectTimeout / time.Second)
566- if secs < 1 {
567- secs = 1
568- }
569- spec := BuildCommandForRemoteCommand(target, command...)
570- opts := []string{
571- "-T",
572- "-o", "UpdateHostKeys=no",
573- "-o", fmt.Sprintf("ConnectTimeout=%d", secs),
574- "-o", "ConnectionAttempts=1",
575- }
576- return CommandSpec{Path: spec.Path, Args: append(opts, spec.Args...)}
577-}
578-
579-// BuildCommandForRemoteLaunch prefixes an explicit complete environment and
580-// absolute executable to a hidden remote mode. Each value remains one quoted
581-// remote-shell word; no caller input is concatenated into an unquoted command.
582-func BuildCommandForRemoteLaunch(target, executable string, environment []string, mode string) CommandSpec {
583- command := make([]string, 0, len(environment)+4)
584- command = append(command, "env", "-i")
585- command = append(command, environment...)
586- command = append(command, executable, mode)
587- return BuildCommandForRemoteCommand(target, command...)
588-}
589-
590-const isolatedLaunchMarker = ".vev-ui-driver-owner"
591-
592-// BuildCommandForRemoteLaunchWithRoot creates a fresh private endpoint root on
593-// the remote host before starting the selected binary. A stable owner token
594-// lets later connections from the same driver reuse that root while rejecting
595-// a root created by another invocation.
596-func BuildCommandForRemoteLaunchWithRoot(target, root, ownerToken, executable string, environment []string, mode string) CommandSpec {
597- if root == "" || ownerToken == "" {
598- return BuildCommandForRemoteCommand(target, "sh", "-c", "exit 1")
599- }
600- script := isolatedLaunchScript(root, ownerToken, executable, environment, mode, false)
601- return BuildCommandForRemoteCommand(target, "sh", "-c", script)
602-}
603-
604-// BuildCommandForRemoteCleanup stops the daemon bound to an owned endpoint and
605-// removes its root only after the owner token has been verified remotely.
606-func BuildCommandForRemoteCleanup(target, root, ownerToken, executable string, environment []string, mode string) CommandSpec {
607- if root == "" || ownerToken == "" {
608- return BuildCommandForRemoteCommand(target, "sh", "-c", "exit 1")
609- }
610- script := isolatedLaunchScript(root, ownerToken, executable, environment, mode, true)
611- return BuildCommandForRemoteCommand(target, "sh", "-c", script)
612-}
613-
614-func isolatedLaunchScript(root, ownerToken, executable string, environment []string, mode string, cleanup bool) string {
615- rootRef := shellQuote(root)
616- markerRef := shellQuote(root + "/" + isolatedLaunchMarker)
617- tokenRef := shellQuote(ownerToken)
618- var script strings.Builder
619- script.WriteString("set -eu; root=")
620- script.WriteString(rootRef)
621- script.WriteString("; marker=")
622- script.WriteString(markerRef)
623- script.WriteString("; token=")
624- script.WriteString(tokenRef)
625- script.WriteString("; if [ -L \"$root\" ]; then exit 1; fi; ")
626- if cleanup {
627- script.WriteString("if [ ! -e \"$root\" ]; then exit 0; fi; ")
628- }
629- script.WriteString("if [ -e \"$root\" ]; then ")
630- script.WriteString("[ -d \"$root\" ] && [ ! -L \"$marker\" ] && [ -f \"$marker\" ] && [ \"$(cat \"$marker\")\" = \"$token\" ] || exit 1; ")
631- if !cleanup {
632- script.WriteString("else umask 077; mkdir -m 700 \"$root\"; created=1; ")
633- script.WriteString("trap 'if [ \"$created\" = 1 ]; then rm -rf \"$root\"; fi' EXIT; ")
634- for _, child := range []string{"config", "state", "runtime", "tmp"} {
635- script.WriteString("mkdir -m 700 \"$root/")
636- script.WriteString(child)
637- script.WriteString("\"; ")
638- }
639- script.WriteString("printf '%s' \"$token\" > \"$marker\"; chmod 600 \"$marker\"; ")
640- }
641- script.WriteString("fi; ")
642- command := make([]string, 0, len(environment)+4)
643- command = append(command, "env", "-i")
644- command = append(command, environment...)
645- command = append(command, executable, mode)
646- if cleanup {
647- script.WriteString("command_status=0; if ")
648- script.WriteString(quotedWords(command))
649- script.WriteString("; then :; else command_status=$?; fi; rm_status=0; rm -rf \"$root\" || rm_status=$?; if [ \"$command_status\" -ne 0 ]; then exit \"$command_status\"; fi; exit \"$rm_status\"")
650- } else {
651- script.WriteString("trap - EXIT; exec ")
652- script.WriteString(quotedWords(command))
653- }
654- return script.String()
655-}
656-
657-func quotedWords(words []string) string {
658- quoted := make([]string, 0, len(words))
659- for _, word := range words {
660- quoted = append(quoted, shellQuote(word))
661- }
662- return strings.Join(quoted, " ")
663-}
664-
665-// RunRemoteCommand executes a command through the local ssh binary without
666-// exposing its output, which may contain configured environment or endpoint
667-// values.
668-func RunRemoteCommand(ctx context.Context, spec CommandSpec) error {
669- if err := ctx.Err(); err != nil {
670- return err
671- }
672- cmd := exec.CommandContext(ctx, spec.Path, spec.Args...)
673- cmd.Stdout = io.Discard
674- cmd.Stderr = io.Discard
675- return cmd.Run()
676-}
677-
678 func shellQuote(s string) string {542 func shellQuote(s string) string {
679 return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"543 return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
680 }544 }
681-
682-// Dial starts ssh target vev _stdio and returns a Transport over the child
683-// process' stdio. Session selection is carried in Hello. The subprocess is
684-// started with exec.Command argv, never through a shell.
685-func Dial(target, session string) (wire.Transport, error) {
686- return DialContext(context.Background(), target, session)
687-}
688-
689-// DialContextWithRuntimeObserver is the option-bearing production constructor.
690-func DialContextWithRuntimeObserver(ctx context.Context, target, session string, logger *slog.Logger, opts ...Option) (wire.Transport, error) {
691- return dialContext(ctx, target, session, logger, opts...)
692-}
693-
694-// DialContextWithLaunch is the explicit-environment variant used by isolated
695-// driver endpoints. An empty executable preserves the normal `vev _stdio`
696-// command.
697-func DialContextWithLaunch(ctx context.Context, target, executable string, environment []string, logger *slog.Logger, opts ...Option) (wire.Transport, error) {
698- return dialContextWithLaunch(ctx, target, "", "", executable, environment, logger, opts...)
699-}
700-
701-// DialContextWithIsolatedLaunch starts a remote child after it has exclusively
702-// created or revalidated its private endpoint root.
703-func DialContextWithIsolatedLaunch(ctx context.Context, target, root, ownerToken, executable string, environment []string, logger *slog.Logger, opts ...Option) (wire.Transport, error) {
704- return dialContextWithLaunch(ctx, target, root, ownerToken, executable, environment, logger, opts...)
705-}
706-
707-// DialContext is like Dial, but the context gates ssh startup. Once the
708-// transport is returned, its Close method owns the subprocess lifetime; the
709-// handshake context must not kill an already-established carriage. Callers may
710-// pass a logger to record ssh start failures and non-clean exits without logging
711-// the generated command line.
712-func DialContext(ctx context.Context, target, session string, logger ...*slog.Logger) (wire.Transport, error) {
713- var log *slog.Logger
714- if len(logger) > 0 {
715- log = logger[0]
716- }
717- return dialContext(ctx, target, session, log)
718-}
719-
720-func dialContext(ctx context.Context, target, session string, log *slog.Logger, opts ...Option) (wire.Transport, error) {
721- return dialContextWithLaunch(ctx, target, "", "", "", nil, log, opts...)
722-}
723-
724-func dialContextWithLaunch(ctx context.Context, target, root, ownerToken, executable string, environment []string, log *slog.Logger, opts ...Option) (wire.Transport, error) {
725- if err := ctx.Err(); err != nil {
726- return nil, err
727- }
728- spec := BuildCommand(target, "")
729- if executable != "" {
730- if root != "" && ownerToken != "" {
731- spec = BuildCommandForRemoteLaunchWithRoot(target, root, ownerToken, executable, environment, "_stdio")
732- } else {
733- spec = BuildCommandForRemoteLaunch(target, executable, environment, "_stdio")
734- }
735- }
736- // The caller's context is also the bounded protocol-handshake context and is
737- // canceled after the first committed publication. Binding it to the command
738- // would kill a healthy long-lived carriage at that boundary. Transport.Close
739- // owns cancellation after Start; the preflight check above keeps canceled
740- // attempts from starting a process.
741- cmd := exec.Command(spec.Path, spec.Args...)
742- stdin, err := cmd.StdinPipe()
743- if err != nil {
744- return nil, fmt.Errorf("sshstdio: stdin pipe: %w", err)
745- }
746- stdout, err := cmd.StdoutPipe()
747- if err != nil {
748- return nil, fmt.Errorf("sshstdio: stdout pipe: %w", err)
749- }
750- var stderr bytes.Buffer
751- cmd.Stderr = &stderr
752- if err := cmd.Start(); err != nil {
753- if log != nil {
754- log.Error("ssh start failed", "target", target, "session", "", "err", err)
755- }
756- return nil, fmt.Errorf("sshstdio: start ssh: %w", err)
757- }
758-
759- waiter := newProcessWaiter(cmd, stdin, &stderr, sshCloseTimeout, log, target, "")
760- transport := newTransport(stdout, stdin, waiter.close, waiter.eofErr)
761- if err := ctx.Err(); err != nil {
762- _ = transport.Close()
763- return nil, err
764- }
765- for _, opt := range opts {
766- if opt != nil {
767- opt(transport)
768- }
769- }
770- return transport, nil
771-}
M internal/adapters/sshstdio/transport_test.go +0 -142
@@ -7,7 +7,6 @@ import (
77 "errors"
88 "io"
99 "log/slog"
10- "os"
1110 "os/exec"
1211 "strings"
1312 "sync"
@@ -24,52 +23,6 @@ import (
2423 // legacy type byte.
2524 const frameHeaderLen = 4
2625
27-func TestBuildCommandUsesExecArgs(t *testing.T) {
28- // BuildCommand deliberately starts `ssh -- target 'vev' '_stdio'`.
29- // Session selection and terminal color capability are carried in vev's Hello
30- // message, not in ssh env.
31- tests := []struct {
32- name string
33- target string
34- session string
35- want []string
36- }{
37- {name: "no session", target: "user@example.com", want: []string{"--", "user@example.com", "'vev' '_stdio'"}},
38- {name: "session is carried by Hello rather than ssh command", target: "user@example.com", session: "work; rm -rf /", want: []string{"--", "user@example.com", "'vev' '_stdio'"}},
39- {name: "target kept as single local arg after option terminator", target: "user@host; touch /tmp/pwn", session: "work", want: []string{"--", "user@host; touch /tmp/pwn", "'vev' '_stdio'"}},
40- }
41-
42- for _, tt := range tests {
43- t.Run(tt.name, func(t *testing.T) {
44- got := BuildCommand(tt.target, tt.session)
45- if got.Path != "ssh" {
46- t.Fatalf("Path = %q, want ssh", got.Path)
47- }
48- if len(got.Args) != len(tt.want) {
49- t.Fatalf("Args len = %d, want %d (%q)", len(got.Args), len(tt.want), got.Args)
50- }
51- for i := range tt.want {
52- if got.Args[i] != tt.want[i] {
53- t.Fatalf("Args[%d] = %q, want %q (all args %q)", i, got.Args[i], tt.want[i], got.Args)
54- }
55- }
56- })
57- }
58-}
59-
60-func TestBuildCommandForModeUsesCanonicalSSHArgs(t *testing.T) {
61- got := BuildCommandForMode("user@example.com", "_quic-bootstrap", "work")
62- want := []string{"--", "user@example.com", "'vev' '_quic-bootstrap'"}
63- if got.Path != "ssh" {
64- t.Fatalf("Path = %q, want ssh", got.Path)
65- }
66- for i := range want {
67- if got.Args[i] != want[i] {
68- t.Fatalf("Args[%d] = %q, want %q (all args %q)", i, got.Args[i], want[i], got.Args)
69- }
70- }
71-}
72-
7326 func TestBuildCommandForRemoteCommandQuotesEveryWord(t *testing.T) {
7427 tests := []struct {
7528 name string
@@ -115,92 +68,6 @@ func TestBuildCommandForRemoteCommandQuotesEveryWord(t *testing.T) {
11568 }
11669 }
11770
118-func TestBuildCommandForObservationPreservesHostTrustAndAuthenticationPolicy(t *testing.T) {
119- spec := BuildCommandForObservation("user@example.com", 5*time.Second, "vev", "cmd", "remote-catalog", "--json")
120- if spec.Path != "ssh" {
121- t.Fatalf("Path = %q, want ssh", spec.Path)
122- }
123- flat := strings.Join(spec.Args, " ")
124- for _, want := range []string{"-T", "UpdateHostKeys=no", "ConnectTimeout=5", "ConnectionAttempts=1"} {
125- if !strings.Contains(flat, want) {
126- t.Fatalf("observation argv %q missing %q", spec.Args, want)
127- }
128- }
129- for _, forbidden := range []string{"BatchMode=", "StrictHostKeyChecking=", "UserKnownHostsFile=", "ProxyCommand="} {
130- if strings.Contains(flat, forbidden) {
131- t.Fatalf("observation argv %q overrides host policy %q", spec.Args, forbidden)
132- }
133- }
134-
135- // Effective behavior: host-specific authentication and trust policy stays
136- // intact while vev still disables TTY allocation, host-key updates, and
137- // unbounded connection attempts.
138- ssh, err := exec.LookPath("ssh")
139- if err != nil {
140- t.Skip("ssh binary not available for effective-config probe")
141- }
142- conflict := "Host *\n RequestTTY yes\n UpdateHostKeys yes\n BatchMode no\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n ProxyCommand fake-proxy %h %p\n"
143- confPath := t.TempDir() + "/ssh_config"
144- if err := os.WriteFile(confPath, []byte(conflict), 0o600); err != nil {
145- t.Fatalf("write conflicting ssh config: %v", err)
146- }
147- sep := -1
148- for i, arg := range spec.Args {
149- if arg == "--" {
150- sep = i
151- break
152- }
153- }
154- if sep < 0 {
155- t.Fatalf("observation argv %q has no option terminator", spec.Args)
156- }
157- probe := append(append([]string{}, spec.Args[:sep]...), "-F", confPath, "-G", "user@example.com")
158- out, err := exec.Command(ssh, probe...).Output()
159- if err != nil {
160- t.Fatalf("ssh -G probe: %v", err)
161- }
162- effective := map[string]string{}
163- for _, line := range strings.Split(string(out), "\n") {
164- key, value, ok := strings.Cut(line, " ")
165- if ok {
166- effective[strings.ToLower(key)] = strings.ToLower(value)
167- }
168- }
169- for key, want := range map[string]string{
170- "connecttimeout": "5", "connectionattempts": "1",
171- } {
172- if effective[key] != want {
173- t.Fatalf("effective ssh %s = %q, want %q (config must not override observation argv)", key, effective[key], want)
174- }
175- }
176- // ssh -G spells boolean keywords true/false where the configuration
177- // spells yes/no; normalize both spellings before comparing.
178- normalize := func(value string) string {
179- switch value {
180- case "yes", "true":
181- return "yes"
182- case "no", "false":
183- return "no"
184- default:
185- return value
186- }
187- }
188- for key, want := range map[string]string{
189- "batchmode": "no", "stricthostkeychecking": "no",
190- "requesttty": "no", "updatehostkeys": "no",
191- } {
192- if got := normalize(effective[key]); got != want {
193- t.Fatalf("effective ssh %s = %q, want %q", key, effective[key], want)
194- }
195- }
196- if effective["userknownhostsfile"] != "/dev/null" {
197- t.Fatalf("effective ssh userknownhostsfile = %q, want /dev/null", effective["userknownhostsfile"])
198- }
199- if !strings.Contains(effective["proxycommand"], "fake-proxy") {
200- t.Fatalf("effective ssh proxycommand = %q, want configured proxy", effective["proxycommand"])
201- }
202-}
203-
20471 func TestCloseInterruptsBlockedSend(t *testing.T) {
20572 writer := &blockedWriter{started: make(chan struct{}), released: make(chan struct{})}
20673 transport := NewTransport(nil, writer, nil)
@@ -348,15 +215,6 @@ func TestTransportUsesCanonicalFrameMaximum(t *testing.T) {
348215 }
349216 }
350217
351-func TestDialContextCanceledBeforeStart(t *testing.T) {
352- ctx, cancel := context.WithCancel(context.Background())
353- cancel()
354- _, err := DialContext(ctx, "example.com", "work")
355- if !errors.Is(err, context.Canceled) {
356- t.Fatalf("DialContext error = %v, want context.Canceled", err)
357- }
358-}
359-
360218 func TestRecvMapsUnexpectedEOFDuringHeaderToSSHExit(t *testing.T) {
361219 sshErr := errors.New("ssh exited with status 255")
362220 tr := newTransport(strings.NewReader("\x00"), io.Discard, nil, func() error { return sshErr })
@@ -7,7 +7,6 @@ import (
7 "errors"7 "errors"
8 "io"8 "io"
9 "log/slog"9 "log/slog"
10- "os"
11 "os/exec"10 "os/exec"
12 "strings"11 "strings"
13 "sync"12 "sync"
@@ -24,52 +23,6 @@ import (
24 // legacy type byte.23 // legacy type byte.
25 const frameHeaderLen = 424 const frameHeaderLen = 4
26 25
27-func TestBuildCommandUsesExecArgs(t *testing.T) {
28- // BuildCommand deliberately starts `ssh -- target 'vev' '_stdio'`.
29- // Session selection and terminal color capability are carried in vev's Hello
30- // message, not in ssh env.
31- tests := []struct {
32- name string
33- target string
34- session string
35- want []string
36- }{
37- {name: "no session", target: "user@example.com", want: []string{"--", "user@example.com", "'vev' '_stdio'"}},
38- {name: "session is carried by Hello rather than ssh command", target: "user@example.com", session: "work; rm -rf /", want: []string{"--", "user@example.com", "'vev' '_stdio'"}},
39- {name: "target kept as single local arg after option terminator", target: "user@host; touch /tmp/pwn", session: "work", want: []string{"--", "user@host; touch /tmp/pwn", "'vev' '_stdio'"}},
40- }
41-
42- for _, tt := range tests {
43- t.Run(tt.name, func(t *testing.T) {
44- got := BuildCommand(tt.target, tt.session)
45- if got.Path != "ssh" {
46- t.Fatalf("Path = %q, want ssh", got.Path)
47- }
48- if len(got.Args) != len(tt.want) {
49- t.Fatalf("Args len = %d, want %d (%q)", len(got.Args), len(tt.want), got.Args)
50- }
51- for i := range tt.want {
52- if got.Args[i] != tt.want[i] {
53- t.Fatalf("Args[%d] = %q, want %q (all args %q)", i, got.Args[i], tt.want[i], got.Args)
54- }
55- }
56- })
57- }
58-}
59-
60-func TestBuildCommandForModeUsesCanonicalSSHArgs(t *testing.T) {
61- got := BuildCommandForMode("user@example.com", "_quic-bootstrap", "work")
62- want := []string{"--", "user@example.com", "'vev' '_quic-bootstrap'"}
63- if got.Path != "ssh" {
64- t.Fatalf("Path = %q, want ssh", got.Path)
65- }
66- for i := range want {
67- if got.Args[i] != want[i] {
68- t.Fatalf("Args[%d] = %q, want %q (all args %q)", i, got.Args[i], want[i], got.Args)
69- }
70- }
71-}
72-
73 func TestBuildCommandForRemoteCommandQuotesEveryWord(t *testing.T) {26 func TestBuildCommandForRemoteCommandQuotesEveryWord(t *testing.T) {
74 tests := []struct {27 tests := []struct {
75 name string28 name string
@@ -115,92 +68,6 @@ func TestBuildCommandForRemoteCommandQuotesEveryWord(t *testing.T) {
115 }68 }
116 }69 }
117 70
118-func TestBuildCommandForObservationPreservesHostTrustAndAuthenticationPolicy(t *testing.T) {
119- spec := BuildCommandForObservation("user@example.com", 5*time.Second, "vev", "cmd", "remote-catalog", "--json")
120- if spec.Path != "ssh" {
121- t.Fatalf("Path = %q, want ssh", spec.Path)
122- }
123- flat := strings.Join(spec.Args, " ")
124- for _, want := range []string{"-T", "UpdateHostKeys=no", "ConnectTimeout=5", "ConnectionAttempts=1"} {
125- if !strings.Contains(flat, want) {
126- t.Fatalf("observation argv %q missing %q", spec.Args, want)
127- }
128- }
129- for _, forbidden := range []string{"BatchMode=", "StrictHostKeyChecking=", "UserKnownHostsFile=", "ProxyCommand="} {
130- if strings.Contains(flat, forbidden) {
131- t.Fatalf("observation argv %q overrides host policy %q", spec.Args, forbidden)
132- }
133- }
134-
135- // Effective behavior: host-specific authentication and trust policy stays
136- // intact while vev still disables TTY allocation, host-key updates, and
137- // unbounded connection attempts.
138- ssh, err := exec.LookPath("ssh")
139- if err != nil {
140- t.Skip("ssh binary not available for effective-config probe")
141- }
142- conflict := "Host *\n RequestTTY yes\n UpdateHostKeys yes\n BatchMode no\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n ProxyCommand fake-proxy %h %p\n"
143- confPath := t.TempDir() + "/ssh_config"
144- if err := os.WriteFile(confPath, []byte(conflict), 0o600); err != nil {
145- t.Fatalf("write conflicting ssh config: %v", err)
146- }
147- sep := -1
148- for i, arg := range spec.Args {
149- if arg == "--" {
150- sep = i
151- break
152- }
153- }
154- if sep < 0 {
155- t.Fatalf("observation argv %q has no option terminator", spec.Args)
156- }
157- probe := append(append([]string{}, spec.Args[:sep]...), "-F", confPath, "-G", "user@example.com")
158- out, err := exec.Command(ssh, probe...).Output()
159- if err != nil {
160- t.Fatalf("ssh -G probe: %v", err)
161- }
162- effective := map[string]string{}
163- for _, line := range strings.Split(string(out), "\n") {
164- key, value, ok := strings.Cut(line, " ")
165- if ok {
166- effective[strings.ToLower(key)] = strings.ToLower(value)
167- }
168- }
169- for key, want := range map[string]string{
170- "connecttimeout": "5", "connectionattempts": "1",
171- } {
172- if effective[key] != want {
173- t.Fatalf("effective ssh %s = %q, want %q (config must not override observation argv)", key, effective[key], want)
174- }
175- }
176- // ssh -G spells boolean keywords true/false where the configuration
177- // spells yes/no; normalize both spellings before comparing.
178- normalize := func(value string) string {
179- switch value {
180- case "yes", "true":
181- return "yes"
182- case "no", "false":
183- return "no"
184- default:
185- return value
186- }
187- }
188- for key, want := range map[string]string{
189- "batchmode": "no", "stricthostkeychecking": "no",
190- "requesttty": "no", "updatehostkeys": "no",
191- } {
192- if got := normalize(effective[key]); got != want {
193- t.Fatalf("effective ssh %s = %q, want %q", key, effective[key], want)
194- }
195- }
196- if effective["userknownhostsfile"] != "/dev/null" {
197- t.Fatalf("effective ssh userknownhostsfile = %q, want /dev/null", effective["userknownhostsfile"])
198- }
199- if !strings.Contains(effective["proxycommand"], "fake-proxy") {
200- t.Fatalf("effective ssh proxycommand = %q, want configured proxy", effective["proxycommand"])
201- }
202-}
203-
204 func TestCloseInterruptsBlockedSend(t *testing.T) {71 func TestCloseInterruptsBlockedSend(t *testing.T) {
205 writer := &blockedWriter{started: make(chan struct{}), released: make(chan struct{})}72 writer := &blockedWriter{started: make(chan struct{}), released: make(chan struct{})}
206 transport := NewTransport(nil, writer, nil)73 transport := NewTransport(nil, writer, nil)
@@ -348,15 +215,6 @@ func TestTransportUsesCanonicalFrameMaximum(t *testing.T) {
348 }215 }
349 }216 }
350 217
351-func TestDialContextCanceledBeforeStart(t *testing.T) {
352- ctx, cancel := context.WithCancel(context.Background())
353- cancel()
354- _, err := DialContext(ctx, "example.com", "work")
355- if !errors.Is(err, context.Canceled) {
356- t.Fatalf("DialContext error = %v, want context.Canceled", err)
357- }
358-}
359-
360 func TestRecvMapsUnexpectedEOFDuringHeaderToSSHExit(t *testing.T) {218 func TestRecvMapsUnexpectedEOFDuringHeaderToSSHExit(t *testing.T) {
361 sshErr := errors.New("ssh exited with status 255")219 sshErr := errors.New("ssh exited with status 255")
362 tr := newTransport(strings.NewReader("\x00"), io.Discard, nil, func() error { return sshErr })220 tr := newTransport(strings.NewReader("\x00"), io.Discard, nil, func() error { return sshErr })

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close