Converts interface names + port into libp2p multiaddrs for listening. When no interfaces are specified, returns wildcard addresses.
217 lines
5.9 KiB
Go
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)
|
|
}
|