11package main
22
33import (
4+ "encoding/json"
45 "fmt"
56 "io"
67 "log/slog"
78 "os"
9+ "os/exec"
810 "os/user"
911 "strings"
1012
@@ -30,7 +32,7 @@ var CmdTunnel = &cli.Command{
3032 "```" ,
3133 "" ,
3234 "If your app has a VPC with `bastion` enabled, you can use this to connect to it." ,
33- "This will forward traffic from the following ranges over SSH:" ,
35+ "This will forward traffic from the following ranges using either SSH or SSM, depending on your bastion configuration :" ,
3436 "- `10.0.4.0/22`" ,
3537 "- `10.0.12.0/22`" ,
3638 "- `10.0.0.0/22`" ,
@@ -57,8 +59,23 @@ var CmdTunnel = &cli.Command{
5759 "" ,
5860 "This needs a network interface on your local machine. You can create this" ,
5961 "with the `sst tunnel install` command." ,
62+ "" ,
63+ ":::note" ,
64+ "When using the Bastion component in SSM mode, the tunnel requires the AWS Session Manager Plugin to be installed." ,
65+ "https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html" ,
66+ ":::" ,
6067 }, "\n " ),
6168 },
69+ Flags : []cli.Flag {
70+ {
71+ Name : "ssm" ,
72+ Type : "bool" ,
73+ Description : cli.Description {
74+ Short : "Use SSM mode instead of SSH" ,
75+ Long : "Use SSM mode instead of SSH. This requires the AWS Session Manager Plugin to be installed." ,
76+ },
77+ },
78+ },
6279 Run : func (c * cli.Cli ) error {
6380 if tunnel .NeedsInstall () {
6481 return util .NewReadableError (nil , "The sst tunnel needs to be installed or upgraded. Run `sudo sst tunnel install`" )
@@ -104,41 +121,127 @@ var CmdTunnel = &cli.Command{
104121 if len (completed .Tunnels ) == 0 {
105122 return util .NewReadableError (nil , "No tunnels found for stage " + stage )
106123 }
107- var tun project.Tunnel
108- for _ , item := range completed .Tunnels {
109- tun = item
124+
125+ forceSSM := c .Bool ("ssm" )
126+
127+ var ssmConfigs []tunnel.SSMConfig
128+ var sshConfigs []tunnel.SSHConfig
129+ var allSubnets []string
130+
131+ for name , tun := range completed .Tunnels {
132+ mode := tun .Mode
133+
134+ if forceSSM && tun .InstanceID != "" {
135+ mode = "ssm"
136+ } else if mode == "" || mode == "ssh" {
137+ if tun .IP != "" && tun .PrivateKey != "" {
138+ mode = "ssh"
139+ } else if tun .InstanceID != "" {
140+ mode = "ssm"
141+ } else {
142+ slog .Warn ("tunnel missing required fields, skipping" , "name" , name )
143+ continue
144+ }
145+ }
146+
147+ if mode == "ssm" {
148+ if tun .InstanceID == "" {
149+ slog .Warn ("SSM tunnel missing instance ID, skipping" , "name" , name )
150+ continue
151+ }
152+ if tun .Region == "" {
153+ slog .Warn ("SSM tunnel missing region, skipping" , "name" , name )
154+ continue
155+ }
156+ ssmConfigs = append (ssmConfigs , tunnel.SSMConfig {
157+ InstanceID : tun .InstanceID ,
158+ Region : tun .Region ,
159+ Subnets : tun .Subnets ,
160+ })
161+ } else if mode == "ssh" {
162+ if tun .IP == "" {
163+ slog .Warn ("SSH tunnel missing IP, skipping" , "name" , name )
164+ continue
165+ }
166+ sshConfigs = append (sshConfigs , tunnel.SSHConfig {
167+ Host : tun .IP ,
168+ Username : tun .Username ,
169+ PrivateKey : tun .PrivateKey ,
170+ Subnets : tun .Subnets ,
171+ })
172+ }
173+
174+ allSubnets = append (allSubnets , tun .Subnets ... )
175+ }
176+
177+ if len (ssmConfigs ) == 0 && len (sshConfigs ) == 0 {
178+ return util .NewReadableError (nil , "No tunnels found. Make sure you have a bastion deployed." )
179+ }
180+
181+ if len (ssmConfigs ) > 0 {
182+ if _ , err := exec .LookPath ("session-manager-plugin" ); err != nil {
183+ return util .NewReadableError (nil , "AWS Session Manager Plugin is required for SSM tunnels but was not found.\n \n Install it from: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html\n \n Alternatively, you can use SSH mode by setting `ssh: true` on your Bastion component." )
184+ }
185+ }
186+
187+ args := []string {
188+ "-n" , "-E" ,
189+ tunnel .BINARY_PATH , "tunnel" , "start" ,
190+ "--subnets" , strings .Join (allSubnets , "," ),
191+ "--print-logs" ,
192+ }
193+
194+ if len (ssmConfigs ) > 0 {
195+ ssmJSON , err := json .Marshal (ssmConfigs )
196+ if err != nil {
197+ return fmt .Errorf ("failed to serialize SSM config: %w" , err )
198+ }
199+ args = append (args , "--ssm-config" , string (ssmJSON ))
200+ }
201+
202+ if len (sshConfigs ) > 0 {
203+ sshJSON , err := json .Marshal (sshConfigs )
204+ if err != nil {
205+ return fmt .Errorf ("failed to serialize SSH config: %w" , err )
206+ }
207+ args = append (args , "--ssh-config" , string (sshJSON ))
110208 }
111- subnets := strings . Join ( tun . Subnets , "," )
209+
112210 // run as root
113211 tunnelCmd := process .CommandContext (
114212 c .Context ,
115- "sudo" , "-n" , "-E" ,
116- tunnel .BINARY_PATH , "tunnel" , "start" ,
117- "--subnets" , subnets ,
118- "--host" , tun .IP ,
119- "--user" , tun .Username ,
120- "--print-logs" ,
213+ "sudo" ,
214+ args ... ,
121215 )
122216 tunnelCmd .Env = append (
123217 os .Environ (),
124218 "SST_SKIP_LOCAL=true" ,
125219 "SST_SKIP_DEPENDENCY_CHECK=true" ,
126- "SSH_PRIVATE_KEY=" + tun .PrivateKey ,
127220 "SST_LOG=" + strings .ReplaceAll (os .Getenv ("SST_LOG" ), ".log" , "_sudo.log" ),
128221 )
129222 tunnelCmd .Stdout = os .Stdout
130223 slog .Info ("starting tunnel" , "cmd" , tunnelCmd .Args )
131224 fmt .Println (ui .TEXT_HIGHLIGHT_BOLD .Render ("Tunnel" ))
132225 fmt .Println ()
133- fmt .Print (ui .TEXT_HIGHLIGHT_BOLD .Render ("▤" ))
134- fmt .Println (ui .TEXT_NORMAL .Render (" " + tun .IP ))
135- fmt .Println ()
136- fmt .Print (ui .TEXT_SUCCESS_BOLD .Render ("➜" ))
137- fmt .Println (ui .TEXT_NORMAL .Render (" Ranges" ))
138- for _ , subnet := range tun .Subnets {
139- fmt .Println (ui .TEXT_DIM .Render (" " + subnet ))
226+
227+ for _ , cfg := range ssmConfigs {
228+ fmt .Print (ui .TEXT_HIGHLIGHT_BOLD .Render ("▤" ))
229+ fmt .Println (ui .TEXT_NORMAL .Render (" " + cfg .InstanceID + " (SSM, " + cfg .Region + ")" ))
230+ for _ , subnet := range cfg .Subnets {
231+ fmt .Println (ui .TEXT_DIM .Render (" " + subnet ))
232+ }
233+ fmt .Println ()
140234 }
141- fmt .Println ()
235+
236+ for _ , cfg := range sshConfigs {
237+ fmt .Print (ui .TEXT_HIGHLIGHT_BOLD .Render ("▤" ))
238+ fmt .Println (ui .TEXT_NORMAL .Render (" " + cfg .Host + " (SSH)" ))
239+ for _ , subnet := range cfg .Subnets {
240+ fmt .Println (ui .TEXT_DIM .Render (" " + subnet ))
241+ }
242+ fmt .Println ()
243+ }
244+
142245 fmt .Println (ui .TEXT_DIM .Render ("Waiting for connections..." ))
143246 fmt .Println ()
144247 stderr , _ := tunnelCmd .StderrPipe ()
@@ -201,56 +304,61 @@ var CmdTunnel = &cli.Command{
201304 Name : "subnets" ,
202305 Type : "string" ,
203306 Description : cli.Description {
204- Short : "The subnet to use for the tunnel" ,
205- Long : "The subnet to use for the tunnel" ,
307+ Short : "The subnets to route through the tunnel" ,
308+ Long : "The subnets to route through the tunnel" ,
206309 },
207310 },
208311 {
209- Name : "host " ,
312+ Name : "ssm-config " ,
210313 Type : "string" ,
211314 Description : cli.Description {
212- Short : "The host to use for the tunnel" ,
213- Long : "The host to use for the tunnel" ,
315+ Short : "JSON-encoded SSM tunnel configurations " ,
316+ Long : "JSON-encoded SSM tunnel configurations " ,
214317 },
215318 },
216319 {
217- Name : "port " ,
320+ Name : "ssh-config " ,
218321 Type : "string" ,
219322 Description : cli.Description {
220- Short : "The port to use for the tunnel" ,
221- Long : "The port to use for the tunnel" ,
222- },
223- },
224- {
225- Name : "user" ,
226- Type : "string" ,
227- Description : cli.Description {
228- Short : "The user to use for the tunnel" ,
229- Long : "The user to use for the tunnel" ,
323+ Short : "JSON-encoded SSH tunnel configurations" ,
324+ Long : "JSON-encoded SSH tunnel configurations" ,
230325 },
231326 },
232327 },
233328 Run : func (c * cli.Cli ) error {
234329 subnets := strings .Split (c .String ("subnets" ), "," )
235- host := c .String ("host" )
236- port := c .String ("port" )
237- user := c .String ("user" )
238- if port == "" {
239- port = "22"
330+ ssmJSON := c .String ("ssm-config" )
331+ sshJSON := c .String ("ssh-config" )
332+
333+ var ssmConfigs []tunnel.SSMConfig
334+ var sshConfigs []tunnel.SSHConfig
335+
336+ if ssmJSON != "" {
337+ if err := json .Unmarshal ([]byte (ssmJSON ), & ssmConfigs ); err != nil {
338+ return util .NewReadableError (err , "failed to parse SSM configuration" )
339+ }
340+ }
341+
342+ if sshJSON != "" {
343+ if err := json .Unmarshal ([]byte (sshJSON ), & sshConfigs ); err != nil {
344+ return util .NewReadableError (err , "failed to parse SSH configuration" )
345+ }
346+ }
347+
348+ if len (ssmConfigs ) == 0 && len (sshConfigs ) == 0 {
349+ return util .NewReadableError (nil , "at least one SSM or SSH tunnel is required" )
240350 }
241- slog .Info ("starting tunnel" , "subnet" , subnets , "host" , host , "port" , port )
351+
352+ slog .Info ("starting tunnel" , "subnets" , subnets , "ssm" , len (ssmConfigs ), "ssh" , len (sshConfigs ))
242353 err := tunnel .Start (subnets ... )
354+ defer tunnel .Stop ()
243355 if err != nil {
244356 return err
245357 }
246- defer tunnel .Stop ()
247358 slog .Info ("tunnel started" )
248- err = tunnel .StartProxy (
249- c .Context ,
250- user ,
251- host + ":" + port ,
252- []byte (os .Getenv ("SSH_PRIVATE_KEY" )),
253- )
359+
360+ err = tunnel .StartProxy (c .Context , ssmConfigs , sshConfigs )
361+
254362 if err != nil {
255363 slog .Error ("failed to start tunnel" , "error" , err )
256364 }
0 commit comments