@cryptotaxi247 / kubo / commits / afd734270

feat(config): dead listener check (#11299)

* docs(server-profile): warn about local reverse proxy gotcha `Swarm.AddrFilters` is consulted on inbound `InterceptAccept` as well as outbound dials, so loopback CIDRs in the filter list cause Kubo to reject every incoming connection from a local nginx or Caddy reverse proxy that fronts a `/ws` (or other libp2p) listener on `127.0.0.1`. The condition is silent: the OS accepts the TCP, then Kubo closes the socket before the libp2p handshake. Add an explicit note to the `Swarm.AddrFilters` section, a new row in the `server` profile override table for the reverse-proxy case, and a matching CAUTION block in the v0.41 changelog. Each pointer says: remove the loopback CIDRs from `Swarm.AddrFilters` only, and keep them in `Addresses.NoAnnounce`. * feat(libp2p): log ERROR for listeners blocked by AddrFilters or NoAnnounce Surface misconfigured listeners at startup and on every libp2p `EvtLocalAddressesUpdated` event, instead of silently dropping incoming connections or staying unadvertised. `findDeadListeners` is a pure function that walks the host's resolved listen addresses (the output of `host.Network().InterfaceListenAddresses()`, matching the post-resolution view used in #11297 for `host.Addrs()`) and matches each IP component against every CIDR rule in `Swarm.AddrFilters` and `Addresses.NoAnnounce`. Working from resolved addresses means wildcard listens like `/ip4/0.0.0.0` and `/ip6/::` are already expanded to concrete interface addresses, so the check does not flag a listener just because the unspecified address itself happens to fall inside a filter CIDR (for example `::` is in `::/3` even though the listener still accepts inbound from globally-routable peers). `MonitorDeadListeners` wires the check into fx: it runs once at startup, subscribes to `event.EvtLocalAddressesUpdated`, and re-runs the check whenever the host's address set changes (NAT mapping comes online, new interface, AutoTLS cert ready). Findings are deduplicated against the previous run so a stable misconfiguration is logged once until it is resolved or a new finding shows up. Loopback `Addresses.NoAnnounce` matches are skipped on the grounds that suppressing loopback advertisement is operator-intent on every `server`-profile node, not a misconfiguration. Loopback in `Swarm.AddrFilters` is the bug pattern that motivated this check; that match is always reported. Each ERROR line names the offending listener, the matching CIDR rule, and the field to remove the rule from to revive the listener: Addresses.Swarm listener "/ip4/127.0.0.1/tcp/8081/ws" matches Swarm.AddrFilters rule "/ip4/127.0.0.0/ipcidr/8", so Kubo rejects every incoming connection to it. Remove "/ip4/127.0.0.0/ipcidr/8" from Swarm.AddrFilters to allow connections to this listener.

Marcin Rataj committed May 4, 2026 at 16:51 UTC afd734270e09b06a96a8fda3709632948413d2c3
6 files changed +311
core/node/groups.go
+1
@@ -201,6 +201,7 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
201 maybeProvide(libp2p.P2PForgeCertMgr(bcfg.Repo.Path(), cfg.AutoTLS, atlsLog), enableAutoTLS),
202 maybeInvoke(libp2p.StartP2PAutoTLS, enableAutoTLS),
203 fx.Provide(libp2p.AddrFilters(cfg.Swarm.AddrFilters)),
204 + fx.Invoke(libp2p.MonitorDeadListeners(cfg.Swarm.AddrFilters, cfg.Addresses.NoAnnounce)),
205 fx.Provide(libp2p.AddrsFactory(cfg.Addresses.Announce, cfg.Addresses.AppendAnnounce, cfg.Addresses.NoAnnounce)),
206 fx.Provide(libp2p.SmuxTransport(cfg.Swarm.Transports)),
207 fx.Provide(libp2p.RelayTransport(enableRelayTransport)),
core/node/libp2p/addrs.go
+168
@@ -12,9 +12,11 @@ import (
12 "github.com/ipfs/kubo/config"
13 p2pforge "github.com/ipshipyard/p2p-forge/client"
14 "github.com/libp2p/go-libp2p"
15 + "github.com/libp2p/go-libp2p/core/event"
16 "github.com/libp2p/go-libp2p/core/host"
17 p2pbhost "github.com/libp2p/go-libp2p/p2p/host/basic"
18 ma "github.com/multiformats/go-multiaddr"
19 + manet "github.com/multiformats/go-multiaddr/net"
20 mamask "github.com/whyrusleeping/multiaddr-filter"
21
22 "github.com/caddyserver/certmagic"
@@ -36,6 +38,172 @@ func AddrFilters(filters []string) func() (*ma.Filters, Libp2pOpts, error) {
38 }
39 }
40
41 +// Sources for deadListenerFinding.Source.
42 +const (
43 + deadListenerSourceAddrFilters = "Swarm.AddrFilters"
44 + deadListenerSourceNoAnnounce = "Addresses.NoAnnounce"
45 +)
46 +
47 +// deadListenerFinding is one resolved listener killed by a CIDR rule:
48 +// `Swarm.AddrFilters` (gater RSTs inbound) or `Addresses.NoAnnounce`
49 +// (listener never advertised).
50 +type deadListenerFinding struct {
51 + Listener string // resolved listen multiaddr (interface-bound)
52 + Source string // deadListenerSourceAddrFilters or deadListenerSourceNoAnnounce
53 + Rule string // matching CIDR rule from Source
54 +}
55 +
56 +// findDeadListeners returns one finding per (listener, rule, source)
57 +// triple whose IP component falls inside a CIDR in addrFilters or
58 +// noAnnounce.
59 +//
60 +// listenAddrs must be already-resolved interface addresses (output of
61 +// `host.Network().InterfaceListenAddresses()`). Without resolution, the
62 +// unspecified address itself can match a broad filter (`::` is in
63 +// `::/3`) even when the listener accepts globally-routable peers.
64 +//
65 +// NoAnnounce matches on loopback are skipped: stripping loopback from
66 +// identify and DHT records is normal operator intent, not a bug.
67 +// AddrFilters matches on loopback are always reported, since that is
68 +// the misconfiguration this check exists to catch.
69 +//
70 +// Listeners without an IP component (`/dns`, `/dnsaddr`) and
71 +// unparseable rules are skipped silently.
72 +func findDeadListeners(listenAddrs []ma.Multiaddr, addrFilters []string, noAnnounce []string) []deadListenerFinding {
73 + check := func(source string, rules []string) []deadListenerFinding {
74 + var out []deadListenerFinding
75 + for _, r := range rules {
76 + mask, err := mamask.NewMask(r)
77 + if err != nil {
78 + // Malformed CIDR (caught upstream for AddrFilters) or
79 + // an exact-match multiaddr in NoAnnounce. Skip either way.
80 + continue
81 + }
82 + f := ma.NewFilters()
83 + f.AddFilter(*mask, ma.ActionDeny)
84 + for _, l := range listenAddrs {
85 + if !f.AddrBlocked(l) {
86 + continue
87 + }
88 + if source == deadListenerSourceNoAnnounce && isLoopbackMultiaddr(l) {
89 + // Suppressing loopback announcement is operator-intent,
90 + // not a misconfiguration.
91 + continue
92 + }
93 + out = append(out, deadListenerFinding{
94 + Listener: l.String(),
95 + Source: source,
96 + Rule: r,
97 + })
98 + }
99 + }
100 + return out
101 + }
102 +
103 + findings := check(deadListenerSourceAddrFilters, addrFilters)
104 + findings = append(findings, check(deadListenerSourceNoAnnounce, noAnnounce)...)
105 + return findings
106 +}
107 +
108 +// isLoopbackMultiaddr reports whether m's IP component is loopback
109 +// (`127.0.0.0/8` or `::1`). Returns false if m has no IP component.
110 +func isLoopbackMultiaddr(m ma.Multiaddr) bool {
111 + ip, err := manet.ToIP(m)
112 + if err != nil {
113 + return false
114 + }
115 + return ip.IsLoopback()
116 +}
117 +
118 +// logDeadListenerFinding writes one ERROR line per finding, naming
119 +// the listener, the matching CIDR rule, and where to remove it from.
120 +// Each line stands alone so operators can grep and act on it.
121 +func logDeadListenerFinding(f deadListenerFinding) {
122 + switch f.Source {
123 + case deadListenerSourceAddrFilters:
124 + log.Errorf(
125 + "Addresses.Swarm listener %q matches Swarm.AddrFilters rule %q, "+
126 + "so Kubo rejects every incoming connection to it. Remove %q "+
127 + "from Swarm.AddrFilters to allow connections to this listener.",
128 + f.Listener, f.Rule, f.Rule,
129 + )
130 + case deadListenerSourceNoAnnounce:
131 + log.Errorf(
132 + "Addresses.Swarm listener %q matches Addresses.NoAnnounce rule %q, "+
133 + "so Kubo will not advertise it to other peers. Remove %q from "+
134 + "Addresses.NoAnnounce to advertise this listener.",
135 + f.Listener, f.Rule, f.Rule,
136 + )
137 + }
138 +}
139 +
140 +// MonitorDeadListeners runs findDeadListeners at startup and on every
141 +// EvtLocalAddressesUpdated. Listen addresses change at runtime (NAT
142 +// mapping, new interface, AutoTLS cert), so a one-shot check would
143 +// miss listeners that appear later.
144 +//
145 +// Findings are deduplicated against the previous run: a stable
146 +// misconfiguration is logged once.
147 +//
148 +// If subscribing to the event bus fails, the runtime monitor is
149 +// disabled and only the startup check runs. The check is diagnostic
150 +// and must never abort node startup.
151 +func MonitorDeadListeners(addrFilters []string, noAnnounce []string) func(fx.Lifecycle, host.Host) error {
152 + return func(lc fx.Lifecycle, h host.Host) error {
153 + seen := make(map[deadListenerFinding]struct{})
154 + runCheck := func() {
155 + listenAddrs, err := h.Network().InterfaceListenAddresses()
156 + if err != nil {
157 + log.Warnf("dead-listener check: read InterfaceListenAddresses: %s", err)
158 + return
159 + }
160 + next := make(map[deadListenerFinding]struct{})
161 + for _, f := range findDeadListeners(listenAddrs, addrFilters, noAnnounce) {
162 + next[f] = struct{}{}
163 + if _, ok := seen[f]; ok {
164 + continue
165 + }
166 + logDeadListenerFinding(f)
167 + }
168 + seen = next
169 + }
170 +
171 + // Startup check, always runs even if the runtime monitor below
172 + // cannot be wired up.
173 + runCheck()
174 +
175 + sub, err := h.EventBus().Subscribe(new(event.EvtLocalAddressesUpdated))
176 + if err != nil {
177 + log.Errorf("dead-listener check: subscribe to EvtLocalAddressesUpdated failed (%s); runtime monitor disabled, startup check already ran", err)
178 + return nil
179 + }
180 +
181 + ctx, cancel := context.WithCancel(context.Background())
182 + lc.Append(fx.Hook{
183 + OnStop: func(_ context.Context) error {
184 + cancel()
185 + return nil
186 + },
187 + })
188 +
189 + go func() {
190 + defer sub.Close()
191 + for {
192 + select {
193 + case <-ctx.Done():
194 + return
195 + case _, ok := <-sub.Out():
196 + if !ok {
197 + return
198 + }
199 + runCheck()
200 + }
201 + }
202 + }()
203 + return nil
204 + }
205 +}
206 +
207 func makeAddrsFactory(announce []string, appendAnnounce []string, noAnnounce []string) (p2pbhost.AddrsFactory, error) {
208 var err error // To assign to the slice in the for loop
209 existing := make(map[string]bool) // To avoid duplicates
core/node/libp2p/addrs_test.go
+130
@@ -4,8 +4,138 @@ import (
4 "testing"
5
6 ma "github.com/multiformats/go-multiaddr"
7 + "github.com/stretchr/testify/require"
8 )
9
10 +// mustMultiaddrs parses a list of multiaddr strings or fails the test.
11 +func mustMultiaddrs(t *testing.T, addrs ...string) []ma.Multiaddr {
12 + t.Helper()
13 + out := make([]ma.Multiaddr, 0, len(addrs))
14 + for _, s := range addrs {
15 + m, err := ma.NewMultiaddr(s)
16 + require.NoError(t, err, "parse %q", s)
17 + out = append(out, m)
18 + }
19 + return out
20 +}
21 +
22 +func TestFindDeadListeners(t *testing.T) {
23 + cases := []struct {
24 + name string
25 + listenAddrs []ma.Multiaddr
26 + addrFilters []string
27 + noAnnounce []string
28 + want []deadListenerFinding
29 + }{
30 + {
31 + name: "empty config produces no findings",
32 + listenAddrs: mustMultiaddrs(t, "/ip4/192.168.1.5/tcp/4001"),
33 + },
34 + {
35 + name: "loopback listener with loopback AddrFilters: one finding",
36 + listenAddrs: mustMultiaddrs(t, "/ip4/127.0.0.1/tcp/8081/ws"),
37 + addrFilters: []string{"/ip4/127.0.0.0/ipcidr/8"},
38 + want: []deadListenerFinding{
39 + {Listener: "/ip4/127.0.0.1/tcp/8081/ws", Source: deadListenerSourceAddrFilters, Rule: "/ip4/127.0.0.0/ipcidr/8"},
40 + },
41 + },
42 + {
43 + name: "loopback NoAnnounce match alone is operator-intent: skipped",
44 + listenAddrs: mustMultiaddrs(t, "/ip4/127.0.0.1/tcp/8081/ws"),
45 + noAnnounce: []string{"/ip4/127.0.0.0/ipcidr/8"},
46 + },
47 + {
48 + name: "loopback in both lists: AddrFilters reported, NoAnnounce skipped",
49 + listenAddrs: mustMultiaddrs(t, "/ip4/127.0.0.1/tcp/8081/ws"),
50 + addrFilters: []string{"/ip4/127.0.0.0/ipcidr/8"},
51 + noAnnounce: []string{"/ip4/127.0.0.0/ipcidr/8"},
52 + want: []deadListenerFinding{
53 + {Listener: "/ip4/127.0.0.1/tcp/8081/ws", Source: deadListenerSourceAddrFilters, Rule: "/ip4/127.0.0.0/ipcidr/8"},
54 + },
55 + },
56 + {
57 + name: "non-loopback NoAnnounce match is reported",
58 + listenAddrs: mustMultiaddrs(t, "/ip4/192.168.1.5/tcp/4001"),
59 + noAnnounce: []string{"/ip4/192.168.0.0/ipcidr/16"},
60 + want: []deadListenerFinding{
61 + {Listener: "/ip4/192.168.1.5/tcp/4001", Source: deadListenerSourceNoAnnounce, Rule: "/ip4/192.168.0.0/ipcidr/16"},
62 + },
63 + },
64 + {
65 + name: "IPv6 loopback (resolved from `::`) with `::/3` AddrFilters: flagged",
66 + listenAddrs: mustMultiaddrs(t, "/ip6/::1/tcp/4001"),
67 + addrFilters: []string{"/ip6/::/ipcidr/3"},
68 + want: []deadListenerFinding{
69 + {Listener: "/ip6/::1/tcp/4001", Source: deadListenerSourceAddrFilters, Rule: "/ip6/::/ipcidr/3"},
70 + },
71 + },
72 + {
73 + name: "IPv6 loopback NoAnnounce-only is operator-intent: skipped",
74 + listenAddrs: mustMultiaddrs(t, "/ip6/::1/tcp/4001"),
75 + noAnnounce: []string{"/ip6/::/ipcidr/3"},
76 + },
77 + {
78 + name: "globally-routable IPv6 (resolved from `::`) is not flagged by `::/3`",
79 + listenAddrs: mustMultiaddrs(t, "/ip6/2604:2dc0:200:484::1/tcp/4001"),
80 + addrFilters: []string{"/ip6/::/ipcidr/3"},
81 + },
82 + {
83 + name: "private LAN listener with matching private CIDR: flagged on AddrFilters",
84 + listenAddrs: mustMultiaddrs(t, "/ip4/192.168.1.5/tcp/4001"),
85 + addrFilters: []string{"/ip4/192.168.0.0/ipcidr/16"},
86 + want: []deadListenerFinding{
87 + {Listener: "/ip4/192.168.1.5/tcp/4001", Source: deadListenerSourceAddrFilters, Rule: "/ip4/192.168.0.0/ipcidr/16"},
88 + },
89 + },
90 + {
91 + name: "DNS listener has no IP component: no finding",
92 + listenAddrs: mustMultiaddrs(t, "/dns/example.com/tcp/443/wss"),
93 + addrFilters: []string{"/ip4/127.0.0.0/ipcidr/8"},
94 + },
95 + {
96 + name: "exact-match NoAnnounce entry is skipped (operator-explicit)",
97 + listenAddrs: mustMultiaddrs(t, "/ip4/127.0.0.1/tcp/8081/ws"),
98 + noAnnounce: []string{"/ip4/127.0.0.1/tcp/8081/ws"},
99 + },
100 + {
101 + name: "malformed filter entry: skipped, valid filters still match",
102 + listenAddrs: mustMultiaddrs(t, "/ip4/127.0.0.1/tcp/8081/ws"),
103 + addrFilters: []string{"garbage", "/ip4/127.0.0.0/ipcidr/8"},
104 + want: []deadListenerFinding{
105 + {Listener: "/ip4/127.0.0.1/tcp/8081/ws", Source: deadListenerSourceAddrFilters, Rule: "/ip4/127.0.0.0/ipcidr/8"},
106 + },
107 + },
108 + {
109 + name: "bootstrapper-style mix: only AddrFilters loopback fires",
110 + listenAddrs: mustMultiaddrs(t,
111 + "/ip4/147.135.44.132/tcp/4001",
112 + "/ip4/127.0.0.1/tcp/8081/ws",
113 + "/ip6/2604:2dc0:200:484::1/tcp/4001",
114 + "/ip6/::1/tcp/4001",
115 + ),
116 + addrFilters: []string{
117 + "/ip4/127.0.0.0/ipcidr/8",
118 + "/ip6/::/ipcidr/3",
119 + },
120 + noAnnounce: []string{
121 + "/ip4/127.0.0.0/ipcidr/8",
122 + "/ip6/::/ipcidr/3",
123 + },
124 + want: []deadListenerFinding{
125 + {Listener: "/ip4/127.0.0.1/tcp/8081/ws", Source: deadListenerSourceAddrFilters, Rule: "/ip4/127.0.0.0/ipcidr/8"},
126 + {Listener: "/ip6/::1/tcp/4001", Source: deadListenerSourceAddrFilters, Rule: "/ip6/::/ipcidr/3"},
127 + },
128 + },
129 + }
130 +
131 + for _, tc := range cases {
132 + t.Run(tc.name, func(t *testing.T) {
133 + got := findDeadListeners(tc.listenAddrs, tc.addrFilters, tc.noAnnounce)
134 + require.ElementsMatch(t, tc.want, got)
135 + })
136 + }
137 +}
138 +
139 // makeAddrsFactory must drop empty multiaddrs from the input list.
140 // A zero-component Multiaddr would otherwise reach the host's signed
141 // peer record and propagate to peers as "/" when they decode the wire
docs/changelogs/v0.41.md
+3
@@ -229,6 +229,9 @@ The command is idempotent. See the [`server` profile docs](https://github.com/ip
229 > [!WARNING]
230 > The `server` profile disables local peer discovery ([`Discovery.MDNS`](https://github.com/ipfs/kubo/blob/master/docs/config.md#discoverymdns) off, loopback filtered), so co-located daemons on the same host and peers on the same LAN will no longer find each other automatically. Apply only on public-internet nodes where that is intended.
231
232 +> [!CAUTION]
233 +> If a manually configured libp2p listener (for example `/ip4/127.0.0.1/tcp/.../ws` fronted by a local nginx or Caddy reverse proxy) terminates inbound on `127.0.0.1`, the new loopback entry in [`Swarm.AddrFilters`](https://github.com/ipfs/kubo/blob/master/docs/config.md#swarmaddrfilters) makes the gater RST every inbound from the proxy before the libp2p handshake. Remove `/ip4/127.0.0.0/ipcidr/8` (and `/ip6/::1/ipcidr/128`, `/ip6/::/ipcidr/3` if the proxy uses IPv6 loopback) from `Swarm.AddrFilters` only; keep them in [`Addresses.NoAnnounce`](https://github.com/ipfs/kubo/blob/master/docs/config.md#addressesnoannounce) so the loopback addresses are still stripped from identify and DHT records.
234 +
235 #### 🐹 Go 1.26, Once More with Feeling
236
237 Kubo first shipped with [Go 1.26](https://go.dev/doc/go1.26) in v0.40.0, but [v0.40.1](https://github.com/ipfs/kubo/blob/master/docs/changelogs/v0.40.md#v0401) had to downgrade to Go 1.25 because of a Windows crash in Go's overlapped I/O layer ([#11214](https://github.com/ipfs/kubo/issues/11214)). Go 1.26.2 fixes that regression upstream ([golang/go#78041](https://github.com/golang/go/issues/78041)), so Kubo is back on Go 1.26 across all platforms.
docs/changelogs/v0.42.md
+5
@@ -11,6 +11,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
11 - [Overview](#overview)
12 - [🔦 Highlights](#-highlights)
13 - [🐛 Fixed pin operations hanging under pinned reprovide strategies](#-fixed-pin-operations-hanging-under-pinned-reprovide-strategies)
14 + - [🚨 ERROR log for listeners blocked by `Swarm.AddrFilters` or `Addresses.NoAnnounce`](#-error-log-for-listeners-blocked-by-swarmaddrfilters-or-addressesnoannounce)
15 - [📝 Changelog](#-changelog)
16 - [👨‍👩‍👧‍👦 Contributors](#-contributors)
17
@@ -24,6 +25,10 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
25
26 The pinner now snapshots the index under the read lock and releases it before the reprovider starts, so pin operations are no longer blocked by the reprovide cycle. The default `Provide.Strategy=all` was not affected.
27
28 +#### 🚨 ERROR log for listeners blocked by `Swarm.AddrFilters` or `Addresses.NoAnnounce`
29 +
30 +Kubo now logs an ERROR when an [`Addresses.Swarm`](https://github.com/ipfs/kubo/blob/master/docs/config.md#addressesswarm) listener is covered by a rule in [`Swarm.AddrFilters`](https://github.com/ipfs/kubo/blob/master/docs/config.md#swarmaddrfilters) (Kubo will reject every incoming connection to it) or [`Addresses.NoAnnounce`](https://github.com/ipfs/kubo/blob/master/docs/config.md#addressesnoannounce) (Kubo will not advertise it to other peers). Each line names the listener, the matching rule, and the field to remove it from. This catches silent misconfigurations like a `/ip4/127.0.0.1/tcp/.../ws` listener behind a local reverse proxy that stops working once `/ip4/127.0.0.0/ipcidr/8` lands in `Swarm.AddrFilters` (for example via the [`server` profile](https://github.com/ipfs/kubo/blob/master/docs/config.md#server-profile)). See the [reverse-proxy override row](https://github.com/ipfs/kubo/blob/master/docs/config.md#overriding-specific-entries) for the fix.
31 +
32 ### 📝 Changelog
33
34 ### 👨‍👩‍👧‍👦 Contributors
docs/config.md
+4
@@ -3225,6 +3225,9 @@ so that a range is neither advertised nor dialed.
3225 > [`server` profile](#server-profile) section for the full list and for
3226 > optional entries operators may add manually.
3227
3228 +> [!CAUTION]
3229 +> If an [`Addresses.Swarm`](#addressesswarm) listener (for example a manually configured `/ip4/127.0.0.1/tcp/.../ws` fronted by a local nginx or Caddy reverse proxy) is covered by an entry in this list, Kubo rejects every incoming connection to it, so the proxy cannot reach Kubo. Kubo logs an ERROR at startup naming the offending rule. Remove the rule from `Swarm.AddrFilters` to allow the listener; keep it in [`Addresses.NoAnnounce`](#addressesnoannounce) if you still want to suppress its announcement.
3230 +
3231 Default: `[]`
3232
3233 Type: `array[string]`
@@ -4300,6 +4303,7 @@ Or skip the profile and populate those fields manually.
4303 | Link-local IPv6 peering | `/ip6/fe80::/ipcidr/10` |
4304 | Multiple daemons peering over `127.0.0.1` | `/ip4/127.0.0.0/ipcidr/8` |
4305 | Multiple daemons peering over IPv6 loopback `::1` | `/ip6/::1/ipcidr/128` and `/ip6/::/ipcidr/3` |
4306 +| Local reverse proxy fronting a `/ws` (or other libp2p) listener on `127.0.0.1` | `/ip4/127.0.0.0/ipcidr/8` from `Swarm.AddrFilters` only (keep it in `Addresses.NoAnnounce`); also drop `/ip6/::1/ipcidr/128` and `/ip6/::/ipcidr/3` from `Swarm.AddrFilters` if the proxy uses IPv6 loopback |
4307 | [Yggdrasil] mesh peering (`200::/8`, `300::/8`) | `/ip6/::/ipcidr/3` |
4308 | NAT64 (`64:ff9b::/96`) reachability | `/ip6/::/ipcidr/3` |
4309