Skip to content

Commit c1a7bd1

Browse files
authored
Network hardening continued (#14)
* Add private IP connection gater * Add relay peers as bootstrappable peers * Switch to EnableNAT to make it much more explicit, default is now disabled * Prevent tests from connecting to bootstrap servers * Test and code cleanup * Fix lint
1 parent cd885f8 commit c1a7bd1

File tree

5 files changed

+129
-64
lines changed

5 files changed

+129
-64
lines changed

client.go

Lines changed: 114 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"slices"
1717
"strings"
1818
"sync"
19+
"testing"
1920
"time"
2021

2122
"github.com/libp2p/go-libp2p"
@@ -28,6 +29,7 @@ import (
2829
"github.com/libp2p/go-libp2p/core/peer"
2930
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
3031
drouting "github.com/libp2p/go-libp2p/p2p/discovery/routing"
32+
"github.com/libp2p/go-libp2p/p2p/net/conngater"
3133
"github.com/multiformats/go-multiaddr"
3234
manet "github.com/multiformats/go-multiaddr/net"
3335

@@ -85,11 +87,7 @@ func NewClient(config Config) (Client, error) {
8587
return nil, err
8688
}
8789

88-
// Get bootstrap peers from DHT library for DHT bootstrapping
89-
bootstrapPeers := dht.GetDefaultBootstrapPeerAddrInfos()
90-
91-
// Determine which peers to use as relays
92-
relayPeers := configureRelayPeers(config.RelayPeers, bootstrapPeers, clientLogger)
90+
bootstrapPeers, relayPeers := getBootstrapAndRelayPeers(config, clientLogger)
9391

9492
// Create and setup libp2p host
9593
h, err := createHost(ctx, hostOpts, config, relayPeers, clientLogger, cancel)
@@ -152,6 +150,34 @@ func NewClient(config Config) (Client, error) {
152150
return c, nil
153151
}
154152

153+
func getBootstrapAndRelayPeers(config Config, clientLogger logger) ([]peer.AddrInfo, []peer.AddrInfo) {
154+
// Get bootstrap peers based on environment
155+
// Test mode: empty list for fast, isolated tests
156+
// Production: IPFS default bootstrap peers
157+
var bootstrapPeers []peer.AddrInfo
158+
if testing.Testing() {
159+
bootstrapPeers = []peer.AddrInfo{}
160+
clientLogger.Infof("Test mode detected - using no bootstrap peers (isolated mode)")
161+
} else {
162+
bootstrapPeers = dht.GetDefaultBootstrapPeerAddrInfos()
163+
clientLogger.Infof("Using %d default IPFS bootstrap peers", len(bootstrapPeers))
164+
}
165+
166+
// Parse custom relay peers if provided
167+
customRelayPeers := parseRelayPeersFromConfig(config.RelayPeers, clientLogger)
168+
169+
// Add custom relay peers to bootstrap list for better peer discovery
170+
// This helps the DHT routing table include your known-good relay peers
171+
if len(customRelayPeers) > 0 {
172+
bootstrapPeers = append(bootstrapPeers, customRelayPeers...)
173+
clientLogger.Infof("Added %d custom relay peers to bootstrap peer list", len(customRelayPeers))
174+
}
175+
176+
// Determine which peers to use as relays (relay peers OR bootstrap peers as fallback)
177+
relayPeers := selectRelayPeers(customRelayPeers, bootstrapPeers, clientLogger)
178+
return bootstrapPeers, relayPeers
179+
}
180+
155181
// Helper functions for NewClient
156182

157183
func getLogger(configLogger logger) logger {
@@ -163,9 +189,61 @@ func getLogger(configLogger logger) logger {
163189
return configLogger
164190
}
165191

192+
// createPrivateIPConnectionGater creates a ConnectionGater that blocks private IP ranges.
193+
// Returns a configured BasicConnectionGater that prevents connections to/from:
194+
// - RFC1918 private networks (10.x, 172.16-31.x, 192.168.x)
195+
// - Link-local addresses (169.254.x, fe80::)
196+
// - Loopback addresses (127.x, ::1)
197+
// - Shared address space (100.64.x)
198+
// - IPv6 unique local addresses (fc00::)
199+
func createPrivateIPConnectionGater(log logger, cancel context.CancelFunc) (*conngater.BasicConnectionGater, error) {
200+
ipFilter, err := conngater.NewBasicConnectionGater(nil)
201+
if err != nil {
202+
cancel()
203+
return nil, fmt.Errorf("failed to create connection gater: %w", err)
204+
}
205+
206+
// Standard private IP ranges to block
207+
privateRanges := []string{
208+
"10.0.0.0/8", // RFC1918 private network
209+
"172.16.0.0/12", // RFC1918 private network
210+
"192.168.0.0/16", // RFC1918 private network
211+
"127.0.0.0/8", // Loopback
212+
"169.254.0.0/16", // Link-local
213+
"100.64.0.0/10", // Shared Address Space (RFC6598)
214+
"fc00::/7", // IPv6 Unique Local Addresses
215+
"fe80::/10", // IPv6 Link-Local Addresses
216+
"::1/128", // IPv6 Loopback
217+
}
218+
219+
for _, cidr := range privateRanges {
220+
_, ipnet, err := net.ParseCIDR(cidr)
221+
if err != nil {
222+
cancel()
223+
return nil, fmt.Errorf("failed to parse CIDR %s: %w", cidr, err)
224+
}
225+
if err := ipFilter.BlockSubnet(ipnet); err != nil {
226+
log.Warnf("Failed to block subnet %s: %v", cidr, err)
227+
}
228+
}
229+
230+
return ipFilter, nil
231+
}
232+
166233
func buildHostOptions(config Config, log logger, cancel context.CancelFunc) ([]libp2p.Option, error) {
167234
hostOpts := []libp2p.Option{libp2p.Identity(config.PrivateKey)}
168235

236+
// Add connection gater to block private IPs if AllowPrivateIPs is false (default)
237+
if !config.AllowPrivateIPs {
238+
ipFilter, err := createPrivateIPConnectionGater(log, cancel)
239+
if err != nil {
240+
return nil, err
241+
}
242+
243+
hostOpts = append(hostOpts, libp2p.ConnectionGater(ipFilter))
244+
log.Infof("Private IP connection gater enabled (blocking RFC1918 and local addresses)")
245+
}
246+
169247
// Configure announce addresses if provided (useful for K8s)
170248
if len(config.AnnounceAddrs) > 0 {
171249
announceAddrs := make([]multiaddr.Multiaddr, 0, len(config.AnnounceAddrs))
@@ -195,14 +273,17 @@ func createHost(_ context.Context, hostOpts []libp2p.Option, config Config, rela
195273
),
196274
)
197275

198-
// Only enable NAT traversal features if not disabled
199-
// NAT features can cause data races in tests due to libp2p's NAT manager using non-thread-safe global state
200-
if !config.DisableNAT {
276+
// Enable NAT features only if explicitly enabled
277+
// UPnP/NAT-PMP scans the local gateway which triggers network scanning alerts
278+
if config.EnableNAT {
201279
hostOpts = append(hostOpts,
202280
libp2p.NATPortMap(),
203281
libp2p.EnableNATService(),
204282
libp2p.EnableHolePunching(),
205283
)
284+
log.Infof("UPnP/NAT-PMP enabled (will scan local gateway for port mapping)")
285+
} else {
286+
log.Infof("UPnP/NAT-PMP disabled (production safe default)")
206287
}
207288

208289
hostOpts = append(hostOpts,
@@ -280,33 +361,43 @@ func setupDHT(ctx context.Context, h host.Host, config Config, bootstrapPeers []
280361
return kadDHT, nil
281362
}
282363

283-
func configureRelayPeers(relayPeersConfig []string, bootstrapPeers []peer.AddrInfo, log logger) []peer.AddrInfo {
284-
if len(relayPeersConfig) == 0 {
285-
log.Infof("Using bootstrap peers as relays")
286-
return bootstrapPeers
364+
// parseRelayPeersFromConfig parses relay peer multiaddr strings into AddrInfo
365+
func parseRelayPeersFromConfig(relayPeersConfig []string, log logger) []peer.AddrInfo {
366+
return parsePeerMultiaddrs(relayPeersConfig, "relay", log)
367+
}
368+
369+
// parsePeerMultiaddrs is a shared helper to parse peer multiaddr strings
370+
func parsePeerMultiaddrs(peerConfigs []string, peerType string, log logger) []peer.AddrInfo {
371+
if len(peerConfigs) == 0 {
372+
return nil
287373
}
288374

289-
relayPeers := make([]peer.AddrInfo, 0, len(relayPeersConfig))
290-
for _, relayStr := range relayPeersConfig {
291-
maddr, err := multiaddr.NewMultiaddr(relayStr)
375+
peers := make([]peer.AddrInfo, 0, len(peerConfigs))
376+
for _, peerStr := range peerConfigs {
377+
maddr, err := multiaddr.NewMultiaddr(peerStr)
292378
if err != nil {
293-
log.Errorf("Invalid relay address %s: %v (hint: use /dns4/ for hostnames, /ip4/ for IP addresses)", relayStr, err)
379+
log.Errorf("Invalid %s address %s: %v (hint: use /dns4/ for hostnames, /ip4/ for IP addresses)", peerType, peerStr, err)
294380
continue
295381
}
296382
addrInfo, err := peer.AddrInfoFromP2pAddr(maddr)
297383
if err != nil {
298-
log.Errorf("Invalid relay peer info %s: %v", relayStr, err)
384+
log.Errorf("Invalid %s peer info %s: %v", peerType, peerStr, err)
299385
continue
300386
}
301-
relayPeers = append(relayPeers, *addrInfo)
387+
peers = append(peers, *addrInfo)
302388
}
303389

304-
if len(relayPeers) > 0 {
305-
log.Infof("Using %d custom relay peer(s)", len(relayPeers))
306-
return relayPeers
390+
return peers
391+
}
392+
393+
// selectRelayPeers determines which peers to use as relays
394+
func selectRelayPeers(customRelayPeers []peer.AddrInfo, bootstrapPeers []peer.AddrInfo, log logger) []peer.AddrInfo {
395+
if len(customRelayPeers) > 0 {
396+
log.Infof("Using %d custom relay peer(s)", len(customRelayPeers))
397+
return customRelayPeers
307398
}
308399

309-
log.Warnf("No valid custom relay peers found, falling back to bootstrap peers as relays")
400+
log.Infof("Using bootstrap peers as relays")
310401
return bootstrapPeers
311402
}
312403

@@ -630,15 +721,7 @@ func (c *client) connectToDiscoveredPeer(ctx context.Context, peerInfo peer.Addr
630721
return
631722
}
632723

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-
724+
// ConnectionGater handles private IP filtering, so just try to connect
642725
if err := c.host.Connect(ctx, peerInfo); err != nil {
643726
if c.shouldLogConnectionError(err) {
644727
c.logger.Debugf("Failed to connect to discovered peer %s: %v", peerInfo.ID.String(), err)

client_additional_test.go

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ func TestConfigureRelayPeersWithValidPeers(t *testing.T) {
7171
testRelayPeerMultiaddr,
7272
}
7373

74-
relayPeers := configureRelayPeers(relayPeersConfig, bootstrapPeers, logger)
74+
customRelayPeers := parseRelayPeersFromConfig(relayPeersConfig, logger)
75+
relayPeers := selectRelayPeers(customRelayPeers, bootstrapPeers, logger)
7576

7677
assert.Len(t, relayPeers, 1)
7778
}
@@ -86,7 +87,8 @@ func TestConfigureRelayPeersWithMixedValidity(t *testing.T) {
8687
"invalid-peer",
8788
}
8889

89-
relayPeers := configureRelayPeers(relayPeersConfig, bootstrapPeers, logger)
90+
customRelayPeers := parseRelayPeersFromConfig(relayPeersConfig, logger)
91+
relayPeers := selectRelayPeers(customRelayPeers, bootstrapPeers, logger)
9092

9193
// Should have 1 valid peer (second one is invalid)
9294
assert.Len(t, relayPeers, 1)
@@ -114,25 +116,6 @@ func TestClientCloseTwice(t *testing.T) {
114116
_ = err
115117
}
116118

117-
func TestClientWithBootstrapPeers(t *testing.T) {
118-
privKey, err := GeneratePrivateKey()
119-
require.NoError(t, err)
120-
121-
config := Config{
122-
Name: testPeerName,
123-
PrivateKey: privKey,
124-
BootstrapPeers: []string{
125-
"/ip4/127.0.0.1/tcp/9999/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N",
126-
},
127-
}
128-
129-
cl, err := NewClient(config)
130-
require.NoError(t, err)
131-
132-
err = cl.Close()
133-
require.NoError(t, err)
134-
}
135-
136119
func TestGetPeersWithNoConnections(t *testing.T) {
137120
privKey, err := GeneratePrivateKey()
138121
require.NoError(t, err)

client_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@ func TestConfigureRelayPeersEmpty(t *testing.T) {
416416
{ID: "QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N"},
417417
}
418418

419-
relayPeers := configureRelayPeers([]string{}, bootstrapPeers, logger)
419+
customRelayPeers := parseRelayPeersFromConfig([]string{}, logger)
420+
relayPeers := selectRelayPeers(customRelayPeers, bootstrapPeers, logger)
420421

421422
// Should return bootstrap peers when no custom relays
422423
assert.Equal(t, bootstrapPeers, relayPeers)
@@ -435,7 +436,8 @@ func TestConfigureRelayPeersInvalidAddresses(t *testing.T) {
435436
"not-a-multiaddr",
436437
}
437438

438-
relayPeers := configureRelayPeers(invalidRelays, bootstrapPeers, logger)
439+
customRelayPeers := parseRelayPeersFromConfig(invalidRelays, logger)
440+
relayPeers := selectRelayPeers(customRelayPeers, bootstrapPeers, logger)
439441

440442
// Should fall back to bootstrap peers on invalid addresses
441443
assert.Equal(t, bootstrapPeers, relayPeers)

cmd/example/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func main() {
5555
Port: *port,
5656
PeerCacheFile: "peer_cache.json", // Enable peer persistence
5757
RelayPeers: relayPeers,
58+
DHTMode: "client",
5859
})
5960
if err != nil {
6061
logger.Errorf("Failed to create P2P client: %v", err)

config.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,6 @@ type Config struct {
3636
// so other peers can identify the sender.
3737
Name string
3838

39-
// BootstrapPeers is an optional list of multiaddr strings for initial peers to connect to.
40-
// If not provided, the client will use libp2p's default bootstrap peers.
41-
// Example: []string{"/ip4/192.168.1.100/tcp/4001/p2p/QmPeerID"}
42-
BootstrapPeers []string
43-
4439
// Logger is an optional logger to use for logging. If not provided, the client will use
4540
// DefaultLogger. Set to a custom implementation to integrate with your logging framework.
4641
Logger logger
@@ -98,11 +93,12 @@ type Config struct {
9893
// The cleanup frequency trades off between memory usage (stale records) and CPU usage.
9994
DHTCleanupInterval time.Duration
10095

101-
// DisableNAT disables NAT traversal features (UPnP/NAT-PMP port mapping, NAT service, hole punching).
102-
// Set to true in test environments where NAT traversal is not needed and can cause data races
103-
// due to libp2p's NAT manager using non-thread-safe global state.
104-
// Default: false (NAT features enabled)
105-
DisableNAT bool
96+
// EnableNAT enables UPnP/NAT-PMP automatic port mapping features.
97+
// When true, the node will scan the local gateway (e.g., 10.0.0.1) to configure port forwarding.
98+
// IMPORTANT: This triggers network scanning alerts on shared hosting (Hetzner, AWS, etc.).
99+
// Only enable for local development behind a home router/NAT.
100+
// Default: false (NAT features disabled for production safety)
101+
EnableNAT bool
106102

107103
// EnableMDNS enables multicast DNS peer discovery on the local network.
108104
// When true, the node broadcasts mDNS queries to discover peers on the same LAN.

0 commit comments

Comments
 (0)