Skip to content

Commit cd885f8

Browse files
authored
Disable MDNS by default and filter out private IPs (#13)
* Disable MDNS by default and filter out private IPs, to prevent possible netscanning actions from libp2p * Simplify privateIP detection * Simplify the private address tests
1 parent ad7e2fd commit cd885f8

File tree

3 files changed

+265
-5
lines changed

3 files changed

+265
-5
lines changed

client.go

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"errors"
1212
"fmt"
1313
"io"
14+
"net"
1415
"os"
1516
"slices"
1617
"strings"
@@ -28,6 +29,7 @@ import (
2829
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
2930
drouting "github.com/libp2p/go-libp2p/p2p/discovery/routing"
3031
"github.com/multiformats/go-multiaddr"
32+
manet "github.com/multiformats/go-multiaddr/net"
3133

3234
ds "github.com/ipfs/go-datastore"
3335
dssync "github.com/ipfs/go-datastore/sync"
@@ -114,12 +116,17 @@ func NewClient(config Config) (Client, error) {
114116
return nil, fmt.Errorf("failed to create pubsub: %w", err)
115117
}
116118

117-
// Set up mDNS discovery
118-
mdnsService := mdns.NewMdnsService(h, "", &discoveryNotifee{h: h, ctx: ctx, logger: clientLogger})
119-
if err := mdnsService.Start(); err != nil {
120-
clientLogger.Errorf("mDNS failed to start: %v", err)
119+
// Set up mDNS discovery (only if explicitly enabled)
120+
var mdnsService mdns.Service
121+
if config.EnableMDNS {
122+
mdnsService = mdns.NewMdnsService(h, "", &discoveryNotifee{h: h, ctx: ctx, logger: clientLogger})
123+
if err := mdnsService.Start(); err != nil {
124+
clientLogger.Errorf("mDNS failed to start: %v", err)
125+
} else {
126+
clientLogger.Infof("mDNS discovery started")
127+
}
121128
} else {
122-
clientLogger.Infof("mDNS discovery started")
129+
clientLogger.Infof("mDNS discovery disabled (production safe default)")
123130
}
124131

125132
c := &client{
@@ -623,6 +630,15 @@ func (c *client) connectToDiscoveredPeer(ctx context.Context, peerInfo peer.Addr
623630
return
624631
}
625632

633+
// Filter private IPs unless explicitly allowed (default: filter for production safety)
634+
if !c.config.AllowPrivateIPs {
635+
peerInfo.Addrs = filterPrivateAddrs(peerInfo.Addrs)
636+
if len(peerInfo.Addrs) == 0 {
637+
// All addresses were private, skip this peer
638+
return
639+
}
640+
}
641+
626642
if err := c.host.Connect(ctx, peerInfo); err != nil {
627643
if c.shouldLogConnectionError(err) {
628644
c.logger.Debugf("Failed to connect to discovered peer %s: %v", peerInfo.ID.String(), err)
@@ -913,3 +929,48 @@ func PrivateKeyFromHex(keyHex string) (crypto.PrivKey, error) {
913929

914930
return priv, nil
915931
}
932+
933+
// filterPrivateAddrs filters out private/local IP addresses from a list of multiaddrs.
934+
// Returns only public routable addresses suitable for cloud/shared hosting environments.
935+
func filterPrivateAddrs(addrs []multiaddr.Multiaddr) []multiaddr.Multiaddr {
936+
filtered := make([]multiaddr.Multiaddr, 0, len(addrs))
937+
938+
for _, addr := range addrs {
939+
// Convert multiaddr to net.Addr to check if it's private
940+
netAddr, err := manet.ToNetAddr(addr)
941+
if err != nil {
942+
// If we can't parse it, skip it
943+
continue
944+
}
945+
946+
// Extract IP address
947+
var ip net.IP
948+
switch v := netAddr.(type) {
949+
case *net.TCPAddr:
950+
ip = v.IP
951+
case *net.UDPAddr:
952+
ip = v.IP
953+
default:
954+
// Unknown address type, skip it
955+
continue
956+
}
957+
958+
// Filter out private/local addresses
959+
if isPrivateIP(ip) {
960+
continue
961+
}
962+
963+
filtered = append(filtered, addr)
964+
}
965+
966+
return filtered
967+
}
968+
969+
// isPrivateIP checks if an IP address is private or local using Go's standard library.
970+
// Returns true for:
971+
// - RFC1918 private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) via IP.IsPrivate()
972+
// - Link-local, loopback, and multicast addresses via built-in methods
973+
// - IPv6 unique local addresses (fc00::/7) via IP.IsPrivate()
974+
func isPrivateIP(ip net.IP) bool {
975+
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
976+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package p2p
2+
3+
import (
4+
"net"
5+
"testing"
6+
7+
"github.com/multiformats/go-multiaddr"
8+
)
9+
10+
func TestFilterPrivateAddrs(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input []string
14+
expected []string
15+
}{
16+
{
17+
name: "mixed_private_and_public_ipv4",
18+
input: []string{
19+
"/ip4/10.0.0.1/tcp/4001", // RFC1918
20+
"/ip4/8.8.8.8/tcp/4001", // public
21+
"/ip4/172.16.0.1/tcp/4001", // RFC1918
22+
"/ip4/1.1.1.1/tcp/4001", // public
23+
"/ip4/192.168.1.1/tcp/4001", // RFC1918
24+
"/ip4/127.0.0.1/tcp/4001", // loopback
25+
"/ip4/169.254.1.1/tcp/4001", // link-local
26+
},
27+
expected: []string{
28+
"/ip4/8.8.8.8/tcp/4001",
29+
"/ip4/1.1.1.1/tcp/4001",
30+
},
31+
},
32+
{
33+
name: "mixed_private_and_public_ipv6",
34+
input: []string{
35+
"/ip6/fc00::1/tcp/4001", // unique local
36+
"/ip6/2001:4860:4860::8888/tcp/4001", // public
37+
"/ip6/fe80::1/tcp/4001", // link-local
38+
},
39+
expected: []string{
40+
"/ip6/2001:4860:4860::8888/tcp/4001",
41+
},
42+
},
43+
{
44+
name: "rfc1918_172_range_boundaries",
45+
input: []string{
46+
"/ip4/172.15.0.1/tcp/4001", // not private (below range)
47+
"/ip4/172.16.0.1/tcp/4001", // private (start of range)
48+
"/ip4/172.31.255.255/tcp/4001", // private (end of range)
49+
"/ip4/172.32.0.1/tcp/4001", // not private (above range)
50+
},
51+
expected: []string{
52+
"/ip4/172.15.0.1/tcp/4001",
53+
"/ip4/172.32.0.1/tcp/4001",
54+
},
55+
},
56+
{
57+
name: "edge_cases",
58+
input: []string{
59+
"invalid-addr", // invalid address
60+
"/ip4/8.8.8.8/tcp/4001", // valid public
61+
"/ip4/10.0.0.1/tcp/4001", // valid private
62+
},
63+
expected: []string{
64+
"/ip4/8.8.8.8/tcp/4001",
65+
},
66+
},
67+
{
68+
name: "empty_input",
69+
input: []string{},
70+
expected: []string{},
71+
},
72+
{
73+
name: "all_private",
74+
input: []string{
75+
"/ip4/10.0.0.1/tcp/4001",
76+
"/ip4/192.168.1.1/tcp/4001",
77+
},
78+
expected: []string{},
79+
},
80+
{
81+
name: "all_public",
82+
input: []string{
83+
"/ip4/8.8.8.8/tcp/4001",
84+
"/ip6/2001:4860:4860::8888/tcp/4001",
85+
},
86+
expected: []string{
87+
"/ip4/8.8.8.8/tcp/4001",
88+
"/ip6/2001:4860:4860::8888/tcp/4001",
89+
},
90+
},
91+
}
92+
93+
for _, tt := range tests {
94+
t.Run(tt.name, func(t *testing.T) {
95+
result := filterPrivateAddrs(testParseMultiaddrs(tt.input))
96+
assertAddrsEqual(t, tt.expected, result)
97+
})
98+
}
99+
}
100+
101+
// testParseMultiaddrs converts string addresses to multiaddr.Multiaddr, skipping invalid ones
102+
func testParseMultiaddrs(addrs []string) []multiaddr.Multiaddr {
103+
result := make([]multiaddr.Multiaddr, 0, len(addrs))
104+
for _, addrStr := range addrs {
105+
addr, err := multiaddr.NewMultiaddr(addrStr)
106+
if err == nil {
107+
result = append(result, addr)
108+
}
109+
}
110+
return result
111+
}
112+
113+
// assertAddrsEqual compares expected string addresses with actual multiaddrs
114+
func assertAddrsEqual(t *testing.T, expected []string, actual []multiaddr.Multiaddr) {
115+
t.Helper()
116+
117+
actualStrs := make([]string, len(actual))
118+
for i, addr := range actual {
119+
actualStrs[i] = addr.String()
120+
}
121+
122+
if len(actualStrs) != len(expected) {
123+
t.Errorf("Expected %d addresses, got %d.\nExpected: %v\nGot: %v",
124+
len(expected), len(actualStrs), expected, actualStrs)
125+
return
126+
}
127+
128+
for i, expectedAddr := range expected {
129+
if actualStrs[i] != expectedAddr {
130+
t.Errorf("Address %d: expected %s, got %s", i, expectedAddr, actualStrs[i])
131+
}
132+
}
133+
}
134+
135+
func TestIsPrivateIP(t *testing.T) {
136+
privateIPs := []string{
137+
// RFC1918
138+
"10.0.0.1", "10.255.255.255",
139+
"172.16.0.1", "172.31.255.255",
140+
"192.168.1.1", "192.168.255.255",
141+
// Link-local
142+
"169.254.1.1",
143+
// Loopback
144+
"127.0.0.1", "127.255.255.255",
145+
// IPv6 unique local and link-local
146+
"fc00::1", "fd00::1", "fe80::1", "::1",
147+
}
148+
149+
publicIPs := []string{
150+
// Public IPv4
151+
"8.8.8.8", "1.1.1.1", "208.67.222.222",
152+
// RFC1918 172.x boundaries (not in 16-31 range)
153+
"172.15.0.1", "172.32.0.1",
154+
// Public IPv6
155+
"2001:4860:4860::8888", "2606:4700:4700::1111",
156+
}
157+
158+
for _, ipStr := range privateIPs {
159+
t.Run(ipStr, func(t *testing.T) {
160+
ip := net.ParseIP(ipStr)
161+
if ip == nil {
162+
t.Fatalf("Failed to parse IP: %s", ipStr)
163+
}
164+
if !isPrivateIP(ip) {
165+
t.Errorf("isPrivateIP(%s) = false, want true", ipStr)
166+
}
167+
})
168+
}
169+
170+
for _, ipStr := range publicIPs {
171+
t.Run(ipStr, func(t *testing.T) {
172+
ip := net.ParseIP(ipStr)
173+
if ip == nil {
174+
t.Fatalf("Failed to parse IP: %s", ipStr)
175+
}
176+
if isPrivateIP(ip) {
177+
t.Errorf("isPrivateIP(%s) = true, want false", ipStr)
178+
}
179+
})
180+
}
181+
}

config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,22 @@ type Config struct {
103103
// due to libp2p's NAT manager using non-thread-safe global state.
104104
// Default: false (NAT features enabled)
105105
DisableNAT bool
106+
107+
// EnableMDNS enables multicast DNS peer discovery on the local network.
108+
// When true, the node broadcasts mDNS queries to discover peers on the same LAN.
109+
// IMPORTANT: Only enable on isolated local networks with proper VLANs. On shared hosting
110+
// (e.g., Hetzner, AWS) without VLANs, mDNS broadcasts appear as network scanning and may
111+
// result in abuse reports.
112+
// Default: false (mDNS disabled for production safety)
113+
// Set to true only for local development networks with proper isolation
114+
EnableMDNS bool
115+
116+
// AllowPrivateIPs allows connections to private/local IP addresses during peer discovery.
117+
// When true, the node will attempt to connect to RFC1918 private networks (10.0.0.0/8,
118+
// 172.16.0.0/12, 192.168.0.0/16), link-local addresses (169.254.0.0/16), and localhost.
119+
// IMPORTANT: Only enable on private networks. On shared hosting, this may trigger network
120+
// scanning alerts.
121+
// Default: false (private IPs filtered for production safety)
122+
// Set to true only for local development or private network deployments
123+
AllowPrivateIPs bool
106124
}

0 commit comments

Comments
 (0)