mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
feat(agent): add second SSH listener on port 22 (cherry-pick #16627) (#16763)
Some checks are pending
Deploy PR / check_pr (push) Waiting to run
Deploy PR / get_info (push) Blocked by required conditions
Deploy PR / comment-pr (push) Blocked by required conditions
Deploy PR / build (push) Blocked by required conditions
Deploy PR / deploy (push) Blocked by required conditions
Some checks are pending
Deploy PR / check_pr (push) Waiting to run
Deploy PR / get_info (push) Blocked by required conditions
Deploy PR / comment-pr (push) Blocked by required conditions
Deploy PR / build (push) Blocked by required conditions
Deploy PR / deploy (push) Blocked by required conditions
Cherry-picked feat(agent): add second SSH listener on port 22 (#16627) Fixes: https://github.com/coder/internal/issues/377 Added an additional SSH listener on port 22, so the agent now listens on both, port one and port 22. --- Change-Id: Ifd986b260f8ac317e37d65111cd4e0bd1dc38af8 Signed-off-by: Thomas Kosiewski <tk@coder.com>
This commit is contained in:
committed by
GitHub
parent
114cf57580
commit
735dc5d794
@ -1193,10 +1193,12 @@ func (a *agent) createTailnet(
|
||||
return nil, xerrors.Errorf("update host signer: %w", err)
|
||||
}
|
||||
|
||||
sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(workspacesdk.AgentSSHPort))
|
||||
for _, port := range []int{workspacesdk.AgentSSHPort, workspacesdk.AgentStandardSSHPort} {
|
||||
sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(port))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("listen on the ssh port: %w", err)
|
||||
return nil, xerrors.Errorf("listen on the ssh port (%v): %w", port, err)
|
||||
}
|
||||
// nolint:revive // We do want to run the deferred functions when createTailnet returns.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = sshListener.Close()
|
||||
@ -1207,6 +1209,7 @@ func (a *agent) createTailnet(
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reconnectingPTYListener, err := network.Listen("tcp", ":"+strconv.Itoa(workspacesdk.AgentReconnectingPTYPort))
|
||||
if err != nil {
|
||||
|
@ -61,17 +61,25 @@ func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
|
||||
}
|
||||
|
||||
var sshPorts = []uint16{workspacesdk.AgentSSHPort, workspacesdk.AgentStandardSSHPort}
|
||||
|
||||
// NOTE: These tests only work when your default shell is bash for some reason.
|
||||
|
||||
func TestAgent_Stats_SSH(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, port := range sshPorts {
|
||||
port := port
|
||||
t.Run(fmt.Sprintf("(:%d)", port), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
//nolint:dogsled
|
||||
conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
||||
|
||||
sshClient, err := conn.SSHClient(ctx)
|
||||
sshClient, err := conn.SSHClientOnPort(ctx, port)
|
||||
require.NoError(t, err)
|
||||
defer sshClient.Close()
|
||||
session, err := sshClient.NewSession()
|
||||
@ -93,6 +101,8 @@ func TestAgent_Stats_SSH(t *testing.T) {
|
||||
_ = stdin.Close()
|
||||
err = session.Wait()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_Stats_ReconnectingPTY(t *testing.T) {
|
||||
@ -266,7 +276,13 @@ func TestAgent_Stats_Magic(t *testing.T) {
|
||||
|
||||
func TestAgent_SessionExec(t *testing.T) {
|
||||
t.Parallel()
|
||||
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
||||
|
||||
for _, port := range sshPorts {
|
||||
port := port
|
||||
t.Run(fmt.Sprintf("(:%d)", port), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
session := setupSSHSessionOnPort(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil, port)
|
||||
|
||||
command := "echo test"
|
||||
if runtime.GOOS == "windows" {
|
||||
@ -275,6 +291,8 @@ func TestAgent_SessionExec(t *testing.T) {
|
||||
output, err := session.Output(command)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", strings.TrimSpace(string(output)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:tparallel // Sub tests need to run sequentially.
|
||||
@ -384,7 +402,13 @@ func TestAgent_SessionTTYShell(t *testing.T) {
|
||||
// it seems like it could be either.
|
||||
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
||||
}
|
||||
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
||||
|
||||
for _, port := range sshPorts {
|
||||
port := port
|
||||
t.Run(fmt.Sprintf("(%d)", port), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
session := setupSSHSessionOnPort(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil, port)
|
||||
command := "sh"
|
||||
if runtime.GOOS == "windows" {
|
||||
command = "cmd.exe"
|
||||
@ -403,6 +427,8 @@ func TestAgent_SessionTTYShell(t *testing.T) {
|
||||
ptty.WriteLine("exit")
|
||||
err = session.Wait()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_SessionTTYExitCode(t *testing.T) {
|
||||
@ -596,16 +622,19 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
|
||||
//nolint:dogsled // Allow the blank identifiers.
|
||||
conn, client, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, setSBInterval)
|
||||
|
||||
sshClient, err := conn.SSHClient(ctx)
|
||||
//nolint:paralleltest // These tests need to swap the banner func.
|
||||
for _, port := range sshPorts {
|
||||
port := port
|
||||
|
||||
sshClient, err := conn.SSHClientOnPort(ctx, port)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = sshClient.Close()
|
||||
})
|
||||
|
||||
//nolint:paralleltest // These tests need to swap the banner func.
|
||||
for i, test := range tests {
|
||||
test := test
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("(:%d)/%d", port, i), func(t *testing.T) {
|
||||
// Set new banner func and wait for the agent to call it to update the
|
||||
// banner.
|
||||
ready := make(chan struct{}, 2)
|
||||
@ -629,6 +658,7 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:paralleltest // This test sets an environment variable.
|
||||
func TestAgent_Session_TTY_QuietLogin(t *testing.T) {
|
||||
@ -2313,6 +2343,17 @@ func setupSSHSession(
|
||||
banner codersdk.BannerConfig,
|
||||
prepareFS func(fs afero.Fs),
|
||||
opts ...func(*agenttest.Client, *agent.Options),
|
||||
) *ssh.Session {
|
||||
return setupSSHSessionOnPort(t, manifest, banner, prepareFS, workspacesdk.AgentSSHPort, opts...)
|
||||
}
|
||||
|
||||
func setupSSHSessionOnPort(
|
||||
t *testing.T,
|
||||
manifest agentsdk.Manifest,
|
||||
banner codersdk.BannerConfig,
|
||||
prepareFS func(fs afero.Fs),
|
||||
port uint16,
|
||||
opts ...func(*agenttest.Client, *agent.Options),
|
||||
) *ssh.Session {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
@ -2326,7 +2367,7 @@ func setupSSHSession(
|
||||
if prepareFS != nil {
|
||||
prepareFS(fs)
|
||||
}
|
||||
sshClient, err := conn.SSHClient(ctx)
|
||||
sshClient, err := conn.SSHClientOnPort(ctx, port)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = sshClient.Close()
|
||||
|
@ -17,7 +17,7 @@ func Get(username string) (string, error) {
|
||||
return "", xerrors.Errorf("username is nonlocal path: %s", username)
|
||||
}
|
||||
//nolint: gosec // input checked above
|
||||
out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", username), "UserShell").Output()
|
||||
out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", username), "UserShell").Output() //nolint:gocritic
|
||||
s, ok := strings.CutPrefix(string(out), "UserShell: ")
|
||||
if ok {
|
||||
return strings.TrimSpace(s), nil
|
||||
|
@ -143,6 +143,12 @@ func (c *AgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, w
|
||||
// SSH pipes the SSH protocol over the returned net.Conn.
|
||||
// This connects to the built-in SSH server in the workspace agent.
|
||||
func (c *AgentConn) SSH(ctx context.Context) (*gonet.TCPConn, error) {
|
||||
return c.SSHOnPort(ctx, AgentSSHPort)
|
||||
}
|
||||
|
||||
// SSHOnPort pipes the SSH protocol over the returned net.Conn.
|
||||
// This connects to the built-in SSH server in the workspace agent on the specified port.
|
||||
func (c *AgentConn) SSHOnPort(ctx context.Context, port uint16) (*gonet.TCPConn, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
@ -150,17 +156,23 @@ func (c *AgentConn) SSH(ctx context.Context) (*gonet.TCPConn, error) {
|
||||
return nil, xerrors.Errorf("workspace agent not reachable in time: %v", ctx.Err())
|
||||
}
|
||||
|
||||
c.Conn.SendConnectedTelemetry(c.agentAddress(), tailnet.TelemetryApplicationSSH)
|
||||
return c.Conn.DialContextTCP(ctx, netip.AddrPortFrom(c.agentAddress(), AgentSSHPort))
|
||||
c.SendConnectedTelemetry(c.agentAddress(), tailnet.TelemetryApplicationSSH)
|
||||
return c.DialContextTCP(ctx, netip.AddrPortFrom(c.agentAddress(), port))
|
||||
}
|
||||
|
||||
// SSHClient calls SSH to create a client that uses a weak cipher
|
||||
// to improve throughput.
|
||||
func (c *AgentConn) SSHClient(ctx context.Context) (*ssh.Client, error) {
|
||||
return c.SSHClientOnPort(ctx, AgentSSHPort)
|
||||
}
|
||||
|
||||
// SSHClientOnPort calls SSH to create a client on a specific port
|
||||
// that uses a weak cipher to improve throughput.
|
||||
func (c *AgentConn) SSHClientOnPort(ctx context.Context, port uint16) (*ssh.Client, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
netConn, err := c.SSH(ctx)
|
||||
netConn, err := c.SSHOnPort(ctx, port)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("ssh: %w", err)
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ var ErrSkipClose = xerrors.New("skip tailnet close")
|
||||
|
||||
const (
|
||||
AgentSSHPort = tailnet.WorkspaceAgentSSHPort
|
||||
AgentStandardSSHPort = tailnet.WorkspaceAgentStandardSSHPort
|
||||
AgentReconnectingPTYPort = tailnet.WorkspaceAgentReconnectingPTYPort
|
||||
AgentSpeedtestPort = tailnet.WorkspaceAgentSpeedtestPort
|
||||
// AgentHTTPAPIServerPort serves a HTTP server with endpoints for e.g.
|
||||
|
@ -52,6 +52,7 @@ const (
|
||||
WorkspaceAgentSSHPort = 1
|
||||
WorkspaceAgentReconnectingPTYPort = 2
|
||||
WorkspaceAgentSpeedtestPort = 3
|
||||
WorkspaceAgentStandardSSHPort = 22
|
||||
)
|
||||
|
||||
// EnvMagicsockDebugLogging enables super-verbose logging for the magicsock
|
||||
@ -745,7 +746,7 @@ func (c *Conn) forwardTCP(src, dst netip.AddrPort) (handler func(net.Conn), opts
|
||||
return nil, nil, false
|
||||
}
|
||||
// See: https://github.com/tailscale/tailscale/blob/c7cea825aea39a00aca71ea02bab7266afc03e7c/wgengine/netstack/netstack.go#L888
|
||||
if dst.Port() == WorkspaceAgentSSHPort || dst.Port() == 22 {
|
||||
if dst.Port() == WorkspaceAgentSSHPort || dst.Port() == WorkspaceAgentStandardSSHPort {
|
||||
opt := tcpip.KeepaliveIdleOption(72 * time.Hour)
|
||||
opts = append(opts, &opt)
|
||||
}
|
||||
|
Reference in New Issue
Block a user