Files
brianmcgee 37bfe97da0 net: add ListenMultiaddrs to InterfaceResolver
Converts interface names + port into libp2p multiaddrs for listening.
When no interfaces are specified, returns wildcard addresses.
2026-02-18 15:09:34 +00:00

217 lines
5.9 KiB
Go

// Package net provides networking utilities such as functions for reading interface ip addresses.
package net
import (
"bytes"
"fmt"
"net"
"slices"
"strconv"
"github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
// InterfaceResolver resolves network interface names to IP addresses.
// This abstraction allows for testing with mock implementations.
type InterfaceResolver interface {
// InterfaceAddresses retrieves all IP network addresses assigned to a specified network interface by name.
// It returns a sorted list of *net.IPNet objects, prioritising IPv6 addresses over IPv4.
// If the interface is down, non-existent, or an error occurs, it returns an error.
InterfaceAddresses(name string) ([]*net.IPNet, error)
// IsReachable returns true if the specified IP address is assigned to any of the specified network interfaces.
IsReachable(addr multiaddr.Multiaddr, interfaces []string) (bool, error)
// ListenMultiaddrs returns multiaddrs suitable for libp2p listening, built from the addresses on the named
// interfaces combined with the given port.
// Excludes multicast, link-local multicast, and link-local unicast addresses.
// When interfaces is empty, returns wildcard addresses for all interfaces.
ListenMultiaddrs(interfaces []string, port uint16) ([]multiaddr.Multiaddr, error)
}
// DefaultResolver uses the system's network interfaces.
type DefaultResolver struct{}
func (DefaultResolver) InterfaceAddresses(name string) ([]*net.IPNet, error) {
iface, err := net.InterfaceByName(name)
if err != nil {
return nil, fmt.Errorf("failed to get network interface %q: %w", name, err)
}
// Skip interfaces that are down
if iface.Flags&net.FlagUp == 0 {
return nil, fmt.Errorf("network interface %q is down", name)
}
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("failed to get addresses for network interface %q: %w", name, err)
}
result := make([]*net.IPNet, 0, len(addrs))
// iterate over the addresses and find all the valid IP v6 addresses
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
return nil, fmt.Errorf("expected a *net.IPNet, got %T", addr)
}
result = append(result, ipNet)
}
// sort by v6 first
slices.SortFunc(result, func(a, b *net.IPNet) int {
aV6 := a.IP.To16() != nil
bV6 := b.IP.To16() != nil
if aV6 && !bV6 {
return -1
} else if !aV6 && bV6 {
return 1
}
// they are either both v4 or both v6
return bytes.Compare(a.IP, b.IP)
})
return result, nil
}
func (DefaultResolver) IsReachable(addr multiaddr.Multiaddr, interfaces []string) (bool, error) {
// convert addr to IP
netAddr, err := manet.ToNetAddr(addr)
if err != nil {
return false, fmt.Errorf("failed to convert multiaddr to net.Addr: %w", err)
}
host, _, err := net.SplitHostPort(netAddr.String())
if err != nil {
return false, fmt.Errorf("failed to split host and port from multiaddr: %w", err)
}
ip := net.ParseIP(host)
if ip == nil {
return false, fmt.Errorf("failed to parse IP address from multiaddr: %w", err)
}
// create a set of interface names for quick lookup
ifaceSet := map[string]struct{}{}
for _, name := range interfaces {
ifaceSet[name] = struct{}{}
}
// get all network interfaces
ifaces, err := net.Interfaces()
if err != nil {
return false, fmt.Errorf("failed to get network interfaces: %w", err)
}
for _, iface := range ifaces {
// if a list of interfaces was provided, check this interface is one of them
if len(ifaceSet) > 0 {
if _, ok := ifaceSet[iface.Name]; !ok {
continue
}
}
// skip interfaces that are down
if iface.Flags&net.FlagUp == 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
// Addrs() returns *net.IPNet or *net.IPAddr
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ipNet.Contains(ip) {
return true, nil
}
}
}
return false, nil
}
func (d DefaultResolver) ListenMultiaddrs(interfaces []string, port uint16) ([]multiaddr.Multiaddr, error) {
// if no interfaces are specified, return all interfaces (minus link local and multicast)
if len(interfaces) == 0 {
return wildcardMultiaddrs(port)
}
var result []multiaddr.Multiaddr
for _, name := range interfaces {
addrs, err := d.InterfaceAddresses(name)
if err != nil {
return nil, fmt.Errorf("failed to get addresses for interface %q: %w", name, err)
}
for _, addr := range addrs {
// skip multicast and link-local addresses
if addr.IP.IsMulticast() || addr.IP.IsLinkLocalMulticast() || addr.IP.IsLinkLocalUnicast() {
continue
}
ma, err := ipToMultiaddr(addr.IP, port)
if err != nil {
return nil, err
}
result = append(result, ma)
}
}
if len(result) == 0 {
return nil, fmt.Errorf("no usable addresses found on interfaces %v", interfaces)
}
return result, nil
}
func wildcardMultiaddrs(port uint16) ([]multiaddr.Multiaddr, error) {
ip4, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port))
if err != nil {
return nil, fmt.Errorf("failed to create wildcard IPv4 multiaddr: %w", err)
}
ip6, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip6/::/tcp/%d", port))
if err != nil {
return nil, fmt.Errorf("failed to create wildcard IPv6 multiaddr: %w", err)
}
return []multiaddr.Multiaddr{ip4, ip6}, nil
}
func ipToMultiaddr(ip net.IP, port uint16) (multiaddr.Multiaddr, error) {
var proto string
if ip.To4() != nil {
proto = "ip4"
} else {
proto = "ip6"
}
ma, err := multiaddr.NewMultiaddr(fmt.Sprintf("/%s/%s/tcp/%d", proto, ip.String(), port))
if err != nil {
return nil, fmt.Errorf("failed to create multiaddr for %s: %w", ip, err)
}
return ma, nil
}
// ConnectString returns a string in "ip:port" format for IPv4 and "[ip]:port" format for IPv6 addresses.
func ConnectString(ip net.IP, port uint16) string {
portStr := strconv.Itoa(int(port))
return net.JoinHostPort(ip.String(), portStr)
}