Skip to content

Commit f57d13c

Browse files
2colorlidel
andauthored
feat: swarm addrs autonat command (#11184)
* feat: add swarm addrs autonat command fixes #11171 by adding a self service way to debug public reachability with autonat * test: add test for ipfs swarm addr autonat command * docs: add ipfs swarm addrs autonat to changelog * test: update failing test * fix: swarm addrs autonat bugfixes and cleanup - fix help text to show capitalized reachability values (Public, Private, Unknown) matching actual output from network.Reachability.String() - default Reachability to "Unknown" instead of empty string when the host interface assertion fails - extract multiaddrsToStrings and writeAddrSection helpers to deduplicate repeated conversion loops and text formatting blocks --------- Co-authored-by: Marcin Rataj <lidel@lidel.org>
1 parent 3a6b1ee commit f57d13c

File tree

5 files changed

+207
-2
lines changed

5 files changed

+207
-2
lines changed

core/commands/commands_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ func TestCommands(t *testing.T) {
198198
"/stats/repo",
199199
"/swarm",
200200
"/swarm/addrs",
201+
"/swarm/addrs/autonat",
201202
"/swarm/addrs/listen",
202203
"/swarm/addrs/local",
203204
"/swarm/connect",

core/commands/swarm.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,9 @@ var swarmAddrsCmd = &cmds.Command{
513513
`,
514514
},
515515
Subcommands: map[string]*cmds.Command{
516-
"local": swarmAddrsLocalCmd,
517-
"listen": swarmAddrsListenCmd,
516+
"autonat": swarmAddrsAutoNATCmd,
517+
"local": swarmAddrsLocalCmd,
518+
"listen": swarmAddrsListenCmd,
518519
},
519520
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
520521
api, err := cmdenv.GetApi(env, req)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
cmds "github.com/ipfs/go-ipfs-cmds"
8+
cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
9+
"github.com/libp2p/go-libp2p/core/network"
10+
ma "github.com/multiformats/go-multiaddr"
11+
)
12+
13+
// reachabilityHost provides access to the AutoNAT reachability status.
14+
type reachabilityHost interface {
15+
Reachability() network.Reachability
16+
}
17+
18+
// confirmedAddrsHost provides access to per-address reachability from AutoNAT V2.
19+
type confirmedAddrsHost interface {
20+
ConfirmedAddrs() (reachable, unreachable, unknown []ma.Multiaddr)
21+
}
22+
23+
// autoNATResult represents the AutoNAT reachability information.
24+
type autoNATResult struct {
25+
Reachability string `json:"reachability"`
26+
Reachable []string `json:"reachable,omitempty"`
27+
Unreachable []string `json:"unreachable,omitempty"`
28+
Unknown []string `json:"unknown,omitempty"`
29+
}
30+
31+
func multiaddrsToStrings(addrs []ma.Multiaddr) []string {
32+
out := make([]string, len(addrs))
33+
for i, a := range addrs {
34+
out[i] = a.String()
35+
}
36+
return out
37+
}
38+
39+
func writeAddrSection(w io.Writer, label string, addrs []string) {
40+
if len(addrs) > 0 {
41+
fmt.Fprintf(w, " %s:\n", label)
42+
for _, addr := range addrs {
43+
fmt.Fprintf(w, " %s\n", addr)
44+
}
45+
}
46+
}
47+
48+
var swarmAddrsAutoNATCmd = &cmds.Command{
49+
Helptext: cmds.HelpText{
50+
Tagline: "Show address reachability as determined by AutoNAT V2.",
51+
ShortDescription: `
52+
'ipfs swarm addrs autonat' shows the reachability status of your node's
53+
addresses as determined by AutoNAT V2.
54+
`,
55+
LongDescription: `
56+
'ipfs swarm addrs autonat' shows the reachability status of your node's
57+
addresses as verified by AutoNAT V2.
58+
59+
AutoNAT V2 probes your node's addresses to determine if they are reachable
60+
from the public internet. This helps understand whether other peers can
61+
dial your node directly.
62+
63+
The output shows:
64+
- Reachability: Overall status (Public, Private, or Unknown)
65+
- Reachable: Addresses confirmed to be publicly reachable
66+
- Unreachable: Addresses that failed reachability checks
67+
- Unknown: Addresses that haven't been tested yet
68+
69+
For more information on AutoNAT V2, see:
70+
https://github.com/libp2p/specs/blob/master/autonat/autonat-v2.md
71+
72+
Example:
73+
74+
> ipfs swarm addrs autonat
75+
AutoNAT V2 Status:
76+
Reachability: Public
77+
78+
Per-Address Reachability:
79+
Reachable:
80+
/ip4/203.0.113.42/tcp/4001
81+
/ip4/203.0.113.42/udp/4001/quic-v1
82+
Unreachable:
83+
/ip6/2001:db8::1/tcp/4001
84+
Unknown:
85+
/ip4/203.0.113.42/udp/4001/webrtc-direct
86+
`,
87+
},
88+
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
89+
nd, err := cmdenv.GetNode(env)
90+
if err != nil {
91+
return err
92+
}
93+
94+
if !nd.IsOnline {
95+
return ErrNotOnline
96+
}
97+
98+
result := autoNATResult{
99+
Reachability: network.ReachabilityUnknown.String(),
100+
}
101+
102+
// Get per-address reachability from AutoNAT V2.
103+
// The host embeds *BasicHost (closableBasicHost, closableRoutedHost)
104+
// which implements ConfirmedAddrs.
105+
if h, ok := nd.PeerHost.(confirmedAddrsHost); ok {
106+
reachable, unreachable, unknown := h.ConfirmedAddrs()
107+
result.Reachable = multiaddrsToStrings(reachable)
108+
result.Unreachable = multiaddrsToStrings(unreachable)
109+
result.Unknown = multiaddrsToStrings(unknown)
110+
}
111+
112+
// Get overall reachability status.
113+
if h, ok := nd.PeerHost.(reachabilityHost); ok {
114+
result.Reachability = h.Reachability().String()
115+
}
116+
117+
return cmds.EmitOnce(res, result)
118+
},
119+
Type: autoNATResult{},
120+
Encoders: cmds.EncoderMap{
121+
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, result autoNATResult) error {
122+
fmt.Fprintln(w, "AutoNAT V2 Status:")
123+
fmt.Fprintf(w, " Reachability: %s\n", result.Reachability)
124+
125+
fmt.Fprintln(w)
126+
fmt.Fprintln(w, "Per-Address Reachability:")
127+
128+
writeAddrSection(w, "Reachable", result.Reachable)
129+
writeAddrSection(w, "Unreachable", result.Unreachable)
130+
writeAddrSection(w, "Unknown", result.Unknown)
131+
132+
if len(result.Reachable) == 0 && len(result.Unreachable) == 0 && len(result.Unknown) == 0 {
133+
fmt.Fprintln(w, " (no address reachability data available)")
134+
}
135+
136+
return nil
137+
}),
138+
},
139+
}

docs/changelogs/v0.40.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
1818
- [IPIP-524: Gateway codec conversion disabled by default](#ipip-524-gateway-codec-conversion-disabled-by-default)
1919
- [Improved IPNS over PubSub validation](#improved-ipns-over-pubsub-validation)
2020
- [New `ipfs diag datastore` commands](#new-ipfs-diag-datastore-commands)
21+
- [🔍 New `ipfs swarm addrs autonat` command](#-new-ipfs-swarm-addrs-autonat-command)
2122
- [🚇 Improved `ipfs p2p` tunnels with foreground mode](#-improved-ipfs-p2p-tunnels-with-foreground-mode)
2223
- [Improved `ipfs dag stat` output](#improved-ipfs-dag-stat-output)
2324
- [🔑 `ipfs key` improvements](#-ipfs-key-improvements)
@@ -129,6 +130,38 @@ New experimental commands for low-level datastore inspection:
129130

130131
The daemon must not be running when using these commands. Run `ipfs diag datastore --help` for usage examples.
131132

133+
#### 🔍 New `ipfs swarm addrs autonat` command
134+
135+
The new `ipfs swarm addrs autonat` command shows the network reachability status of your node's addresses as verified by AutoNAT V2. AutoNAT V2 leverages other nodes in the IPFS network to test your node's external public reachability, providing a self-service way to debug connectivity.
136+
137+
Public reachability is important for:
138+
139+
- **Direct data fetching**: Other nodes can fetch data directly from your node without NAT hole punching.
140+
- **Browser access**: Web browsers can connect to your node directly for content retrieval.
141+
- **DHT participation**: Your node can act as a DHT server, helping to maintain the distributed hash table and making content routing more robust.
142+
143+
The command displays:
144+
145+
- Overall reachability status (public, private, or unknown)
146+
- Per-address reachability showing which specific addresses are reachable, unreachable, or unknown
147+
148+
Example output:
149+
```
150+
AutoNAT V2 Status:
151+
Reachability: public
152+
153+
Per-Address Reachability:
154+
Reachable:
155+
/ip4/203.0.113.42/tcp/4001
156+
/ip4/203.0.113.42/udp/4001/quic-v1
157+
Unreachable:
158+
/ip6/2001:db8::1/tcp/4001
159+
Unknown:
160+
/ip4/203.0.113.42/udp/4001/webrtc-direct
161+
```
162+
163+
This helps diagnose connectivity issues and understand if your node is publicly reachable. See the [AutoNAT V2 spec](https://github.com/libp2p/specs/blob/master/autonat/autonat-v2.md) for more details.
164+
132165
#### 🚇 Improved `ipfs p2p` tunnels with foreground mode
133166

134167
P2P tunnels can now run like SSH port forwarding: start a tunnel, use it, and it cleans up automatically when you're done.

test/cli/swarm_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,35 @@ func TestSwarm(t *testing.T) {
9292
assert.ElementsMatch(t, outputIdentify.Addresses, otherNodeIDOutput.Addresses)
9393
assert.ElementsMatch(t, outputIdentify.Protocols, otherNodeIDOutput.Protocols)
9494
})
95+
96+
t.Run("ipfs swarm addrs autonat returns valid reachability status", func(t *testing.T) {
97+
t.Parallel()
98+
node := harness.NewT(t).NewNode().Init().StartDaemon()
99+
defer node.StopDaemon()
100+
101+
res := node.RunIPFS("swarm", "addrs", "autonat", "--enc=json")
102+
assert.NoError(t, res.Err)
103+
104+
var output struct {
105+
Reachability string `json:"reachability"`
106+
Reachable []string `json:"reachable"`
107+
Unreachable []string `json:"unreachable"`
108+
Unknown []string `json:"unknown"`
109+
}
110+
err := json.Unmarshal(res.Stdout.Bytes(), &output)
111+
assert.NoError(t, err)
112+
113+
// Reachability must be one of the valid states
114+
// Note: network.Reachability constants use capital first letter
115+
validStates := []string{"Public", "Private", "Unknown"}
116+
assert.Contains(t, validStates, output.Reachability,
117+
"Reachability should be one of: Public, Private, Unknown")
118+
119+
// For a newly started node, reachability is typically Unknown initially
120+
// as AutoNAT hasn't completed probing yet. This is expected behavior.
121+
// The important thing is that the command runs and returns valid data.
122+
totalAddrs := len(output.Reachable) + len(output.Unreachable) + len(output.Unknown)
123+
t.Logf("Reachability: %s, Total addresses: %d (reachable: %d, unreachable: %d, unknown: %d)",
124+
output.Reachability, totalAddrs, len(output.Reachable), len(output.Unreachable), len(output.Unknown))
125+
})
95126
}

0 commit comments

Comments
 (0)