Skip to content

Commit 9136c14

Browse files
committed
Merge branch 'main' of https://github.com/coder/coder into bq/imrefactor-resources-data
2 parents 295f8f7 + 1aee8da commit 9136c14

File tree

81 files changed

+2650
-278
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+2650
-278
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ provisionersdk/proto/*.go linguist-generated=true
1212
*.tfstate.dot linguist-generated=true
1313
*.tfplan.dot linguist-generated=true
1414
site/src/api/typesGenerated.ts linguist-generated=true
15+
site/src/pages/SetupPage/countries.tsx linguist-generated=true

.github/workflows/typos.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ darcula = "darcula"
1414
Hashi = "Hashi"
1515
trialer = "trialer"
1616
encrypter = "encrypter"
17-
hel = "hel" # as in helsinki
17+
hel = "hel" # as in helsinki
1818

1919
[files]
2020
extend-exclude = [
@@ -31,4 +31,5 @@ extend-exclude = [
3131
"**/*.test.tsx",
3232
"**/pnpm-lock.yaml",
3333
"tailnet/testdata/**",
34+
"site/src/pages/SetupPage/countries.tsx",
3435
]

agent/agentssh/agentssh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
9999
}
100100

101101
forwardHandler := &ssh.ForwardedTCPHandler{}
102-
unixForwardHandler := &forwardedUnixHandler{log: logger}
102+
unixForwardHandler := newForwardedUnixHandler(logger)
103103

104104
metrics := newSSHServerMetrics(prometheusRegistry)
105105
s := &Server{

agent/agentssh/forward.go

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package agentssh
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"io/fs"
68
"net"
79
"os"
810
"path/filepath"
911
"sync"
12+
"syscall"
1013

1114
"github.com/gliderlabs/ssh"
1215
gossh "golang.org/x/crypto/ssh"
@@ -33,22 +36,29 @@ type forwardedStreamLocalPayload struct {
3336
type forwardedUnixHandler struct {
3437
sync.Mutex
3538
log slog.Logger
36-
forwards map[string]net.Listener
39+
forwards map[forwardKey]net.Listener
40+
}
41+
42+
type forwardKey struct {
43+
sessionID string
44+
addr string
45+
}
46+
47+
func newForwardedUnixHandler(log slog.Logger) *forwardedUnixHandler {
48+
return &forwardedUnixHandler{
49+
log: log,
50+
forwards: make(map[forwardKey]net.Listener),
51+
}
3752
}
3853

3954
func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server, req *gossh.Request) (bool, []byte) {
4055
h.log.Debug(ctx, "handling SSH unix forward")
41-
h.Lock()
42-
if h.forwards == nil {
43-
h.forwards = make(map[string]net.Listener)
44-
}
45-
h.Unlock()
4656
conn, ok := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
4757
if !ok {
4858
h.log.Warn(ctx, "SSH unix forward request from client with no gossh connection")
4959
return false, nil
5060
}
51-
log := h.log.With(slog.F("remote_addr", conn.RemoteAddr()))
61+
log := h.log.With(slog.F("session_id", ctx.SessionID()), slog.F("remote_addr", conn.RemoteAddr()))
5262

5363
switch req.Type {
5464
case "streamlocal-forward@openssh.com":
@@ -62,14 +72,22 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
6272
addr := reqPayload.SocketPath
6373
log = log.With(slog.F("socket_path", addr))
6474
log.Debug(ctx, "request begin SSH unix forward")
75+
76+
key := forwardKey{
77+
sessionID: ctx.SessionID(),
78+
addr: addr,
79+
}
80+
6581
h.Lock()
66-
_, ok := h.forwards[addr]
82+
_, ok := h.forwards[key]
6783
h.Unlock()
6884
if ok {
69-
log.Warn(ctx, "SSH unix forward request for socket path that is already being forwarded (maybe to another client?)",
70-
slog.F("socket_path", addr),
71-
)
72-
return false, nil
85+
// In cases where `ExitOnForwardFailure=yes` is set, returning false
86+
// here will cause the connection to be closed. To avoid this, and
87+
// to match OpenSSH behavior, we silently ignore the second forward
88+
// request.
89+
log.Warn(ctx, "SSH unix forward request for socket path that is already being forwarded on this session, ignoring")
90+
return true, nil
7391
}
7492

7593
// Create socket parent dir if not exists.
@@ -83,12 +101,20 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
83101
return false, nil
84102
}
85103

86-
ln, err := net.Listen("unix", addr)
104+
// Remove existing socket if it exists. We do not use os.Remove() here
105+
// so that directories are kept. Note that it's possible that we will
106+
// overwrite a regular file here. Both of these behaviors match OpenSSH,
107+
// however, which is why we unlink.
108+
err = unlink(addr)
109+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
110+
log.Warn(ctx, "remove existing socket for SSH unix forward request", slog.Error(err))
111+
return false, nil
112+
}
113+
114+
lc := &net.ListenConfig{}
115+
ln, err := lc.Listen(ctx, "unix", addr)
87116
if err != nil {
88-
log.Warn(ctx, "listen on Unix socket for SSH unix forward request",
89-
slog.F("socket_path", addr),
90-
slog.Error(err),
91-
)
117+
log.Warn(ctx, "listen on Unix socket for SSH unix forward request", slog.Error(err))
92118
return false, nil
93119
}
94120
log.Debug(ctx, "SSH unix forward listening on socket")
@@ -99,7 +125,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
99125
//
100126
// This is also what the upstream TCP version of this code does.
101127
h.Lock()
102-
h.forwards[addr] = ln
128+
h.forwards[key] = ln
103129
h.Unlock()
104130
log.Debug(ctx, "SSH unix forward added to cache")
105131

@@ -115,9 +141,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
115141
c, err := ln.Accept()
116142
if err != nil {
117143
if !xerrors.Is(err, net.ErrClosed) {
118-
log.Warn(ctx, "accept on local Unix socket for SSH unix forward request",
119-
slog.Error(err),
120-
)
144+
log.Warn(ctx, "accept on local Unix socket for SSH unix forward request", slog.Error(err))
121145
}
122146
// closed below
123147
log.Debug(ctx, "SSH unix forward listener closed")
@@ -131,10 +155,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
131155
go func() {
132156
ch, reqs, err := conn.OpenChannel("forwarded-streamlocal@openssh.com", payload)
133157
if err != nil {
134-
h.log.Warn(ctx, "open SSH unix forward channel to client",
135-
slog.F("socket_path", addr),
136-
slog.Error(err),
137-
)
158+
h.log.Warn(ctx, "open SSH unix forward channel to client", slog.Error(err))
138159
_ = c.Close()
139160
return
140161
}
@@ -144,12 +165,11 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
144165
}
145166

146167
h.Lock()
147-
ln2, ok := h.forwards[addr]
148-
if ok && ln2 == ln {
149-
delete(h.forwards, addr)
168+
if ln2, ok := h.forwards[key]; ok && ln2 == ln {
169+
delete(h.forwards, key)
150170
}
151171
h.Unlock()
152-
log.Debug(ctx, "SSH unix forward listener removed from cache", slog.F("path", addr))
172+
log.Debug(ctx, "SSH unix forward listener removed from cache")
153173
_ = ln.Close()
154174
}()
155175

@@ -162,13 +182,22 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
162182
h.log.Warn(ctx, "parse cancel-streamlocal-forward@openssh.com (SSH unix forward) request payload from client", slog.Error(err))
163183
return false, nil
164184
}
165-
log.Debug(ctx, "request to cancel SSH unix forward", slog.F("path", reqPayload.SocketPath))
185+
log.Debug(ctx, "request to cancel SSH unix forward", slog.F("socket_path", reqPayload.SocketPath))
186+
187+
key := forwardKey{
188+
sessionID: ctx.SessionID(),
189+
addr: reqPayload.SocketPath,
190+
}
191+
166192
h.Lock()
167-
ln, ok := h.forwards[reqPayload.SocketPath]
193+
ln, ok := h.forwards[key]
194+
delete(h.forwards, key)
168195
h.Unlock()
169-
if ok {
170-
_ = ln.Close()
196+
if !ok {
197+
log.Warn(ctx, "SSH unix forward not found in cache")
198+
return true, nil
171199
}
200+
_ = ln.Close()
172201
return true, nil
173202

174203
default:
@@ -209,3 +238,15 @@ func directStreamLocalHandler(_ *ssh.Server, _ *gossh.ServerConn, newChan gossh.
209238

210239
Bicopy(ctx, ch, dconn)
211240
}
241+
242+
// unlink removes files and unlike os.Remove, directories are kept.
243+
func unlink(path string) error {
244+
// Ignore EINTR like os.Remove, see ignoringEINTR in os/file_posix.go
245+
// for more details.
246+
for {
247+
err := syscall.Unlink(path)
248+
if !errors.Is(err, syscall.EINTR) {
249+
return err
250+
}
251+
}
252+
}

cli/ssh_test.go

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ import (
2626
"github.com/stretchr/testify/require"
2727
"golang.org/x/crypto/ssh"
2828
gosshagent "golang.org/x/crypto/ssh/agent"
29+
"golang.org/x/sync/errgroup"
2930
"golang.org/x/xerrors"
3031

3132
"cdr.dev/slog"
3233
"cdr.dev/slog/sloggers/slogtest"
3334

3435
"github.com/coder/coder/v2/agent"
36+
"github.com/coder/coder/v2/agent/agentssh"
3537
"github.com/coder/coder/v2/agent/agenttest"
3638
"github.com/coder/coder/v2/cli/clitest"
3739
"github.com/coder/coder/v2/cli/cliui"
@@ -738,8 +740,8 @@ func TestSSH(t *testing.T) {
738740
defer cancel()
739741

740742
tmpdir := tempDirUnixSocket(t)
741-
agentSock := filepath.Join(tmpdir, "agent.sock")
742-
l, err := net.Listen("unix", agentSock)
743+
localSock := filepath.Join(tmpdir, "local.sock")
744+
l, err := net.Listen("unix", localSock)
743745
require.NoError(t, err)
744746
defer l.Close()
745747
remoteSock := filepath.Join(tmpdir, "remote.sock")
@@ -748,7 +750,7 @@ func TestSSH(t *testing.T) {
748750
"ssh",
749751
workspace.Name,
750752
"--remote-forward",
751-
fmt.Sprintf("%s:%s", remoteSock, agentSock),
753+
fmt.Sprintf("%s:%s", remoteSock, localSock),
752754
)
753755
clitest.SetupConfig(t, client, root)
754756
pty := ptytest.New(t).Attach(inv)
@@ -771,6 +773,116 @@ func TestSSH(t *testing.T) {
771773
<-cmdDone
772774
})
773775

776+
// Test that we can forward a local unix socket to a remote unix socket and
777+
// that new SSH sessions take over the socket without closing active socket
778+
// connections.
779+
t.Run("RemoteForwardUnixSocketMultipleSessionsOverwrite", func(t *testing.T) {
780+
if runtime.GOOS == "windows" {
781+
t.Skip("Test not supported on windows")
782+
}
783+
784+
t.Parallel()
785+
786+
client, workspace, agentToken := setupWorkspaceForAgent(t)
787+
788+
_ = agenttest.New(t, client.URL, agentToken)
789+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
790+
791+
// Wait super super long so this doesn't flake on -race test.
792+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong*2)
793+
defer cancel()
794+
795+
tmpdir := tempDirUnixSocket(t)
796+
797+
localSock := filepath.Join(tmpdir, "local.sock")
798+
l, err := net.Listen("unix", localSock)
799+
require.NoError(t, err)
800+
defer l.Close()
801+
testutil.Go(t, func() {
802+
for {
803+
fd, err := l.Accept()
804+
if err != nil {
805+
if !errors.Is(err, net.ErrClosed) {
806+
assert.NoError(t, err, "listener accept failed")
807+
}
808+
return
809+
}
810+
811+
testutil.Go(t, func() {
812+
defer fd.Close()
813+
agentssh.Bicopy(ctx, fd, fd)
814+
})
815+
}
816+
})
817+
818+
remoteSock := filepath.Join(tmpdir, "remote.sock")
819+
820+
var done []func() error
821+
for i := 0; i < 2; i++ {
822+
id := fmt.Sprintf("ssh-%d", i)
823+
inv, root := clitest.New(t,
824+
"ssh",
825+
workspace.Name,
826+
"--remote-forward",
827+
fmt.Sprintf("%s:%s", remoteSock, localSock),
828+
)
829+
inv.Logger = inv.Logger.Named(id)
830+
clitest.SetupConfig(t, client, root)
831+
pty := ptytest.New(t).Attach(inv)
832+
inv.Stderr = pty.Output()
833+
cmdDone := tGo(t, func() {
834+
err := inv.WithContext(ctx).Run()
835+
assert.NoError(t, err, "ssh command failed: %s", id)
836+
})
837+
838+
// Since something was output, it should be safe to write input.
839+
// This could show a prompt or "running startup scripts", so it's
840+
// not indicative of the SSH connection being ready.
841+
_ = pty.Peek(ctx, 1)
842+
843+
// Ensure the SSH connection is ready by testing the shell
844+
// input/output.
845+
pty.WriteLine("echo ping' 'pong")
846+
pty.ExpectMatchContext(ctx, "ping pong")
847+
848+
d := &net.Dialer{}
849+
fd, err := d.DialContext(ctx, "unix", remoteSock)
850+
require.NoError(t, err, id)
851+
852+
// Ping / pong to ensure the socket is working.
853+
_, err = fd.Write([]byte("hello world"))
854+
require.NoError(t, err, id)
855+
856+
buf := make([]byte, 11)
857+
_, err = fd.Read(buf)
858+
require.NoError(t, err, id)
859+
require.Equal(t, "hello world", string(buf), id)
860+
861+
done = append(done, func() error {
862+
// Redo ping / pong to ensure that the socket
863+
// connections still work.
864+
_, err := fd.Write([]byte("hello world"))
865+
assert.NoError(t, err, id)
866+
867+
buf := make([]byte, 11)
868+
_, err = fd.Read(buf)
869+
assert.NoError(t, err, id)
870+
assert.Equal(t, "hello world", string(buf), id)
871+
872+
pty.WriteLine("exit")
873+
<-cmdDone
874+
return nil
875+
})
876+
}
877+
878+
var eg errgroup.Group
879+
for _, d := range done {
880+
eg.Go(d)
881+
}
882+
err = eg.Wait()
883+
require.NoError(t, err)
884+
})
885+
774886
t.Run("FileLogging", func(t *testing.T) {
775887
t.Parallel()
776888

cli/start.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ func buildWorkspaceStartRequest(inv *clibase.Invocation, client *codersdk.Client
125125
}
126126

127127
func startWorkspace(inv *clibase.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.WorkspaceBuild, error) {
128+
if workspace.DormantAt != nil {
129+
_, _ = fmt.Fprintln(inv.Stdout, "Activating dormant workspace...")
130+
err := client.UpdateWorkspaceDormancy(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceDormancy{
131+
Dormant: false,
132+
})
133+
if err != nil {
134+
return codersdk.WorkspaceBuild{}, xerrors.Errorf("activate workspace: %w", err)
135+
}
136+
}
128137
req, err := buildWorkspaceStartRequest(inv, client, workspace, parameterFlags, action)
129138
if err != nil {
130139
return codersdk.WorkspaceBuild{}, err

0 commit comments

Comments
 (0)