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`" ,
@@ -39,7 +41,7 @@ var CmdTunnel = &cli.Command{
3941 "The tunnel allows your local machine to access resources that are in the VPC." ,
4042 "" ,
4143 ":::note" ,
42- "The tunnel is only available for apps that have a VPC with `bastion` enabled. " ,
44+ "The tunnel is only available for apps that have a VPC with `bastion` enabled, or apps that have a Bastion component " ,
4345 ":::" ,
4446 "" ,
4547 "If you are running `sst dev`, this tunnel will be started automatically under the" ,
@@ -57,6 +59,11 @@ 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 },
6269 Run : func (c * cli.Cli ) error {
@@ -104,41 +111,117 @@ var CmdTunnel = &cli.Command{
104111 if len (completed .Tunnels ) == 0 {
105112 return util .NewReadableError (nil , "No tunnels found for stage " + stage )
106113 }
107- var tun project.Tunnel
108- for _ , item := range completed .Tunnels {
109- tun = item
114+
115+ var ssmConfigs []tunnel.SSMConfig
116+ var sshConfigs []tunnel.SSHConfig
117+ var allSubnets []string
118+
119+ for name , tun := range completed .Tunnels {
120+ mode := tun .Mode
121+
122+ // backwards compatible for vpc bastion v1
123+ if mode == "" {
124+ mode = "ssh"
125+ }
126+
127+ if mode == "ssm" {
128+ if tun .InstanceID == "" {
129+ slog .Warn ("SSM tunnel missing instance ID, skipping" , "name" , name )
130+ continue
131+ }
132+ if tun .Region == "" {
133+ slog .Warn ("SSM tunnel missing region, skipping" , "name" , name )
134+ continue
135+ }
136+ ssmConfigs = append (ssmConfigs , tunnel.SSMConfig {
137+ InstanceID : tun .InstanceID ,
138+ Region : tun .Region ,
139+ Subnets : tun .Subnets ,
140+ })
141+ } else if mode == "ssh" {
142+ if tun .IP == "" && tun .PrivateKey == "" {
143+ slog .Warn ("SSH tunnel missing IP, skipping" , "name" , name )
144+ continue
145+ }
146+ sshConfigs = append (sshConfigs , tunnel.SSHConfig {
147+ Host : tun .IP ,
148+ Username : tun .Username ,
149+ PrivateKey : tun .PrivateKey ,
150+ Subnets : tun .Subnets ,
151+ })
152+ }
153+
154+ allSubnets = append (allSubnets , tun .Subnets ... )
155+ }
156+
157+ if len (ssmConfigs ) == 0 && len (sshConfigs ) == 0 {
158+ return util .NewReadableError (nil , "No tunnels found. Make sure you have a bastion deployed." )
110159 }
111- subnets := strings .Join (tun .Subnets , "," )
160+
161+ if len (ssmConfigs ) > 0 {
162+ if _ , err := exec .LookPath ("session-manager-plugin" ); err != nil {
163+ 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 `ssm: false` on your Bastion component." )
164+ }
165+ }
166+
167+ args := []string {
168+ "-n" , "-E" ,
169+ tunnel .BINARY_PATH , "tunnel" , "start" ,
170+ "--subnets" , strings .Join (allSubnets , "," ),
171+ "--print-logs" ,
172+ }
173+
174+ if len (ssmConfigs ) > 0 {
175+ ssmJSON , err := json .Marshal (ssmConfigs )
176+ if err != nil {
177+ return fmt .Errorf ("failed to serialize SSM config: %w" , err )
178+ }
179+ args = append (args , "--ssm-config" , string (ssmJSON ))
180+ }
181+
182+ if len (sshConfigs ) > 0 {
183+ sshJSON , err := json .Marshal (sshConfigs )
184+ if err != nil {
185+ return fmt .Errorf ("failed to serialize SSH config: %w" , err )
186+ }
187+ args = append (args , "--ssh-config" , string (sshJSON ))
188+ }
189+
112190 // run as root
113191 tunnelCmd := process .CommandContext (
114192 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" ,
193+ "sudo" ,
194+ args ... ,
121195 )
122196 tunnelCmd .Env = append (
123197 os .Environ (),
124198 "SST_SKIP_LOCAL=true" ,
125199 "SST_SKIP_DEPENDENCY_CHECK=true" ,
126- "SSH_PRIVATE_KEY=" + tun .PrivateKey ,
127200 "SST_LOG=" + strings .ReplaceAll (os .Getenv ("SST_LOG" ), ".log" , "_sudo.log" ),
128201 )
129202 tunnelCmd .Stdout = os .Stdout
130203 slog .Info ("starting tunnel" , "cmd" , tunnelCmd .Args )
131204 fmt .Println (ui .TEXT_HIGHLIGHT_BOLD .Render ("Tunnel" ))
132205 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 ))
206+
207+ for _ , cfg := range ssmConfigs {
208+ fmt .Print (ui .TEXT_HIGHLIGHT_BOLD .Render ("▤" ))
209+ fmt .Println (ui .TEXT_NORMAL .Render (" " + cfg .InstanceID + " (SSM, " + cfg .Region + ")" ))
210+ for _ , subnet := range cfg .Subnets {
211+ fmt .Println (ui .TEXT_DIM .Render (" " + subnet ))
212+ }
213+ fmt .Println ()
140214 }
141- fmt .Println ()
215+
216+ for _ , cfg := range sshConfigs {
217+ fmt .Print (ui .TEXT_HIGHLIGHT_BOLD .Render ("▤" ))
218+ fmt .Println (ui .TEXT_NORMAL .Render (" " + cfg .Host + " (SSH)" ))
219+ for _ , subnet := range cfg .Subnets {
220+ fmt .Println (ui .TEXT_DIM .Render (" " + subnet ))
221+ }
222+ fmt .Println ()
223+ }
224+
142225 fmt .Println (ui .TEXT_DIM .Render ("Waiting for connections..." ))
143226 fmt .Println ()
144227 stderr , _ := tunnelCmd .StderrPipe ()
@@ -201,56 +284,61 @@ var CmdTunnel = &cli.Command{
201284 Name : "subnets" ,
202285 Type : "string" ,
203286 Description : cli.Description {
204- Short : "The subnet to use for the tunnel" ,
205- Long : "The subnet to use for the tunnel" ,
206- },
207- },
208- {
209- Name : "host" ,
210- Type : "string" ,
211- Description : cli.Description {
212- Short : "The host to use for the tunnel" ,
213- Long : "The host to use for the tunnel" ,
287+ Short : "The subnets to route through the tunnel" ,
288+ Long : "The subnets to route through the tunnel" ,
214289 },
215290 },
216291 {
217- Name : "port " ,
292+ Name : "ssm-config " ,
218293 Type : "string" ,
219294 Description : cli.Description {
220- Short : "The port to use for the tunnel" ,
221- Long : "The port to use for the tunnel" ,
295+ Short : "JSON-encoded SSM tunnel configurations " ,
296+ Long : "JSON-encoded SSM tunnel configurations " ,
222297 },
223298 },
224299 {
225- Name : "user " ,
300+ Name : "ssh-config " ,
226301 Type : "string" ,
227302 Description : cli.Description {
228- Short : "The user to use for the tunnel" ,
229- Long : "The user to use for the tunnel" ,
303+ Short : "JSON-encoded SSH tunnel configurations " ,
304+ Long : "JSON-encoded SSH tunnel configurations " ,
230305 },
231306 },
232307 },
233308 Run : func (c * cli.Cli ) error {
234309 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"
310+ ssmJSON := c .String ("ssm-config" )
311+ sshJSON := c .String ("ssh-config" )
312+
313+ var ssmConfigs []tunnel.SSMConfig
314+ var sshConfigs []tunnel.SSHConfig
315+
316+ if ssmJSON != "" {
317+ if err := json .Unmarshal ([]byte (ssmJSON ), & ssmConfigs ); err != nil {
318+ return util .NewReadableError (err , "failed to parse SSM configuration" )
319+ }
320+ }
321+
322+ if sshJSON != "" {
323+ if err := json .Unmarshal ([]byte (sshJSON ), & sshConfigs ); err != nil {
324+ return util .NewReadableError (err , "failed to parse SSH configuration" )
325+ }
240326 }
241- slog .Info ("starting tunnel" , "subnet" , subnets , "host" , host , "port" , port )
327+
328+ if len (ssmConfigs ) == 0 && len (sshConfigs ) == 0 {
329+ return util .NewReadableError (nil , "at least one SSM or SSH tunnel is required" )
330+ }
331+
332+ slog .Info ("starting tunnel" , "subnets" , subnets , "ssm" , len (ssmConfigs ), "ssh" , len (sshConfigs ))
242333 err := tunnel .Start (subnets ... )
334+ defer tunnel .Stop ()
243335 if err != nil {
244336 return err
245337 }
246- defer tunnel .Stop ()
247338 slog .Info ("tunnel started" )
248- err = tunnel .StartProxy (
249- c .Context ,
250- user ,
251- host + ":" + port ,
252- []byte (os .Getenv ("SSH_PRIVATE_KEY" )),
253- )
339+
340+ err = tunnel .StartProxy (c .Context , ssmConfigs , sshConfigs )
341+
254342 if err != nil {
255343 slog .Error ("failed to start tunnel" , "error" , err )
256344 }
0 commit comments