| 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 | | |