From 8110abcb9da7e552407252a6e43891411afd0336 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 10 Apr 2026 18:27:21 +0400 Subject: [PATCH] Better support for ivp6 and more tests --- storage/nat.nim | 7 ++- storage/utils/natutils.nim | 80 ++++++++++++++++++---------------- tests/storage/testnat.nim | 45 ++++++++++++++----- tests/storage/testnatutils.nim | 67 ++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 48 deletions(-) create mode 100644 tests/storage/testnatutils.nim diff --git a/storage/nat.nim b/storage/nat.nim index 60dedc49..f11d9786 100644 --- a/storage/nat.nim +++ b/storage/nat.nim @@ -137,7 +137,12 @@ proc getRoutePrefSrc(bindIp: IpAddress): (Option[IpAddress], PrefSrcStatus) = let bindAddress = initTAddress(bindIp, Port(0)) if bindAddress.isAnyLocal(): - let ip = getRouteIpv4() + let ip = + if bindIp.family == IpAddressFamily.IPv6: + getRouteIpv6() + else: + getRouteIpv4() + if ip.isErr(): # No route was found, log error and continue without IP. error "No routable IP address found, check your network connection", diff --git a/storage/utils/natutils.nim b/storage/utils/natutils.nim index 45ad7589..37228236 100644 --- a/storage/utils/natutils.nim +++ b/storage/utils/natutils.nim @@ -1,6 +1,6 @@ {.push raises: [].} -import std/[net, tables, hashes], pkg/results, chronos, chronicles +import std/[net, tables, hashes, options], pkg/results, chronos, chronicles import pkg/libp2p @@ -10,32 +10,6 @@ type NatStrategy* = enum NatPmp NatNone -type IpLimits* = object - limit*: uint - ips: Table[IpAddress, uint] - -func hash*(ip: IpAddress): Hash = - case ip.family - of IpAddressFamily.IPv6: - hash(ip.address_v6) - of IpAddressFamily.IPv4: - hash(ip.address_v4) - -func inc*(ipLimits: var IpLimits, ip: IpAddress): bool = - let val = ipLimits.ips.getOrDefault(ip, 0) - if val < ipLimits.limit: - ipLimits.ips[ip] = val + 1 - true - else: - false - -func dec*(ipLimits: var IpLimits, ip: IpAddress) = - let val = ipLimits.ips.getOrDefault(ip, 0) - if val == 1: - ipLimits.ips.del(ip) - elif val > 1: - ipLimits.ips[ip] = val - 1 - func isGlobalUnicast*(address: TransportAddress): bool = if address.isGlobal() and address.isUnicast(): true else: false @@ -43,18 +17,11 @@ func isGlobalUnicast*(address: IpAddress): bool = let a = initTAddress(address, Port(0)) a.isGlobalUnicast() -proc getRouteIpv4*(): Result[IpAddress, cstring] = - # Avoiding Exception with initTAddress and can't make it work with static. - # Note: `publicAddress` is only used an "example" IP to find the best route, - # no data is send over the network to this IP! - let - publicAddress = TransportAddress( - family: AddressFamily.IPv4, address_v4: [1'u8, 1, 1, 1], port: Port(0) - ) - route = getBestRoute(publicAddress) +proc getRoute(publicAddress: TransportAddress): Result[IpAddress, cstring] = + let route = getBestRoute(publicAddress) if route.source.isUnspecified(): - err("No best ipv4 route found") + err("No best route found") else: let ip = try: @@ -64,3 +31,42 @@ proc getRouteIpv4*(): Result[IpAddress, cstring] = error "Address conversion error", exception = e.name, msg = e.msg return err("Invalid IP address") ok(ip) + +proc getRouteIpv4*(): Result[IpAddress, cstring] = + # Avoiding Exception with initTAddress and can't make it work with static. + # Note: `publicAddress` is only used an "example" IP to find the best route, + # no data is send over the network to this IP! + let publicAddress = TransportAddress( + family: AddressFamily.IPv4, address_v4: [1'u8, 1, 1, 1], port: Port(0) + ) + + return getRoute(publicAddress) + +proc getRouteIpv6*(): Result[IpAddress, cstring] = + # Note: `googleDnsIpv6` is only used as an "example" IP to find the best route, + # no data is sent over the network to this IP! + const googleDnsIpv6 = TransportAddress( + family: AddressFamily.IPv6, + # 2001:4860:4860::8888 + address_v6: [32'u8, 1, 72, 96, 72, 96, 0, 0, 0, 0, 0, 0, 0, 0, 136, 136], + port: Port(0), + ) + + return getRoute(googleDnsIpv6) + +# If bindIp is a anyLocal address (0.0.0.0 or ::), +# the function will find the best ip address. +# Otherwise, it will just return the ip as it is. +proc getBestLocalAddress*(bindIp: IpAddress): Option[IpAddress] = + let bindAddress = initTAddress(bindIp, Port(0)) + if bindAddress.isAnyLocal(): + let ip = + if bindIp.family == IpAddressFamily.IPv6: + getRouteIpv6() + else: + getRouteIpv4() + if ip.isOk(): + return some(ip.get()) + return none(IpAddress) + else: + return some(bindIp) diff --git a/tests/storage/testnat.nim b/tests/storage/testnat.nim index 21faa156..eb93c1ff 100644 --- a/tests/storage/testnat.nim +++ b/tests/storage/testnat.nim @@ -21,16 +21,18 @@ suite "NAT Address Tests": # Expected results let - expectedDiscoveryAddrs = @[ - MultiAddress.init("/ip4/8.8.8.8/udp/1234").expect("valid multiaddr"), - MultiAddress.init("/ip4/8.8.8.8/udp/1234").expect("valid multiaddr"), - MultiAddress.init("/ip4/8.8.8.8/udp/1234").expect("valid multiaddr"), - ] - expectedlibp2pAddrs = @[ - MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), - MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), - MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), - ] + expectedDiscoveryAddrs = + @[ + MultiAddress.init("/ip4/8.8.8.8/udp/1234").expect("valid multiaddr"), + MultiAddress.init("/ip4/8.8.8.8/udp/1234").expect("valid multiaddr"), + MultiAddress.init("/ip4/8.8.8.8/udp/1234").expect("valid multiaddr"), + ] + expectedlibp2pAddrs = + @[ + MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), + MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), + MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), + ] #ipv6Addr = MultiAddress.init("/ip6/::1/tcp/5000").expect("valid multiaddr") addrs = @[localAddr, anyAddr, publicAddr] @@ -41,3 +43,26 @@ suite "NAT Address Tests": # Verify results check(discoveryAddrs == expectedDiscoveryAddrs) check(libp2pAddrs == expectedlibp2pAddrs) + +suite "setupAddress": + test "public bind IP with NatNone returns bind IP": + let + bindIp = parseIpAddress("8.8.8.8") + natConfig = NatConfig(hasExtIp: false, nat: NatStrategy.NatNone) + (ip, tcpPort, udpPort) = + setupAddress(natConfig, bindIp, Port(5000), Port(5001), "test") + + check ip == some(bindIp) + check tcpPort == some(Port(5000)) + check udpPort == some(Port(5001)) + + test "private bind IP with NatNone returns no IP": + let + bindIp = parseIpAddress("192.168.1.1") + natConfig = NatConfig(hasExtIp: false, nat: NatStrategy.NatNone) + (ip, tcpPort, udpPort) = + setupAddress(natConfig, bindIp, Port(5000), Port(5001), "test") + + check ip == none(IpAddress) + check tcpPort == some(Port(5000)) + check udpPort == some(Port(5001)) diff --git a/tests/storage/testnatutils.nim b/tests/storage/testnatutils.nim new file mode 100644 index 00000000..fcc48561 --- /dev/null +++ b/tests/storage/testnatutils.nim @@ -0,0 +1,67 @@ +import std/[unittest, net, options] +import pkg/chronos +import ../../storage/utils/natutils + +suite "isGlobalUnicast": + test "localhost IPv4 is not global unicast": + check not isGlobalUnicast(parseIpAddress("127.0.0.1")) + + test "unspecified IPv4 is not global unicast": + check not isGlobalUnicast(parseIpAddress("0.0.0.0")) + + test "link-local IPv4 is not global unicast": + check not isGlobalUnicast(parseIpAddress("169.254.1.1")) + + test "private IPv4 is not global unicast": + check not isGlobalUnicast(parseIpAddress("10.0.0.1")) + + test "public IPv4 is global unicast": + check isGlobalUnicast(parseIpAddress("8.8.8.8")) + + test "localhost IPv6 is not global unicast": + check not isGlobalUnicast(parseIpAddress("::1")) + + test "unspecified IPv6 is not global unicast": + check not isGlobalUnicast(parseIpAddress("::")) + + test "link-local IPv6 is not global unicast": + check not isGlobalUnicast(parseIpAddress("fe80::1")) + + test "private IPv6 is not global unicast": + check not isGlobalUnicast(parseIpAddress("fc00::1")) + + test "public IPv6 is global unicast": + check isGlobalUnicast(parseIpAddress("2606:4700::1")) + +suite "getRoute": + test "getRouteIpv4 returns a valid IPv4": + let res = getRouteIpv4() + + check res.isOk + check res.get().family == IpAddressFamily.IPv4 + + test "getRouteIpv6 returns a valid IPv6": + let res = getRouteIpv6() + # If the machine does not have a global route because + # it is not configured for IPv6, the test will fail + # because it didn't find the best route. In that case, + # we can just skip the test, because it is not a problem + # with the test itself but the machine configuration. + if res.isErr: + check res.error == "No best route found" + else: + check res.get().family == IpAddressFamily.IPv6 + +suite "getBestLocalAddress": + test "specific IPv4 is returned as it is": + let ip = parseIpAddress("192.168.1.1") + check getBestLocalAddress(ip) == some(ip) + + test "specific IPv6 is returned as it is": + let ip = parseIpAddress("2606:4700::1") + check getBestLocalAddress(ip) == some(ip) + + test "0.0.0.0 resolves to a local IPv4": + let res = getBestLocalAddress(parseIpAddress("0.0.0.0")) + check res.isSome + check res.get().family == IpAddressFamily.IPv4