Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e8b1a57

Browse files
authored
feat: Add support for VS Code and JetBrains Gateway via SSH (#956)
* Improve CLI documentation * feat: Allow workspace resources to attach multiple agents This enables a "kubernetes_pod" to attach multiple agents that could be for multiple services. Each agent is required to have a unique name, so SSH syntax is: `coder ssh <workspace>.<agent>` A resource can have zero agents too, they aren't required. * Add tree view * Improve table UI * feat: Allow workspace resources to attach multiple agents This enables a "kubernetes_pod" to attach multiple agents that could be for multiple services. Each agent is required to have a unique name, so SSH syntax is: `coder ssh <workspace>.<agent>` A resource can have zero agents too, they aren't required. * Rename `tunnel` to `skip-tunnel` This command was `true` by default, which causes a confusing user experience. * Add disclaimer about editing templates * Add help to template create * Improve workspace create flow * Add end-to-end test for config-ssh * Improve testing of config-ssh * Fix workspace list * feat: Add support for VS Code and JetBrains Gateway via SSH This fixes various bugs that made this not work: - Incorrect max message size in `peer` - Incorrect reader buffer size in `peer` - Lack of SFTP support in `agent` - Lack of direct-tcpip support in `agent` - Misuse of command from session. It should always use the shell - Blocking on SSH session, only allowing one at a time Fixes #833 too. * Fix config-ssh command with socat
1 parent fb9dc4f commit e8b1a57

File tree

7 files changed

+75
-49
lines changed

7 files changed

+75
-49
lines changed

.github/workflows/coder.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ jobs:
158158
terraform_version: 1.1.2
159159
terraform_wrapper: false
160160

161+
- name: Install socat
162+
if: runner.os == 'Linux'
163+
run: sudo apt-get install -y socat
164+
161165
- name: Test with Mock Database
162166
shell: bash
163167
env:

agent/agent.go

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"github.com/coder/coder/pty"
2222
"github.com/coder/retry"
2323

24+
"github.com/pkg/sftp"
25+
2426
"github.com/gliderlabs/ssh"
2527
gossh "golang.org/x/crypto/ssh"
2628
"golang.org/x/xerrors"
@@ -121,7 +123,7 @@ func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
121123

122124
switch channel.Protocol() {
123125
case "ssh":
124-
a.sshServer.HandleConn(channel.NetConn())
126+
go a.sshServer.HandleConn(channel.NetConn())
125127
default:
126128
a.options.Logger.Warn(ctx, "unhandled protocol from channel",
127129
slog.F("protocol", channel.Protocol()),
@@ -146,7 +148,10 @@ func (a *agent) init(ctx context.Context) {
146148
sshLogger := a.options.Logger.Named("ssh-server")
147149
forwardHandler := &ssh.ForwardedTCPHandler{}
148150
a.sshServer = &ssh.Server{
149-
ChannelHandlers: ssh.DefaultChannelHandlers,
151+
ChannelHandlers: map[string]ssh.ChannelHandler{
152+
"direct-tcpip": ssh.DirectTCPIPHandler,
153+
"session": ssh.DefaultSessionHandler,
154+
},
150155
ConnectionFailedCallback: func(conn net.Conn, err error) {
151156
sshLogger.Info(ctx, "ssh connection ended", slog.Error(err))
152157
},
@@ -185,61 +190,54 @@ func (a *agent) init(ctx context.Context) {
185190
NoClientAuth: true,
186191
}
187192
},
193+
SubsystemHandlers: map[string]ssh.SubsystemHandler{
194+
"sftp": func(session ssh.Session) {
195+
server, err := sftp.NewServer(session)
196+
if err != nil {
197+
a.options.Logger.Debug(session.Context(), "initialize sftp server", slog.Error(err))
198+
return
199+
}
200+
defer server.Close()
201+
err = server.Serve()
202+
if errors.Is(err, io.EOF) {
203+
return
204+
}
205+
a.options.Logger.Debug(session.Context(), "sftp server exited with error", slog.Error(err))
206+
},
207+
},
188208
}
189209

190210
go a.run(ctx)
191211
}
192212

193213
func (a *agent) handleSSHSession(session ssh.Session) error {
194-
var (
195-
command string
196-
args = []string{}
197-
err error
198-
)
199-
200214
currentUser, err := user.Current()
201215
if err != nil {
202216
return xerrors.Errorf("get current user: %w", err)
203217
}
204218
username := currentUser.Username
205219

220+
shell, err := usershell.Get(username)
221+
if err != nil {
222+
return xerrors.Errorf("get user shell: %w", err)
223+
}
224+
206225
// gliderlabs/ssh returns a command slice of zero
207226
// when a shell is requested.
227+
command := session.RawCommand()
208228
if len(session.Command()) == 0 {
209-
command, err = usershell.Get(username)
210-
if err != nil {
211-
return xerrors.Errorf("get user shell: %w", err)
212-
}
213-
} else {
214-
command = session.Command()[0]
215-
if len(session.Command()) > 1 {
216-
args = session.Command()[1:]
217-
}
229+
command = shell
218230
}
219231

220-
signals := make(chan ssh.Signal)
221-
breaks := make(chan bool)
222-
defer close(signals)
223-
defer close(breaks)
224-
go func() {
225-
for {
226-
select {
227-
case <-session.Context().Done():
228-
return
229-
// Ignore signals and breaks for now!
230-
case <-signals:
231-
case <-breaks:
232-
}
233-
}
234-
}()
235-
236-
cmd := exec.CommandContext(session.Context(), command, args...)
232+
// OpenSSH executes all commands with the users current shell.
233+
// We replicate that behavior for IDE support.
234+
cmd := exec.CommandContext(session.Context(), shell, "-c", command)
237235
cmd.Env = append(os.Environ(), session.Environ()...)
238236
executablePath, err := os.Executable()
239237
if err != nil {
240238
return xerrors.Errorf("getting os executable: %w", err)
241239
}
242-
cmd.Env = append(session.Environ(), fmt.Sprintf(`GIT_SSH_COMMAND="%s gitssh --"`, executablePath))
240+
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_SSH_COMMAND="%s gitssh --"`, executablePath))
243241

244242
sshPty, windowSize, isPty := session.Pty()
245243
if isPty {
@@ -268,7 +266,7 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
268266
}
269267

270268
cmd.Stdout = session
271-
cmd.Stderr = session
269+
cmd.Stderr = session.Stderr()
272270
// This blocks forever until stdin is received if we don't
273271
// use StdinPipe. It's unknown what causes this.
274272
stdinPipe, err := cmd.StdinPipe()
@@ -282,8 +280,7 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
282280
if err != nil {
283281
return xerrors.Errorf("start: %w", err)
284282
}
285-
_ = cmd.Wait()
286-
return nil
283+
return cmd.Wait()
287284
}
288285

289286
// isClosed returns whether the API is closed or not.

agent/agent_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import (
55
"fmt"
66
"io"
77
"net"
8+
"os"
89
"os/exec"
10+
"path/filepath"
911
"runtime"
1012
"strconv"
1113
"strings"
1214
"testing"
1315

1416
"github.com/pion/webrtc/v3"
17+
"github.com/pkg/sftp"
1518
"github.com/stretchr/testify/require"
1619
"go.uber.org/goleak"
1720
"golang.org/x/crypto/ssh"
@@ -94,6 +97,7 @@ func TestAgent(t *testing.T) {
9497

9598
local, err := net.Listen("tcp", "127.0.0.1:0")
9699
require.NoError(t, err)
100+
defer local.Close()
97101
tcpAddr, valid = local.Addr().(*net.TCPAddr)
98102
require.True(t, valid)
99103
localPort := tcpAddr.Port
@@ -113,6 +117,21 @@ func TestAgent(t *testing.T) {
113117
conn.Close()
114118
<-done
115119
})
120+
121+
t.Run("SFTP", func(t *testing.T) {
122+
t.Parallel()
123+
sshClient, err := setupAgent(t).SSHClient()
124+
require.NoError(t, err)
125+
client, err := sftp.NewClient(sshClient)
126+
require.NoError(t, err)
127+
tempFile := filepath.Join(t.TempDir(), "sftp")
128+
file, err := client.Create(tempFile)
129+
require.NoError(t, err)
130+
err = file.Close()
131+
require.NoError(t, err)
132+
_, err = os.Stat(tempFile)
133+
require.NoError(t, err)
134+
})
116135
}
117136

118137
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {

cli/cliui/resources_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import (
44
"testing"
55
"time"
66

7+
"github.com/stretchr/testify/require"
8+
79
"github.com/coder/coder/cli/cliui"
810
"github.com/coder/coder/coderd/database"
911
"github.com/coder/coder/codersdk"
1012
"github.com/coder/coder/pty/ptytest"
11-
"github.com/stretchr/testify/require"
1213
)
1314

1415
func TestWorkspaceResources(t *testing.T) {

go.mod

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ require (
4848
github.com/fatih/color v1.13.0
4949
github.com/gliderlabs/ssh v0.3.3
5050
github.com/go-chi/chi/v5 v5.0.7
51+
github.com/go-chi/httprate v0.5.3
5152
github.com/go-chi/render v1.0.1
5253
github.com/go-playground/validator/v10 v10.10.1
5354
github.com/gohugoio/hugo v0.96.0
@@ -60,6 +61,7 @@ require (
6061
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f
6162
github.com/hashicorp/terraform-exec v0.15.0
6263
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87
64+
github.com/jedib0t/go-pretty/v6 v6.3.0
6365
github.com/justinas/nosurf v1.1.1
6466
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
6567
github.com/lib/pq v1.10.5
@@ -72,7 +74,9 @@ require (
7274
github.com/pion/transport v0.13.0
7375
github.com/pion/webrtc/v3 v3.1.27
7476
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
77+
github.com/pkg/sftp v1.13.4
7578
github.com/quasilyte/go-ruleguard/dsl v0.3.19
79+
github.com/robfig/cron/v3 v3.0.1
7680
github.com/rs/zerolog v1.26.1
7781
github.com/spf13/cobra v1.4.0
7882
github.com/spf13/pflag v1.0.5
@@ -83,20 +87,19 @@ require (
8387
go.uber.org/atomic v1.9.0
8488
go.uber.org/goleak v1.1.12
8589
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
90+
golang.org/x/mod v0.5.1
8691
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
8792
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
8893
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886
94+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
8995
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
9096
google.golang.org/api v0.74.0
9197
google.golang.org/protobuf v1.28.0
98+
gopkg.in/DataDog/dd-trace-go.v1 v1.37.1
9299
nhooyr.io/websocket v1.8.7
93100
storj.io/drpc v0.0.30
94101
)
95102

96-
require github.com/go-chi/httprate v0.5.3
97-
98-
require github.com/jedib0t/go-pretty/v6 v6.3.0
99-
100103
require (
101104
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
102105
github.com/BurntSushi/toml v1.0.0 // indirect
@@ -171,6 +174,7 @@ require (
171174
github.com/json-iterator/go v1.1.12 // indirect
172175
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
173176
github.com/klauspost/compress v1.15.0 // indirect
177+
github.com/kr/fs v0.1.0 // indirect
174178
github.com/leodido/go-urn v1.2.1 // indirect
175179
github.com/lucas-clemente/quic-go v0.25.1-0.20220307142123-ad1cb27c1b64 // indirect
176180
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
@@ -222,7 +226,6 @@ require (
222226
github.com/prometheus/procfs v0.7.3 // indirect
223227
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92 // indirect
224228
github.com/rivo/uniseg v0.2.0 // indirect
225-
github.com/robfig/cron/v3 v3.0.1
226229
github.com/russross/blackfriday/v2 v2.1.0 // indirect
227230
github.com/sirupsen/logrus v1.8.1 // indirect
228231
github.com/spf13/afero v1.8.1 // indirect
@@ -235,16 +238,13 @@ require (
235238
github.com/zclconf/go-cty v1.10.0 // indirect
236239
github.com/zeebo/errs v1.2.2 // indirect
237240
go.opencensus.io v0.23.0 // indirect
238-
golang.org/x/mod v0.5.1
239241
golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
240-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
241242
golang.org/x/text v0.3.7 // indirect
242243
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
243244
golang.org/x/tools v0.1.9 // indirect
244245
google.golang.org/appengine v1.6.7 // indirect
245246
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
246247
google.golang.org/grpc v1.45.0 // indirect
247-
gopkg.in/DataDog/dd-trace-go.v1 v1.37.1
248248
gopkg.in/coreos/go-oidc.v2 v2.2.1 // indirect
249249
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
250250
gopkg.in/square/go-jose.v2 v2.6.0 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,7 @@ github.com/klauspost/crc32 v1.2.0/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3H
11031103
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
11041104
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
11051105
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
1106+
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
11061107
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
11071108
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
11081109
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -1438,6 +1439,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
14381439
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
14391440
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
14401441
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
1442+
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
1443+
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
14411444
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14421445
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14431446
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

peer/channel.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121
// For some reason messages larger just don't work...
2222
// This shouldn't be a huge deal for real-world usage.
2323
// See: https://github.com/pion/datachannel/issues/59
24-
maxMessageLength = 32 * 1024 // 32 KB
24+
maxMessageLength = 64 * 1024 // 64 KB
2525
)
2626

2727
// newChannel creates a new channel and initializes it.
@@ -145,7 +145,9 @@ func (c *Channel) init() {
145145
if c.opts.Unordered {
146146
c.reader = c.rwc
147147
} else {
148-
c.reader = bufio.NewReader(c.rwc)
148+
// This must be the max message length otherwise a short
149+
// buffer error can occur.
150+
c.reader = bufio.NewReaderSize(c.rwc, maxMessageLength)
149151
}
150152
close(c.opened)
151153
})

0 commit comments

Comments
 (0)