2022-05-18 14:10:40 +00:00
package cli
import (
"context"
"fmt"
"net"
2023-05-24 19:38:40 +00:00
"net/netip"
2022-05-18 14:10:40 +00:00
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"golang.org/x/xerrors"
2023-06-21 20:22:43 +00:00
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
2024-03-26 17:44:31 +00:00
"github.com/coder/coder/v2/codersdk/workspacesdk"
2024-03-15 16:24:38 +00:00
"github.com/coder/serpent"
2022-05-18 14:10:40 +00:00
)
2024-03-17 14:45:26 +00:00
func ( r * RootCmd ) portForward ( ) * serpent . Command {
2022-05-18 14:10:40 +00:00
var (
2023-12-08 16:01:13 +00:00
tcpForwards [ ] string // <port>:<port>
udpForwards [ ] string // <port>:<port>
disableAutostart bool
2022-05-18 14:10:40 +00:00
)
2023-03-23 22:42:20 +00:00
client := new ( codersdk . Client )
2024-03-17 14:45:26 +00:00
cmd := & serpent . Command {
2022-05-18 14:10:40 +00:00
Use : "port-forward <workspace>" ,
2023-07-25 02:22:41 +00:00
Short : ` Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R". ` ,
2022-05-18 14:10:40 +00:00
Aliases : [ ] string { "tunnel" } ,
2023-03-23 22:42:20 +00:00
Long : formatExamples (
2022-07-11 16:08:09 +00:00
example {
Description : "Port forward a single TCP port from 1234 in the workspace to port 5678 on your local machine" ,
Command : "coder port-forward <workspace> --tcp 5678:1234" ,
} ,
example {
Description : "Port forward a single UDP port from port 9000 to port 9000 on your local machine" ,
Command : "coder port-forward <workspace> --udp 9000" ,
} ,
example {
Description : "Port forward multiple TCP ports and a UDP port" ,
Command : "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53" ,
} ,
2022-10-03 08:58:43 +00:00
example {
Description : "Port forward multiple ports (TCP or UDP) in condensed syntax" ,
Command : "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012" ,
} ,
2023-05-24 19:38:40 +00:00
example {
Description : "Port forward specifying the local address to bind to" ,
Command : "coder port-forward <workspace> --tcp 1.2.3.4:8080:8080" ,
} ,
2022-07-11 16:08:09 +00:00
) ,
2024-03-15 16:24:38 +00:00
Middleware : serpent . Chain (
serpent . RequireNArgs ( 1 ) ,
2023-03-23 22:42:20 +00:00
r . InitClient ( client ) ,
) ,
2024-03-15 16:24:38 +00:00
Handler : func ( inv * serpent . Invocation ) error {
2023-03-23 22:42:20 +00:00
ctx , cancel := context . WithCancel ( inv . Context ( ) )
2022-08-02 14:44:59 +00:00
defer cancel ( )
2022-09-13 20:55:56 +00:00
specs , err := parsePortForwards ( tcpForwards , udpForwards )
2022-05-18 14:10:40 +00:00
if err != nil {
return xerrors . Errorf ( "parse port-forward specs: %w" , err )
}
if len ( specs ) == 0 {
return xerrors . New ( "no port-forwards requested" )
}
2024-04-13 18:39:57 +00:00
workspace , workspaceAgent , err := getWorkspaceAndAgent ( ctx , inv , client , ! disableAutostart , inv . Args [ 0 ] )
2022-05-18 14:10:40 +00:00
if err != nil {
return err
}
2022-05-19 18:04:44 +00:00
if workspace . LatestBuild . Transition != codersdk . WorkspaceTransitionStart {
2022-05-18 14:10:40 +00:00
return xerrors . New ( "workspace must be in start transition to port-forward" )
}
if workspace . LatestBuild . Job . CompletedAt == nil {
2023-03-23 22:42:20 +00:00
err = cliui . WorkspaceBuild ( ctx , inv . Stderr , client , workspace . LatestBuild . ID )
2022-05-18 14:10:40 +00:00
if err != nil {
return err
}
}
2023-07-12 15:21:54 +00:00
err = cliui . Agent ( ctx , inv . Stderr , workspaceAgent . ID , cliui . AgentOptions {
Fetch : client . WorkspaceAgent ,
Wait : false ,
2022-05-18 14:10:40 +00:00
} )
if err != nil {
return xerrors . Errorf ( "await agent: %w" , err )
}
2023-11-14 18:56:27 +00:00
logger := inv . Logger
2023-06-21 20:22:43 +00:00
if r . verbose {
2023-11-14 18:56:27 +00:00
logger = logger . AppendSinks ( sloghuman . Sink ( inv . Stdout ) ) . Leveled ( slog . LevelDebug )
2023-06-21 20:22:43 +00:00
}
if r . disableDirect {
_ , _ = fmt . Fprintln ( inv . Stderr , "Direct connections disabled." )
}
2024-03-26 17:44:31 +00:00
conn , err := workspacesdk . New ( client ) .
DialAgent ( ctx , workspaceAgent . ID , & workspacesdk . DialAgentOptions {
Logger : logger ,
BlockEndpoints : r . disableDirect ,
} )
2022-05-18 14:10:40 +00:00
if err != nil {
2022-09-02 23:26:01 +00:00
return err
2022-05-18 14:10:40 +00:00
}
defer conn . Close ( )
// Start all listeners.
var (
wg = new ( sync . WaitGroup )
listeners = make ( [ ] net . Listener , len ( specs ) )
closeAllListeners = func ( ) {
2023-12-11 10:51:56 +00:00
logger . Debug ( ctx , "closing all listeners" )
2022-05-18 14:10:40 +00:00
for _ , l := range listeners {
if l == nil {
continue
}
_ = l . Close ( )
}
}
)
2022-08-02 14:44:59 +00:00
defer closeAllListeners ( )
2022-05-18 14:10:40 +00:00
for i , spec := range specs {
2023-11-01 09:45:13 +00:00
l , err := listenAndPortForward ( ctx , inv , conn , wg , spec , logger )
2022-05-18 14:10:40 +00:00
if err != nil {
2023-12-11 10:51:56 +00:00
logger . Error ( ctx , "failed to listen" , slog . F ( "spec" , spec ) , slog . Error ( err ) )
2022-05-18 14:10:40 +00:00
return err
}
listeners [ i ] = l
}
2024-03-20 16:44:12 +00:00
stopUpdating := client . UpdateWorkspaceUsageContext ( ctx , workspace . ID )
2022-05-18 14:10:40 +00:00
// Wait for the context to be canceled or for a signal and close
// all listeners.
var closeErr error
2022-08-02 14:44:59 +00:00
wg . Add ( 1 )
2022-05-18 14:10:40 +00:00
go func ( ) {
2022-08-02 14:44:59 +00:00
defer wg . Done ( )
2022-05-18 14:10:40 +00:00
sigs := make ( chan os . Signal , 1 )
signal . Notify ( sigs , syscall . SIGINT , syscall . SIGTERM )
select {
case <- ctx . Done ( ) :
2023-12-11 10:51:56 +00:00
logger . Debug ( ctx , "command context expired waiting for signal" , slog . Error ( ctx . Err ( ) ) )
2022-05-18 14:10:40 +00:00
closeErr = ctx . Err ( )
2023-12-11 10:51:56 +00:00
case sig := <- sigs :
logger . Debug ( ctx , "received signal" , slog . F ( "signal" , sig ) )
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stderr , "\nReceived signal, closing all listeners and active connections" )
2022-05-18 14:10:40 +00:00
}
cancel ( )
2024-03-20 16:44:12 +00:00
stopUpdating ( )
2022-05-18 14:10:40 +00:00
closeAllListeners ( )
} ( )
2022-11-13 17:33:05 +00:00
conn . AwaitReachable ( ctx )
2023-12-11 10:51:56 +00:00
logger . Debug ( ctx , "read to accept connections to forward" )
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintln ( inv . Stderr , "Ready!" )
2022-05-18 14:10:40 +00:00
wg . Wait ( )
return closeErr
} ,
}
2024-03-15 16:24:38 +00:00
cmd . Options = serpent . OptionSet {
2023-03-23 22:42:20 +00:00
{
Flag : "tcp" ,
FlagShorthand : "p" ,
Env : "CODER_PORT_FORWARD_TCP" ,
Description : "Forward TCP port(s) from the workspace to the local machine." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringArrayOf ( & tcpForwards ) ,
2023-03-23 22:42:20 +00:00
} ,
{
Flag : "udp" ,
Env : "CODER_PORT_FORWARD_UDP" ,
Description : "Forward UDP port(s) from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols." ,
2024-03-15 16:24:38 +00:00
Value : serpent . StringArrayOf ( & udpForwards ) ,
2023-03-23 22:42:20 +00:00
} ,
2024-03-15 16:24:38 +00:00
sshDisableAutostartOption ( serpent . BoolOf ( & disableAutostart ) ) ,
2023-03-23 22:42:20 +00:00
}
2022-05-18 14:10:40 +00:00
return cmd
}
2023-11-01 09:45:13 +00:00
func listenAndPortForward (
ctx context . Context ,
2024-03-15 16:24:38 +00:00
inv * serpent . Invocation ,
2024-03-26 17:44:31 +00:00
conn * workspacesdk . AgentConn ,
2023-11-01 09:45:13 +00:00
wg * sync . WaitGroup ,
spec portForwardSpec ,
logger slog . Logger ,
) ( net . Listener , error ) {
logger = logger . With ( slog . F ( "network" , spec . listenNetwork ) , slog . F ( "address" , spec . listenAddress ) )
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintf ( inv . Stderr , "Forwarding '%v://%v' locally to '%v://%v' in the workspace\n" , spec . listenNetwork , spec . listenAddress , spec . dialNetwork , spec . dialAddress )
2022-05-18 14:10:40 +00:00
2023-12-11 10:51:56 +00:00
l , err := inv . Net . Listen ( spec . listenNetwork , spec . listenAddress )
2022-05-18 14:10:40 +00:00
if err != nil {
return nil , xerrors . Errorf ( "listen '%v://%v': %w" , spec . listenNetwork , spec . listenAddress , err )
}
2023-11-01 09:45:13 +00:00
logger . Debug ( ctx , "listening" )
2022-05-18 14:10:40 +00:00
wg . Add ( 1 )
go func ( spec portForwardSpec ) {
defer wg . Done ( )
for {
netConn , err := l . Accept ( )
if err != nil {
2022-10-17 16:45:29 +00:00
// Silently ignore net.ErrClosed errors.
if xerrors . Is ( err , net . ErrClosed ) {
2023-11-01 09:45:13 +00:00
logger . Debug ( ctx , "listener closed" )
2022-10-17 16:45:29 +00:00
return
}
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintf ( inv . Stderr , "Error accepting connection from '%v://%v': %v\n" , spec . listenNetwork , spec . listenAddress , err )
_ , _ = fmt . Fprintln ( inv . Stderr , "Killing listener" )
2022-05-18 14:10:40 +00:00
return
}
2023-11-01 09:45:13 +00:00
logger . Debug ( ctx , "accepted connection" , slog . F ( "remote_addr" , netConn . RemoteAddr ( ) ) )
2022-05-18 14:10:40 +00:00
go func ( netConn net . Conn ) {
defer netConn . Close ( )
remoteConn , err := conn . DialContext ( ctx , spec . dialNetwork , spec . dialAddress )
if err != nil {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintf ( inv . Stderr , "Failed to dial '%v://%v' in workspace: %s\n" , spec . dialNetwork , spec . dialAddress , err )
2022-05-18 14:10:40 +00:00
return
}
defer remoteConn . Close ( )
2023-11-01 09:45:13 +00:00
logger . Debug ( ctx , "dialed remote" , slog . F ( "remote_addr" , netConn . RemoteAddr ( ) ) )
2022-05-18 14:10:40 +00:00
2023-04-06 16:39:22 +00:00
agentssh . Bicopy ( ctx , netConn , remoteConn )
2023-11-01 09:45:13 +00:00
logger . Debug ( ctx , "connection closing" , slog . F ( "remote_addr" , netConn . RemoteAddr ( ) ) )
2022-05-18 14:10:40 +00:00
} ( netConn )
}
} ( spec )
return l , nil
}
type portForwardSpec struct {
2022-09-13 20:55:56 +00:00
listenNetwork string // tcp, udp
2022-05-18 14:10:40 +00:00
listenAddress string // <ip>:<port> or path
2022-09-13 20:55:56 +00:00
dialNetwork string // tcp, udp
2022-05-18 14:10:40 +00:00
dialAddress string // <ip>:<port> or path
}
2022-09-13 20:55:56 +00:00
func parsePortForwards ( tcpSpecs , udpSpecs [ ] string ) ( [ ] portForwardSpec , error ) {
2022-05-18 14:10:40 +00:00
specs := [ ] portForwardSpec { }
2022-10-03 08:58:43 +00:00
for _ , specEntry := range tcpSpecs {
for _ , spec := range strings . Split ( specEntry , "," ) {
ports , err := parseSrcDestPorts ( spec )
if err != nil {
return nil , xerrors . Errorf ( "failed to parse TCP port-forward specification %q: %w" , spec , err )
}
2022-05-18 14:10:40 +00:00
2022-10-03 08:58:43 +00:00
for _ , port := range ports {
specs = append ( specs , portForwardSpec {
listenNetwork : "tcp" ,
2023-05-24 19:38:40 +00:00
listenAddress : port . local . String ( ) ,
2022-10-03 08:58:43 +00:00
dialNetwork : "tcp" ,
2023-05-24 19:38:40 +00:00
dialAddress : port . remote . String ( ) ,
2022-10-03 08:58:43 +00:00
} )
}
}
2022-05-18 14:10:40 +00:00
}
2022-10-03 08:58:43 +00:00
for _ , specEntry := range udpSpecs {
for _ , spec := range strings . Split ( specEntry , "," ) {
ports , err := parseSrcDestPorts ( spec )
if err != nil {
return nil , xerrors . Errorf ( "failed to parse UDP port-forward specification %q: %w" , spec , err )
}
2022-05-18 14:10:40 +00:00
2022-10-03 08:58:43 +00:00
for _ , port := range ports {
specs = append ( specs , portForwardSpec {
listenNetwork : "udp" ,
2023-05-24 19:38:40 +00:00
listenAddress : port . local . String ( ) ,
2022-10-03 08:58:43 +00:00
dialNetwork : "udp" ,
2023-05-24 19:38:40 +00:00
dialAddress : port . remote . String ( ) ,
2022-10-03 08:58:43 +00:00
} )
}
}
2022-05-18 14:10:40 +00:00
}
// Check for duplicate entries.
locals := map [ string ] struct { } { }
for _ , spec := range specs {
localStr := fmt . Sprintf ( "%v:%v" , spec . listenNetwork , spec . listenAddress )
if _ , ok := locals [ localStr ] ; ok {
return nil , xerrors . Errorf ( "local %v %v is specified twice" , spec . listenNetwork , spec . listenAddress )
}
locals [ localStr ] = struct { } { }
}
return specs , nil
}
func parsePort ( in string ) ( uint16 , error ) {
port , err := strconv . ParseUint ( strings . TrimSpace ( in ) , 10 , 16 )
if err != nil {
return 0 , xerrors . Errorf ( "parse port %q: %w" , in , err )
}
if port == 0 {
return 0 , xerrors . New ( "port cannot be 0" )
}
return uint16 ( port ) , nil
}
2022-10-03 08:58:43 +00:00
type parsedSrcDestPort struct {
2023-05-24 19:38:40 +00:00
local , remote netip . AddrPort
2022-10-03 08:58:43 +00:00
}
func parseSrcDestPorts ( in string ) ( [ ] parsedSrcDestPort , error ) {
2023-05-24 19:38:40 +00:00
var (
err error
parts = strings . Split ( in , ":" )
localAddr = netip . AddrFrom4 ( [ 4 ] byte { 127 , 0 , 0 , 1 } )
remoteAddr = netip . AddrFrom4 ( [ 4 ] byte { 127 , 0 , 0 , 1 } )
)
switch len ( parts ) {
case 1 :
2022-05-18 14:10:40 +00:00
// Duplicate the single part
parts = append ( parts , parts [ 0 ] )
2023-05-24 19:38:40 +00:00
case 2 :
// Check to see if the first part is an IP address.
_localAddr , err := netip . ParseAddr ( parts [ 0 ] )
if err != nil {
break
}
// The first part is the local address, so duplicate the port.
localAddr = _localAddr
parts = [ ] string { parts [ 1 ] , parts [ 1 ] }
case 3 :
_localAddr , err := netip . ParseAddr ( parts [ 0 ] )
if err != nil {
return nil , xerrors . Errorf ( "invalid port specification %q; invalid ip %q: %w" , in , parts [ 0 ] , err )
}
localAddr = _localAddr
parts = parts [ 1 : ]
default :
return nil , xerrors . Errorf ( "invalid port specification %q" , in )
2022-05-18 14:10:40 +00:00
}
2023-05-24 19:38:40 +00:00
2022-10-03 08:58:43 +00:00
if ! strings . Contains ( parts [ 0 ] , "-" ) {
2023-05-24 19:38:40 +00:00
localPort , err := parsePort ( parts [ 0 ] )
2022-10-03 08:58:43 +00:00
if err != nil {
return nil , xerrors . Errorf ( "parse local port from %q: %w" , in , err )
}
2023-05-24 19:38:40 +00:00
remotePort , err := parsePort ( parts [ 1 ] )
2022-10-03 08:58:43 +00:00
if err != nil {
return nil , xerrors . Errorf ( "parse remote port from %q: %w" , in , err )
}
2022-05-18 14:10:40 +00:00
2023-05-24 19:38:40 +00:00
return [ ] parsedSrcDestPort { {
local : netip . AddrPortFrom ( localAddr , localPort ) ,
remote : netip . AddrPortFrom ( remoteAddr , remotePort ) ,
} } , nil
2022-10-03 08:58:43 +00:00
}
local , err := parsePortRange ( parts [ 0 ] )
2022-05-18 14:10:40 +00:00
if err != nil {
2022-10-03 08:58:43 +00:00
return nil , xerrors . Errorf ( "parse local port range from %q: %w" , in , err )
2022-05-18 14:10:40 +00:00
}
2022-10-03 08:58:43 +00:00
remote , err := parsePortRange ( parts [ 1 ] )
2022-05-18 14:10:40 +00:00
if err != nil {
2022-10-03 08:58:43 +00:00
return nil , xerrors . Errorf ( "parse remote port range from %q: %w" , in , err )
}
if len ( local ) != len ( remote ) {
return nil , xerrors . Errorf ( "port ranges must be the same length, got %d ports forwarded to %d ports" , len ( local ) , len ( remote ) )
}
var out [ ] parsedSrcDestPort
for i := range local {
out = append ( out , parsedSrcDestPort {
2023-05-24 19:38:40 +00:00
local : netip . AddrPortFrom ( localAddr , local [ i ] ) ,
remote : netip . AddrPortFrom ( remoteAddr , remote [ i ] ) ,
2022-10-03 08:58:43 +00:00
} )
2022-05-18 14:10:40 +00:00
}
2022-10-03 08:58:43 +00:00
return out , nil
}
2022-05-18 14:10:40 +00:00
2022-10-03 08:58:43 +00:00
func parsePortRange ( in string ) ( [ ] uint16 , error ) {
parts := strings . Split ( in , "-" )
if len ( parts ) != 2 {
return nil , xerrors . Errorf ( "invalid port range specification %q" , in )
}
start , err := parsePort ( parts [ 0 ] )
if err != nil {
return nil , xerrors . Errorf ( "parse range start port from %q: %w" , in , err )
}
end , err := parsePort ( parts [ 1 ] )
if err != nil {
return nil , xerrors . Errorf ( "parse range end port from %q: %w" , in , err )
}
if end < start {
return nil , xerrors . Errorf ( "range end port %v is less than start port %v" , end , start )
}
var ports [ ] uint16
for i := start ; i <= end ; i ++ {
ports = append ( ports , i )
}
return ports , nil
2022-05-18 14:10:40 +00:00
}