Skip to content

Commit 220f6e5

Browse files
authored
openssh: update SSH config when DEVBOX_GATEWAY is set (#378)
Update `~/.config/devbox/ssh/config` to include the value of `DEVBOX_GATEWAY` when it's set. This makes debugging easier by clearing out previous host keys and disabling strict host key checking. It also creates an alias of `gateway.devbox-debug` so that: ssh gateway.devbox-debug will always connect to whatever is in `DEVBOX_GATEWAY`.
1 parent ebf6227 commit 220f6e5

File tree

6 files changed

+180
-29
lines changed

6 files changed

+180
-29
lines changed

internal/cloud/cloud.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,27 @@ import (
2323
)
2424

2525
func Shell(configDir string) error {
26-
if err := openssh.SetupDevbox(); err != nil {
26+
username, vmHostname := parseVMEnvVar()
27+
if username == "" {
28+
username = promptUsername()
29+
}
30+
debug.Log("username: %s", username)
31+
sshClient := openssh.Client{
32+
Username: username,
33+
Addr: "gateway.devbox.sh",
34+
}
35+
// When developing we can use this env variable to point
36+
// to a different gateway
37+
var err error
38+
if envGateway := os.Getenv("DEVBOX_GATEWAY"); envGateway != "" {
39+
sshClient.Addr = envGateway
40+
err = openssh.SetupInsecureDebug(envGateway)
41+
} else {
42+
err = openssh.SetupDevbox()
43+
}
44+
if err != nil {
2745
return err
2846
}
29-
3047
if err := sshshim.Setup(); err != nil {
3148
return err
3249
}
@@ -36,21 +53,15 @@ func Shell(configDir string) error {
3653
fmt.Println("Blazingly fast remote development that feels local")
3754
fmt.Print("\n")
3855

39-
username, vmHostname := parseVMEnvVar()
40-
if username == "" {
41-
username = promptUsername()
42-
}
43-
debug.Log("username: %s", username)
44-
4556
if vmHostname == "" {
4657
s1 := stepper.Start("Creating a virtual machine on the cloud...")
47-
vmHostname = getVirtualMachine(username)
58+
vmHostname = getVirtualMachine(sshClient)
4859
s1.Success("Created virtual machine")
4960
}
5061
debug.Log("vm_hostname: %s", vmHostname)
5162

5263
s2 := stepper.Start("Starting file syncing...")
53-
err := syncFiles(username, vmHostname, configDir)
64+
err = syncFiles(username, vmHostname, configDir)
5465
if err != nil {
5566
s2.Fail("Starting file syncing [FAILED]")
5667
log.Fatal(err)
@@ -93,17 +104,7 @@ func (vm vm) redact() *vm {
93104
return &vm
94105
}
95106

96-
func getVirtualMachine(username string) string {
97-
client := openssh.Client{
98-
Username: username,
99-
Hostname: "gateway.devbox.sh",
100-
}
101-
102-
// When developing we can use this env variable to point
103-
// to a different gateway
104-
if envGateway := os.Getenv("DEVBOX_GATEWAY"); envGateway != "" {
105-
client.Hostname = envGateway
106-
}
107+
func getVirtualMachine(client openssh.Client) string {
107108
sshOut, err := client.Exec("auth")
108109
if err != nil {
109110
log.Fatalln("error requesting VM:", err)
@@ -165,7 +166,7 @@ func syncFiles(username, hostname, configDir string) error {
165166
func shell(username, hostname, configDir string) error {
166167
client := &openssh.Client{
167168
Username: username,
168-
Hostname: hostname,
169+
Addr: hostname,
169170
ProjectDirName: projectDirName(configDir),
170171
}
171172
return client.Shell()

internal/cloud/openssh/client.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616

1717
type Client struct {
1818
Username string
19-
Hostname string
19+
Addr string
2020
ProjectDirName string
2121
}
2222

@@ -50,7 +50,7 @@ func (c *Client) Exec(remoteCmd string) ([]byte, error) {
5050
}
5151

5252
func (c *Client) cmd(sshArgs ...string) *exec.Cmd {
53-
host, port := c.hostPort()
53+
host, port := splitHostPort(c.Addr)
5454
cmd := exec.Command("ssh", sshArgs...)
5555
cmd.Args = append(cmd.Args, destination(c.Username, host))
5656

@@ -62,10 +62,12 @@ func (c *Client) cmd(sshArgs ...string) *exec.Cmd {
6262
return cmd
6363
}
6464

65-
func (c *Client) hostPort() (host string, port int) {
66-
host, portStr, err := net.SplitHostPort(c.Hostname)
65+
// splitHostPort is like net.SplitHostPort except it defaults to port 22 if the
66+
// port in the address is missing or invalid.
67+
func splitHostPort(addr string) (host string, port int) {
68+
host, portStr, err := net.SplitHostPort(addr)
6769
if err != nil {
68-
return c.Hostname, 22
70+
return addr, 22
6971
}
7072
port, err = net.LookupPort("tcp", portStr)
7173
if err != nil {

internal/cloud/openssh/config.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ import (
1717
"github.com/pkg/errors"
1818
)
1919

20+
// These must match what's in sshConfigTmpl. We should eventually make the hosts
21+
// a template variable.
22+
const (
23+
gatewayProdHost = "gateway.devbox.sh"
24+
gatewayDevHost = "gateway.dev.devbox.sh"
25+
)
26+
2027
//go:embed sshconfig.tmpl
2128
var sshConfigText string
2229
var sshConfigTmpl = template.Must(template.New("sshconfig").Parse(sshConfigText))
@@ -28,11 +35,31 @@ var sshKnownHosts []byte
2835
// to Devbox Cloud hosts. It does nothing if Devbox Cloud is already
2936
// configured.
3037
func SetupDevbox() error {
38+
return setupDevbox("", 0)
39+
}
40+
41+
// SetupInsecureDebug is like SetupDevbox, but also configures an additional
42+
// gateway with host key checking disabled. If gatewayAddr is a
43+
// well-known *.devbox.sh gateway, then SetupInsecureDebug doesn't add any
44+
// extra hosts and acts identically to SetupDevbox.
45+
func SetupInsecureDebug(gatewayAddr string) error {
46+
host, port := splitHostPort(gatewayAddr)
47+
if host != gatewayProdHost && host != gatewayDevHost {
48+
return setupDevbox(host, port)
49+
}
50+
return setupDevbox("", 0)
51+
}
52+
53+
func setupDevbox(debugHost string, debugPort int) error {
3154
devboxSSHDir, err := devboxSSHDir()
3255
if err != nil {
3356
return err
3457
}
3558

59+
// Try to remove any old debug host keys. It's okay if this fails.
60+
devboxKnownHostsDebug := filepath.Join(devboxSSHDir, "known_hosts_debug")
61+
_ = os.Remove(devboxKnownHostsDebug)
62+
3663
devboxKnownHostsPath := filepath.Join(devboxSSHDir, "known_hosts")
3764
devboxKnownHosts, err := editFile(devboxKnownHostsPath, 0644)
3865
if err != nil {
@@ -53,13 +80,20 @@ func SetupDevbox() error {
5380
}
5481
defer devboxSSHConfig.Close()
5582

56-
err = errors.WithStack(sshConfigTmpl.Execute(devboxSSHConfig, struct {
83+
tmplData := struct {
5784
ConfigVersion string
5885
ConfigDir string
86+
DebugGateway struct {
87+
Host string
88+
Port int
89+
}
5990
}{
6091
ConfigVersion: "0.0.1",
6192
ConfigDir: devboxSSHDir,
62-
}))
93+
}
94+
tmplData.DebugGateway.Host = debugHost
95+
tmplData.DebugGateway.Port = debugPort
96+
err = errors.WithStack(sshConfigTmpl.Execute(devboxSSHConfig, tmplData))
6397
if err != nil {
6498
return errors.WithStack(err)
6599
}
@@ -69,6 +103,16 @@ func SetupDevbox() error {
69103
if err := updateUserSSHConfig(devboxIncludePath); err != nil {
70104
return err
71105
}
106+
107+
// Create the known_hosts_debug file with the correct permissions if a
108+
// debug gateway is configured. It's okay if this fails because it's
109+
// only used for debugging.
110+
if debugHost != "" {
111+
f, err := os.OpenFile(devboxKnownHostsDebug, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
112+
if err == nil {
113+
f.Close()
114+
}
115+
}
72116
return nil
73117
}
74118

internal/cloud/openssh/config_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package openssh
22

33
import (
4+
"bytes"
45
_ "embed"
56
"io/fs"
67
"os"
@@ -145,6 +146,72 @@ func TestSetupDevbox(t *testing.T) {
145146
})
146147
}
147148

149+
//go:embed testdata/devbox-ssh-debug-config.golden
150+
var goldenDevboxSSHDebugConfig []byte
151+
152+
func TestSetupInsecureDebug(t *testing.T) {
153+
wantAddr := "127.0.0.1:2222"
154+
want := fstest.MapFS{
155+
".config": &fstest.MapFile{Mode: fs.ModeDir | 0755},
156+
".config/devbox": &fstest.MapFile{Mode: fs.ModeDir | 0755},
157+
".config/devbox/ssh": &fstest.MapFile{Mode: fs.ModeDir | 0700},
158+
".config/devbox/ssh/config": &fstest.MapFile{
159+
Data: goldenDevboxSSHDebugConfig,
160+
Mode: 0644,
161+
},
162+
".config/devbox/ssh/known_hosts": &fstest.MapFile{
163+
Data: sshKnownHosts,
164+
Mode: 0644,
165+
},
166+
".config/devbox/ssh/known_hosts_debug": &fstest.MapFile{Mode: 0644},
167+
168+
".ssh": &fstest.MapFile{Mode: fs.ModeDir | 0700},
169+
".ssh/config": &fstest.MapFile{
170+
Data: []byte("Include \"$HOME/.config/devbox/ssh/config\"\n"),
171+
Mode: 0644,
172+
},
173+
}
174+
175+
t.Run("NoConfigs", func(t *testing.T) {
176+
in := fstest.MapFS{}
177+
workdir := fsToDir(t, in)
178+
t.Setenv("HOME", workdir)
179+
if err := SetupInsecureDebug(wantAddr); err != nil {
180+
t.Errorf("got SetupInsecureDebug(%q) error: %v", wantAddr, err)
181+
}
182+
got := os.DirFS(workdir)
183+
fsEqual(t, got, want)
184+
})
185+
t.Run("ChangeHost", func(t *testing.T) {
186+
input := fstest.MapFS{}
187+
for k, v := range want {
188+
input[k] = v
189+
}
190+
191+
// Set the initial config to have a debug host = 127.0.0.2 so we
192+
// can check that it gets changed back to 127.0.0.1.
193+
input[".config/devbox/ssh/config"] = &fstest.MapFile{
194+
Data: bytes.ReplaceAll(goldenDevboxSSHDebugConfig, []byte("127.0.0.1"), []byte("127.0.0.2")),
195+
Mode: 0644,
196+
}
197+
198+
// Put something in known_hosts_debug so we can check that it
199+
// gets cleared out.
200+
input[".config/devbox/ssh/known_hosts_debug"] = &fstest.MapFile{
201+
Data: []byte("[127.0.0.1]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAPY1ms2jt+QPvhq89J8KF7rfTCFUi6X6Ik4O9EIAT/c\n"),
202+
Mode: 0644,
203+
}
204+
205+
workdir := fsToDir(t, input)
206+
t.Setenv("HOME", workdir)
207+
if err := SetupInsecureDebug(wantAddr); err != nil {
208+
t.Errorf("got SetupInsecureDebug(%q) error: %v", wantAddr, err)
209+
}
210+
got := os.DirFS(workdir)
211+
fsEqual(t, got, want)
212+
})
213+
}
214+
148215
//go:embed testdata/test.vm.devbox-vms.internal.golden
149216
var goldenVMKey []byte
150217

internal/cloud/openssh/sshconfig.tmpl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@ Host *.devbox-vms.internal
1919
UserKnownHostsFile "{{ .ConfigDir }}/known_hosts"
2020
ServerAliveInterval 50
2121
ServerAliveCountMax 3
22+
23+
{{- if .DebugGateway.Host }}
24+
25+
Host gateway.devbox-debug {{ .DebugGateway.Host }}
26+
Hostname {{ .DebugGateway.Host }}
27+
Port {{ .DebugGateway.Port }}
28+
StrictHostKeyChecking no
29+
UserKnownHostsFile "{{ .ConfigDir }}/known_hosts_debug"
30+
31+
{{- end }}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Autogenerated by devbox. Do not modify manually.
2+
# ConfigVersion: 0.0.1
3+
4+
Host proxy.devbox.sh
5+
StrictHostKeyChecking no
6+
UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts"
7+
8+
Host gateway.devbox.sh gateway.dev.devbox.sh
9+
HostKeyAlgorithms ssh-ed25519
10+
StrictHostKeyChecking yes
11+
UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts"
12+
13+
Host *.devbox-vms.internal
14+
Port 2222
15+
16+
IdentityFile "$HOME/.config/devbox/ssh/keys/%h"
17+
PreferredAuthentications publickey
18+
StrictHostKeyChecking no
19+
UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts"
20+
ServerAliveInterval 50
21+
ServerAliveCountMax 3
22+
23+
Host gateway.devbox-debug 127.0.0.1
24+
Hostname 127.0.0.1
25+
Port 2222
26+
StrictHostKeyChecking no
27+
UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts_debug"

0 commit comments

Comments
 (0)