@@ -4,14 +4,58 @@ import (
44 "bytes"
55 "fmt"
66 "net"
7+ "os"
78 "os/exec"
89 "strings"
910 "sync"
1011 "time"
12+
13+ "github.com/jr-k/d4s/internal/secrets"
1114)
1215
1316const socatImage = "alpine/socat"
1417
18+ // sshAuth holds per-context ssh authentication settings resolved
19+ // from the OS keychain.
20+ type sshAuth struct {
21+ extraArgs []string
22+ env []string
23+ batchMode bool
24+ }
25+
26+ func resolveSSHAuth (contextName string ) sshAuth {
27+ auth := sshAuth {batchMode : true }
28+ creds , err := secrets .Load (contextName )
29+ if err != nil || creds == nil {
30+ return auth
31+ }
32+ auth .extraArgs = creds .SSHArgs ()
33+ if creds .HasSecret () {
34+ // BatchMode disables askpass, so it must be off when a stored
35+ // secret has to be served through SSH_ASKPASS.
36+ auth .batchMode = false
37+ auth .env = append (os .Environ (), secrets .AskpassEnv (contextName )... )
38+ }
39+ return auth
40+ }
41+
42+ func (a sshAuth ) baseArgs () []string {
43+ args := []string {
44+ "-o" , "StrictHostKeyChecking=accept-new" ,
45+ "-o" , "ConnectTimeout=10" ,
46+ }
47+ if a .batchMode {
48+ args = append (args , "-o" , "BatchMode=yes" )
49+ }
50+ return append (args , a .extraArgs ... )
51+ }
52+
53+ func (a sshAuth ) apply (cmd * exec.Cmd ) {
54+ if a .env != nil {
55+ cmd .Env = a .env
56+ }
57+ }
58+
1559type Tunnel struct {
1660 // direct mode (ssh -N -L): persistent ssh process
1761 cmd * exec.Cmd
@@ -32,14 +76,15 @@ type Tunnel struct {
3276// plain ssh -L tunnel to 127.0.0.1:hostPort is used.
3377// Otherwise (overlay networks, unpublished ports), each connection is piped
3478// through a socat process running inside the container's network namespace.
35- func NewTunnel (sshHost string , localPort uint16 , containerID string , containerPort , hostPort uint16 ) (* Tunnel , error ) {
79+ func NewTunnel (contextName , sshHost string , localPort uint16 , containerID string , containerPort , hostPort uint16 ) (* Tunnel , error ) {
80+ auth := resolveSSHAuth (contextName )
3681 if hostPort > 0 {
37- return newDirectTunnel (sshHost , localPort , hostPort )
82+ return newDirectTunnel (auth , sshHost , localPort , hostPort )
3883 }
39- return newNetnsTunnel (sshHost , localPort , containerID , containerPort )
84+ return newNetnsTunnel (auth , sshHost , localPort , containerID , containerPort )
4085}
4186
42- func newDirectTunnel (sshHost string , localPort , hostPort uint16 ) (* Tunnel , error ) {
87+ func newDirectTunnel (auth sshAuth , sshHost string , localPort , hostPort uint16 ) (* Tunnel , error ) {
4388 user , addr := parseSSHHost (sshHost )
4489 host , port := splitHostPort (addr )
4590
@@ -56,16 +101,15 @@ func newDirectTunnel(sshHost string, localPort, hostPort uint16) (*Tunnel, error
56101 "-N" ,
57102 "-L" , localBind ,
58103 "-l" , user ,
59- "-o" , "StrictHostKeyChecking=accept-new" ,
60- "-o" , "ConnectTimeout=10" ,
61104 "-o" , "ExitOnForwardFailure=yes" ,
62- "-o" , "BatchMode=yes" ,
63105 "-p" , port ,
64- host ,
65106 }
107+ args = append (args , auth .baseArgs ()... )
108+ args = append (args , host )
66109
67110 cmd := exec .Command ("ssh" , args ... )
68111 cmd .Stdin = nil
112+ auth .apply (cmd )
69113
70114 var stderr bytes.Buffer
71115 cmd .Stderr = & stderr
@@ -114,11 +158,11 @@ func newDirectTunnel(sshHost string, localPort, hostPort uint16) (*Tunnel, error
114158 return nil , fmt .Errorf ("tunnel did not become ready within 5s" )
115159}
116160
117- func newNetnsTunnel (sshHost string , localPort uint16 , containerID string , containerPort uint16 ) (* Tunnel , error ) {
161+ func newNetnsTunnel (auth sshAuth , sshHost string , localPort uint16 , containerID string , containerPort uint16 ) (* Tunnel , error ) {
118162 user , addr := parseSSHHost (sshHost )
119163 host , port := splitHostPort (addr )
120164
121- if err := ensureSocatImage (user , host , port ); err != nil {
165+ if err := ensureSocatImage (auth , user , host , port ); err != nil {
122166 return nil , err
123167 }
124168
@@ -138,38 +182,38 @@ func newNetnsTunnel(sshHost string, localPort uint16, containerID string, contai
138182 containerID , socatImage , containerPort ,
139183 )
140184
141- go t .acceptLoop (user , host , port , remoteCmd )
185+ go t .acceptLoop (auth , user , host , port , remoteCmd )
142186
143187 return t , nil
144188}
145189
146- func (t * Tunnel ) acceptLoop (user , host , port , remoteCmd string ) {
190+ func (t * Tunnel ) acceptLoop (auth sshAuth , user , host , port , remoteCmd string ) {
147191 for {
148192 conn , err := t .listener .Accept ()
149193 if err != nil {
150194 return
151195 }
152- go t .handleConn (conn , user , host , port , remoteCmd )
196+ go t .handleConn (auth , conn , user , host , port , remoteCmd )
153197 }
154198}
155199
156- func (t * Tunnel ) handleConn (conn net.Conn , user , host , port , remoteCmd string ) {
200+ func (t * Tunnel ) handleConn (auth sshAuth , conn net.Conn , user , host , port , remoteCmd string ) {
157201 defer conn .Close ()
158202
159- cmd := exec . Command ( "ssh" ,
203+ args := [] string {
160204 "-l" , user ,
161205 "-p" , port ,
162- "-o" , "StrictHostKeyChecking=accept-new" ,
163- "-o" , "ConnectTimeout=10" ,
164- "-o" , "BatchMode=yes" ,
165206 "-o" , "ControlMaster=auto" ,
166207 "-o" , "ControlPath=/tmp/d4s-ssh-%r@%h-%p" ,
167208 "-o" , "ControlPersist=60s" ,
168- host ,
169- remoteCmd ,
170- )
209+ }
210+ args = append (args , auth .baseArgs ()... )
211+ args = append (args , host , remoteCmd )
212+
213+ cmd := exec .Command ("ssh" , args ... )
171214 cmd .Stdin = conn
172215 cmd .Stdout = conn
216+ auth .apply (cmd )
173217
174218 t .mu .Lock ()
175219 if t .closed {
@@ -186,20 +230,20 @@ func (t *Tunnel) handleConn(conn net.Conn, user, host, port, remoteCmd string) {
186230 t .mu .Unlock ()
187231}
188232
189- func ensureSocatImage (user , host , port string ) error {
233+ func ensureSocatImage (auth sshAuth , user , host , port string ) error {
190234 check := fmt .Sprintf (
191235 "docker image inspect %s >/dev/null 2>&1 || docker pull %s >/dev/null 2>&1" ,
192236 socatImage , socatImage ,
193237 )
194- cmd := exec . Command ( "ssh" ,
238+ args := [] string {
195239 "-l" , user ,
196240 "-p" , port ,
197- "-o" , "StrictHostKeyChecking=accept-new" ,
198- "-o" , "ConnectTimeout=10" ,
199- "-o" , "BatchMode=yes" ,
200- host ,
201- check ,
202- )
241+ }
242+ args = append ( args , auth . baseArgs () ... )
243+ args = append ( args , host , check )
244+
245+ cmd := exec . Command ( "ssh" , args ... )
246+ auth . apply ( cmd )
203247 var stderr bytes.Buffer
204248 cmd .Stderr = & stderr
205249 if err := cmd .Run (); err != nil {
0 commit comments