From cd0c6e152965f44a2794935c41881fb207c8f78c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 10 Jun 2024 12:46:35 -0400 Subject: [PATCH] Use netip.AddrPort rather than ips.IPPort (#3094) --- api/info/client.go | 5 +- api/info/service.go | 11 +- config/config.go | 8 +- genesis/bootstrappers.go | 6 +- message/mock_outbound_message_builder.go | 3 +- message/outbound_msg_builder.go | 17 +- nat/nat.go | 37 ++- nat/no_router.go | 23 +- nat/pmp.go | 8 +- nat/upnp.go | 14 +- network/config.go | 15 +- network/dialer/dialer.go | 6 +- network/dialer/dialer_test.go | 20 +- network/dialer_test.go | 27 +-- network/example_test.go | 3 +- network/ip_tracker_test.go | 2 +- network/listener_test.go | 11 +- network/network.go | 19 +- network/network_test.go | 41 ++-- network/peer/example_test.go | 11 +- network/peer/info.go | 5 +- network/peer/ip.go | 11 +- network/peer/ip_signer.go | 15 +- network/peer/ip_signer_test.go | 21 +- network/peer/ip_test.go | 26 +- network/peer/peer.go | 57 ++--- network/peer/peer_test.go | 8 +- network/peer/test_peer.go | 15 +- network/test_network.go | 228 ++++++++---------- .../inbound_conn_upgrade_throttler.go | 27 ++- .../inbound_conn_upgrade_throttler_test.go | 13 +- network/tracked_ip.go | 9 +- network/tracked_ip_test.go | 17 +- node/config.go | 6 +- node/node.go | 61 +++-- utils/atomic.go | 30 ++- utils/atomic_test.go | 45 ++++ utils/beacon/beacon.go | 11 +- utils/beacon/set.go | 22 +- utils/beacon/set_test.go | 27 +-- utils/dynamicip/ifconfig_resolver.go | 21 +- utils/dynamicip/opendns_resolver.go | 17 +- utils/dynamicip/resolver.go | 4 +- utils/dynamicip/updater.go | 24 +- utils/dynamicip/updater_test.go | 48 ++-- utils/ips/claimed_ip_port.go | 11 +- utils/ips/dynamic_ip_port.go | 56 ----- utils/ips/ip.go | 57 +++++ utils/ips/ip_port.go | 104 -------- utils/ips/ip_test.go | 176 -------------- utils/ips/lookup.go | 13 +- utils/ips/lookup_test.go | 12 +- 52 files changed, 662 insertions(+), 822 deletions(-) delete mode 100644 utils/ips/dynamic_ip_port.go create mode 100644 utils/ips/ip.go delete mode 100644 utils/ips/ip_port.go delete mode 100644 utils/ips/ip_test.go diff --git a/api/info/client.go b/api/info/client.go index 6caafd422233..15812cd5c213 100644 --- a/api/info/client.go +++ b/api/info/client.go @@ -5,6 +5,7 @@ package info import ( "context" + "net/netip" "time" "github.com/ava-labs/avalanchego/ids" @@ -19,7 +20,7 @@ var _ Client = (*client)(nil) type Client interface { GetNodeVersion(context.Context, ...rpc.Option) (*GetNodeVersionReply, error) GetNodeID(context.Context, ...rpc.Option) (ids.NodeID, *signer.ProofOfPossession, error) - GetNodeIP(context.Context, ...rpc.Option) (string, error) + GetNodeIP(context.Context, ...rpc.Option) (netip.AddrPort, error) GetNetworkID(context.Context, ...rpc.Option) (uint32, error) GetNetworkName(context.Context, ...rpc.Option) (string, error) GetBlockchainID(context.Context, string, ...rpc.Option) (ids.ID, error) @@ -54,7 +55,7 @@ func (c *client) GetNodeID(ctx context.Context, options ...rpc.Option) (ids.Node return res.NodeID, res.NodePOP, err } -func (c *client) GetNodeIP(ctx context.Context, options ...rpc.Option) (string, error) { +func (c *client) GetNodeIP(ctx context.Context, options ...rpc.Option) (netip.AddrPort, error) { res := &GetNodeIPReply{} err := c.requester.SendRequest(ctx, "info.getNodeIP", struct{}{}, res, options...) return res.IP, err diff --git a/api/info/service.go b/api/info/service.go index 929251d25aab..fd0117c5a088 100644 --- a/api/info/service.go +++ b/api/info/service.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/http" + "net/netip" "github.com/gorilla/rpc/v2" "go.uber.org/zap" @@ -17,8 +18,8 @@ import ( "github.com/ava-labs/avalanchego/network/peer" "github.com/ava-labs/avalanchego/snow/networking/benchlist" "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/json" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" @@ -37,7 +38,7 @@ type Info struct { Parameters log logging.Logger validators validators.Manager - myIP ips.DynamicIPPort + myIP *utils.Atomic[netip.AddrPort] networking network.Network chainManager chains.Manager vmManager vms.Manager @@ -67,7 +68,7 @@ func NewService( validators validators.Manager, chainManager chains.Manager, vmManager vms.Manager, - myIP ips.DynamicIPPort, + myIP *utils.Atomic[netip.AddrPort], network network.Network, benchlist benchlist.Manager, ) (http.Handler, error) { @@ -144,7 +145,7 @@ type GetNetworkIDReply struct { // GetNodeIPReply are the results from calling GetNodeIP type GetNodeIPReply struct { - IP string `json:"ip"` + IP netip.AddrPort `json:"ip"` } // GetNodeIP returns the IP of this node @@ -154,7 +155,7 @@ func (i *Info) GetNodeIP(_ *http.Request, _ *struct{}, reply *GetNodeIPReply) er zap.String("method", "getNodeIP"), ) - reply.IP = i.myIP.IPPort().String() + reply.IP = i.myIP.Get() return nil } diff --git a/config/config.go b/config/config.go index 44147b19dbd1..422eceedb6e9 100644 --- a/config/config.go +++ b/config/config.go @@ -450,7 +450,7 @@ func getStateSyncConfig(v *viper.Viper) (node.StateSyncConfig, error) { if ip == "" { continue } - addr, err := ips.ToIPPort(ip) + addr, err := ips.ParseAddrPort(ip) if err != nil { return node.StateSyncConfig{}, fmt.Errorf("couldn't parse state sync ip %s: %w", ip, err) } @@ -507,14 +507,13 @@ func getBootstrapConfig(v *viper.Viper, networkID uint32) (node.BootstrapConfig, if ip == "" { continue } - - addr, err := ips.ToIPPort(ip) + addr, err := ips.ParseAddrPort(ip) if err != nil { return node.BootstrapConfig{}, fmt.Errorf("couldn't parse bootstrap ip %s: %w", ip, err) } config.Bootstrappers = append(config.Bootstrappers, genesis.Bootstrapper{ // ID is populated below - IP: ips.IPDesc(addr), + IP: addr, }) } @@ -525,7 +524,6 @@ func getBootstrapConfig(v *viper.Viper, networkID uint32) (node.BootstrapConfig, if id == "" { continue } - nodeID, err := ids.NodeIDFromString(id) if err != nil { return node.BootstrapConfig{}, fmt.Errorf("couldn't parse bootstrap peer id %s: %w", id, err) diff --git a/genesis/bootstrappers.go b/genesis/bootstrappers.go index 4f39279ebfdd..e8bf95bc2c05 100644 --- a/genesis/bootstrappers.go +++ b/genesis/bootstrappers.go @@ -6,12 +6,12 @@ package genesis import ( "encoding/json" "fmt" + "net/netip" _ "embed" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/sampler" ) @@ -31,8 +31,8 @@ func init() { // Represents the relationship between the nodeID and the nodeIP. // The bootstrapper is sometimes called "anchor" or "beacon" node. type Bootstrapper struct { - ID ids.NodeID `json:"id"` - IP ips.IPDesc `json:"ip"` + ID ids.NodeID `json:"id"` + IP netip.AddrPort `json:"ip"` } // GetBootstrappers returns all default bootstrappers for the provided network. diff --git a/message/mock_outbound_message_builder.go b/message/mock_outbound_message_builder.go index cff8ed554caf..917d764028fe 100644 --- a/message/mock_outbound_message_builder.go +++ b/message/mock_outbound_message_builder.go @@ -10,6 +10,7 @@ package message import ( + netip "net/netip" reflect "reflect" time "time" @@ -283,7 +284,7 @@ func (mr *MockOutboundMsgBuilderMockRecorder) GetStateSummaryFrontier(arg0, arg1 } // Handshake mocks base method. -func (m *MockOutboundMsgBuilder) Handshake(arg0 uint32, arg1 uint64, arg2 ips.IPPort, arg3 string, arg4, arg5, arg6 uint32, arg7 uint64, arg8, arg9 []byte, arg10 []ids.ID, arg11, arg12 []uint32, arg13, arg14 []byte) (OutboundMessage, error) { +func (m *MockOutboundMsgBuilder) Handshake(arg0 uint32, arg1 uint64, arg2 netip.AddrPort, arg3 string, arg4, arg5, arg6 uint32, arg7 uint64, arg8, arg9 []byte, arg10 []ids.ID, arg11, arg12 []uint32, arg13, arg14 []byte) (OutboundMessage, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Handshake", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14) ret0, _ := ret[0].(OutboundMessage) diff --git a/message/outbound_msg_builder.go b/message/outbound_msg_builder.go index 1b02d8fa74f5..78aacdce3e08 100644 --- a/message/outbound_msg_builder.go +++ b/message/outbound_msg_builder.go @@ -4,6 +4,7 @@ package message import ( + "net/netip" "time" "github.com/ava-labs/avalanchego/ids" @@ -21,7 +22,7 @@ type OutboundMsgBuilder interface { Handshake( networkID uint32, myTime uint64, - ip ips.IPPort, + ip netip.AddrPort, client string, major uint32, minor uint32, @@ -228,7 +229,7 @@ func (b *outMsgBuilder) Pong() (OutboundMessage, error) { func (b *outMsgBuilder) Handshake( networkID uint32, myTime uint64, - ip ips.IPPort, + ip netip.AddrPort, client string, major uint32, minor uint32, @@ -244,14 +245,16 @@ func (b *outMsgBuilder) Handshake( ) (OutboundMessage, error) { subnetIDBytes := make([][]byte, len(trackedSubnets)) encodeIDs(trackedSubnets, subnetIDBytes) + // TODO: Use .AsSlice() after v1.12.x activates. + addr := ip.Addr().As16() return b.builder.createOutbound( &p2p.Message{ Message: &p2p.Message_Handshake{ Handshake: &p2p.Handshake{ NetworkId: networkID, MyTime: myTime, - IpAddr: ip.IP.To16(), - IpPort: uint32(ip.Port), + IpAddr: addr[:], + IpPort: uint32(ip.Port()), IpSigningTime: ipSigningTime, IpNodeIdSig: ipNodeIDSig, TrackedSubnets: subnetIDBytes, @@ -299,10 +302,12 @@ func (b *outMsgBuilder) GetPeerList( func (b *outMsgBuilder) PeerList(peers []*ips.ClaimedIPPort, bypassThrottling bool) (OutboundMessage, error) { claimIPPorts := make([]*p2p.ClaimedIpPort, len(peers)) for i, p := range peers { + // TODO: Use .AsSlice() after v1.12.x activates. + ip := p.AddrPort.Addr().As16() claimIPPorts[i] = &p2p.ClaimedIpPort{ X509Certificate: p.Cert.Raw, - IpAddr: p.IPPort.IP.To16(), - IpPort: uint32(p.IPPort.Port), + IpAddr: ip[:], + IpPort: uint32(p.AddrPort.Port()), Timestamp: p.Timestamp, Signature: p.Signature, TxId: ids.Empty[:], diff --git a/nat/nat.go b/nat/nat.go index a6e37078e7a6..28cdb1083eac 100644 --- a/nat/nat.go +++ b/nat/nat.go @@ -4,13 +4,13 @@ package nat import ( - "net" + "net/netip" "sync" "time" "go.uber.org/zap" - "github.com/ava-labs/avalanchego/utils/ips" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" ) @@ -29,7 +29,7 @@ type Router interface { // Undo a port mapping UnmapPort(intPort, extPort uint16) error // Return our external IP - ExternalIP() (net.IP, error) + ExternalIP() (netip.Addr, error) } // GetRouter returns a router on the current network. @@ -63,7 +63,13 @@ func NewPortMapper(log logging.Logger, r Router) *Mapper { // Map external port [extPort] (exposed to the internet) to internal port [intPort] (where our process is listening) // and set [ip]. Does this every [updateTime]. [ip] may be nil. -func (m *Mapper) Map(intPort, extPort uint16, desc string, ip ips.DynamicIPPort, updateTime time.Duration) { +func (m *Mapper) Map( + intPort uint16, + extPort uint16, + desc string, + ip *utils.Atomic[netip.AddrPort], + updateTime time.Duration, +) { if !m.r.SupportsNAT() { return } @@ -110,7 +116,13 @@ func (m *Mapper) retryMapPort(intPort, extPort uint16, desc string, timeout time // keepPortMapping runs in the background to keep a port mapped. It renews the mapping from [extPort] // to [intPort]] every [updateTime]. Updates [ip] every [updateTime]. -func (m *Mapper) keepPortMapping(intPort, extPort uint16, desc string, ip ips.DynamicIPPort, updateTime time.Duration) { +func (m *Mapper) keepPortMapping( + intPort uint16, + extPort uint16, + desc string, + ip *utils.Atomic[netip.AddrPort], + updateTime time.Duration, +) { updateTimer := time.NewTimer(updateTime) defer func(extPort uint16) { @@ -150,22 +162,25 @@ func (m *Mapper) keepPortMapping(intPort, extPort uint16, desc string, ip ips.Dy } } -func (m *Mapper) updateIP(ip ips.DynamicIPPort) { +func (m *Mapper) updateIP(ip *utils.Atomic[netip.AddrPort]) { if ip == nil { return } - newIP, err := m.r.ExternalIP() + newAddr, err := m.r.ExternalIP() if err != nil { m.log.Error("failed to get external IP", zap.Error(err), ) return } - oldIP := ip.IPPort().IP - ip.SetIP(newIP) - if !oldIP.Equal(newIP) { + oldAddrPort := ip.Get() + oldAddr := oldAddrPort.Addr() + if newAddr != oldAddr { + port := oldAddrPort.Port() + ip.Set(netip.AddrPortFrom(newAddr, port)) m.log.Info("external IP updated", - zap.Stringer("newIP", newIP), + zap.Stringer("oldIP", oldAddr), + zap.Stringer("newIP", newAddr), ) } } diff --git a/nat/no_router.go b/nat/no_router.go index 19c68dac5538..ebdf6015020c 100644 --- a/nat/no_router.go +++ b/nat/no_router.go @@ -6,6 +6,7 @@ package nat import ( "errors" "net" + "net/netip" "time" ) @@ -19,7 +20,7 @@ var ( const googleDNSServer = "8.8.8.8:80" type noRouter struct { - ip net.IP + ip netip.Addr ipErr error } @@ -35,26 +36,30 @@ func (noRouter) UnmapPort(uint16, uint16) error { return nil } -func (r noRouter) ExternalIP() (net.IP, error) { +func (r noRouter) ExternalIP() (netip.Addr, error) { return r.ip, r.ipErr } -func getOutboundIP() (net.IP, error) { +func getOutboundIP() (netip.Addr, error) { conn, err := net.Dial("udp", googleDNSServer) if err != nil { - return nil, err + return netip.Addr{}, err } - addr := conn.LocalAddr() + localAddr := conn.LocalAddr() if err := conn.Close(); err != nil { - return nil, err + return netip.Addr{}, err } - udpAddr, ok := addr.(*net.UDPAddr) + udpAddr, ok := localAddr.(*net.UDPAddr) if !ok { - return nil, errFetchingIP + return netip.Addr{}, errFetchingIP } - return udpAddr.IP, nil + addr := udpAddr.AddrPort().Addr() + if addr.Is4In6() { + addr = addr.Unmap() + } + return addr, nil } // NewNoRouter returns a router that assumes the network is public diff --git a/nat/pmp.go b/nat/pmp.go index ecee9793f934..c10bdbdbc4fa 100644 --- a/nat/pmp.go +++ b/nat/pmp.go @@ -6,7 +6,7 @@ package nat import ( "errors" "math" - "net" + "net/netip" "time" "github.com/jackpal/gateway" @@ -66,12 +66,12 @@ func (r *pmpRouter) UnmapPort(internalPort uint16, _ uint16) error { return err } -func (r *pmpRouter) ExternalIP() (net.IP, error) { +func (r *pmpRouter) ExternalIP() (netip.Addr, error) { response, err := r.client.GetExternalAddress() if err != nil { - return nil, err + return netip.Addr{}, err } - return response.ExternalIPAddress[:], nil + return netip.AddrFrom4(response.ExternalIPAddress), nil } func getPMPRouter() *pmpRouter { diff --git a/nat/upnp.go b/nat/upnp.go index d1aab02398b3..943017dc7560 100644 --- a/nat/upnp.go +++ b/nat/upnp.go @@ -7,11 +7,14 @@ import ( "fmt" "math" "net" + "net/netip" "time" "github.com/huin/goupnp" "github.com/huin/goupnp/dcps/internetgateway1" "github.com/huin/goupnp/dcps/internetgateway2" + + "github.com/ava-labs/avalanchego/utils/ips" ) const ( @@ -111,17 +114,12 @@ func (r *upnpRouter) localIP() (net.IP, error) { return nil, fmt.Errorf("couldn't find the local address in the same network as %s", deviceIP) } -func (r *upnpRouter) ExternalIP() (net.IP, error) { +func (r *upnpRouter) ExternalIP() (netip.Addr, error) { str, err := r.client.GetExternalIPAddress() if err != nil { - return nil, err - } - - ip := net.ParseIP(str) - if ip == nil { - return nil, fmt.Errorf("invalid IP %s", str) + return netip.Addr{}, err } - return ip, nil + return ips.ParseAddr(str) } func (r *upnpRouter) MapPort( diff --git a/network/config.go b/network/config.go index 3004a12bdc5b..de8eb44e14a0 100644 --- a/network/config.go +++ b/network/config.go @@ -6,6 +6,7 @@ package network import ( "crypto" "crypto/tls" + "net/netip" "time" "github.com/ava-labs/avalanchego/ids" @@ -14,9 +15,9 @@ import ( "github.com/ava-labs/avalanchego/snow/networking/tracker" "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/compression" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/set" ) @@ -110,12 +111,12 @@ type Config struct { TLSKeyLogFile string `json:"tlsKeyLogFile"` - MyNodeID ids.NodeID `json:"myNodeID"` - MyIPPort ips.DynamicIPPort `json:"myIP"` - NetworkID uint32 `json:"networkID"` - MaxClockDifference time.Duration `json:"maxClockDifference"` - PingFrequency time.Duration `json:"pingFrequency"` - AllowPrivateIPs bool `json:"allowPrivateIPs"` + MyNodeID ids.NodeID `json:"myNodeID"` + MyIPPort *utils.Atomic[netip.AddrPort] `json:"myIP"` + NetworkID uint32 `json:"networkID"` + MaxClockDifference time.Duration `json:"maxClockDifference"` + PingFrequency time.Duration `json:"pingFrequency"` + AllowPrivateIPs bool `json:"allowPrivateIPs"` SupportedACPs set.Set[uint32] `json:"supportedACPs"` ObjectedACPs set.Set[uint32] `json:"objectedACPs"` diff --git a/network/dialer/dialer.go b/network/dialer/dialer.go index 109b63cc2002..2517184fedcc 100644 --- a/network/dialer/dialer.go +++ b/network/dialer/dialer.go @@ -7,12 +7,12 @@ import ( "context" "fmt" "net" + "net/netip" "time" "go.uber.org/zap" "github.com/ava-labs/avalanchego/network/throttling" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" ) @@ -22,7 +22,7 @@ var _ Dialer = (*dialer)(nil) type Dialer interface { // If [ctx] is canceled, gives up trying to connect to [ip] // and returns an error. - Dial(ctx context.Context, ip ips.IPPort) (net.Conn, error) + Dial(ctx context.Context, ip netip.AddrPort) (net.Conn, error) } type dialer struct { @@ -62,7 +62,7 @@ func NewDialer(network string, dialerConfig Config, log logging.Logger) Dialer { } } -func (d *dialer) Dial(ctx context.Context, ip ips.IPPort) (net.Conn, error) { +func (d *dialer) Dial(ctx context.Context, ip netip.AddrPort) (net.Conn, error) { if err := d.throttler.Acquire(ctx); err != nil { return nil, err } diff --git a/network/dialer/dialer_test.go b/network/dialer/dialer_test.go index a824b8b03e08..01b3f640667b 100644 --- a/network/dialer/dialer_test.go +++ b/network/dialer/dialer_test.go @@ -6,14 +6,12 @@ package dialer import ( "context" "net" - "strconv" - "strings" + "net/netip" "testing" "time" "github.com/stretchr/testify/require" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" ) @@ -22,7 +20,11 @@ import ( func TestDialerCancelDial(t *testing.T) { require := require.New(t) - l, err := net.Listen("tcp", "127.0.0.1:") + listenAddrPort := netip.AddrPortFrom( + netip.AddrFrom4([4]byte{127, 0, 0, 1}), + 0, + ) + l, err := net.Listen("tcp", listenAddrPort.String()) require.NoError(err) done := make(chan struct{}) @@ -43,12 +45,8 @@ func TestDialerCancelDial(t *testing.T) { } }() - port, err := strconv.Atoi(strings.Split(l.Addr().String(), ":")[1]) + listenedAddrPort, err := netip.ParseAddrPort(l.Addr().String()) require.NoError(err) - myIP := ips.IPPort{ - IP: net.ParseIP("127.0.0.1"), - Port: uint16(port), - } // Create a dialer dialer := NewDialer( @@ -63,11 +61,11 @@ func TestDialerCancelDial(t *testing.T) { // Make an outgoing connection with a cancelled context ctx, cancel := context.WithCancel(context.Background()) cancel() - _, err = dialer.Dial(ctx, myIP) + _, err = dialer.Dial(ctx, listenedAddrPort) require.ErrorIs(err, context.Canceled) // Make an outgoing connection with a non-cancelled context - conn, err := dialer.Dial(context.Background(), myIP) + conn, err := dialer.Dial(context.Background(), listenedAddrPort) require.NoError(err) _ = conn.Close() diff --git a/network/dialer_test.go b/network/dialer_test.go index 7a60d056d66d..d1567d20ad1b 100644 --- a/network/dialer_test.go +++ b/network/dialer_test.go @@ -7,9 +7,9 @@ import ( "context" "errors" "net" + "net/netip" "github.com/ava-labs/avalanchego/network/dialer" - "github.com/ava-labs/avalanchego/utils/ips" ) var ( @@ -20,33 +20,32 @@ var ( type testDialer struct { // maps [ip.String] to a listener - listeners map[string]*testListener + listeners map[netip.AddrPort]*testListener } func newTestDialer() *testDialer { return &testDialer{ - listeners: make(map[string]*testListener), + listeners: make(map[netip.AddrPort]*testListener), } } -func (d *testDialer) NewListener() (ips.DynamicIPPort, *testListener) { +func (d *testDialer) NewListener() (netip.AddrPort, *testListener) { // Uses a private IP to easily enable testing AllowPrivateIPs - ip := ips.NewDynamicIPPort( - net.IPv4(10, 0, 0, 0), + addrPort := netip.AddrPortFrom( + netip.AddrFrom4([4]byte{10, 0, 0, 0}), uint16(len(d.listeners)+1), ) - staticIP := ip.IPPort() - listener := newTestListener(staticIP) - d.AddListener(staticIP, listener) - return ip, listener + listener := newTestListener(addrPort) + d.AddListener(addrPort, listener) + return addrPort, listener } -func (d *testDialer) AddListener(ip ips.IPPort, listener *testListener) { - d.listeners[ip.String()] = listener +func (d *testDialer) AddListener(ip netip.AddrPort, listener *testListener) { + d.listeners[ip] = listener } -func (d *testDialer) Dial(ctx context.Context, ip ips.IPPort) (net.Conn, error) { - listener, ok := d.listeners[ip.String()] +func (d *testDialer) Dial(ctx context.Context, ip netip.AddrPort) (net.Conn, error) { + listener, ok := d.listeners[ip] if !ok { return nil, errRefused } diff --git a/network/example_test.go b/network/example_test.go index bfac03fba44f..0fef075f8101 100644 --- a/network/example_test.go +++ b/network/example_test.go @@ -16,7 +16,6 @@ import ( "github.com/ava-labs/avalanchego/snow/networking/router" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/version" @@ -110,7 +109,7 @@ func ExampleNewTestNetwork() { // gossip will enable connecting to all the remaining nodes in the network. bootstrappers := genesis.SampleBootstrappers(constants.FujiID, 5) for _, bootstrapper := range bootstrappers { - network.ManuallyTrack(bootstrapper.ID, ips.IPPort(bootstrapper.IP)) + network.ManuallyTrack(bootstrapper.ID, bootstrapper.IP) } // Typically network.StartClose() should be called based on receiving a diff --git a/network/ip_tracker_test.go b/network/ip_tracker_test.go index edae70de5b98..bbfbdb958773 100644 --- a/network/ip_tracker_test.go +++ b/network/ip_tracker_test.go @@ -25,7 +25,7 @@ func newTestIPTracker(t *testing.T) *ipTracker { func newerTestIP(ip *ips.ClaimedIPPort) *ips.ClaimedIPPort { return ips.NewClaimedIPPort( ip.Cert, - ip.IPPort, + ip.AddrPort, ip.Timestamp+1, ip.Signature, ) diff --git a/network/listener_test.go b/network/listener_test.go index 5d6073c6b383..a0167e817baa 100644 --- a/network/listener_test.go +++ b/network/listener_test.go @@ -5,19 +5,18 @@ package network import ( "net" - - "github.com/ava-labs/avalanchego/utils/ips" + "net/netip" ) var _ net.Listener = (*testListener)(nil) type testListener struct { - ip ips.IPPort + ip netip.AddrPort inbound chan net.Conn closed chan struct{} } -func newTestListener(ip ips.IPPort) *testListener { +func newTestListener(ip netip.AddrPort) *testListener { return &testListener{ ip: ip, inbound: make(chan net.Conn), @@ -41,7 +40,7 @@ func (l *testListener) Close() error { func (l *testListener) Addr() net.Addr { return &net.TCPAddr{ - IP: l.ip.IP, - Port: int(l.ip.Port), + IP: l.ip.Addr().AsSlice(), + Port: int(l.ip.Port()), } } diff --git a/network/network.go b/network/network.go index 51ded9b8cf91..2aee13a910d9 100644 --- a/network/network.go +++ b/network/network.go @@ -9,6 +9,7 @@ import ( "fmt" "math" "net" + "net/netip" "strings" "sync" "sync/atomic" @@ -77,7 +78,7 @@ type Network interface { // Attempt to connect to this IP. The network will never stop attempting to // connect to this ID. - ManuallyTrack(nodeID ids.NodeID, ip ips.IPPort) + ManuallyTrack(nodeID ids.NodeID, ip netip.AddrPort) // PeerInfo returns information about peers. If [nodeIDs] is empty, returns // info about all peers that have finished the handshake. Otherwise, returns @@ -448,7 +449,7 @@ func (n *network) Connected(nodeID ids.NodeID) { peerIP := peer.IP() newIP := ips.NewClaimedIPPort( peer.Cert(), - peerIP.IPPort, + peerIP.AddrPort, peerIP.Timestamp, peerIP.TLSSignature, ) @@ -548,7 +549,7 @@ func (n *network) Dispatch() error { // call this function inside the go-routine, rather than the main // accept loop. remoteAddr := conn.RemoteAddr().String() - ip, err := ips.ToIPPort(remoteAddr) + ip, err := ips.ParseAddrPort(remoteAddr) if err != nil { n.peerConfig.Log.Error("failed to parse remote address", zap.String("peerIP", remoteAddr), @@ -597,7 +598,7 @@ func (n *network) Dispatch() error { return errs.Err } -func (n *network) ManuallyTrack(nodeID ids.NodeID, ip ips.IPPort) { +func (n *network) ManuallyTrack(nodeID ids.NodeID, ip netip.AddrPort) { n.ipTracker.ManuallyTrack(nodeID) n.peersLock.Lock() @@ -637,7 +638,7 @@ func (n *network) track(ip *ips.ClaimedIPPort) error { // lock. signedIP := peer.SignedIP{ UnsignedIP: peer.UnsignedIP{ - IPPort: ip.IPPort, + AddrPort: ip.AddrPort, Timestamp: ip.Timestamp, }, TLSSignature: ip.Signature, @@ -663,9 +664,9 @@ func (n *network) track(ip *ips.ClaimedIPPort) error { tracked, isTracked := n.trackedIPs[ip.NodeID] if isTracked { // Stop tracking the old IP and start tracking the new one. - tracked = tracked.trackNewIP(ip.IPPort) + tracked = tracked.trackNewIP(ip.AddrPort) } else { - tracked = newTrackedIP(ip.IPPort) + tracked = newTrackedIP(ip.AddrPort) } n.trackedIPs[ip.NodeID] = tracked n.dial(ip.NodeID, tracked) @@ -798,7 +799,7 @@ func (n *network) disconnectedFromConnected(peer peer.Peer, nodeID ids.NodeID) { // The peer that is disconnecting from us finished the handshake if ip, wantsConnection := n.ipTracker.GetIP(nodeID); wantsConnection { - tracked := newTrackedIP(ip.IPPort) + tracked := newTrackedIP(ip.AddrPort) n.trackedIPs[nodeID] = tracked n.dial(nodeID, tracked) } @@ -898,7 +899,7 @@ func (n *network) dial(nodeID ids.NodeID, ip *trackedIP) { // nodeID leaves the validator set. This is why we continue the loop // rather than returning even though we will never initiate an // outbound connection with this IP. - if !n.config.AllowPrivateIPs && ip.ip.IP.IsPrivate() { + if !n.config.AllowPrivateIPs && !ips.IsPublic(ip.ip.Addr()) { n.peerConfig.Log.Verbo("skipping connection dial", zap.String("reason", "outbound connections to private IPs are prohibited"), zap.Stringer("nodeID", nodeID), diff --git a/network/network_test.go b/network/network_test.go index 5ae2cef5af3e..85390da90dff 100644 --- a/network/network_test.go +++ b/network/network_test.go @@ -6,7 +6,7 @@ package network import ( "context" "crypto" - "net" + "net/netip" "sync" "testing" "time" @@ -26,6 +26,7 @@ import ( "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/staking" "github.com/ava-labs/avalanchego/subnets" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/ips" @@ -178,7 +179,7 @@ func newTestNetwork(t *testing.T, count int) (*testDialer, []*testListener, []id config := defaultConfig config.TLSConfig = peer.TLSConfig(*tlsCert, nil) config.MyNodeID = nodeID - config.MyIPPort = ip + config.MyIPPort = utils.NewAtomic(ip) config.TLSKey = tlsCert.PrivateKey.(crypto.Signer) config.BLSKey = blsKey @@ -279,7 +280,7 @@ func newFullyConnectedTestNetwork(t *testing.T, handlers []router.InboundHandler for i, net := range networks { if i != 0 { config := configs[0] - net.ManuallyTrack(config.MyNodeID, config.MyIPPort.IPPort()) + net.ManuallyTrack(config.MyNodeID, config.MyIPPort.Get()) } go func(net Network) { @@ -418,10 +419,10 @@ func TestTrackVerifiesSignatures(t *testing.T) { err = network.Track([]*ips.ClaimedIPPort{ ips.NewClaimedIPPort( stakingCert, - ips.IPPort{ - IP: net.IPv4(123, 132, 123, 123), - Port: 10000, - }, + netip.AddrPortFrom( + netip.AddrFrom4([4]byte{123, 132, 123, 123}), + 10000, + ), 1000, // timestamp nil, // signature ), @@ -487,7 +488,7 @@ func TestTrackDoesNotDialPrivateIPs(t *testing.T) { for i, net := range networks { if i != 0 { config := configs[0] - net.ManuallyTrack(config.MyNodeID, config.MyIPPort.IPPort()) + net.ManuallyTrack(config.MyNodeID, config.MyIPPort.Get()) } go func(net Network) { @@ -576,7 +577,7 @@ func TestDialDeletesNonValidators(t *testing.T) { require.NoError(net.Track([]*ips.ClaimedIPPort{ ips.NewClaimedIPPort( stakingCert, - ip.IPPort, + ip.AddrPort, ip.Timestamp, ip.TLSSignature, ), @@ -628,23 +629,23 @@ func TestDialContext(t *testing.T) { neverDialedNodeID = ids.GenerateTestNodeID() dialedNodeID = ids.GenerateTestNodeID() - dynamicNeverDialedIP, neverDialedListener = dialer.NewListener() - dynamicDialedIP, dialedListener = dialer.NewListener() + neverDialedIP, neverDialedListener = dialer.NewListener() + dialedIP, dialedListener = dialer.NewListener() - neverDialedIP = &trackedIP{ - ip: dynamicNeverDialedIP.IPPort(), + neverDialedTrackedIP = &trackedIP{ + ip: neverDialedIP, } - dialedIP = &trackedIP{ - ip: dynamicDialedIP.IPPort(), + dialedTrackedIP = &trackedIP{ + ip: dialedIP, } ) - network.ManuallyTrack(neverDialedNodeID, neverDialedIP.ip) - network.ManuallyTrack(dialedNodeID, dialedIP.ip) + network.ManuallyTrack(neverDialedNodeID, neverDialedIP) + network.ManuallyTrack(dialedNodeID, dialedIP) // Sanity check that when a non-cancelled context is given, // we actually dial the peer. - network.dial(dialedNodeID, dialedIP) + network.dial(dialedNodeID, dialedTrackedIP) gotDialedIPConn := make(chan struct{}) go func() { @@ -656,7 +657,7 @@ func TestDialContext(t *testing.T) { // Asset that when [n.onCloseCtx] is cancelled, dial returns immediately. // That is, [neverDialedListener] doesn't accept a connection. network.onCloseCtxCancel() - network.dial(neverDialedNodeID, neverDialedIP) + network.dial(neverDialedNodeID, neverDialedTrackedIP) gotNeverDialedIPConn := make(chan struct{}) go func() { @@ -718,7 +719,7 @@ func TestAllowConnectionAsAValidator(t *testing.T) { for i, net := range networks { if i != 0 { config := configs[0] - net.ManuallyTrack(config.MyNodeID, config.MyIPPort.IPPort()) + net.ManuallyTrack(config.MyNodeID, config.MyIPPort.Get()) } go func(net Network) { diff --git a/network/peer/example_test.go b/network/peer/example_test.go index d6c8ba20c913..59e5268fb623 100644 --- a/network/peer/example_test.go +++ b/network/peer/example_test.go @@ -6,13 +6,12 @@ package peer import ( "context" "fmt" - "net" + "net/netip" "time" "github.com/ava-labs/avalanchego/message" "github.com/ava-labs/avalanchego/snow/networking/router" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/ips" ) func ExampleStartTestPeer() { @@ -20,10 +19,10 @@ func ExampleStartTestPeer() { ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() - peerIP := ips.IPPort{ - IP: net.IPv6loopback, - Port: 9651, - } + peerIP := netip.AddrPortFrom( + netip.IPv6Loopback(), + 9651, + ) peer, err := StartTestPeer( ctx, peerIP, diff --git a/network/peer/info.go b/network/peer/info.go index 00ccaec7953b..928c47ff26ee 100644 --- a/network/peer/info.go +++ b/network/peer/info.go @@ -4,6 +4,7 @@ package peer import ( + "net/netip" "time" "github.com/ava-labs/avalanchego/ids" @@ -12,8 +13,8 @@ import ( ) type Info struct { - IP string `json:"ip"` - PublicIP string `json:"publicIP,omitempty"` + IP netip.AddrPort `json:"ip"` + PublicIP netip.AddrPort `json:"publicIP,omitempty"` ID ids.NodeID `json:"nodeID"` Version string `json:"version"` LastSent time.Time `json:"lastSent"` diff --git a/network/peer/ip.go b/network/peer/ip.go index a873f1668d6a..443396d344d2 100644 --- a/network/peer/ip.go +++ b/network/peer/ip.go @@ -8,12 +8,13 @@ import ( "crypto/rand" "errors" "fmt" + "net" + "net/netip" "time" "github.com/ava-labs/avalanchego/staking" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/hashing" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/wrappers" ) @@ -26,7 +27,7 @@ var ( // ensure that the most updated IP claim is tracked by peers for a given // validator. type UnsignedIP struct { - ips.IPPort + AddrPort netip.AddrPort Timestamp uint64 } @@ -49,9 +50,11 @@ func (ip *UnsignedIP) Sign(tlsSigner crypto.Signer, blsSigner *bls.SecretKey) (* func (ip *UnsignedIP) bytes() []byte { p := wrappers.Packer{ - Bytes: make([]byte, ips.IPPortLen+wrappers.LongLen), + Bytes: make([]byte, net.IPv6len+wrappers.ShortLen+wrappers.LongLen), } - ips.PackIP(&p, ip.IPPort) + addrBytes := ip.AddrPort.Addr().As16() + p.PackFixedBytes(addrBytes[:]) + p.PackShort(ip.AddrPort.Port()) p.PackLong(ip.Timestamp) return p.Bytes } diff --git a/network/peer/ip_signer.go b/network/peer/ip_signer.go index 1c38d4e67528..1053cfce3e62 100644 --- a/network/peer/ip_signer.go +++ b/network/peer/ip_signer.go @@ -5,16 +5,17 @@ package peer import ( "crypto" + "net/netip" "sync" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/timer/mockable" ) // IPSigner will return a signedIP for the current value of our dynamic IP. type IPSigner struct { - ip ips.DynamicIPPort + ip *utils.Atomic[netip.AddrPort] clock mockable.Clock tlsSigner crypto.Signer blsSigner *bls.SecretKey @@ -27,7 +28,7 @@ type IPSigner struct { } func NewIPSigner( - ip ips.DynamicIPPort, + ip *utils.Atomic[netip.AddrPort], tlsSigner crypto.Signer, blsSigner *bls.SecretKey, ) *IPSigner { @@ -49,8 +50,8 @@ func (s *IPSigner) GetSignedIP() (*SignedIP, error) { s.signedIPLock.RLock() signedIP := s.signedIP s.signedIPLock.RUnlock() - ip := s.ip.IPPort() - if signedIP != nil && signedIP.IPPort.Equal(ip) { + ip := s.ip.Get() + if signedIP != nil && signedIP.AddrPort == ip { return signedIP, nil } @@ -62,13 +63,13 @@ func (s *IPSigner) GetSignedIP() (*SignedIP, error) { // same time, we should verify that we are the first thread to attempt to // update it. signedIP = s.signedIP - if signedIP != nil && signedIP.IPPort.Equal(ip) { + if signedIP != nil && signedIP.AddrPort == ip { return signedIP, nil } // We should now sign our new IP at the current timestamp. unsignedIP := UnsignedIP{ - IPPort: ip, + AddrPort: ip, Timestamp: s.clock.Unix(), } signedIP, err := unsignedIP.Sign(s.tlsSigner, s.blsSigner) diff --git a/network/peer/ip_signer_test.go b/network/peer/ip_signer_test.go index 315becd8f082..cff9b2cbbda2 100644 --- a/network/peer/ip_signer_test.go +++ b/network/peer/ip_signer_test.go @@ -5,24 +5,24 @@ package peer import ( "crypto" - "net" + "net/netip" "testing" "time" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/staking" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" ) func TestIPSigner(t *testing.T) { require := require.New(t) - dynIP := ips.NewDynamicIPPort( - net.IPv6loopback, + dynIP := utils.NewAtomic(netip.AddrPortFrom( + netip.IPv6Loopback(), 0, - ) + )) tlsCert, err := staking.NewTLSCert() require.NoError(err) @@ -37,22 +37,25 @@ func TestIPSigner(t *testing.T) { signedIP1, err := s.GetSignedIP() require.NoError(err) - require.Equal(dynIP.IPPort(), signedIP1.IPPort) + require.Equal(dynIP.Get(), signedIP1.AddrPort) require.Equal(uint64(10), signedIP1.Timestamp) s.clock.Set(time.Unix(11, 0)) signedIP2, err := s.GetSignedIP() require.NoError(err) - require.Equal(dynIP.IPPort(), signedIP2.IPPort) + require.Equal(dynIP.Get(), signedIP2.AddrPort) require.Equal(uint64(10), signedIP2.Timestamp) require.Equal(signedIP1.TLSSignature, signedIP2.TLSSignature) - dynIP.SetIP(net.IPv4(1, 2, 3, 4)) + dynIP.Set(netip.AddrPortFrom( + netip.AddrFrom4([4]byte{1, 2, 3, 4}), + dynIP.Get().Port(), + )) signedIP3, err := s.GetSignedIP() require.NoError(err) - require.Equal(dynIP.IPPort(), signedIP3.IPPort) + require.Equal(dynIP.Get(), signedIP3.AddrPort) require.Equal(uint64(11), signedIP3.Timestamp) require.NotEqual(signedIP2.TLSSignature, signedIP3.TLSSignature) } diff --git a/network/peer/ip_test.go b/network/peer/ip_test.go index 142d675ecfaa..4c3f62d27694 100644 --- a/network/peer/ip_test.go +++ b/network/peer/ip_test.go @@ -5,7 +5,7 @@ package peer import ( "crypto" - "net" + "net/netip" "testing" "time" @@ -13,7 +13,6 @@ import ( "github.com/ava-labs/avalanchego/staking" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" ) func TestSignedIpVerify(t *testing.T) { @@ -31,6 +30,10 @@ func TestSignedIpVerify(t *testing.T) { require.NoError(t, err) now := time.Now() + addrPort := netip.AddrPortFrom( + netip.AddrFrom4([4]byte{1, 2, 3, 4}), + 1, + ) type test struct { name string @@ -49,10 +52,7 @@ func TestSignedIpVerify(t *testing.T) { blsSigner: blsKey1, expectedCert: cert1, ip: UnsignedIP{ - IPPort: ips.IPPort{ - IP: net.IPv4(1, 2, 3, 4), - Port: 1, - }, + AddrPort: addrPort, Timestamp: uint64(now.Unix()) - 1, }, maxTimestamp: now, @@ -64,10 +64,7 @@ func TestSignedIpVerify(t *testing.T) { blsSigner: blsKey1, expectedCert: cert1, ip: UnsignedIP{ - IPPort: ips.IPPort{ - IP: net.IPv4(1, 2, 3, 4), - Port: 1, - }, + AddrPort: addrPort, Timestamp: uint64(now.Unix()), }, maxTimestamp: now, @@ -79,10 +76,7 @@ func TestSignedIpVerify(t *testing.T) { blsSigner: blsKey1, expectedCert: cert1, ip: UnsignedIP{ - IPPort: ips.IPPort{ - IP: net.IPv4(1, 2, 3, 4), - Port: 1, - }, + AddrPort: addrPort, Timestamp: uint64(now.Unix()) + 1, }, maxTimestamp: now, @@ -94,10 +88,6 @@ func TestSignedIpVerify(t *testing.T) { blsSigner: blsKey1, expectedCert: cert2, // note this isn't cert1 ip: UnsignedIP{ - IPPort: ips.IPPort{ - IP: net.IPv4(1, 2, 3, 4), - Port: 1, - }, Timestamp: uint64(now.Unix()), }, maxTimestamp: now, diff --git a/network/peer/peer.go b/network/peer/peer.go index a87bca708544..a92791ff72ee 100644 --- a/network/peer/peer.go +++ b/network/peer/peer.go @@ -10,6 +10,7 @@ import ( "io" "math" "net" + "net/netip" "sync" "sync/atomic" "time" @@ -269,11 +270,6 @@ func (p *peer) AwaitReady(ctx context.Context) error { } func (p *peer) Info() Info { - publicIPStr := "" - if !p.ip.IsZero() { - publicIPStr = p.ip.IPPort.String() - } - uptimes := make(map[ids.ID]json.Uint32, p.MySubnets.Len()) for subnetID := range p.MySubnets { uptime, exist := p.ObservedUptime(subnetID) @@ -288,9 +284,10 @@ func (p *peer) Info() Info { primaryUptime = 0 } + ip, _ := ips.ParseAddrPort(p.conn.RemoteAddr().String()) return Info{ - IP: p.conn.RemoteAddr().String(), - PublicIP: publicIPStr, + IP: ip, + PublicIP: p.ip.AddrPort, ID: p.id, Version: p.version.String(), LastSent: p.LastSent(), @@ -526,10 +523,10 @@ func (p *peer) writeMessages() { ) return } - if mySignedIP.Port == 0 { + if port := mySignedIP.AddrPort.Port(); port == 0 { p.Log.Error("signed IP has invalid port", zap.Stringer("nodeID", p.id), - zap.Uint16("port", mySignedIP.Port), + zap.Uint16("port", port), ) return } @@ -540,7 +537,7 @@ func (p *peer) writeMessages() { msg, err := p.MessageCreator.Handshake( p.NetworkID, p.Clock.Unix(), - mySignedIP.IPPort, + mySignedIP.AddrPort, myVersion.Name, uint32(myVersion.Major), uint32(myVersion.Minor), @@ -731,7 +728,7 @@ func (p *peer) shouldDisconnect() bool { return true } - // Avoid unnecessary signature verifications by only verifing the signature + // Avoid unnecessary signature verifications by only verifying the signature // once per validation period. p.txIDOfVerifiedBLSKey = vdr.TxID return false @@ -1038,23 +1035,25 @@ func (p *peer) handleHandshake(msg *p2p.Handshake) { } } - // "net.IP" type in Golang is 16-byte - if ipLen := len(msg.IpAddr); ipLen != net.IPv6len { + addr, ok := ips.AddrFromSlice(msg.IpAddr) + if !ok { p.Log.Debug(malformedMessageLog, zap.Stringer("nodeID", p.id), zap.Stringer("messageOp", message.HandshakeOp), zap.String("field", "ip"), - zap.Int("ipLen", ipLen), + zap.Int("ipLen", len(msg.IpAddr)), ) p.StartClose() return } + + port := uint16(msg.IpPort) if msg.IpPort == 0 { p.Log.Debug(malformedMessageLog, zap.Stringer("nodeID", p.id), zap.Stringer("messageOp", message.HandshakeOp), zap.String("field", "port"), - zap.Uint32("port", msg.IpPort), + zap.Uint16("port", port), ) p.StartClose() return @@ -1062,10 +1061,10 @@ func (p *peer) handleHandshake(msg *p2p.Handshake) { p.ip = &SignedIP{ UnsignedIP: UnsignedIP{ - IPPort: ips.IPPort{ - IP: msg.IpAddr, - Port: uint16(msg.IpPort), - }, + AddrPort: netip.AddrPortFrom( + addr, + port, + ), Timestamp: msg.IpSigningTime, }, TLSSignature: msg.IpNodeIdSig, @@ -1224,23 +1223,25 @@ func (p *peer) handlePeerList(msg *p2p.PeerList) { return } - // "net.IP" type in Golang is 16-byte - if ipLen := len(claimedIPPort.IpAddr); ipLen != net.IPv6len { + addr, ok := ips.AddrFromSlice(claimedIPPort.IpAddr) + if !ok { p.Log.Debug(malformedMessageLog, zap.Stringer("nodeID", p.id), zap.Stringer("messageOp", message.PeerListOp), zap.String("field", "ip"), - zap.Int("ipLen", ipLen), + zap.Int("ipLen", len(claimedIPPort.IpAddr)), ) p.StartClose() return } - if claimedIPPort.IpPort == 0 { + + port := uint16(claimedIPPort.IpPort) + if port == 0 { p.Log.Debug(malformedMessageLog, zap.Stringer("nodeID", p.id), zap.Stringer("messageOp", message.PeerListOp), zap.String("field", "port"), - zap.Uint32("port", claimedIPPort.IpPort), + zap.Uint16("port", port), ) p.StartClose() return @@ -1248,10 +1249,10 @@ func (p *peer) handlePeerList(msg *p2p.PeerList) { discoveredIPs[i] = ips.NewClaimedIPPort( tlsCert, - ips.IPPort{ - IP: claimedIPPort.IpAddr, - Port: uint16(claimedIPPort.IpPort), - }, + netip.AddrPortFrom( + addr, + port, + ), claimedIPPort.Timestamp, claimedIPPort.Signature, ) diff --git a/network/peer/peer_test.go b/network/peer/peer_test.go index 4a0399bc3a1e..e29edbe17ba6 100644 --- a/network/peer/peer_test.go +++ b/network/peer/peer_test.go @@ -7,6 +7,7 @@ import ( "context" "crypto" "net" + "net/netip" "testing" "time" @@ -22,9 +23,9 @@ import ( "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/staking" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math/meter" "github.com/ava-labs/avalanchego/utils/resource" @@ -106,7 +107,10 @@ func newRawTestPeer(t *testing.T, config Config) *rawTestPeer { require.NoError(err) nodeID := ids.NodeIDFromCert(cert) - ip := ips.NewDynamicIPPort(net.IPv6loopback, 1) + ip := utils.NewAtomic(netip.AddrPortFrom( + netip.IPv6Loopback(), + 1, + )) tls := tlsCert.PrivateKey.(crypto.Signer) bls, err := bls.NewSecretKey() require.NoError(err) diff --git a/network/peer/test_peer.go b/network/peer/test_peer.go index a4df06b72ee0..ae03594f8e67 100644 --- a/network/peer/test_peer.go +++ b/network/peer/test_peer.go @@ -7,6 +7,7 @@ import ( "context" "crypto" "net" + "net/netip" "time" "github.com/prometheus/client_golang/prometheus" @@ -19,9 +20,9 @@ import ( "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/staking" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math/meter" "github.com/ava-labs/avalanchego/utils/resource" @@ -47,7 +48,7 @@ const maxMessageToSend = 1024 // peer. func StartTestPeer( ctx context.Context, - ip ips.IPPort, + ip netip.AddrPort, networkID uint32, router router.InboundHandler, ) (Peer, error) { @@ -98,7 +99,6 @@ func StartTestPeer( return nil, err } - signerIP := ips.NewDynamicIPPort(net.IPv6zero, 1) tlsKey := tlsCert.PrivateKey.(crypto.Signer) blsKey, err := bls.NewSecretKey() if err != nil { @@ -123,7 +123,14 @@ func StartTestPeer( MaxClockDifference: time.Minute, ResourceTracker: resourceTracker, UptimeCalculator: uptime.NoOpCalculator, - IPSigner: NewIPSigner(signerIP, tlsKey, blsKey), + IPSigner: NewIPSigner( + utils.NewAtomic(netip.AddrPortFrom( + netip.IPv6Loopback(), + 1, + )), + tlsKey, + blsKey, + ), }, conn, cert, diff --git a/network/test_network.go b/network/test_network.go index 6a6bcdfcc08c..8644eb359ae1 100644 --- a/network/test_network.go +++ b/network/test_network.go @@ -8,6 +8,7 @@ import ( "errors" "math" "net" + "net/netip" "runtime" "sync" @@ -24,9 +25,9 @@ import ( "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/staking" "github.com/ava-labs/avalanchego/subnets" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math/meter" "github.com/ava-labs/avalanchego/utils/resource" @@ -89,114 +90,20 @@ func NewTestNetwork( return nil, err } - networkConfig := Config{ - ThrottlerConfig: ThrottlerConfig{ - InboundConnUpgradeThrottlerConfig: throttling.InboundConnUpgradeThrottlerConfig{ - UpgradeCooldown: constants.DefaultInboundConnUpgradeThrottlerCooldown, - MaxRecentConnsUpgraded: int(math.Ceil(constants.DefaultInboundThrottlerMaxConnsPerSec * constants.DefaultInboundConnUpgradeThrottlerCooldown.Seconds())), - }, - - InboundMsgThrottlerConfig: throttling.InboundMsgThrottlerConfig{ - MsgByteThrottlerConfig: throttling.MsgByteThrottlerConfig{ - VdrAllocSize: constants.DefaultInboundThrottlerVdrAllocSize, - AtLargeAllocSize: constants.DefaultInboundThrottlerAtLargeAllocSize, - NodeMaxAtLargeBytes: constants.DefaultInboundThrottlerNodeMaxAtLargeBytes, - }, - - BandwidthThrottlerConfig: throttling.BandwidthThrottlerConfig{ - RefillRate: constants.DefaultInboundThrottlerBandwidthRefillRate, - MaxBurstSize: constants.DefaultInboundThrottlerBandwidthMaxBurstSize, - }, - - CPUThrottlerConfig: throttling.SystemThrottlerConfig{ - MaxRecheckDelay: constants.DefaultInboundThrottlerCPUMaxRecheckDelay, - }, - - DiskThrottlerConfig: throttling.SystemThrottlerConfig{ - MaxRecheckDelay: constants.DefaultInboundThrottlerDiskMaxRecheckDelay, - }, - - MaxProcessingMsgsPerNode: constants.DefaultInboundThrottlerMaxProcessingMsgsPerNode, - }, - OutboundMsgThrottlerConfig: throttling.MsgByteThrottlerConfig{ - VdrAllocSize: constants.DefaultOutboundThrottlerVdrAllocSize, - AtLargeAllocSize: constants.DefaultOutboundThrottlerAtLargeAllocSize, - NodeMaxAtLargeBytes: constants.DefaultOutboundThrottlerNodeMaxAtLargeBytes, - }, - - MaxInboundConnsPerSec: constants.DefaultInboundThrottlerMaxConnsPerSec, - }, - - HealthConfig: HealthConfig{ - Enabled: true, - MinConnectedPeers: constants.DefaultNetworkHealthMinPeers, - MaxTimeSinceMsgReceived: constants.DefaultNetworkHealthMaxTimeSinceMsgReceived, - MaxTimeSinceMsgSent: constants.DefaultNetworkHealthMaxTimeSinceMsgSent, - MaxPortionSendQueueBytesFull: constants.DefaultNetworkHealthMaxPortionSendQueueFill, - MaxSendFailRate: constants.DefaultNetworkHealthMaxSendFailRate, - SendFailRateHalflife: constants.DefaultHealthCheckAveragerHalflife, - }, - - ProxyEnabled: constants.DefaultNetworkTCPProxyEnabled, - ProxyReadHeaderTimeout: constants.DefaultNetworkTCPProxyReadTimeout, - - DialerConfig: dialer.Config{ - ThrottleRps: constants.DefaultOutboundConnectionThrottlingRps, - ConnectionTimeout: constants.DefaultOutboundConnectionTimeout, - }, - - TimeoutConfig: TimeoutConfig{ - PingPongTimeout: constants.DefaultPingPongTimeout, - ReadHandshakeTimeout: constants.DefaultNetworkReadHandshakeTimeout, - }, - - PeerListGossipConfig: PeerListGossipConfig{ - PeerListNumValidatorIPs: constants.DefaultNetworkPeerListNumValidatorIPs, - PeerListPullGossipFreq: constants.DefaultNetworkPeerListPullGossipFreq, - PeerListBloomResetFreq: constants.DefaultNetworkPeerListBloomResetFreq, - }, - - DelayConfig: DelayConfig{ - InitialReconnectDelay: constants.DefaultNetworkInitialReconnectDelay, - MaxReconnectDelay: constants.DefaultNetworkMaxReconnectDelay, - }, - - MaxClockDifference: constants.DefaultNetworkMaxClockDifference, - CompressionType: constants.DefaultNetworkCompressionType, - PingFrequency: constants.DefaultPingFrequency, - AllowPrivateIPs: !constants.ProductionNetworkIDs.Contains(networkID), - UptimeMetricFreq: constants.DefaultUptimeMetricFreq, - MaximumInboundMessageTimeout: constants.DefaultNetworkMaximumInboundTimeout, - - RequireValidatorToConnect: constants.DefaultNetworkRequireValidatorToConnect, - PeerReadBufferSize: constants.DefaultNetworkPeerReadBufferSize, - PeerWriteBufferSize: constants.DefaultNetworkPeerWriteBufferSize, - } - - networkConfig.NetworkID = networkID - networkConfig.TrackedSubnets = trackedSubnets - tlsCert, err := staking.NewTLSCert() if err != nil { return nil, err } - tlsConfig := peer.TLSConfig(*tlsCert, nil) - networkConfig.TLSConfig = tlsConfig - networkConfig.TLSKey = tlsCert.PrivateKey.(crypto.Signer) - networkConfig.BLSKey, err = bls.NewSecretKey() + + blsKey, err := bls.NewSecretKey() if err != nil { return nil, err } - networkConfig.Validators = currentValidators - networkConfig.Beacons = validators.NewManager() - // This never actually does anything because we never initialize the P-chain - networkConfig.UptimeCalculator = uptime.NoOpCalculator - // TODO actually monitor usage // TestNetwork doesn't use disk so we don't need to track it, but we should // still have guardrails around cpu/memory usage. - networkConfig.ResourceTracker, err = tracker.NewResourceTracker( + resourceTracker, err := tracker.NewResourceTracker( metrics, resource.NoUsage, &meter.ContinuousFactory{}, @@ -205,31 +112,110 @@ func NewTestNetwork( if err != nil { return nil, err } - networkConfig.CPUTargeter = tracker.NewTargeter( - logging.NoLog{}, - &tracker.TargeterConfig{ - VdrAlloc: float64(runtime.NumCPU()), - MaxNonVdrUsage: .8 * float64(runtime.NumCPU()), - MaxNonVdrNodeUsage: float64(runtime.NumCPU()) / 8, - }, - currentValidators, - networkConfig.ResourceTracker.CPUTracker(), - ) - networkConfig.DiskTargeter = tracker.NewTargeter( - logging.NoLog{}, - &tracker.TargeterConfig{ - VdrAlloc: 1000 * units.GiB, - MaxNonVdrUsage: 1000 * units.GiB, - MaxNonVdrNodeUsage: 1000 * units.GiB, - }, - currentValidators, - networkConfig.ResourceTracker.DiskTracker(), - ) - - networkConfig.MyIPPort = ips.NewDynamicIPPort(net.IPv4zero, 1) return NewNetwork( - &networkConfig, + &Config{ + HealthConfig: HealthConfig{ + Enabled: true, + MinConnectedPeers: constants.DefaultNetworkHealthMinPeers, + MaxTimeSinceMsgReceived: constants.DefaultNetworkHealthMaxTimeSinceMsgReceived, + MaxTimeSinceMsgSent: constants.DefaultNetworkHealthMaxTimeSinceMsgSent, + MaxPortionSendQueueBytesFull: constants.DefaultNetworkHealthMaxPortionSendQueueFill, + MaxSendFailRate: constants.DefaultNetworkHealthMaxSendFailRate, + SendFailRateHalflife: constants.DefaultHealthCheckAveragerHalflife, + }, + PeerListGossipConfig: PeerListGossipConfig{ + PeerListNumValidatorIPs: constants.DefaultNetworkPeerListNumValidatorIPs, + PeerListPullGossipFreq: constants.DefaultNetworkPeerListPullGossipFreq, + PeerListBloomResetFreq: constants.DefaultNetworkPeerListBloomResetFreq, + }, + TimeoutConfig: TimeoutConfig{ + PingPongTimeout: constants.DefaultPingPongTimeout, + ReadHandshakeTimeout: constants.DefaultNetworkReadHandshakeTimeout, + }, + DelayConfig: DelayConfig{ + InitialReconnectDelay: constants.DefaultNetworkInitialReconnectDelay, + MaxReconnectDelay: constants.DefaultNetworkMaxReconnectDelay, + }, + ThrottlerConfig: ThrottlerConfig{ + InboundConnUpgradeThrottlerConfig: throttling.InboundConnUpgradeThrottlerConfig{ + UpgradeCooldown: constants.DefaultInboundConnUpgradeThrottlerCooldown, + MaxRecentConnsUpgraded: int(math.Ceil(constants.DefaultInboundThrottlerMaxConnsPerSec * constants.DefaultInboundConnUpgradeThrottlerCooldown.Seconds())), + }, + InboundMsgThrottlerConfig: throttling.InboundMsgThrottlerConfig{ + MsgByteThrottlerConfig: throttling.MsgByteThrottlerConfig{ + VdrAllocSize: constants.DefaultInboundThrottlerVdrAllocSize, + AtLargeAllocSize: constants.DefaultInboundThrottlerAtLargeAllocSize, + NodeMaxAtLargeBytes: constants.DefaultInboundThrottlerNodeMaxAtLargeBytes, + }, + BandwidthThrottlerConfig: throttling.BandwidthThrottlerConfig{ + RefillRate: constants.DefaultInboundThrottlerBandwidthRefillRate, + MaxBurstSize: constants.DefaultInboundThrottlerBandwidthMaxBurstSize, + }, + CPUThrottlerConfig: throttling.SystemThrottlerConfig{ + MaxRecheckDelay: constants.DefaultInboundThrottlerCPUMaxRecheckDelay, + }, + DiskThrottlerConfig: throttling.SystemThrottlerConfig{ + MaxRecheckDelay: constants.DefaultInboundThrottlerDiskMaxRecheckDelay, + }, + MaxProcessingMsgsPerNode: constants.DefaultInboundThrottlerMaxProcessingMsgsPerNode, + }, + OutboundMsgThrottlerConfig: throttling.MsgByteThrottlerConfig{ + VdrAllocSize: constants.DefaultOutboundThrottlerVdrAllocSize, + AtLargeAllocSize: constants.DefaultOutboundThrottlerAtLargeAllocSize, + NodeMaxAtLargeBytes: constants.DefaultOutboundThrottlerNodeMaxAtLargeBytes, + }, + MaxInboundConnsPerSec: constants.DefaultInboundThrottlerMaxConnsPerSec, + }, + ProxyEnabled: constants.DefaultNetworkTCPProxyEnabled, + ProxyReadHeaderTimeout: constants.DefaultNetworkTCPProxyReadTimeout, + DialerConfig: dialer.Config{ + ThrottleRps: constants.DefaultOutboundConnectionThrottlingRps, + ConnectionTimeout: constants.DefaultOutboundConnectionTimeout, + }, + TLSConfig: peer.TLSConfig(*tlsCert, nil), + MyIPPort: utils.NewAtomic(netip.AddrPortFrom( + netip.IPv4Unspecified(), + 1, + )), + NetworkID: networkID, + MaxClockDifference: constants.DefaultNetworkMaxClockDifference, + PingFrequency: constants.DefaultPingFrequency, + AllowPrivateIPs: !constants.ProductionNetworkIDs.Contains(networkID), + CompressionType: constants.DefaultNetworkCompressionType, + TLSKey: tlsCert.PrivateKey.(crypto.Signer), + BLSKey: blsKey, + TrackedSubnets: trackedSubnets, + Beacons: validators.NewManager(), + Validators: currentValidators, + UptimeCalculator: uptime.NoOpCalculator, + UptimeMetricFreq: constants.DefaultUptimeMetricFreq, + RequireValidatorToConnect: constants.DefaultNetworkRequireValidatorToConnect, + MaximumInboundMessageTimeout: constants.DefaultNetworkMaximumInboundTimeout, + PeerReadBufferSize: constants.DefaultNetworkPeerReadBufferSize, + PeerWriteBufferSize: constants.DefaultNetworkPeerWriteBufferSize, + ResourceTracker: resourceTracker, + CPUTargeter: tracker.NewTargeter( + logging.NoLog{}, + &tracker.TargeterConfig{ + VdrAlloc: float64(runtime.NumCPU()), + MaxNonVdrUsage: .8 * float64(runtime.NumCPU()), + MaxNonVdrNodeUsage: float64(runtime.NumCPU()) / 8, + }, + currentValidators, + resourceTracker.CPUTracker(), + ), + DiskTargeter: tracker.NewTargeter( + logging.NoLog{}, + &tracker.TargeterConfig{ + VdrAlloc: 1000 * units.GiB, + MaxNonVdrUsage: 1000 * units.GiB, + MaxNonVdrNodeUsage: 1000 * units.GiB, + }, + currentValidators, + resourceTracker.DiskTracker(), + ), + }, msgCreator, metrics, log, diff --git a/network/throttling/inbound_conn_upgrade_throttler.go b/network/throttling/inbound_conn_upgrade_throttler.go index 4df5ee39b776..4067d80b2b29 100644 --- a/network/throttling/inbound_conn_upgrade_throttler.go +++ b/network/throttling/inbound_conn_upgrade_throttler.go @@ -4,10 +4,10 @@ package throttling import ( + "net/netip" "sync" "time" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" @@ -36,7 +36,7 @@ type InboundConnUpgradeThrottler interface { // Must only be called after [Dispatch] has been called. // If [ip] is a local IP, this method always returns true. // Must not be called after [Stop] has been called. - ShouldUpgrade(ip ips.IPPort) bool + ShouldUpgrade(ip netip.AddrPort) bool } type InboundConnUpgradeThrottlerConfig struct { @@ -73,12 +73,12 @@ func (*noInboundConnUpgradeThrottler) Dispatch() {} func (*noInboundConnUpgradeThrottler) Stop() {} -func (*noInboundConnUpgradeThrottler) ShouldUpgrade(ips.IPPort) bool { +func (*noInboundConnUpgradeThrottler) ShouldUpgrade(netip.AddrPort) bool { return true } type ipAndTime struct { - ip string + ip netip.Addr cooldownElapsedAt time.Time } @@ -92,7 +92,7 @@ type inboundConnUpgradeThrottler struct { done chan struct{} // IP --> Present if ShouldUpgrade(ipStr) returned true // within the last [UpgradeCooldown]. - recentIPs set.Set[string] + recentIPs set.Set[netip.Addr] // Sorted in order of increasing time // of last call to ShouldUpgrade that returned true. // For each IP in this channel, ShouldUpgrade(ipStr) @@ -101,28 +101,29 @@ type inboundConnUpgradeThrottler struct { } // Returns whether we should upgrade an inbound connection from [ipStr]. -func (n *inboundConnUpgradeThrottler) ShouldUpgrade(ip ips.IPPort) bool { - if ip.IP.IsLoopback() { +func (n *inboundConnUpgradeThrottler) ShouldUpgrade(addrPort netip.AddrPort) bool { + // Only use addr (not port). This mitigates DoS attacks from many nodes on one + // host. + addr := addrPort.Addr() + if addr.IsLoopback() { // Don't rate-limit loopback IPs return true } - // Only use IP (not port). This mitigates DoS - // attacks from many nodes on one host. - ipStr := ip.IP.String() + n.lock.Lock() defer n.lock.Unlock() - if n.recentIPs.Contains(ipStr) { + if n.recentIPs.Contains(addr) { // We recently upgraded an inbound connection from this IP return false } select { case n.recentIPsAndTimes <- ipAndTime{ - ip: ipStr, + ip: addr, cooldownElapsedAt: n.clock.Time().Add(n.UpgradeCooldown), }: - n.recentIPs.Add(ipStr) + n.recentIPs.Add(addr) return true default: return false diff --git a/network/throttling/inbound_conn_upgrade_throttler_test.go b/network/throttling/inbound_conn_upgrade_throttler_test.go index 2f6cd926451e..802593db8b02 100644 --- a/network/throttling/inbound_conn_upgrade_throttler_test.go +++ b/network/throttling/inbound_conn_upgrade_throttler_test.go @@ -4,22 +4,21 @@ package throttling import ( - "net" + "net/netip" "testing" "time" "github.com/stretchr/testify/require" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" ) var ( - host1 = ips.IPPort{IP: net.IPv4(1, 2, 3, 4), Port: 9651} - host2 = ips.IPPort{IP: net.IPv4(1, 2, 3, 5), Port: 9653} - host3 = ips.IPPort{IP: net.IPv4(1, 2, 3, 6), Port: 9655} - host4 = ips.IPPort{IP: net.IPv4(1, 2, 3, 7), Port: 9657} - loopbackIP = ips.IPPort{IP: net.IPv4(127, 0, 0, 1), Port: 9657} + host1 = netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 9651) + host2 = netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 5}), 9653) + host3 = netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 6}), 9655) + host4 = netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 7}), 9657) + loopbackIP = netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 9657) ) func TestNoInboundConnUpgradeThrottler(t *testing.T) { diff --git a/network/tracked_ip.go b/network/tracked_ip.go index 6a95bbee5a47..87377ec669e5 100644 --- a/network/tracked_ip.go +++ b/network/tracked_ip.go @@ -5,10 +5,9 @@ package network import ( "math/rand" + "net/netip" "sync" "time" - - "github.com/ava-labs/avalanchego/utils/ips" ) func init() { @@ -19,20 +18,20 @@ type trackedIP struct { delayLock sync.RWMutex delay time.Duration - ip ips.IPPort + ip netip.AddrPort stopTrackingOnce sync.Once onStopTracking chan struct{} } -func newTrackedIP(ip ips.IPPort) *trackedIP { +func newTrackedIP(ip netip.AddrPort) *trackedIP { return &trackedIP{ ip: ip, onStopTracking: make(chan struct{}), } } -func (ip *trackedIP) trackNewIP(newIP ips.IPPort) *trackedIP { +func (ip *trackedIP) trackNewIP(newIP netip.AddrPort) *trackedIP { ip.stopTracking() return &trackedIP{ delay: ip.getDelay(), diff --git a/network/tracked_ip_test.go b/network/tracked_ip_test.go index 90207e48a6e9..4e735668ecc9 100644 --- a/network/tracked_ip_test.go +++ b/network/tracked_ip_test.go @@ -4,7 +4,7 @@ package network import ( - "net" + "net/netip" "testing" "time" @@ -17,6 +17,11 @@ import ( var ( ip *ips.ClaimedIPPort otherIP *ips.ClaimedIPPort + + defaultLoopbackAddrPort = netip.AddrPortFrom( + netip.AddrFrom4([4]byte{127, 0, 0, 1}), + 9651, + ) ) func init() { @@ -31,10 +36,7 @@ func init() { } ip = ips.NewClaimedIPPort( stakingCert, - ips.IPPort{ - IP: net.IPv4(127, 0, 0, 1), - Port: 9651, - }, + defaultLoopbackAddrPort, 1, // timestamp nil, // signature ) @@ -51,10 +53,7 @@ func init() { } otherIP = ips.NewClaimedIPPort( stakingCert, - ips.IPPort{ - IP: net.IPv4(127, 0, 0, 1), - Port: 9651, - }, + defaultLoopbackAddrPort, 1, // timestamp nil, // signature ) diff --git a/node/config.go b/node/config.go index f5f8c1332530..6f8d3e1ec549 100644 --- a/node/config.go +++ b/node/config.go @@ -5,6 +5,7 @@ package node import ( "crypto/tls" + "net/netip" "time" "github.com/ava-labs/avalanchego/api/server" @@ -18,7 +19,6 @@ import ( "github.com/ava-labs/avalanchego/subnets" "github.com/ava-labs/avalanchego/trace" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/profiler" "github.com/ava-labs/avalanchego/utils/set" @@ -85,8 +85,8 @@ type StakingConfig struct { } type StateSyncConfig struct { - StateSyncIDs []ids.NodeID `json:"stateSyncIDs"` - StateSyncIPs []ips.IPPort `json:"stateSyncIPs"` + StateSyncIDs []ids.NodeID `json:"stateSyncIDs"` + StateSyncIPs []netip.AddrPort `json:"stateSyncIPs"` } type BootstrapConfig struct { diff --git a/node/node.go b/node/node.go index 09fb05d06e86..63946140258d 100644 --- a/node/node.go +++ b/node/node.go @@ -13,6 +13,7 @@ import ( "io" "io/fs" "net" + "net/netip" "os" "path/filepath" "strconv" @@ -437,20 +438,26 @@ func (n *Node) initNetworking(reg prometheus.Registerer) error { // Record the bound address to enable inclusion in process context file. n.stakingAddress = listener.Addr().String() - ipPort, err := ips.ToIPPort(n.stakingAddress) + stakingAddrPort, err := ips.ParseAddrPort(n.stakingAddress) if err != nil { return err } - var dynamicIP ips.DynamicIPPort + var ( + publicAddr netip.Addr + atomicIP *utils.Atomic[netip.AddrPort] + ) switch { case n.Config.PublicIP != "": // Use the specified public IP. - ipPort.IP = net.ParseIP(n.Config.PublicIP) - if ipPort.IP == nil { - return fmt.Errorf("invalid IP Address: %s", n.Config.PublicIP) + publicAddr, err = ips.ParseAddr(n.Config.PublicIP) + if err != nil { + return fmt.Errorf("invalid public IP address %q: %w", n.Config.PublicIP, err) } - dynamicIP = ips.NewDynamicIPPort(ipPort.IP, ipPort.Port) + atomicIP = utils.NewAtomic(netip.AddrPortFrom( + publicAddr, + stakingAddrPort.Port(), + )) n.ipUpdater = dynamicip.NewNoUpdater() case n.Config.PublicIPResolutionService != "": // Use dynamic IP resolution. @@ -461,40 +468,46 @@ func (n *Node) initNetworking(reg prometheus.Registerer) error { // Use that to resolve our public IP. ctx, cancel := context.WithTimeout(context.Background(), ipResolutionTimeout) - ipPort.IP, err = resolver.Resolve(ctx) + publicAddr, err = resolver.Resolve(ctx) cancel() if err != nil { return fmt.Errorf("couldn't resolve public IP: %w", err) } - dynamicIP = ips.NewDynamicIPPort(ipPort.IP, ipPort.Port) - n.ipUpdater = dynamicip.NewUpdater(dynamicIP, resolver, n.Config.PublicIPResolutionFreq) + atomicIP = utils.NewAtomic(netip.AddrPortFrom( + publicAddr, + stakingAddrPort.Port(), + )) + n.ipUpdater = dynamicip.NewUpdater(atomicIP, resolver, n.Config.PublicIPResolutionFreq) default: - ipPort.IP, err = n.router.ExternalIP() + publicAddr, err = n.router.ExternalIP() if err != nil { return fmt.Errorf("public IP / IP resolution service not given and failed to resolve IP with NAT: %w", err) } - dynamicIP = ips.NewDynamicIPPort(ipPort.IP, ipPort.Port) + atomicIP = utils.NewAtomic(netip.AddrPortFrom( + publicAddr, + stakingAddrPort.Port(), + )) n.ipUpdater = dynamicip.NewNoUpdater() } - if ipPort.IP.IsLoopback() || ipPort.IP.IsPrivate() { + if !ips.IsPublic(publicAddr) { n.Log.Warn("P2P IP is private, you will not be publicly discoverable", - zap.Stringer("ip", ipPort), + zap.Stringer("ip", publicAddr), ) } // Regularly update our public IP and port mappings. n.portMapper.Map( - ipPort.Port, - ipPort.Port, + stakingAddrPort.Port(), + stakingAddrPort.Port(), stakingPortName, - dynamicIP, + atomicIP, n.Config.PublicIPResolutionFreq, ) go n.ipUpdater.Dispatch(n.Log) n.Log.Info("initializing networking", - zap.Stringer("ip", ipPort), + zap.Stringer("ip", atomicIP.Get()), ) tlsKey, ok := n.Config.StakingTLSCert.PrivateKey.(crypto.Signer) @@ -617,7 +630,7 @@ func (n *Node) initNetworking(reg prometheus.Registerer) error { // add node configs to network config n.Config.NetworkConfig.MyNodeID = n.ID - n.Config.NetworkConfig.MyIPPort = dynamicIP + n.Config.NetworkConfig.MyIPPort = atomicIP n.Config.NetworkConfig.NetworkID = n.Config.NetworkID n.Config.NetworkConfig.Validators = n.vdrs n.Config.NetworkConfig.Beacons = n.bootstrappers @@ -709,7 +722,7 @@ func (n *Node) Dispatch() error { // Add bootstrap nodes to the peer network for _, bootstrapper := range n.Config.Bootstrappers { - n.Net.ManuallyTrack(bootstrapper.ID, ips.IPPort(bootstrapper.IP)) + n.Net.ManuallyTrack(bootstrapper.ID, bootstrapper.IP) } // Start P2P connections @@ -962,7 +975,7 @@ func (n *Node) initAPIServer() error { ) return err } - hostIsPublic = !ip.IsLoopback() && !ip.IsPrivate() + hostIsPublic = ips.IsPublic(ip) n.Log.Debug("finished HTTP host lookup", zap.String("host", n.Config.HTTPHost), @@ -977,8 +990,8 @@ func (n *Node) initAPIServer() error { return err } - addr := listener.Addr().String() - ipPort, err := ips.ToIPPort(addr) + addrStr := listener.Addr().String() + addrPort, err := ips.ParseAddrPort(addrStr) if err != nil { return err } @@ -991,8 +1004,8 @@ func (n *Node) initAPIServer() error { ) n.portMapper.Map( - ipPort.Port, - ipPort.Port, + addrPort.Port(), + addrPort.Port(), httpPortName, nil, n.Config.PublicIPResolutionFreq, diff --git a/utils/atomic.go b/utils/atomic.go index 3bb125ee8af6..7236d9a50de8 100644 --- a/utils/atomic.go +++ b/utils/atomic.go @@ -3,13 +3,27 @@ package utils -import "sync" +import ( + "encoding/json" + "sync" +) + +var ( + _ json.Marshaler = (*Atomic[struct{}])(nil) + _ json.Unmarshaler = (*Atomic[struct{}])(nil) +) type Atomic[T any] struct { lock sync.RWMutex value T } +func NewAtomic[T any](value T) *Atomic[T] { + return &Atomic[T]{ + value: value, + } +} + func (a *Atomic[T]) Get() T { a.lock.RLock() defer a.lock.RUnlock() @@ -23,3 +37,17 @@ func (a *Atomic[T]) Set(value T) { a.value = value } + +func (a *Atomic[T]) MarshalJSON() ([]byte, error) { + a.lock.RLock() + defer a.lock.RUnlock() + + return json.Marshal(a.value) +} + +func (a *Atomic[T]) UnmarshalJSON(b []byte) error { + a.lock.Lock() + defer a.lock.Unlock() + + return json.Unmarshal(b, &a.value) +} diff --git a/utils/atomic_test.go b/utils/atomic_test.go index 3fa74063c18a..eee159d783f9 100644 --- a/utils/atomic_test.go +++ b/utils/atomic_test.go @@ -4,6 +4,8 @@ package utils import ( + "encoding/json" + "net/netip" "testing" "github.com/stretchr/testify/require" @@ -24,3 +26,46 @@ func TestAtomic(t *testing.T) { a.Set(false) require.False(a.Get()) } + +func TestAtomicJSON(t *testing.T) { + tests := []struct { + name string + value *Atomic[netip.AddrPort] + expected string + }{ + { + name: "zero value", + value: new(Atomic[netip.AddrPort]), + expected: `""`, + }, + { + name: "ipv4 value", + value: NewAtomic(netip.AddrPortFrom( + netip.AddrFrom4([4]byte{1, 2, 3, 4}), + 12345, + )), + expected: `"1.2.3.4:12345"`, + }, + { + name: "ipv6 loopback", + value: NewAtomic(netip.AddrPortFrom( + netip.IPv6Loopback(), + 12345, + )), + expected: `"[::1]:12345"`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + b, err := json.Marshal(test.value) + require.NoError(err) + require.Equal(test.expected, string(b)) + + var parsed Atomic[netip.AddrPort] + require.NoError(json.Unmarshal([]byte(test.expected), &parsed)) + require.Equal(test.value.Get(), parsed.Get()) + }) + } +} diff --git a/utils/beacon/beacon.go b/utils/beacon/beacon.go index 38ac6df5b0f5..112c50f6db22 100644 --- a/utils/beacon/beacon.go +++ b/utils/beacon/beacon.go @@ -4,23 +4,24 @@ package beacon import ( + "net/netip" + "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/ips" ) var _ Beacon = (*beacon)(nil) type Beacon interface { ID() ids.NodeID - IP() ips.IPPort + IP() netip.AddrPort } type beacon struct { id ids.NodeID - ip ips.IPPort + ip netip.AddrPort } -func New(id ids.NodeID, ip ips.IPPort) Beacon { +func New(id ids.NodeID, ip netip.AddrPort) Beacon { return &beacon{ id: id, ip: ip, @@ -31,6 +32,6 @@ func (b *beacon) ID() ids.NodeID { return b.id } -func (b *beacon) IP() ips.IPPort { +func (b *beacon) IP() netip.AddrPort { return b.ip } diff --git a/utils/beacon/set.go b/utils/beacon/set.go index 8b6970b55421..56a292203ed5 100644 --- a/utils/beacon/set.go +++ b/utils/beacon/set.go @@ -5,10 +5,10 @@ package beacon import ( "errors" + "net/netip" "strings" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/ips" ) var ( @@ -25,7 +25,7 @@ type Set interface { Add(Beacon) error RemoveByID(ids.NodeID) error - RemoveByIP(ips.IPPort) error + RemoveByIP(netip.AddrPort) error Len() int @@ -35,14 +35,14 @@ type Set interface { type set struct { ids map[ids.NodeID]int - ips map[string]int + ips map[netip.AddrPort]int beacons []Beacon } func NewSet() Set { return &set{ ids: make(map[ids.NodeID]int), - ips: make(map[string]int), + ips: make(map[netip.AddrPort]int), } } @@ -53,14 +53,14 @@ func (s *set) Add(b Beacon) error { return errDuplicateID } - ipStr := b.IP().String() - _, duplicateIP := s.ips[ipStr] + ip := b.IP() + _, duplicateIP := s.ips[ip] if duplicateIP { return errDuplicateIP } s.ids[id] = len(s.beacons) - s.ips[ipStr] = len(s.beacons) + s.ips[ip] = len(s.beacons) s.beacons = append(s.beacons, b) return nil } @@ -71,12 +71,12 @@ func (s *set) RemoveByID(idToRemove ids.NodeID) error { return errUnknownID } toRemove := s.beacons[indexToRemove] - ipToRemove := toRemove.IP().String() + ipToRemove := toRemove.IP() indexToMove := len(s.beacons) - 1 toMove := s.beacons[indexToMove] idToMove := toMove.ID() - ipToMove := toMove.IP().String() + ipToMove := toMove.IP() s.ids[idToMove] = indexToRemove s.ips[ipToMove] = indexToRemove @@ -89,8 +89,8 @@ func (s *set) RemoveByID(idToRemove ids.NodeID) error { return nil } -func (s *set) RemoveByIP(ip ips.IPPort) error { - indexToRemove, exists := s.ips[ip.String()] +func (s *set) RemoveByIP(ip netip.AddrPort) error { + indexToRemove, exists := s.ips[ip] if !exists { return errUnknownIP } diff --git a/utils/beacon/set_test.go b/utils/beacon/set_test.go index 976d0582e3ff..04e250909fb5 100644 --- a/utils/beacon/set_test.go +++ b/utils/beacon/set_test.go @@ -4,13 +4,12 @@ package beacon import ( - "net" + "net/netip" "testing" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/ips" ) func TestSet(t *testing.T) { @@ -20,18 +19,18 @@ func TestSet(t *testing.T) { id1 := ids.BuildTestNodeID([]byte{1}) id2 := ids.BuildTestNodeID([]byte{2}) - ip0 := ips.IPPort{ - IP: net.IPv4zero, - Port: 0, - } - ip1 := ips.IPPort{ - IP: net.IPv4zero, - Port: 1, - } - ip2 := ips.IPPort{ - IP: net.IPv4zero, - Port: 2, - } + ip0 := netip.AddrPortFrom( + netip.IPv4Unspecified(), + 0, + ) + ip1 := netip.AddrPortFrom( + netip.IPv4Unspecified(), + 1, + ) + ip2 := netip.AddrPortFrom( + netip.IPv4Unspecified(), + 2, + ) b0 := New(id0, ip0) b1 := New(id1, ip1) diff --git a/utils/dynamicip/ifconfig_resolver.go b/utils/dynamicip/ifconfig_resolver.go index 36c8d5adf04c..754a17f86a31 100644 --- a/utils/dynamicip/ifconfig_resolver.go +++ b/utils/dynamicip/ifconfig_resolver.go @@ -7,9 +7,11 @@ import ( "context" "fmt" "io" - "net" "net/http" + "net/netip" "strings" + + "github.com/ava-labs/avalanchego/utils/ips" ) var _ Resolver = (*ifConfigResolver)(nil) @@ -19,29 +21,24 @@ type ifConfigResolver struct { url string } -func (r *ifConfigResolver) Resolve(ctx context.Context) (net.IP, error) { +func (r *ifConfigResolver) Resolve(ctx context.Context) (netip.Addr, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.url, nil) if err != nil { - return nil, err + return netip.Addr{}, err } resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return netip.Addr{}, err } defer resp.Body.Close() ipBytes, err := io.ReadAll(resp.Body) if err != nil { // Drop any error to report the original error - return nil, fmt.Errorf("failed to read response from %q: %w", r.url, err) + return netip.Addr{}, fmt.Errorf("failed to read response from %q: %w", r.url, err) } - ipStr := string(ipBytes) - ipStr = strings.TrimSpace(ipStr) - ipResolved := net.ParseIP(ipStr) - if ipResolved == nil { - return nil, fmt.Errorf("couldn't parse IP from %q", ipStr) - } - return ipResolved, nil + ipStr := strings.TrimSpace(string(ipBytes)) + return ips.ParseAddr(ipStr) } diff --git a/utils/dynamicip/opendns_resolver.go b/utils/dynamicip/opendns_resolver.go index 5c39c95535fc..ccf75653c81e 100644 --- a/utils/dynamicip/opendns_resolver.go +++ b/utils/dynamicip/opendns_resolver.go @@ -7,6 +7,9 @@ import ( "context" "errors" "net" + "net/netip" + + "github.com/ava-labs/avalanchego/utils/ips" ) const openDNSUrl = "resolver1.opendns.com:53" @@ -34,13 +37,15 @@ func newOpenDNSResolver() Resolver { } } -func (r *openDNSResolver) Resolve(ctx context.Context) (net.IP, error) { - ips, err := r.resolver.LookupIP(ctx, "ip", "myip.opendns.com") +func (r *openDNSResolver) Resolve(ctx context.Context) (netip.Addr, error) { + resolvedIPs, err := r.resolver.LookupIP(ctx, "ip", "myip.opendns.com") if err != nil { - return nil, err + return netip.Addr{}, err } - if len(ips) == 0 { - return nil, errOpenDNSNoIP + for _, ip := range resolvedIPs { + if addr, ok := ips.AddrFromSlice(ip); ok { + return addr, nil + } } - return ips[0], nil + return netip.Addr{}, errOpenDNSNoIP } diff --git a/utils/dynamicip/resolver.go b/utils/dynamicip/resolver.go index 45ad3778bc01..05f8896c7b36 100644 --- a/utils/dynamicip/resolver.go +++ b/utils/dynamicip/resolver.go @@ -7,7 +7,7 @@ import ( "context" "errors" "fmt" - "net" + "net/netip" "strings" ) @@ -29,7 +29,7 @@ var errUnknownResolver = errors.New("unknown resolver") // Resolver resolves our public IP type Resolver interface { // Resolve and return our public IP. - Resolve(context.Context) (net.IP, error) + Resolve(context.Context) (netip.Addr, error) } // Returns a new Resolver that uses the given service diff --git a/utils/dynamicip/updater.go b/utils/dynamicip/updater.go index 9a59c9fd25e0..18c41e2fd742 100644 --- a/utils/dynamicip/updater.go +++ b/utils/dynamicip/updater.go @@ -5,11 +5,12 @@ package dynamicip import ( "context" + "net/netip" "time" "go.uber.org/zap" - "github.com/ava-labs/avalanchego/utils/ips" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" ) @@ -30,7 +31,7 @@ type Updater interface { type updater struct { // The IP we periodically modify. - dynamicIP ips.DynamicIPPort + dynamicIP *utils.Atomic[netip.AddrPort] // Used to find out what our public IP is. resolver Resolver // The parent of all contexts passed into resolver.Resolve(). @@ -49,7 +50,7 @@ type updater struct { // every [updateFreq]. Uses [resolver] to find // out what our public IP is. func NewUpdater( - dynamicIP ips.DynamicIPPort, + dynamicIP *utils.Atomic[netip.AddrPort], resolver Resolver, updateFreq time.Duration, ) Updater { @@ -73,13 +74,16 @@ func (u *updater) Dispatch(log logging.Logger) { close(u.doneChan) }() + var ( + initialAddrPort = u.dynamicIP.Get() + oldAddr = initialAddrPort.Addr() + port = initialAddrPort.Port() + ) for { select { case <-ticker.C: - oldIP := u.dynamicIP.IPPort().IP - ctx, cancel := context.WithTimeout(u.rootCtx, ipResolutionTimeout) - newIP, err := u.resolver.Resolve(ctx) + newAddr, err := u.resolver.Resolve(ctx) cancel() if err != nil { log.Warn("couldn't resolve public IP. If this machine's IP recently changed, it may be sharing the wrong public IP with peers", @@ -88,11 +92,13 @@ func (u *updater) Dispatch(log logging.Logger) { continue } - if !newIP.Equal(oldIP) { - u.dynamicIP.SetIP(newIP) + if newAddr != oldAddr { + u.dynamicIP.Set(netip.AddrPortFrom(newAddr, port)) log.Info("updated public IP", - zap.Stringer("newIP", newIP), + zap.Stringer("oldIP", oldAddr), + zap.Stringer("newIP", newAddr), ) + oldAddr = newAddr } case <-u.rootCtx.Done(): return diff --git a/utils/dynamicip/updater_test.go b/utils/dynamicip/updater_test.go index 66c9a21c4c6a..3d0cbcaceca7 100644 --- a/utils/dynamicip/updater_test.go +++ b/utils/dynamicip/updater_test.go @@ -5,42 +5,51 @@ package dynamicip import ( "context" - "net" + "net/netip" "testing" "time" "github.com/stretchr/testify/require" - "github.com/ava-labs/avalanchego/utils/ips" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" ) var _ Resolver = (*mockResolver)(nil) type mockResolver struct { - onResolve func(context.Context) (net.IP, error) + onResolve func(context.Context) (netip.Addr, error) } -func (r *mockResolver) Resolve(ctx context.Context) (net.IP, error) { +func (r *mockResolver) Resolve(ctx context.Context) (netip.Addr, error) { return r.onResolve(ctx) } func TestNewUpdater(t *testing.T) { require := require.New(t) - originalIP := net.IPv4zero - originalPort := 9651 - dynamicIP := ips.NewDynamicIPPort(originalIP, uint16(originalPort)) - newIP := net.IPv4(1, 2, 3, 4) + + const ( + port = 9651 + updateFrequency = time.Millisecond + stopTimeout = 5 * time.Second + ) + + var ( + originalAddr = netip.IPv4Unspecified() + originalAddrPort = netip.AddrPortFrom(originalAddr, port) + newAddr = netip.AddrFrom4([4]byte{1, 2, 3, 4}) + expectedNewAddrPort = netip.AddrPortFrom(newAddr, port) + dynamicIP = utils.NewAtomic(originalAddrPort) + ) resolver := &mockResolver{ - onResolve: func(context.Context) (net.IP, error) { - return newIP, nil + onResolve: func(context.Context) (netip.Addr, error) { + return newAddr, nil }, } - updateFreq := time.Millisecond updaterIntf := NewUpdater( dynamicIP, resolver, - updateFreq, + updateFrequency, ) // Assert NewUpdater returns expected type @@ -53,28 +62,23 @@ func TestNewUpdater(t *testing.T) { require.NotNil(updater.rootCtx) require.NotNil(updater.rootCtxCancel) require.NotNil(updater.doneChan) - require.Equal(updateFreq, updater.updateFreq) + require.Equal(updateFrequency, updater.updateFreq) // Start updating the IP address - go updaterIntf.Dispatch(logging.NoLog{}) + go updater.Dispatch(logging.NoLog{}) // Assert that the IP is updated within 5s. - expectedIP := ips.IPPort{ - IP: newIP, - Port: uint16(originalPort), - } require.Eventually( func() bool { - return expectedIP.Equal(dynamicIP.IPPort()) + return dynamicIP.Get() == expectedNewAddrPort }, 5*time.Second, - updateFreq, + updateFrequency, ) // Make sure stopChan and doneChan are closed when stop is called - updaterIntf.Stop() + updater.Stop() - stopTimeout := 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), stopTimeout) defer cancel() select { diff --git a/utils/ips/claimed_ip_port.go b/utils/ips/claimed_ip_port.go index 2ef6c0a71087..3721de9d624c 100644 --- a/utils/ips/claimed_ip_port.go +++ b/utils/ips/claimed_ip_port.go @@ -4,6 +4,9 @@ package ips import ( + "net" + "net/netip" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/staking" "github.com/ava-labs/avalanchego/utils/hashing" @@ -12,7 +15,7 @@ import ( const ( // Certificate length, signature length, IP, timestamp, tx ID - baseIPCertDescLen = 2*wrappers.IntLen + IPPortLen + wrappers.LongLen + ids.IDLen + baseIPCertDescLen = 2*wrappers.IntLen + net.IPv6len + wrappers.ShortLen + wrappers.LongLen + ids.IDLen preimageLen = ids.IDLen + wrappers.LongLen ) @@ -22,7 +25,7 @@ type ClaimedIPPort struct { // The peer's certificate. Cert *staking.Certificate // The peer's claimed IP and port. - IPPort IPPort + AddrPort netip.AddrPort // The time the peer claimed to own this IP and port. Timestamp uint64 // [Cert]'s signature over the IPPort and timestamp. @@ -38,13 +41,13 @@ type ClaimedIPPort struct { func NewClaimedIPPort( cert *staking.Certificate, - ipPort IPPort, + ipPort netip.AddrPort, timestamp uint64, signature []byte, ) *ClaimedIPPort { ip := &ClaimedIPPort{ Cert: cert, - IPPort: ipPort, + AddrPort: ipPort, Timestamp: timestamp, Signature: signature, NodeID: ids.NodeIDFromCert(cert), diff --git a/utils/ips/dynamic_ip_port.go b/utils/ips/dynamic_ip_port.go deleted file mode 100644 index 0b83ab5924f1..000000000000 --- a/utils/ips/dynamic_ip_port.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package ips - -import ( - "encoding/json" - "net" - "sync" -) - -var _ DynamicIPPort = (*dynamicIPPort)(nil) - -// An IPPort that can change. -// Safe for use by multiple goroutines. -type DynamicIPPort interface { - // Returns the IP + port pair. - IPPort() IPPort - // Changes the IP. - SetIP(ip net.IP) -} - -type dynamicIPPort struct { - lock sync.RWMutex - ipPort IPPort -} - -func NewDynamicIPPort(ip net.IP, port uint16) DynamicIPPort { - return &dynamicIPPort{ - ipPort: IPPort{ - IP: ip, - Port: port, - }, - } -} - -func (i *dynamicIPPort) IPPort() IPPort { - i.lock.RLock() - defer i.lock.RUnlock() - - return i.ipPort -} - -func (i *dynamicIPPort) SetIP(ip net.IP) { - i.lock.Lock() - defer i.lock.Unlock() - - i.ipPort.IP = ip -} - -func (i *dynamicIPPort) MarshalJSON() ([]byte, error) { - i.lock.RLock() - defer i.lock.RUnlock() - - return json.Marshal(i.ipPort) -} diff --git a/utils/ips/ip.go b/utils/ips/ip.go new file mode 100644 index 000000000000..2194a0941426 --- /dev/null +++ b/utils/ips/ip.go @@ -0,0 +1,57 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package ips + +import "net/netip" + +// IsPublic returns true if the provided address is considered to be a public +// IP. +func IsPublic(addr netip.Addr) bool { + return addr.IsGlobalUnicast() && !addr.IsPrivate() +} + +// ParseAddr returns the IP address from the provided string. If the string +// represents an IPv4 address in an IPv6 address, the IPv4 address is returned. +func ParseAddr(s string) (netip.Addr, error) { + addr, err := netip.ParseAddr(s) + if err != nil { + return netip.Addr{}, err + } + if addr.Is4In6() { + addr = addr.Unmap() + } + return addr, nil +} + +// ParseAddrPort returns the IP:port address from the provided string. If the +// string represents an IPv4 address in an IPv6 address, the IPv4 address is +// returned. +func ParseAddrPort(s string) (netip.AddrPort, error) { + addrPort, err := netip.ParseAddrPort(s) + if err != nil { + return netip.AddrPort{}, err + } + addr := addrPort.Addr() + if addr.Is4In6() { + addrPort = netip.AddrPortFrom( + addr.Unmap(), + addrPort.Port(), + ) + } + return addrPort, nil +} + +// AddrFromSlice returns the IP address from the provided byte slice. If the +// byte slice represents an IPv4 address in an IPv6 address, the IPv4 address is +// returned. +func AddrFromSlice(b []byte) (netip.Addr, bool) { + addr, ok := netip.AddrFromSlice(b) + if !ok { + return netip.Addr{}, false + } + if addr.Is4In6() { + addr = addr.Unmap() + } + return addr, true +} diff --git a/utils/ips/ip_port.go b/utils/ips/ip_port.go deleted file mode 100644 index eea203525a87..000000000000 --- a/utils/ips/ip_port.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package ips - -import ( - "errors" - "fmt" - "net" - "strconv" - - "github.com/ava-labs/avalanchego/utils/wrappers" -) - -const ( - IPPortLen = net.IPv6len + wrappers.ShortLen - nullStr = "null" -) - -var ( - errMissingQuotes = errors.New("first and last characters should be quotes") - errBadIP = errors.New("bad ip format") -) - -type IPDesc IPPort - -func (ipDesc IPDesc) String() string { - return IPPort(ipDesc).String() -} - -func (ipDesc IPDesc) MarshalJSON() ([]byte, error) { - return []byte(`"` + ipDesc.String() + `"`), nil -} - -func (ipDesc *IPDesc) UnmarshalJSON(b []byte) error { - str := string(b) - if str == nullStr { // If "null", do nothing - return nil - } else if len(str) < 2 { - return errMissingQuotes - } - - lastIndex := len(str) - 1 - if str[0] != '"' || str[lastIndex] != '"' { - return errMissingQuotes - } - - ipPort, err := ToIPPort(str[1:lastIndex]) - if err != nil { - return fmt.Errorf("couldn't decode to IPPort: %w", err) - } - *ipDesc = IPDesc(ipPort) - - return nil -} - -// An IP and a port. -type IPPort struct { - IP net.IP `json:"ip"` - Port uint16 `json:"port"` -} - -func (ipPort IPPort) Equal(other IPPort) bool { - return ipPort.Port == other.Port && ipPort.IP.Equal(other.IP) -} - -func (ipPort IPPort) String() string { - return net.JoinHostPort(ipPort.IP.String(), strconv.FormatUint(uint64(ipPort.Port), 10)) -} - -// IsZero returns if the IP or port is zeroed out -func (ipPort IPPort) IsZero() bool { - ip := ipPort.IP - return ipPort.Port == 0 || - len(ip) == 0 || - ip.Equal(net.IPv4zero) || - ip.Equal(net.IPv6zero) -} - -func ToIPPort(str string) (IPPort, error) { - host, portStr, err := net.SplitHostPort(str) - if err != nil { - return IPPort{}, errBadIP - } - port, err := strconv.ParseUint(portStr, 10 /*=base*/, 16 /*=size*/) - if err != nil { - // TODO: Should this return a locally defined error? (e.g. errBadPort) - return IPPort{}, err - } - ip := net.ParseIP(host) - if ip == nil { - return IPPort{}, errBadIP - } - return IPPort{ - IP: ip, - Port: uint16(port), - }, nil -} - -// PackIP packs an ip port pair to the byte array -func PackIP(p *wrappers.Packer, ip IPPort) { - p.PackFixedBytes(ip.IP.To16()) - p.PackShort(ip.Port) -} diff --git a/utils/ips/ip_test.go b/utils/ips/ip_test.go deleted file mode 100644 index 903f26a2d070..000000000000 --- a/utils/ips/ip_test.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package ips - -import ( - "encoding/json" - "net" - "strconv" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestIPPortEqual(t *testing.T) { - tests := []struct { - ipPort string - ipPort1 IPPort - ipPort2 IPPort - result bool - }{ - // Expected equal - { - `"127.0.0.1:0"`, - IPPort{net.ParseIP("127.0.0.1"), 0}, - IPPort{net.ParseIP("127.0.0.1"), 0}, - true, - }, - { - `"[::1]:0"`, - IPPort{net.ParseIP("::1"), 0}, - IPPort{net.ParseIP("::1"), 0}, - true, - }, - { - `"127.0.0.1:0"`, - IPPort{net.ParseIP("127.0.0.1"), 0}, - IPPort{net.ParseIP("::ffff:127.0.0.1"), 0}, - true, - }, - - // Expected unequal - { - `"127.0.0.1:0"`, - IPPort{net.ParseIP("127.0.0.1"), 0}, - IPPort{net.ParseIP("1.2.3.4"), 0}, - false, - }, - { - `"[::1]:0"`, - IPPort{net.ParseIP("::1"), 0}, - IPPort{net.ParseIP("2001::1"), 0}, - false, - }, - { - `"127.0.0.1:0"`, - IPPort{net.ParseIP("127.0.0.1"), 0}, - IPPort{net.ParseIP("127.0.0.1"), 1}, - false, - }, - } - for i, tt := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - require := require.New(t) - - ipPort := IPDesc{} - require.NoError(ipPort.UnmarshalJSON([]byte(tt.ipPort))) - require.Equal(tt.ipPort1, IPPort(ipPort)) - - ipPortJSON, err := json.Marshal(ipPort) - require.NoError(err) - require.Equal(tt.ipPort, string(ipPortJSON)) - - require.Equal(tt.result, tt.ipPort1.Equal(tt.ipPort2)) - }) - } -} - -func TestIPPortString(t *testing.T) { - tests := []struct { - ipPort IPPort - result string - }{ - {IPPort{net.ParseIP("127.0.0.1"), 0}, "127.0.0.1:0"}, - {IPPort{net.ParseIP("::1"), 42}, "[::1]:42"}, - {IPPort{net.ParseIP("::ffff:127.0.0.1"), 65535}, "127.0.0.1:65535"}, - {IPPort{net.IP{}, 1234}, ":1234"}, - } - for _, tt := range tests { - t.Run(tt.result, func(t *testing.T) { - require.Equal(t, tt.result, tt.ipPort.String()) - }) - } -} - -func TestToIPPortError(t *testing.T) { - tests := []struct { - in string - out IPPort - expectedErr error - }{ - { - in: "", - out: IPPort{}, - expectedErr: errBadIP, - }, - { - in: ":", - out: IPPort{}, - expectedErr: strconv.ErrSyntax, - }, - { - in: "abc:", - out: IPPort{}, - expectedErr: strconv.ErrSyntax, - }, - { - in: ":abc", - out: IPPort{}, - expectedErr: strconv.ErrSyntax, - }, - { - in: "abc:abc", - out: IPPort{}, - expectedErr: strconv.ErrSyntax, - }, - { - in: "127.0.0.1:", - out: IPPort{}, - expectedErr: strconv.ErrSyntax, - }, - { - in: ":1", - out: IPPort{}, - expectedErr: errBadIP, - }, - { - in: "::1", - out: IPPort{}, - expectedErr: errBadIP, - }, - { - in: "::1:42", - out: IPPort{}, - expectedErr: errBadIP, - }, - } - for _, tt := range tests { - t.Run(tt.in, func(t *testing.T) { - require := require.New(t) - - result, err := ToIPPort(tt.in) - require.ErrorIs(err, tt.expectedErr) - require.Equal(tt.out, result) - }) - } -} - -func TestToIPPort(t *testing.T) { - tests := []struct { - in string - out IPPort - }{ - {"127.0.0.1:42", IPPort{net.ParseIP("127.0.0.1"), 42}}, - {"[::1]:42", IPPort{net.ParseIP("::1"), 42}}, - } - for _, tt := range tests { - t.Run(tt.in, func(t *testing.T) { - require := require.New(t) - - result, err := ToIPPort(tt.in) - require.NoError(err) - require.Equal(tt.out, result) - }) - } -} diff --git a/utils/ips/lookup.go b/utils/ips/lookup.go index cdf9176f9568..cf4158d233b3 100644 --- a/utils/ips/lookup.go +++ b/utils/ips/lookup.go @@ -6,6 +6,7 @@ package ips import ( "errors" "net" + "net/netip" ) var errNoIPsFound = errors.New("no IPs found") @@ -15,20 +16,22 @@ var errNoIPsFound = errors.New("no IPs found") // pick any of the IPs. // // Note: IPv4 is preferred because `net.Listen` prefers IPv4. -func Lookup(hostname string) (net.IP, error) { +func Lookup(hostname string) (netip.Addr, error) { ips, err := net.LookupIP(hostname) if err != nil { - return nil, err + return netip.Addr{}, err } if len(ips) == 0 { - return nil, errNoIPsFound + return netip.Addr{}, errNoIPsFound } for _, ip := range ips { ipv4 := ip.To4() if ipv4 != nil { - return ipv4, nil + addr, _ := AddrFromSlice(ipv4) + return addr, nil } } - return ips[0], nil + addr, _ := AddrFromSlice(ips[0]) + return addr, nil } diff --git a/utils/ips/lookup_test.go b/utils/ips/lookup_test.go index 9fecccc54593..4f5621dfce7b 100644 --- a/utils/ips/lookup_test.go +++ b/utils/ips/lookup_test.go @@ -4,7 +4,7 @@ package ips import ( - "net" + "net/netip" "testing" "github.com/stretchr/testify/require" @@ -13,23 +13,23 @@ import ( func TestLookup(t *testing.T) { tests := []struct { host string - ip net.IP + ip netip.Addr }{ { host: "127.0.0.1", - ip: net.ParseIP("127.0.0.1").To4(), + ip: netip.AddrFrom4([4]byte{127, 0, 0, 1}), }, { host: "localhost", - ip: net.ParseIP("127.0.0.1").To4(), + ip: netip.AddrFrom4([4]byte{127, 0, 0, 1}), }, { host: "::", - ip: net.IPv6zero, + ip: netip.IPv6Unspecified(), }, { host: "0.0.0.0", - ip: net.ParseIP("0.0.0.0").To4(), + ip: netip.IPv4Unspecified(), }, } for _, tt := range tests {