Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.history/
.idea/
p2p_poc
peer_cache.json
164 changes: 161 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"sync"
"time"
Expand All @@ -21,6 +23,7 @@ import (
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
drouting "github.com/libp2p/go-libp2p/p2p/discovery/routing"
"github.com/libp2p/go-libp2p/p2p/net/conngater"
"github.com/multiformats/go-multiaddr"
)

Expand Down Expand Up @@ -70,8 +73,8 @@ func NewClient(config Config) (P2PClient, error) {
hostOpts = append(hostOpts, libp2p.Identity(config.PrivateKey))

// Configure announce addresses if provided (useful for K8s)
var announceAddrs []multiaddr.Multiaddr
if len(config.AnnounceAddrs) > 0 {
Comment on lines +74 to 75
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The announceAddrs variable is declared but the existing address factory logic using config.AnnounceAddrs is removed without preserving its functionality. This will break the original announce address feature when both custom announce addresses and the new address factory are used together.

Copilot uses AI. Check for mistakes.
var announceAddrs []multiaddr.Multiaddr
for _, addrStr := range config.AnnounceAddrs {
maddr, err := multiaddr.NewMultiaddr(addrStr)
if err != nil {
Expand All @@ -87,15 +90,88 @@ func NewClient(config Config) (P2PClient, error) {
logger.Infof("Using custom announce addresses: %v", config.AnnounceAddrs)
}

// define address factory to remove all private IPs from being broadcasted
addressFactory := func(addrs []multiaddr.Multiaddr) []multiaddr.Multiaddr {
var publicAddrs []multiaddr.Multiaddr
for _, addr := range addrs {
// if IP is not private, add it to the list
logger.Infof("address is private? %v", isPrivateIP(config, addr))
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple debug log statements in address filtering logic that will be called frequently during network operations. Consider using Debug level logging or removing verbose logging to avoid performance impact in production.

Suggested change
logger.Infof("address is private? %v", isPrivateIP(config, addr))
logger.Debugf("address is private? %v", isPrivateIP(config, addr))

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This debug log statement should be at debug level rather than info level to avoid excessive logging in production.

Suggested change
logger.Infof("address is private? %v", isPrivateIP(config, addr))
logger.Debugf("address is private? %v", isPrivateIP(config, addr))

Copilot uses AI. Check for mistakes.
if !isPrivateIP(config, addr) || config.AllowPrivateIPs {
publicAddrs = append(publicAddrs, addr)
}
}
// If a user specified a broadcast IP append it here
if len(announceAddrs) > 0 {
// here we're appending the external facing multiaddr we created above to the addressFactory so it will be broadcast out when I connect to a bootstrap node.
publicAddrs = append(publicAddrs, announceAddrs...)
}

// If we still don't have any advertisable addresses then attempt to grab it from `https://ifconfig.me/ip`
if len(publicAddrs) == 0 {
// If no public addresses are set, let's attempt to grab it publicly
// Ignore errors because we don't care if we can't find it
ifconfig, err := GetPublicIP(context.Background())
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using context.Background() instead of the existing context (ctx) breaks the cancellation chain. This HTTP request won't be cancelled when the client shuts down, potentially causing resource leaks.

Suggested change
ifconfig, err := GetPublicIP(context.Background())
ifconfig, err := GetPublicIP(ctx)

Copilot uses AI. Check for mistakes.
if err != nil {
logger.Infof("Failed to get public IP address: %v", err)
}
if len(ifconfig) > 0 {
logger.Infof("Public IP address: %v", ifconfig)
addr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", ifconfig, config.Port))
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format specifier should be %d instead of %s for config.Port since it's an integer type.

Suggested change
addr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", ifconfig, config.Port))
addr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", ifconfig, config.Port))

Copilot uses AI. Check for mistakes.
if err != nil {
logger.Infof("Failed to create multiaddr from public IP: %v", err)
}
if addr != nil {
publicAddrs = append(publicAddrs, addr)
}
}
}
if len(publicAddrs) > 0 {
logger.Infof("Using advertising addresses: %v", publicAddrs[0])
}
return publicAddrs
}

// Create an IP filter to optionally block private network ranges from being dialed
ipFilter, err := conngater.NewBasicConnectionGater(nil)
if err != nil {
return nil, err
}

// By default, filter private IPs
if !config.AllowPrivateIPs {
// Add private IP blocks to be filtered out
for _, cidr := range []string{
"10.0.0.0/8", // Private network 10.0.0.0 to 10.255.255.255
"172.16.0.0/12", // Private network 172.16.0.0 to 172.31.255.255
"192.168.0.0/16", // Private network 192.168.0.0 to 192.168.255.255
"127.0.0.0/16", // Local network
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect CIDR notation for localhost. The loopback network should be '127.0.0.0/8', not '127.0.0.0/16'. The /16 mask would only cover 127.0.x.x instead of the full 127.x.x.x range.

Suggested change
"127.0.0.0/16", // Local network
"127.0.0.0/8", // Local network

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CIDR mask for localhost should be /8 not /16. The standard localhost range is 127.0.0.0/8 which covers 127.0.0.0 to 127.255.255.255.

Suggested change
"127.0.0.0/16", // Local network
"127.0.0.0/8", // Local network

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CIDR mask for localhost should be /8 not /16. The correct localhost range is 127.0.0.0/8.

Suggested change
"127.0.0.0/16", // Local network
"127.0.0.0/8", // Local network

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CIDR 127.0.0.0/16 is incorrect for localhost addresses. It should be 127.0.0.0/8 to properly cover the entire 127.x.x.x range used for loopback addresses.

Suggested change
"127.0.0.0/16", // Local network
"127.0.0.0/8", // Local network (loopback)

Copilot uses AI. Check for mistakes.
"100.64.0.0/10", // Shared Address Space
"169.254.0.0/16", // Link-local addresses
} {
var ipnet *net.IPNet
var err error
_, ipnet, err = net.ParseCIDR(cidr)
if err != nil {
return nil, err
}
err = ipFilter.BlockSubnet(ipnet)
if err != nil {
continue
}
}
}

// Create libp2p host
hostOpts = append(hostOpts,
libp2p.ListenAddrStrings(
"/ip4/0.0.0.0/tcp/0",
"/ip6/::/tcp/0",
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", config.Port), // Listen on all interfaces
fmt.Sprintf("/ip6/::/tcp/%d", config.Port),
),
libp2p.EnableNATService(),
libp2p.EnableHolePunching(),
libp2p.EnableRelay(),
libp2p.AddrsFactory(addressFactory),
libp2p.ConnectionGater(ipFilter),
)

h, err := libp2p.New(hostOpts...)
Expand Down Expand Up @@ -634,3 +710,85 @@ func PrivateKeyFromHex(keyHex string) (crypto.PrivKey, error) {

return priv, nil
}

// Function to check if an IP address is private
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function comment is incomplete. It should document the return value, parameters, and provide examples of what constitutes a private IP address.

Suggested change
// Function to check if an IP address is private
// isPrivateIP checks if the given multiaddr represents a private IP address.
//
// Parameters:
// addr: multiaddr.Multiaddr - The multiaddr to check. It should contain an IPv4 or IPv6 address.
//
// Returns:
// bool - Returns true if the IP address is private (e.g., RFC1918 for IPv4, unique local or link-local for IPv6, or loopback addresses), false otherwise.
//
// Examples of private IP addresses:
// IPv4: "10.0.0.1", "172.16.5.4", "192.168.1.100", "127.0.0.1"
// IPv6: "::1" (loopback), "fc00::1" (unique local), "fe80::1" (link-local)

Copilot uses AI. Check for mistakes.
func isPrivateIP(config Config, addr multiaddr.Multiaddr) bool {
config.Logger.Infof("Multiaddr: %s", addr.String())
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple debug log statements in address filtering logic that will be called frequently during network operations. Consider using Debug level logging or removing verbose logging to avoid performance impact in production.

Copilot uses AI. Check for mistakes.
ipStr, err := extractIPFromMultiaddr(addr)
if err != nil {
return false
}
config.Logger.Infof("Extracted IP: %s", ipStr)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These debug log statements should be at debug level rather than info level, or removed entirely for production code as they will create excessive logging.

Copilot uses AI. Check for mistakes.
// Check for IPv6 loopback
if ipStr == "::1" {
return true
}

ip := net.ParseIP(ipStr)
if ip == nil || ip.To4() == nil {
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition ip.To4() == nil will incorrectly return false for all IPv6 addresses, even though the function handles IPv6 private ranges. This should check if the IP is neither IPv4 nor IPv6, or handle IPv6 addresses properly.

Suggested change
if ip == nil || ip.To4() == nil {
if ip == nil {

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic incorrectly rejects IPv6 addresses. The ip.To4() == nil check means IPv6 addresses will always return false, but IPv6 private ranges are defined below. Remove the ip.To4() == nil condition.

Suggested change
if ip == nil || ip.To4() == nil {
if ip == nil {

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IPv6 addresses will be incorrectly classified as non-private. The condition ip.To4() == nil returns false for all IPv6 addresses, causing the function to return false even for private IPv6 ranges that are defined later in the function.

Suggested change
if ip == nil || ip.To4() == nil {
if ip == nil {

Copilot uses AI. Check for mistakes.
return false
}
Comment on lines +670 to +672
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function only handles IPv4 addresses due to the ip.To4() check, but the extractIPFromMultiaddr function only extracts IPv4 addresses. IPv6 private addresses (like link-local fe80::/10) will not be detected as private. Consider adding IPv6 support or documenting this limitation.

Copilot uses AI. Check for mistakes.

config.Logger.Infof("Parsed IP: %s", ip.String())
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple debug log statements in address filtering logic that will be called frequently during network operations. Consider using Debug level logging or removing verbose logging to avoid performance impact in production.

Copilot uses AI. Check for mistakes.

// Define private IPv4 and IPv6 ranges
privateRanges := []*net.IPNet{
// IPv4
{IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(8, 32)},
{IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(12, 32)},
{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)},
{IP: net.ParseIP("127.0.0.0"), Mask: net.CIDRMask(8, 32)},
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The private IP ranges are duplicated between the connection gater logic (lines 139-146) and the isPrivateIP function (lines 728-737). This duplication could lead to inconsistencies if one is updated without the other.

Copilot uses AI. Check for mistakes.
// IPv6
{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)}, // Unique local address
{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)}, // Link-local unicast
}

// Check if the IP falls into any of the private ranges or is loopback (::1)
for _, r := range privateRanges {
if r.Contains(ip) {
config.Logger.Infof("Found private IP: %s", ip)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple debug log statements in address filtering logic that will be called frequently during network operations. Consider using Debug level logging or removing verbose logging to avoid performance impact in production.

Copilot uses AI. Check for mistakes.
return true
}
}

return false
}

// Function to extract IP information from a Multiaddr (supports IPv4 and IPv6)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function comment is incomplete. It should document the return values, error conditions, and provide examples of expected input/output formats.

Suggested change
// Function to extract IP information from a Multiaddr (supports IPv4 and IPv6)
// extractIPFromMultiaddr extracts the IP address (IPv4 or IPv6) from a multiaddr.Multiaddr.
//
// Returns:
// - string: The extracted IP address as a string if found.
// - error: An error if the IP address cannot be extracted or the multiaddr does not contain an IP4/IP6 protocol.
//
// Error conditions:
// - If the multiaddr does not contain an IP4 or IP6 protocol, an error is returned.
// - If the protocol value cannot be extracted, an error is returned.
//
// Example:
// addr, _ := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/4001")
// ip, err := extractIPFromMultiaddr(addr)
// // ip == "127.0.0.1", err == nil
//
// addr, _ := multiaddr.NewMultiaddr("/ip6/::1/tcp/4001")
// ip, err := extractIPFromMultiaddr(addr)
// // ip == "::1", err == nil
//
// addr, _ := multiaddr.NewMultiaddr("/tcp/4001")
// ip, err := extractIPFromMultiaddr(addr)
// // ip == "", err != nil

Copilot uses AI. Check for mistakes.
func extractIPFromMultiaddr(addr multiaddr.Multiaddr) (string, error) {
ip, err := addr.ValueForProtocol(multiaddr.P_IP4)
if err == nil && ip != "" {
return ip, nil
}
return addr.ValueForProtocol(multiaddr.P_IP6)
}

// GetPublicIP fetches the public IP address from ifconfig.me
func GetPublicIP(ctx context.Context) (string, error) {
transport := &http.Transport{
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
// Force the use of IPv4 by specifying 'tcp4' as the network
return (&net.Dialer{}).DialContext(ctx, "tcp4", addr)
},
TLSHandshakeTimeout: 10 * time.Second,
}
client := &http.Client{
Transport: transport,
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://ifconfig.me/ip", nil)
if err != nil {
return "", err
}

resp, err := client.Do(req)
if err != nil {
return "", err
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}

return string(body), resp.Body.Close()
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response body should be closed in a defer statement after checking for errors, not in the return statement. The current approach may not close the body if there's an error reading it.

Suggested change
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), resp.Body.Close()
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response body should be closed before returning, not as part of the return statement. If resp.Body.Close() returns an error, it will be ignored. The body should be closed in a defer statement or before the return.

Suggested change
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), resp.Body.Close()
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response body should be closed before returning, not as part of the return statement. If resp.Body.Close() returns an error, it will be ignored. The body should be closed in a defer statement or the close error should be handled properly.

Suggested change
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), resp.Body.Close()
defer func() {
if cerr := resp.Body.Close(); cerr != nil {
log.Printf("error closing response body: %v", cerr)
}
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return statement combines the response body with the Close() error, which will always return an error type as the second value instead of a string. The body should be closed in a defer statement and only the string should be returned.

Suggested change
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), resp.Body.Close()
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning the result of resp.Body.Close() as the error. This should be handled separately - close the body and return nil as the error, or store the close error in a variable if needed.

Suggested change
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), resp.Body.Close()
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil

Copilot uses AI. Check for mistakes.
}
6 changes: 6 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ type Config struct {
// Example: []string{"/ip4/203.0.113.1/tcp/4001"}
// If not provided, libp2p will automatically detect and announce local addresses.
AnnounceAddrs []string

// AllowPrivateIPs indicates whether to allow connections to peers with private IP addresses.
AllowPrivateIPs bool

// Port is the network port to listen on for incoming connections. If zero, a random port will be chosen.
Port int
}
4 changes: 2 additions & 2 deletions example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ module github.com/ordishs/p2p_poc/example
go 1.25.1

require (
github.com/ordishs/gocore v1.0.81
github.com/bsv-blockchain/go-p2p-message-bus v0.0.0
github.com/libp2p/go-libp2p v0.43.0
github.com/ordishs/gocore v1.0.81
)

require (
Expand Down Expand Up @@ -39,7 +40,6 @@ require (
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.3.0 // indirect
github.com/libp2p/go-libp2p v0.43.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.34.0 // indirect
github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect
Expand Down
13 changes: 8 additions & 5 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import (
"syscall"
"time"

p2p "github.com/bsv-blockchain/go-p2p-message-bus"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ordishs/gocore"
p2p "github.com/bsv-blockchain/go-p2p-message-bus"
)

func main() {
name := flag.String("name", "", "Your node name")
privateKey := flag.String("key", "", "Private key hex (will generate if not provided)")
topics := flag.String("topics", "broadcast_p2p_poc", "Comma-separated list of topics to subscribe to")
port := flag.Int("port", 0, "port to listen on (0 for random)")
noBroadcast := flag.Bool("no-broadcast", false, "Disable message broadcasting")

flag.Parse()
Expand Down Expand Up @@ -62,10 +63,12 @@ func main() {

// Create P2P client
client, err := p2p.NewClient(p2p.Config{
Name: *name,
Logger: logger,
PrivateKey: privKey,
PeerCacheFile: "peer_cache.json", // Enable peer persistence
Name: *name,
Logger: logger,
PrivateKey: privKey,
Port: *port,
AllowPrivateIPs: false,
PeerCacheFile: "peer_cache.json", // Enable peer persistence
})
if err != nil {
logger.Fatalf("Failed to create P2P client: %v", err)
Expand Down
Loading