master
go 710 lines 18.7 KB
Raw
1 package commands
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "strconv"
9 "strings"
10 "text/tabwriter"
11 "time"
12
13 core "github.com/ipfs/kubo/core"
14 cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
15 p2p "github.com/ipfs/kubo/p2p"
16
17 cmds "github.com/ipfs/go-ipfs-cmds"
18 peer "github.com/libp2p/go-libp2p/core/peer"
19 pstore "github.com/libp2p/go-libp2p/core/peerstore"
20 protocol "github.com/libp2p/go-libp2p/core/protocol"
21 ma "github.com/multiformats/go-multiaddr"
22 madns "github.com/multiformats/go-multiaddr-dns"
23 )
24
25 // P2PProtoPrefix is the default required prefix for protocol names
26 const P2PProtoPrefix = "/x/"
27
28 // P2PListenerInfoOutput is output type of ls command
29 type P2PListenerInfoOutput struct {
30 Protocol string
31 ListenAddress string
32 TargetAddress string
33 }
34
35 // P2PStreamInfoOutput is output type of streams command
36 type P2PStreamInfoOutput struct {
37 HandlerID string
38 Protocol string
39 OriginAddress string
40 TargetAddress string
41 }
42
43 // P2PLsOutput is output type of ls command
44 type P2PLsOutput struct {
45 Listeners []P2PListenerInfoOutput
46 }
47
48 // P2PStreamsOutput is output type of streams command
49 type P2PStreamsOutput struct {
50 Streams []P2PStreamInfoOutput
51 }
52
53 // P2PForegroundOutput is output type for foreground mode status messages
54 type P2PForegroundOutput struct {
55 Status string // "active" or "closing"
56 Protocol string
57 Address string
58 }
59
60 const (
61 allowCustomProtocolOptionName = "allow-custom-protocol"
62 reportPeerIDOptionName = "report-peer-id"
63 foregroundOptionName = "foreground"
64 )
65
66 var resolveTimeout = 10 * time.Second
67
68 // P2PCmd is the 'ipfs p2p' command
69 var P2PCmd = &cmds.Command{
70 Status: cmds.Experimental,
71 Helptext: cmds.HelpText{
72 Tagline: "Libp2p stream mounting.",
73 ShortDescription: `
74 Create and use tunnels to remote peers over libp2p
75
76 Note: this command is experimental and subject to change as usecases and APIs
77 are refined`,
78 },
79
80 Subcommands: map[string]*cmds.Command{
81 "stream": p2pStreamCmd,
82 "forward": p2pForwardCmd,
83 "listen": p2pListenCmd,
84 "close": p2pCloseCmd,
85 "ls": p2pLsCmd,
86 },
87 }
88
89 var p2pForwardCmd = &cmds.Command{
90 Status: cmds.Experimental,
91 Helptext: cmds.HelpText{
92 Tagline: "Forward connections to libp2p service.",
93 ShortDescription: `
94 Forward connections made to <listen-address> to <target-address> via libp2p.
95
96 Creates a local TCP listener that tunnels connections through libp2p to a
97 remote peer's p2p listener. Similar to SSH port forwarding (-L flag).
98
99 ARGUMENTS:
100
101 <protocol> Protocol name (must start with '` + P2PProtoPrefix + `')
102 <listen-address> Local multiaddr (e.g., /ip4/127.0.0.1/tcp/3000)
103 <target-address> Remote peer multiaddr (e.g., /p2p/PeerID)
104
105 FOREGROUND MODE (--foreground, -f):
106
107 By default, the forwarder runs in the daemon and the command returns
108 immediately. Use --foreground to block until interrupted:
109
110 - Ctrl+C or SIGTERM: Removes the forwarder and exits
111 - 'ipfs p2p close': Removes the forwarder and exits
112 - Daemon shutdown: Forwarder is automatically removed
113
114 Useful for systemd services or scripts that need cleanup on exit.
115
116 EXAMPLES:
117
118 # Persistent forwarder (command returns immediately)
119 ipfs p2p forward /x/myapp /ip4/127.0.0.1/tcp/3000 /p2p/PeerID
120
121 # Temporary forwarder (removed when command exits)
122 ipfs p2p forward -f /x/myapp /ip4/127.0.0.1/tcp/3000 /p2p/PeerID
123
124 Learn more: https://github.com/ipfs/kubo/blob/master/docs/p2p-tunnels.md
125 `,
126 },
127 Arguments: []cmds.Argument{
128 cmds.StringArg("protocol", true, false, "Protocol name."),
129 cmds.StringArg("listen-address", true, false, "Listening endpoint."),
130 cmds.StringArg("target-address", true, false, "Target endpoint."),
131 },
132 Options: []cmds.Option{
133 cmds.BoolOption(allowCustomProtocolOptionName, "Don't require /x/ prefix"),
134 cmds.BoolOption(foregroundOptionName, "f", "Run in foreground; forwarder is removed when command exits"),
135 },
136 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
137 n, err := p2pGetNode(env)
138 if err != nil {
139 return err
140 }
141
142 protoOpt := req.Arguments[0]
143 listenOpt := req.Arguments[1]
144 targetOpt := req.Arguments[2]
145
146 proto := protocol.ID(protoOpt)
147
148 listen, err := ma.NewMultiaddr(listenOpt)
149 if err != nil {
150 return err
151 }
152
153 targets, err := parseIpfsAddr(targetOpt)
154 if err != nil {
155 return err
156 }
157
158 allowCustom, _ := req.Options[allowCustomProtocolOptionName].(bool)
159
160 if !allowCustom && !strings.HasPrefix(string(proto), P2PProtoPrefix) {
161 return errors.New("protocol name must be within '" + P2PProtoPrefix + "' namespace")
162 }
163
164 listener, err := forwardLocal(n.Context(), n.P2P, n.Peerstore, proto, listen, targets)
165 if err != nil {
166 return err
167 }
168
169 foreground, _ := req.Options[foregroundOptionName].(bool)
170 if foreground {
171 if err := res.Emit(&P2PForegroundOutput{
172 Status: "active",
173 Protocol: protoOpt,
174 Address: listenOpt,
175 }); err != nil {
176 return err
177 }
178 // Wait for either context cancellation (Ctrl+C/daemon shutdown)
179 // or listener removal (ipfs p2p close)
180 select {
181 case <-req.Context.Done():
182 // SIGTERM/Ctrl+C - cleanup silently (CLI stream already closing)
183 n.P2P.ListenersLocal.Close(func(l p2p.Listener) bool {
184 return l == listener
185 })
186 return nil
187 case <-listener.Done():
188 // Closed via "ipfs p2p close" - emit closing message
189 return res.Emit(&P2PForegroundOutput{
190 Status: "closing",
191 Protocol: protoOpt,
192 Address: listenOpt,
193 })
194 }
195 }
196
197 return nil
198 },
199 Type: P2PForegroundOutput{},
200 Encoders: cmds.EncoderMap{
201 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *P2PForegroundOutput) error {
202 if out.Status == "active" {
203 fmt.Fprintf(w, "Forwarding %s to %s, waiting for interrupt...\n", out.Protocol, out.Address)
204 } else if out.Status == "closing" {
205 fmt.Fprintf(w, "Received interrupt, removing forwarder for %s\n", out.Protocol)
206 }
207 return nil
208 }),
209 },
210 }
211
212 // parseIpfsAddr is a function that takes in addr string and return ipfsAddrs
213 func parseIpfsAddr(addr string) (*peer.AddrInfo, error) {
214 multiaddr, err := ma.NewMultiaddr(addr)
215 if err != nil {
216 return nil, err
217 }
218
219 pi, err := peer.AddrInfoFromP2pAddr(multiaddr)
220 if err == nil {
221 return pi, nil
222 }
223
224 // resolve multiaddr whose protocol is not ma.P_IPFS
225 ctx, cancel := context.WithTimeout(context.Background(), resolveTimeout)
226 defer cancel()
227 addrs, err := madns.Resolve(ctx, multiaddr)
228 if err != nil {
229 return nil, err
230 }
231 if len(addrs) == 0 {
232 return nil, errors.New("fail to resolve the multiaddr:" + multiaddr.String())
233 }
234 var info peer.AddrInfo
235 for _, addr := range addrs {
236 taddr, id := peer.SplitAddr(addr)
237 if id == "" {
238 // not an ipfs addr, skipping.
239 continue
240 }
241 switch info.ID {
242 case "":
243 info.ID = id
244 case id:
245 default:
246 return nil, fmt.Errorf(
247 "ambiguous multiaddr %s could refer to %s or %s",
248 multiaddr,
249 info.ID,
250 id,
251 )
252 }
253 info.Addrs = append(info.Addrs, taddr)
254 }
255 return &info, nil
256 }
257
258 var p2pListenCmd = &cmds.Command{
259 Status: cmds.Experimental,
260 Helptext: cmds.HelpText{
261 Tagline: "Create libp2p service.",
262 ShortDescription: `
263 Create a libp2p protocol handler that forwards incoming connections to
264 <target-address>.
265
266 When a remote peer connects using 'ipfs p2p forward', the connection is
267 forwarded to your local service. Similar to SSH port forwarding (server side).
268
269 ARGUMENTS:
270
271 <protocol> Protocol name (must start with '` + P2PProtoPrefix + `')
272 <target-address> Local multiaddr (e.g., /ip4/127.0.0.1/tcp/3000)
273
274 FOREGROUND MODE (--foreground, -f):
275
276 By default, the listener runs in the daemon and the command returns
277 immediately. Use --foreground to block until interrupted:
278
279 - Ctrl+C or SIGTERM: Removes the listener and exits
280 - 'ipfs p2p close': Removes the listener and exits
281 - Daemon shutdown: Listener is automatically removed
282
283 Useful for systemd services or scripts that need cleanup on exit.
284
285 EXAMPLES:
286
287 # Persistent listener (command returns immediately)
288 ipfs p2p listen /x/myapp /ip4/127.0.0.1/tcp/3000
289
290 # Temporary listener (removed when command exits)
291 ipfs p2p listen -f /x/myapp /ip4/127.0.0.1/tcp/3000
292
293 # Report connecting peer ID to the target application
294 ipfs p2p listen -r /x/myapp /ip4/127.0.0.1/tcp/3000
295
296 Learn more: https://github.com/ipfs/kubo/blob/master/docs/p2p-tunnels.md
297 `,
298 },
299 Arguments: []cmds.Argument{
300 cmds.StringArg("protocol", true, false, "Protocol name."),
301 cmds.StringArg("target-address", true, false, "Target endpoint."),
302 },
303 Options: []cmds.Option{
304 cmds.BoolOption(allowCustomProtocolOptionName, "Don't require /x/ prefix"),
305 cmds.BoolOption(reportPeerIDOptionName, "r", "Send remote base58 peerid to target when a new connection is established"),
306 cmds.BoolOption(foregroundOptionName, "f", "Run in foreground; listener is removed when command exits"),
307 },
308 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
309 n, err := p2pGetNode(env)
310 if err != nil {
311 return err
312 }
313
314 protoOpt := req.Arguments[0]
315 targetOpt := req.Arguments[1]
316
317 proto := protocol.ID(protoOpt)
318
319 target, err := ma.NewMultiaddr(targetOpt)
320 if err != nil {
321 return err
322 }
323
324 // port can't be 0
325 if err := checkPort(target); err != nil {
326 return err
327 }
328
329 allowCustom, _ := req.Options[allowCustomProtocolOptionName].(bool)
330 reportPeerID, _ := req.Options[reportPeerIDOptionName].(bool)
331
332 if !allowCustom && !strings.HasPrefix(string(proto), P2PProtoPrefix) {
333 return errors.New("protocol name must be within '" + P2PProtoPrefix + "' namespace")
334 }
335
336 listener, err := n.P2P.ForwardRemote(n.Context(), proto, target, reportPeerID)
337 if err != nil {
338 return err
339 }
340
341 foreground, _ := req.Options[foregroundOptionName].(bool)
342 if foreground {
343 if err := res.Emit(&P2PForegroundOutput{
344 Status: "active",
345 Protocol: protoOpt,
346 Address: targetOpt,
347 }); err != nil {
348 return err
349 }
350 // Wait for either context cancellation (Ctrl+C/daemon shutdown)
351 // or listener removal (ipfs p2p close)
352 select {
353 case <-req.Context.Done():
354 // SIGTERM/Ctrl+C - cleanup silently (CLI stream already closing)
355 n.P2P.ListenersP2P.Close(func(l p2p.Listener) bool {
356 return l == listener
357 })
358 return nil
359 case <-listener.Done():
360 // Closed via "ipfs p2p close" - emit closing message
361 return res.Emit(&P2PForegroundOutput{
362 Status: "closing",
363 Protocol: protoOpt,
364 Address: targetOpt,
365 })
366 }
367 }
368
369 return nil
370 },
371 Type: P2PForegroundOutput{},
372 Encoders: cmds.EncoderMap{
373 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *P2PForegroundOutput) error {
374 if out.Status == "active" {
375 fmt.Fprintf(w, "Listening on %s, forwarding to %s, waiting for interrupt...\n", out.Protocol, out.Address)
376 } else if out.Status == "closing" {
377 fmt.Fprintf(w, "Received interrupt, removing listener for %s\n", out.Protocol)
378 }
379 return nil
380 }),
381 },
382 }
383
384 // checkPort checks whether target multiaddr contains tcp or udp protocol
385 // and whether the port is equal to 0
386 func checkPort(target ma.Multiaddr) error {
387 // get tcp or udp port from multiaddr
388 getPort := func() (string, error) {
389 sport, _ := target.ValueForProtocol(ma.P_TCP)
390 if sport != "" {
391 return sport, nil
392 }
393
394 sport, _ = target.ValueForProtocol(ma.P_UDP)
395 if sport != "" {
396 return sport, nil
397 }
398 return "", errors.New("address does not contain tcp or udp protocol")
399 }
400
401 sport, err := getPort()
402 if err != nil {
403 return err
404 }
405
406 port, err := strconv.Atoi(sport)
407 if err != nil {
408 return err
409 }
410
411 if port == 0 {
412 return errors.New("port can not be 0")
413 }
414
415 return nil
416 }
417
418 // forwardLocal forwards local connections to a libp2p service
419 func forwardLocal(ctx context.Context, p *p2p.P2P, ps pstore.Peerstore, proto protocol.ID, bindAddr ma.Multiaddr, addr *peer.AddrInfo) (p2p.Listener, error) {
420 ps.AddAddrs(addr.ID, addr.Addrs, pstore.TempAddrTTL)
421 return p.ForwardLocal(ctx, addr.ID, proto, bindAddr)
422 }
423
424 const (
425 p2pHeadersOptionName = "headers"
426 )
427
428 var p2pLsCmd = &cmds.Command{
429 Status: cmds.Experimental,
430 Helptext: cmds.HelpText{
431 Tagline: "List active p2p listeners.",
432 },
433 Options: []cmds.Option{
434 cmds.BoolOption(p2pHeadersOptionName, "v", "Print table headers (Protocol, Listen, Target)."),
435 },
436 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
437 n, err := p2pGetNode(env)
438 if err != nil {
439 return err
440 }
441
442 output := &P2PLsOutput{}
443
444 n.P2P.ListenersLocal.Lock()
445 for _, listener := range n.P2P.ListenersLocal.Listeners {
446 output.Listeners = append(output.Listeners, P2PListenerInfoOutput{
447 Protocol: string(listener.Protocol()),
448 ListenAddress: listener.ListenAddress().String(),
449 TargetAddress: listener.TargetAddress().String(),
450 })
451 }
452 n.P2P.ListenersLocal.Unlock()
453
454 n.P2P.ListenersP2P.Lock()
455 for _, listener := range n.P2P.ListenersP2P.Listeners {
456 output.Listeners = append(output.Listeners, P2PListenerInfoOutput{
457 Protocol: string(listener.Protocol()),
458 ListenAddress: listener.ListenAddress().String(),
459 TargetAddress: listener.TargetAddress().String(),
460 })
461 }
462 n.P2P.ListenersP2P.Unlock()
463
464 return cmds.EmitOnce(res, output)
465 },
466 Type: P2PLsOutput{},
467 Encoders: cmds.EncoderMap{
468 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *P2PLsOutput) error {
469 headers, _ := req.Options[p2pHeadersOptionName].(bool)
470 tw := tabwriter.NewWriter(w, 1, 2, 1, ' ', 0)
471 for _, listener := range out.Listeners {
472 if headers {
473 fmt.Fprintln(tw, "Protocol\tListen Address\tTarget Address")
474 }
475
476 fmt.Fprintf(tw, "%s\t%s\t%s\n", listener.Protocol, listener.ListenAddress, listener.TargetAddress)
477 }
478 tw.Flush()
479
480 return nil
481 }),
482 },
483 }
484
485 const (
486 p2pAllOptionName = "all"
487 p2pProtocolOptionName = "protocol"
488 p2pListenAddressOptionName = "listen-address"
489 p2pTargetAddressOptionName = "target-address"
490 )
491
492 var p2pCloseCmd = &cmds.Command{
493 Status: cmds.Experimental,
494 Helptext: cmds.HelpText{
495 Tagline: "Stop listening for new connections to forward.",
496 },
497 Options: []cmds.Option{
498 cmds.BoolOption(p2pAllOptionName, "a", "Close all listeners."),
499 cmds.StringOption(p2pProtocolOptionName, "p", "Match protocol name"),
500 cmds.StringOption(p2pListenAddressOptionName, "l", "Match listen address"),
501 cmds.StringOption(p2pTargetAddressOptionName, "t", "Match target address"),
502 },
503 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
504 n, err := p2pGetNode(env)
505 if err != nil {
506 return err
507 }
508
509 closeAll, _ := req.Options[p2pAllOptionName].(bool)
510 protoOpt, p := req.Options[p2pProtocolOptionName].(string)
511 listenOpt, l := req.Options[p2pListenAddressOptionName].(string)
512 targetOpt, t := req.Options[p2pTargetAddressOptionName].(string)
513
514 proto := protocol.ID(protoOpt)
515
516 var target, listen ma.Multiaddr
517
518 if l {
519 listen, err = ma.NewMultiaddr(listenOpt)
520 if err != nil {
521 return err
522 }
523 }
524
525 if t {
526 target, err = ma.NewMultiaddr(targetOpt)
527 if err != nil {
528 return err
529 }
530 }
531
532 if !(closeAll || p || l || t) {
533 return errors.New("no matching options given")
534 }
535
536 if closeAll && (p || l || t) {
537 return errors.New("can't combine --all with other matching options")
538 }
539
540 match := func(listener p2p.Listener) bool {
541 if closeAll {
542 return true
543 }
544 if p && proto != listener.Protocol() {
545 return false
546 }
547 if l && !listen.Equal(listener.ListenAddress()) {
548 return false
549 }
550 if t && !target.Equal(listener.TargetAddress()) {
551 return false
552 }
553 return true
554 }
555
556 done := n.P2P.ListenersLocal.Close(match)
557 done += n.P2P.ListenersP2P.Close(match)
558
559 return cmds.EmitOnce(res, done)
560 },
561 Type: int(0),
562 Encoders: cmds.EncoderMap{
563 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out int) error {
564 fmt.Fprintf(w, "Closed %d stream(s)\n", out)
565 return nil
566 }),
567 },
568 }
569
570 ///////
571 // Stream
572 //
573
574 // p2pStreamCmd is the 'ipfs p2p stream' command
575 var p2pStreamCmd = &cmds.Command{
576 Status: cmds.Experimental,
577 Helptext: cmds.HelpText{
578 Tagline: "P2P stream management.",
579 ShortDescription: "Create and manage p2p streams",
580 },
581
582 Subcommands: map[string]*cmds.Command{
583 "ls": p2pStreamLsCmd,
584 "close": p2pStreamCloseCmd,
585 },
586 }
587
588 var p2pStreamLsCmd = &cmds.Command{
589 Status: cmds.Experimental,
590 Helptext: cmds.HelpText{
591 Tagline: "List active p2p streams.",
592 },
593 Options: []cmds.Option{
594 cmds.BoolOption(p2pHeadersOptionName, "v", "Print table headers (ID, Protocol, Local, Remote)."),
595 },
596 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
597 n, err := p2pGetNode(env)
598 if err != nil {
599 return err
600 }
601
602 output := &P2PStreamsOutput{}
603
604 n.P2P.Streams.Lock()
605 for id, s := range n.P2P.Streams.Streams {
606 output.Streams = append(output.Streams, P2PStreamInfoOutput{
607 HandlerID: strconv.FormatUint(id, 10),
608
609 Protocol: string(s.Protocol),
610
611 OriginAddress: s.OriginAddr.String(),
612 TargetAddress: s.TargetAddr.String(),
613 })
614 }
615 n.P2P.Streams.Unlock()
616
617 return cmds.EmitOnce(res, output)
618 },
619 Type: P2PStreamsOutput{},
620 Encoders: cmds.EncoderMap{
621 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *P2PStreamsOutput) error {
622 headers, _ := req.Options[p2pHeadersOptionName].(bool)
623 tw := tabwriter.NewWriter(w, 1, 2, 1, ' ', 0)
624 for _, stream := range out.Streams {
625 if headers {
626 fmt.Fprintln(tw, "ID\tProtocol\tOrigin\tTarget")
627 }
628
629 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", stream.HandlerID, stream.Protocol, stream.OriginAddress, stream.TargetAddress)
630 }
631 tw.Flush()
632
633 return nil
634 }),
635 },
636 }
637
638 var p2pStreamCloseCmd = &cmds.Command{
639 Status: cmds.Experimental,
640 Helptext: cmds.HelpText{
641 Tagline: "Close active p2p stream.",
642 },
643 Arguments: []cmds.Argument{
644 cmds.StringArg("id", false, false, "Stream identifier"),
645 },
646 Options: []cmds.Option{
647 cmds.BoolOption(p2pAllOptionName, "a", "Close all streams."),
648 },
649 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
650 n, err := p2pGetNode(env)
651 if err != nil {
652 return err
653 }
654
655 closeAll, _ := req.Options[p2pAllOptionName].(bool)
656 var handlerID uint64
657
658 if !closeAll {
659 if len(req.Arguments) == 0 {
660 return errors.New("no id specified")
661 }
662
663 handlerID, err = strconv.ParseUint(req.Arguments[0], 10, 64)
664 if err != nil {
665 return err
666 }
667 }
668
669 toClose := make([]*p2p.Stream, 0, 1)
670 n.P2P.Streams.Lock()
671 for id, stream := range n.P2P.Streams.Streams {
672 if !closeAll && handlerID != id {
673 continue
674 }
675 toClose = append(toClose, stream)
676 if !closeAll {
677 break
678 }
679 }
680 n.P2P.Streams.Unlock()
681
682 for _, s := range toClose {
683 n.P2P.Streams.Reset(s)
684 }
685
686 return nil
687 },
688 }
689
690 func p2pGetNode(env cmds.Environment) (*core.IpfsNode, error) {
691 nd, err := cmdenv.GetNode(env)
692 if err != nil {
693 return nil, err
694 }
695
696 config, err := nd.Repo.Config()
697 if err != nil {
698 return nil, err
699 }
700
701 if !config.Experimental.Libp2pStreamMounting {
702 return nil, errors.New("libp2p stream mounting not enabled")
703 }
704
705 if !nd.IsOnline {
706 return nil, ErrNotOnline
707 }
708
709 return nd, nil
710 }