Projects STRLCPY cdebug Commits 0d995038
🤬
  • ■ ■ ■ ■ ■ ■
    cmd/portforward/portforward.go
    skipped 3 lines
    4 4   "context"
    5 5   "errors"
    6 6   "fmt"
    7  - "os"
    8  - "os/signal"
    9 7   "strings"
    10  - "syscall"
     8 + "sync"
     9 + "time"
    11 10   
    12 11   "github.com/docker/docker/api/types"
    13 12   "github.com/docker/docker/api/types/container"
    skipped 4 lines
    18 17   
    19 18   "github.com/iximiuz/cdebug/pkg/cliutil"
    20 19   "github.com/iximiuz/cdebug/pkg/docker"
    21  - "github.com/iximiuz/cdebug/pkg/jsonutil"
     20 + "github.com/iximiuz/cdebug/pkg/signalutil"
    22 21   "github.com/iximiuz/cdebug/pkg/uuid"
    23 22  )
    24 23   
    skipped 13 lines
    38 37  // - LOCAL_PORT:REMOTE_PORT # binds REMOTE_IP:REMOTE_PORT to LOCAL_PORT on localhost
    39 38  // - LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
    40 39  //
    41  -// - LOCAL_IP:LOCAL_PORT:REMOTE_PORT # similar to LOCAL_PORT:REMOTE_PORT but LOCAL_IP is used instead of localhost
    42  -// - LOCAL_IP:LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
     40 +// - LOCAL_HOST:LOCAL_PORT:REMOTE_PORT # similar to LOCAL_PORT:REMOTE_PORT but LOCAL_HOST is used instead of localhost
     41 +// - LOCAL_HOST:LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
    43 42  //
    44 43  // Remote port forwarding's possible modes (kinda sorta as in ssh -R):
    45 44  // - coming soon...
    skipped 8 lines
    54 53  var (
    55 54   errNoAddr = errors.New("target container must have at least one IP address")
    56 55   errBadLocalPort = errors.New("bad local port")
     56 + errBadRemoteHost = errors.New("bad remote host")
    57 57   errBadRemotePort = errors.New("bad remote port")
    58 58  )
    59 59   
    60 60  type options struct {
    61  - target string
    62  - locals []string
    63  - remotes []string
    64  - output string
    65  - quiet bool
     61 + target string
     62 + locals []string
     63 + remotes []string
     64 + runningTimeout time.Duration
     65 + output string
     66 + quiet bool
    66 67  }
    67 68   
    68 69  func NewCommand(cli cliutil.CLI) *cobra.Command {
    skipped 30 lines
    99 100   "local",
    100 101   "L",
    101 102   nil,
    102  - `Local port forwarding in the form [[LOCAL_IP:]LOCAL_PORT:][REMOTE_IP|REMOTE_NETWORK|REMOTE_ALIAS:]REMOTE_PORT`,
     103 + `Local port forwarding in the form [[LOCAL_HOST:]LOCAL_PORT:][REMOTE_HOST:]REMOTE_PORT`,
    103 104   )
    104 105   
    105 106   flags.StringSliceVarP(
    skipped 1 lines
    107 108   "remote",
    108 109   "R",
    109 110   nil,
    110  - `Remote port forwarding in the form [REMOTE_IP|REMOTE_NETWORK|REMOTE_ALIAS:]REMOTE_PORT:LOCAL_IP:LOCAL_PORT`,
     111 + `Remote port forwarding in the form [REMOTE_HOST:]REMOTE_PORT:LOCAL_HOST:LOCAL_PORT`,
     112 + )
     113 + 
     114 + flags.DurationVar(
     115 + &opts.output,
     116 + "running-timeout",
     117 + &opts.runningTimeout,
     118 + `How long to wait until the target is up and running`,
    111 119   )
    112 120   
    113 121   flags.BoolVarP(
    skipped 21 lines
    135 143   return err
    136 144   }
    137 145   
    138  - target, err := client.ContainerInspect(ctx, opts.target)
    139  - if err != nil {
     146 + target, err := getRunningTarget(ctx, opts.target, opts.runningTimeout)
     147 + if err := validateTarget(target); err != nil {
    140 148   return err
    141 149   }
    142  - if err := validateTarget(target); err != nil {
     150 + 
     151 + locals, err := parseLocalForwardings(target, opts.locals)
     152 + if err != nil {
    143 153   return err
    144 154   }
    145 155   
    skipped 2 lines
    148 158   return fmt.Errorf("cannot pull forwarder image %q: %w", forwarderImage, err)
    149 159   }
    150 160   
    151  - locals, err := parseLocalForwardings(target, opts.locals)
    152  - if err != nil {
    153  - return err
    154  - }
     161 + ctx, cancel := context.WithCancel(signalutil.InterruptibleContext(ctx))
     162 + 
     163 + var wg sync.WaitGroup
     164 + for _, fwd := range locals {
     165 + wg.Add(1)
     166 + 
     167 + go func() {
     168 + defer wg.Done()
    155 169   
    156  - // TODO: It's probably a good idea to monitor forwarders too.
    157  - for i, l := range locals {
    158  - forwarder, err := startLocalForwarder(ctx, client, target, l)
    159  - if err != nil {
    160  - return err
    161  - }
    162  - defer func() {
    163  - if err := client.ContainerKill(ctx, forwarder.ID, "KILL"); err != nil {
    164  - logrus.Debugf("Cannot kill forwarder container: %s", err)
     170 + if err := runLocalForwarder(ctx, client, target, fwd); err != nil {
     171 + logger.Warnf("forwading error: %s", err)
    165 172   }
    166 173   }()
    167  - 
    168  - if len(l.localPort) == 0 {
    169  - for _, bindings := range forwarder.NetworkSettings.Ports {
    170  - locals[i].localPort = bindings[0].HostPort
    171  - break
    172  - }
    173  - }
    174 174   }
    175  - 
    176  - switch opts.output {
    177  - case outFormatJSON:
    178  - cli.PrintOut(localForwardingsToJSON(locals) + "\n")
    179  - case outFormatText:
    180  - cli.PrintOut(localForwardingsToText(locals) + "\n")
    181  - default:
    182  - panic("unreachable!")
    183  - }
     175 + wg.Wait()
    184 176   
    185  - signalCh := make(chan os.Signal, 128)
    186  - signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
    187  - defer close(signalCh)
     177 + return nil
     178 +}
    188 179   
    189  - targetStatusCh, targetErrCh := client.ContainerWait(ctx, target.ID, container.WaitConditionNotRunning)
    190  - select {
    191  - case <-signalCh:
    192  - cli.PrintAux("Exiting...")
     180 +func getRunningTarget(
     181 + ctx context.Context,
     182 + target string,
     183 + runningTimeout time.Duration,
     184 +) (target types.ContainerJSON, err error) {
     185 + ctx, cancel := context.WithTimeout(ctx, runningTimeout)
     186 + defer cancel()
    193 187   
    194  - case err := <-targetErrCh:
     188 + for {
     189 + target, err = client.ContainerInspect(ctx, opts.target)
    195 190   if err != nil {
    196  - return fmt.Errorf("waiting for target container failed: %w", err)
     191 + return target, err
     192 + }
     193 + if target.State != nil && target.State.Running {
     194 + return target, nil
    197 195   }
    198 196   
    199  - case <-targetStatusCh:
     197 + select {
     198 + case <-ctx.Done():
     199 + return target, fmt.Errorf("target is not running after %s", runningTimeout)
     200 + case <-time.After(100 * time.Millisecond):
     201 + }
    200 202   }
    201  - 
    202  - return nil
    203 203  }
    204 204   
    205 205  func validateTarget(target types.ContainerJSON) error {
    206  - if target.State == nil || !target.State.Running {
    207  - return errors.New("target container found but it's not running")
    208  - }
    209  - 
    210 206   hasIP := false
    211 207   for _, net := range target.NetworkSettings.Networks {
    212 208   hasIP = hasIP || len(net.IPAddress) > 0
    skipped 6 lines
    219 215  }
    220 216   
    221 217  type forwarding struct {
    222  - localIP string
     218 + localHost string
    223 219   localPort string
    224  - remoteIP string
     220 + remoteHost string
    225 221   remotePort string
    226 222  }
    227 223   
    228  -func (f forwarding) toDockerPortSpec() string {
    229  - // ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
    230  - return f.localIP + ":" + f.localPort + ":" + f.remotePort
    231  -}
    232  - 
    233 224  func parseLocalForwardings(
    234 225   target types.ContainerJSON,
    235 226   locals []string,
    skipped 20 lines
    256 247   return forwarding{}, errBadRemotePort
    257 248   }
    258 249   
    259  - remoteIP, err := unambiguousIP(target)
    260  - if err != nil {
     250 + if _, err := unambiguousIP(target); err != nil {
    261 251   return forwarding{}, err
    262 252   }
    263 253   
    264  - // localPort will be later assigned by Docker dynamically
    265 254   return forwarding{
    266  - localIP: "127.0.0.1",
    267  - remoteIP: remoteIP,
    268 255   remotePort: parts[0],
    269 256   }, nil
    270 257   }
    271 258   
    272  - if len(parts) == 2 {
    273  - // Either LOCAL_PORT:REMOTE_PORT or REMOTE_<IP|ALIAS|NETWORK>:REMOTE_PORT
    274  - 
     259 + if len(parts) == 2 { // Either LOCAL_PORT:REMOTE_PORT or REMOTE_HOST:REMOTE_PORT
    275 260   if _, err := nat.ParsePort(parts[1]); err != nil {
    276 261   return forwarding{}, errBadRemotePort
    277 262   }
    278 263   
    279 264   if _, err := nat.ParsePort(parts[0]); err == nil {
    280 265   // Case 2: LOCAL_PORT:REMOTE_PORT
    281  - remoteIP, err := unambiguousIP(target)
    282  - if err != nil {
     266 + if _, err := unambiguousIP(target); err != nil {
    283 267   return forwarding{}, err
    284 268   }
    285 269   
    286 270   return forwarding{
    287  - localIP: "127.0.0.1",
    288 271   localPort: parts[0],
    289  - remoteIP: remoteIP,
    290 272   remotePort: parts[1],
    291 273   }, nil
    292 274   }
    293 275   
    294  - // Case 3: REMOTE_<IP|ALIAS|NETWORK>:REMOTE_PORT
    295  - remoteIP, err := lookupTargetIP(target, parts[0])
    296  - if err != nil {
    297  - return forwarding{}, err
    298  - }
    299  - 
    300  - // localPort will be later assigned by Docker dynamically
     276 + // Case 3: REMOTE_HOST:REMOTE_PORT
    301 277   return forwarding{
    302  - localIP: "127.0.0.1",
     278 + remoteHost: parts[0],
    303 279   remotePort: parts[1],
    304  - remoteIP: remoteIP,
    305 280   }, nil
    306 281   }
    307 282   
    308 283   if len(parts) == 3 {
    309  - // Either LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT or LOCAL_IP:LOCAL_PORT:REMOTE_PORT
     284 + // Either LOCAL_PORT:REMOTE_HOST:REMOTE_PORT or (LOCAL_HOST:LOCAL_PORT:REMOTE_PORT | LOCAL_HOST::REMOTE_PORT)
     285 + if _, err := nat.ParsePort(parts[3]); err != nil {
     286 + return forwarding{}, errBadRemotePort
     287 + }
    310 288   
    311 289   if _, err := nat.ParsePort(parts[0]); err == nil {
    312  - // Case 4: LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
    313  - remoteIP, err := lookupTargetIP(target, parts[1])
    314  - if err != nil {
    315  - return forwarding{}, err
    316  - }
    317  - 
     290 + // Case 4: LOCAL_PORT:REMOTE_HOST:REMOTE_PORT
    318 291   if _, err := nat.ParsePort(parts[2]); err != nil {
    319 292   return forwarding{}, errBadRemotePort
     293 + }
     294 + if len(parts[1]) == 0 {
     295 + return forwarding{}, errBadRemoteHost
    320 296   }
    321 297   
    322 298   return forwarding{
    323  - localIP: "127.0.0.1",
    324 299   localPort: parts[0],
    325  - remoteIP: remoteIP,
     300 + remoteHost: parts[1],
    326 301   remotePort: parts[2],
    327 302   }, nil
    328 303   }
    329 304   
    330  - // Case 5: LOCAL_IP:LOCAL_PORT:REMOTE_PORT
    331  - remoteIP, err := unambiguousIP(target)
    332  - if err != nil {
     305 + // Case 5: LOCAL_HOST:LOCAL_PORT:REMOTE_PORT or LOCAL_HOST::REMOTE_PORT
     306 + if _, err := unambiguousIP(target); err != nil {
    333 307   return forwarding{}, err
    334 308   }
    335 309   
    336 310   return forwarding{
    337  - localIP: parts[0],
     311 + localHost: parts[0],
    338 312   localPort: parts[1],
    339  - remoteIP: remoteIP,
    340 313   remotePort: parts[2],
    341 314   }, nil
    342 315   }
    343 316   
    344  - // Case 6: LOCAL_IP:LOCAL_PORT:REMOTE_<IP|ALIAS|NET>:REMOTE_PORT
    345  - if _, err := nat.ParsePort(parts[1]); err != nil {
     317 + // Case 6: LOCAL_HOST:LOCAL_PORT:REMOTE_HOST:REMOTE_PORT or LOCAL_HOST::REMOTE_HOST:REMOTE_PORT
     318 + if _, err := nat.ParsePort(parts[1]); err != nil && len(parts[1]) > 0 {
    346 319   return forwarding{}, errBadLocalPort
    347 320   }
    348 321   if _, err := nat.ParsePort(parts[3]); err != nil {
    349 322   return forwarding{}, errBadRemotePort
    350 323   }
    351 324   
    352  - remoteIP, err := lookupTargetIP(target, parts[2])
    353  - if err != nil {
    354  - return forwarding{}, err
    355  - }
    356  - 
    357 325   return forwarding{
    358  - localIP: parts[0],
     326 + localHost: parts[0],
    359 327   localPort: parts[1],
    360  - remoteIP: remoteIP,
     328 + remoteHost: parts[2],
    361 329   remotePort: parts[3],
    362 330   }, nil
    363 331  }
    skipped 55 lines
    419 387   client dockerclient.CommonAPIClient,
    420 388   target types.ContainerJSON,
    421 389   fwd forwarding,
    422  -) (types.ContainerJSON, error) {
    423  - exposedPorts, portBindings, err := nat.ParsePortSpecs([]string{fwd.toDockerPortSpec()})
     390 +) error {
     391 + if len(fwd.remoteHost) == 0 {
     392 + forwarder, err := startLocalForwarderToTargetPublicPort(
     393 + ctx,
     394 + client,
     395 + target,
     396 + fwd.localHost,
     397 + fwd.localPort,
     398 + fwd.remotePort,
     399 + )
     400 + if err != nil {
     401 + return err
     402 + }
     403 + return maintainLocalForwarderToTargetPublicPort(ctx, client, target, forwarder)
     404 + }
     405 + 
     406 + // TODO: startLocalForwarderThroughTargetNetns()
     407 + return errors.New("Implement me!")
     408 +}
     409 + 
     410 +func startLocalForwarderToTargetPublicPort(
     411 + ctx context.Context,
     412 + client dockerclient.CommonAPIClient,
     413 + target types.ContainerJSON,
     414 + localHost string,
     415 + localPort string,
     416 + remotePort string,
     417 +) (forwarder types.ContainerJSON, err error) {
     418 + remoteIP, err := unambiguousIP(target)
    424 419   if err != nil {
    425  - return types.ContainerJSON{}, err
     420 + return forwarder, err
     421 + }
     422 + network, err := targetNetworkByIP(target, remoteIP)
     423 + if err != nil {
     424 + return forwarder, err
    426 425   }
    427 426   
    428  - network, err := targetNetworkByIP(target, fwd.remoteIP)
     427 + exposedPorts, portBindings, err := nat.ParsePortSpecs([]string{
     428 + localHost + ":" + localPort + ":" + remotePort,
     429 + })
    429 430   if err != nil {
    430  - return types.ContainerJSON{}, err
     431 + return forwarder, err
    431 432   }
    432 433   
    433 434   resp, err := client.ContainerCreate(
    skipped 2 lines
    436 437   Image: forwarderImage,
    437 438   Entrypoint: []string{"socat"},
    438 439   Cmd: []string{
    439  - fmt.Sprintf("TCP-LISTEN:%s,fork", fwd.remotePort),
    440  - fmt.Sprintf("TCP-CONNECT:%s:%s", fwd.remoteIP, fwd.remotePort),
     440 + fmt.Sprintf("TCP-LISTEN:%s,fork", remotePort),
     441 + fmt.Sprintf("TCP-CONNECT:%s:%s", remoteIP, remotePort),
    441 442   },
    442 443   ExposedPorts: exposedPorts,
    443 444   },
    skipped 7 lines
    451 452   "cdebug-fwd-"+uuid.ShortID(),
    452 453   )
    453 454   if err != nil {
    454  - return types.ContainerJSON{}, fmt.Errorf("cannot create forwarder container: %w", err)
     455 + return forwarder, fmt.Errorf("cannot create forwarder container: %w", err)
    455 456   }
    456 457   
    457 458   if err := client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
    458  - return types.ContainerJSON{}, fmt.Errorf("cannot start forwarder container: %w", err)
     459 + return forwarder, fmt.Errorf("cannot start forwarder container: %w", err)
    459 460   }
    460 461   
    461  - forwarder, err := client.ContainerInspect(ctx, resp.ID)
     462 + forwarder, err = client.ContainerInspect(ctx, resp.ID)
    462 463   if err != nil {
    463 464   return forwarder, fmt.Errorf("cannot inspect forwarder container: %w", err)
    464 465   }
     466 + if forwarder.State != nil && forwarder.State.Running {
     467 + return forwarder, fmt.Errorf("cannot inspect forwarder container: %w", err)
     468 + }
     469 + 
    465 470   return forwarder, nil
    466 471  }
    467 472   
    468  -func localForwardingsToJSON(fwds []forwarding) string {
    469  - out := []map[string]string{}
    470  - for _, f := range fwds {
    471  - out = append(out, map[string]string{
    472  - "localHost": f.localIP,
    473  - "localPort": f.localPort,
    474  - "remoteHost": f.remoteIP,
    475  - "remotePort": f.remotePort,
    476  - })
     473 +func maintainLocalForwarderToTargetPublicPort(
     474 + ctx context.Context,
     475 + client dockerclient.CommonAPIClient,
     476 + target types.ContainerJSON,
     477 + forwarder types.ContainerJSON,
     478 +) error {
     479 + targetStatusCh, targetErrCh := client.ContainerWait(ctx, target.ID, container.WaitConditionNotRunning)
     480 + fwderStatusCh, fwderErrCh := client.ContainerWait(ctx, forwarder.ID, container.WaitConditionNotRunning)
     481 + 
     482 + defer func() {
     483 + if err := client.ContainerKill(ctx, forwarder.ID, "KILL"); err != nil {
     484 + logrus.Debugf("Cannot kill forwarder container: %s", err)
     485 + }
     486 + }()
     487 + 
     488 + // TODO: If target exits - kill forwarder, wait for N seconds, restart forwarder (or exit).
     489 + // If forwarder exits - try restarting it (< K times); otherwise, exit.
     490 + for {
     491 + select {
     492 + case err := <-targetErrCh:
     493 + if err != nil {
     494 + cli.PrintAux("Exiting...")
     495 + cancel()
     496 + return fmt.Errorf("waiting for target container failed: %w", err)
     497 + }
     498 + 
     499 + case <-targetStatusCh:
     500 + target, err = getRunningTarget(ctx, opts.target, opts.runningTimeout)
     501 + if err != nil {
     502 + cli.PrintAux("Exiting...")
     503 + cancel()
     504 + } else {
     505 + targetStatusCh, targetErrCh = client.ContainerWait(ctx, target.ID, container.WaitConditionNotRunning)
     506 + }
     507 + }
    477 508   }
    478  - return jsonutil.DumpIndent(out)
    479 509  }
    480 510   
    481  -func localForwardingsToText(fwds []forwarding) string {
    482  - out := []string{}
    483  - for _, f := range fwds {
    484  - out = append(out, fmt.Sprintf(
    485  - "Forwarding %s:%s to %s:%s",
    486  - f.localIP, f.localPort, f.remoteIP, f.remotePort,
    487  - ))
    488  - }
    489  - return strings.Join(out, "\n")
    490  -}
     511 +// func localForwardingsToJSON(fwds []forwarding) string {
     512 +// out := []map[string]string{}
     513 +// for _, f := range fwds {
     514 +// out = append(out, map[string]string{
     515 +// "localHost": f.localHost,
     516 +// "localPort": f.localPort,
     517 +// "remoteHost": f.remoteHost,
     518 +// "remotePort": f.remotePort,
     519 +// })
     520 +// }
     521 +// return jsonutil.DumpIndent(out)
     522 +// }
     523 +//
     524 +// func localForwardingsToText(fwds []forwarding) string {
     525 +// out := []string{}
     526 +// for _, f := range fwds {
     527 +// out = append(out, fmt.Sprintf(
     528 +// "Forwarding %s:%s to %s:%s",
     529 +// f.localHost, f.localPort, f.remoteHost, f.remotePort,
     530 +// ))
     531 +// }
     532 +// return strings.Join(out, "\n")
     533 +// }
    491 534   
  • ■ ■ ■ ■ ■ ■
    pkg/signalutil/signalutil.go
     1 +package signalutil
     2 + 
     3 +import (
     4 + "context"
     5 + "os"
     6 + "os/signal"
     7 + "syscall"
     8 +)
     9 + 
     10 +func InterruptibleContext(ctx context.Context) context.Context {
     11 + ctx, cancel := context.WithCancel(ctx)
     12 + 
     13 + go func() {
     14 + defer cancel()
     15 + 
     16 + signalCh := make(chan os.Signal, 128)
     17 + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
     18 + defer signal.Stop(signalCh)
     19 + 
     20 + select {
     21 + case <-signalCh:
     22 + case <-ctx.Done():
     23 + }
     24 + }()
     25 + 
     26 + return ctx
     27 +}
     28 + 
Please wait...
Page is in error, reload to recover