chore(autonat-v2): add utils (#1657)

This commit is contained in:
Gabriel Cruz
2025-09-03 16:04:46 -03:00
committed by GitHub
parent 8add5aaaab
commit 061195195b
16 changed files with 357 additions and 82 deletions

View File

@@ -85,6 +85,7 @@ when defined(libp2p_autotls_support):
../crypto/rsa,
../utils/heartbeat,
../transports/transport,
../utils/ipaddr,
../transports/tcptransport,
../nameresolving/dnsresolver
@@ -150,7 +151,10 @@ when defined(libp2p_autotls_support):
if self.config.ipAddress.isNone():
try:
self.config.ipAddress = Opt.some(getPublicIPAddress())
except AutoTLSError as exc:
except ValueError as exc:
error "Failed to get public IP address", err = exc.msg
return false
except OSError as exc:
error "Failed to get public IP address", err = exc.msg
return false
self.managerFut = self.run(switch)

View File

@@ -22,7 +22,7 @@ const
type AutoTLSError* = object of LPError
when defined(libp2p_autotls_support):
import net, strutils
import strutils
from times import DateTime, toTime, toUnix
import stew/base36
import
@@ -33,36 +33,6 @@ when defined(libp2p_autotls_support):
../nameresolving/nameresolver,
./acme/client
proc checkedGetPrimaryIPAddr*(): IpAddress {.raises: [AutoTLSError].} =
# This is so that we don't need to catch Exceptions directly
# since we support 1.6.16 and getPrimaryIPAddr before nim 2 didn't have explicit .raises. pragmas
try:
return getPrimaryIPAddr()
except Exception as exc:
raise newException(AutoTLSError, "Error while getting primary IP address", exc)
proc isIPv4*(ip: IpAddress): bool =
ip.family == IpAddressFamily.IPv4
proc isPublic*(ip: IpAddress): bool {.raises: [AutoTLSError].} =
let ip = $ip
try:
not (
ip.startsWith("10.") or
(ip.startsWith("172.") and parseInt(ip.split(".")[1]) in 16 .. 31) or
ip.startsWith("192.168.") or ip.startsWith("127.") or ip.startsWith("169.254.")
)
except ValueError as exc:
raise newException(AutoTLSError, "Failed to parse IP address", exc)
proc getPublicIPAddress*(): IpAddress {.raises: [AutoTLSError].} =
let ip = checkedGetPrimaryIPAddr()
if not ip.isIPv4():
raise newException(AutoTLSError, "Host does not have an IPv4 address")
if not ip.isPublic():
raise newException(AutoTLSError, "Host does not have a public IPv4 address")
return ip
proc asMoment*(dt: DateTime): Moment =
let unixTime: int64 = dt.toTime.toUnix
return Moment.init(unixTime, Second)

View File

@@ -843,6 +843,14 @@ proc init*(
res.data.finish()
ok(res)
proc getPart*(ma: MultiAddress, codec: MultiCodec): MaResult[MultiAddress] =
## Returns the first multiaddress in ``value`` with codec ``codec``
for part in ma:
let part = ?part
if codec == ?part.protoCode:
return ok(part)
err("no such codec in multiaddress")
proc getProtocol(name: string): MAProtocol {.inline.} =
let mc = MultiCodec.codec(name)
if mc != InvalidMultiCodec:
@@ -1119,3 +1127,32 @@ proc getRepeatedField*(
err(ProtoError.IncorrectBlob)
else:
ok(true)
proc areAddrsConsistent*(a, b: MultiAddress): bool =
## Checks if two multiaddresses have the same protocol stack.
let protosA = a.protocols().get()
let protosB = b.protocols().get()
if protosA.len != protosB.len:
return false
for idx in 0 ..< protosA.len:
let protoA = protosA[idx]
let protoB = protosB[idx]
if protoA != protoB:
if idx == 0:
# allow DNS ↔ IP at the first component
if protoB == multiCodec("dns") or protoB == multiCodec("dnsaddr"):
if not (protoA == multiCodec("ip4") or protoA == multiCodec("ip6")):
return false
elif protoB == multiCodec("dns4"):
if protoA != multiCodec("ip4"):
return false
elif protoB == multiCodec("dns6"):
if protoA != multiCodec("ip6"):
return false
else:
return false
else:
return false
true

View File

@@ -12,7 +12,7 @@
import results
import chronos, chronicles
import ../../../switch, ../../../multiaddress, ../../../peerid
import core
import types
logScope:
topics = "libp2p autonat"

View File

@@ -20,9 +20,9 @@ import
../../../peerid,
../../../utils/[semaphore, future],
../../../errors
import core
import types
export core
export types
logScope:
topics = "libp2p autonat"

View File

@@ -14,11 +14,11 @@ import chronos, metrics
import ../../../switch
import ../../../wire
import client
from core import NetworkReachability, AutonatUnreachableError
from types import NetworkReachability, AutonatUnreachableError
import ../../../utils/heartbeat
import ../../../crypto/crypto
export core.NetworkReachability
export NetworkReachability
logScope:
topics = "libp2p autonatservice"

View File

@@ -10,25 +10,35 @@
{.push raises: [].}
import results, chronos, chronicles
import ../../../multiaddress, ../../../peerid #, ../../../errors
import ../../../protobuf/minprotobuf
logScope:
topics = "libp2p autonat v2"
const
AutonatV2DialRequestCodec* = "/libp2p/autonat/2/dial-request"
AutonatV2DialBackCodec* = "/libp2p/autonat/2/dial-back"
import
../../../multiaddress, ../../../peerid, ../../../protobuf/minprotobuf, ../../../switch
from ../autonat/types import NetworkReachability
type
# DialBack and DialBackResponse are not defined as AutonatV2Msg as per the spec
# likely because they are expected in response to some other message
AutonatV2Codec* {.pure.} = enum
DialRequest = "/libp2p/autonat/2/dial-request"
DialBack = "/libp2p/autonat/2/dial-back"
AutonatV2Response* = object
reachability*: NetworkReachability
dialResp*: DialResponse
addrs*: Opt[MultiAddress]
AutonatV2Error* = object of LPError
Nonce* = uint64
AddrIdx* = uint32
NumBytes* = uint64
MsgType* {.pure.} = enum
Unused = 0 # nim requires the first variant to be zero
DialRequest = 1
DialResponse = 2
DialDataRequest = 3
DialDataResponse = 4
# DialBack and DialBackResponse are not defined as AutonatV2Msg as per the spec
# likely because they are expected in response to some other message
DialRequest
DialResponse
DialDataRequest
DialDataResponse
ResponseStatus* {.pure.} = enum
EInternalError = 0
@@ -47,30 +57,28 @@ type
DialRequest* = object
addrs*: seq[MultiAddress]
nonce*: uint64
nonce*: Nonce
DialResponse* = object
status*: ResponseStatus
addrIdx*: Opt[uint32]
addrIdx*: Opt[AddrIdx]
dialStatus*: Opt[DialStatus]
DialBack* = object
nonce*: uint64
nonce*: Nonce
DialBackResponse* = object
status*: DialBackStatus
DialDataRequest* = object
addrIdx*: uint32
numBytes*: uint64
addrIdx*: AddrIdx
numBytes*: NumBytes
DialDataResponse* = object
data*: seq[byte]
AutonatV2Msg* = object
case msgType*: MsgType
of MsgType.Unused:
discard
of MsgType.DialRequest:
dialReq*: DialRequest
of MsgType.DialResponse:
@@ -92,7 +100,7 @@ proc encode*(dialReq: DialRequest): ProtoBuffer =
proc decode*(T: typedesc[DialRequest], pb: ProtoBuffer): Opt[T] =
var
addrs: seq[MultiAddress]
nonce: uint64
nonce: Nonce
if not ?pb.getRepeatedField(1, addrs).toOpt():
return Opt.none(T)
if not ?pb.getField(2, nonce).toOpt():
@@ -114,13 +122,13 @@ proc encode*(dialResp: DialResponse): ProtoBuffer =
proc decode*(T: typedesc[DialResponse], pb: ProtoBuffer): Opt[T] =
var
status: uint
addrIdx: uint32
addrIdx: AddrIdx
dialStatus: uint
if not ?pb.getField(1, status).toOpt():
return Opt.none(T)
var optAddrIdx = Opt.none(uint32)
var optAddrIdx = Opt.none(AddrIdx)
if ?pb.getField(2, addrIdx).toOpt():
optAddrIdx = Opt.some(addrIdx)
@@ -144,7 +152,7 @@ proc encode*(dialBack: DialBack): ProtoBuffer =
encoded
proc decode*(T: typedesc[DialBack], pb: ProtoBuffer): Opt[T] =
var nonce: uint64
var nonce: Nonce
if not ?pb.getField(1, nonce).toOpt():
return Opt.none(T)
Opt.some(T(nonce: nonce))
@@ -172,8 +180,8 @@ proc encode*(dialDataReq: DialDataRequest): ProtoBuffer =
proc decode*(T: typedesc[DialDataRequest], pb: ProtoBuffer): Opt[T] =
var
addrIdx: uint32
numBytes: uint64
addrIdx: AddrIdx
numBytes: NumBytes
if not ?pb.getField(1, addrIdx).toOpt():
return Opt.none(T)
if not ?pb.getField(2, numBytes).toOpt():
@@ -193,20 +201,25 @@ proc decode*(T: typedesc[DialDataResponse], pb: ProtoBuffer): Opt[T] =
return Opt.none(T)
Opt.some(T(data: data))
proc protoField(msgType: MsgType): int =
case msgType
of MsgType.DialRequest: 1.int
of MsgType.DialResponse: 2.int
of MsgType.DialDataRequest: 3.int
of MsgType.DialDataResponse: 4.int
# AutonatV2Msg
proc encode*(msg: AutonatV2Msg): ProtoBuffer =
var encoded = initProtoBuffer()
case msg.msgType
of MsgType.Unused:
doAssert false, "invalid enum variant: Unused"
of MsgType.DialRequest:
encoded.write(MsgType.DialRequest.int, msg.dialReq.encode())
encoded.write(MsgType.DialRequest.protoField, msg.dialReq.encode())
of MsgType.DialResponse:
encoded.write(MsgType.DialResponse.int, msg.dialResp.encode())
encoded.write(MsgType.DialResponse.protoField, msg.dialResp.encode())
of MsgType.DialDataRequest:
encoded.write(MsgType.DialDataRequest.int, msg.dialDataReq.encode())
encoded.write(MsgType.DialDataRequest.protoField, msg.dialDataReq.encode())
of MsgType.DialDataResponse:
encoded.write(MsgType.DialDataResponse.int, msg.dialDataResp.encode())
encoded.write(MsgType.DialDataResponse.protoField, msg.dialDataResp.encode())
encoded.finish()
encoded
@@ -215,19 +228,19 @@ proc decode*(T: typedesc[AutonatV2Msg], pb: ProtoBuffer): Opt[T] =
msgTypeOrd: uint32
msg: ProtoBuffer
if ?pb.getField(MsgType.DialRequest.int, msg).toOpt():
if ?pb.getField(MsgType.DialRequest.protoField, msg).toOpt():
let dialReq = DialRequest.decode(msg).valueOr:
return Opt.none(AutonatV2Msg)
Opt.some(AutonatV2Msg(msgType: MsgType.DialRequest, dialReq: dialReq))
elif ?pb.getField(MsgType.DialResponse.int, msg).toOpt():
elif ?pb.getField(MsgType.DialResponse.protoField, msg).toOpt():
let dialResp = DialResponse.decode(msg).valueOr:
return Opt.none(AutonatV2Msg)
Opt.some(AutonatV2Msg(msgType: MsgType.DialResponse, dialResp: dialResp))
elif ?pb.getField(MsgType.DialDataRequest.int, msg).toOpt():
elif ?pb.getField(MsgType.DialDataRequest.protoField, msg).toOpt():
let dialDataReq = DialDataRequest.decode(msg).valueOr:
return Opt.none(AutonatV2Msg)
Opt.some(AutonatV2Msg(msgType: MsgType.DialDataRequest, dialDataReq: dialDataReq))
elif ?pb.getField(MsgType.DialDataResponse.int, msg).toOpt():
elif ?pb.getField(MsgType.DialDataResponse.protoField, msg).toOpt():
let dialDataResp = DialDataResponse.decode(msg).valueOr:
return Opt.none(AutonatV2Msg)
Opt.some(

View File

@@ -0,0 +1,47 @@
{.push raises: [].}
import results
import chronos
import
../../protocol,
../../../switch,
../../../multiaddress,
../../../multicodec,
../../../peerid,
../../../protobuf/minprotobuf,
../autonat/service,
./types
proc asNetworkReachability*(self: DialResponse): NetworkReachability =
if self.status == EInternalError:
return Unknown
if self.status == ERequestRejected:
return Unknown
if self.status == EDialRefused:
return Unknown
# if got here it means a dial was attempted
let dialStatus = self.dialStatus.valueOr:
return Unknown
if dialStatus == Unused:
return Unknown
if dialStatus == EDialError:
return NotReachable
if dialStatus == EDialBackError:
return NotReachable
return Reachable
proc asAutonatV2Response*(
self: DialResponse, testAddrs: seq[MultiAddress]
): AutonatV2Response =
let addrIdx = self.addrIdx.valueOr:
return AutonatV2Response(
reachability: self.asNetworkReachability(),
dialResp: self,
addrs: Opt.none(MultiAddress),
)
AutonatV2Response(
reachability: self.asNetworkReachability(),
dialResp: self,
addrs: Opt.some(testAddrs[addrIdx]),
)

View File

@@ -18,9 +18,9 @@ import
../multicodec,
../muxers/muxer,
../upgrademngrs/upgrade,
../protocols/connectivity/autonat/core
../protocols/connectivity/autonat/types
export core.NetworkReachability
export types.NetworkReachability
logScope:
topics = "libp2p transport"

74
libp2p/utils/ipaddr.nim Normal file
View File

@@ -0,0 +1,74 @@
import net, strutils
import ../switch, ../multiaddress, ../multicodec
proc isIPv4*(ip: IpAddress): bool =
ip.family == IpAddressFamily.IPv4
proc isIPv6*(ip: IpAddress): bool =
ip.family == IpAddressFamily.IPv6
proc isPrivate*(ip: string): bool {.raises: [ValueError].} =
ip.startsWith("10.") or
(ip.startsWith("172.") and parseInt(ip.split(".")[1]) in 16 .. 31) or
ip.startsWith("192.168.") or ip.startsWith("127.") or ip.startsWith("169.254.")
proc isPrivate*(ip: IpAddress): bool {.raises: [ValueError].} =
isPrivate($ip)
proc isPublic*(ip: string): bool {.raises: [ValueError].} =
not isPrivate(ip)
proc isPublic*(ip: IpAddress): bool {.raises: [ValueError].} =
isPublic($ip)
proc getPublicIPAddress*(): IpAddress {.raises: [OSError, ValueError].} =
let ip =
try:
getPrimaryIPAddr()
except OSError as exc:
raise exc
except ValueError as exc:
raise exc
except Exception as exc:
raise newException(OSError, "Could not get primary IP address")
if not ip.isIPv4():
raise newException(ValueError, "Host does not have an IPv4 address")
if not ip.isPublic():
raise newException(ValueError, "Host does not have a public IPv4 address")
ip
proc ipAddrMatches*(
lookup: MultiAddress, addrs: seq[MultiAddress], ip4: bool = true
): bool =
## Checks ``lookup``'s IP is in any of addrs
let ipType =
if ip4:
multiCodec("ip4")
else:
multiCodec("ip6")
let lookup = lookup.getPart(ipType).valueOr:
return false
for ma in addrs:
ma[0].withValue(ipAddr):
if ipAddr == lookup:
return true
false
proc ipSupport*(addrs: seq[MultiAddress]): (bool, bool) =
## Returns ipv4 and ipv6 support status of a list of MultiAddresses
var ipv4 = false
var ipv6 = false
for ma in addrs:
ma[0].withValue(addrIp):
if IP4.match(addrIp):
ipv4 = true
elif IP6.match(addrIp):
ipv6 = true
(ipv4, ipv6)

View File

@@ -14,7 +14,7 @@
import chronos
import
../../libp2p/[protocols/connectivity/autonat/client, peerid, multiaddress, switch]
from ../../libp2p/protocols/connectivity/autonat/core import
from ../../libp2p/protocols/connectivity/autonat/types import
NetworkReachability, AutonatUnreachableError, AutonatError
type

View File

@@ -13,12 +13,12 @@ import std/options
import chronos
import
../libp2p/[
switch,
transports/tcptransport,
upgrademngrs/upgrade,
builders,
protocols/connectivity/autonatv2/types,
# nameresolving/nameresolver,
# nameresolving/mockresolver,
protocols/connectivity/autonatv2/utils,
],
./helpers
@@ -107,3 +107,45 @@ suite "AutonatV2":
# DialBackResponse
checkEncodeDecode(DialBackResponse(status: DialBackStatus.Ok))
asyncTest "asNetworkReachability":
check asNetworkReachability(DialResponse(status: EInternalError)) == Unknown
check asNetworkReachability(DialResponse(status: ERequestRejected)) == Unknown
check asNetworkReachability(DialResponse(status: EDialRefused)) == Unknown
check asNetworkReachability(
DialResponse(status: ResponseStatus.Ok, dialStatus: Opt.none(DialStatus))
) == Unknown
check asNetworkReachability(
DialResponse(status: ResponseStatus.Ok, dialStatus: Opt.some(Unused))
) == Unknown
check asNetworkReachability(
DialResponse(status: ResponseStatus.Ok, dialStatus: Opt.some(EDialError))
) == NotReachable
check asNetworkReachability(
DialResponse(status: ResponseStatus.Ok, dialStatus: Opt.some(EDialBackError))
) == NotReachable
check asNetworkReachability(
DialResponse(status: ResponseStatus.Ok, dialStatus: Opt.some(DialStatus.Ok))
) == Reachable
asyncTest "asAutonatV2Response":
let addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/4000").get()]
let errorDialResp = DialResponse(
status: ResponseStatus.Ok,
addrIdx: Opt.none(AddrIdx),
dialStatus: Opt.none(DialStatus),
)
check asAutonatV2Response(errorDialResp, addrs) ==
AutonatV2Response(
reachability: Unknown, dialResp: errorDialResp, addrs: Opt.none(MultiAddress)
)
let correctDialResp = DialResponse(
status: ResponseStatus.Ok,
addrIdx: Opt.some(0.AddrIdx),
dialStatus: Opt.some(DialStatus.Ok),
)
check asAutonatV2Response(correctDialResp, addrs) ==
AutonatV2Response(
reachability: Reachable, dialResp: correctDialResp, addrs: Opt.some(addrs[0])
)

View File

@@ -14,7 +14,7 @@ import unittest2
import ../libp2p/protocols/connectivity/dcutr/core as dcore
import ../libp2p/protocols/connectivity/dcutr/[client, server]
from ../libp2p/protocols/connectivity/autonat/core import NetworkReachability
from ../libp2p/protocols/connectivity/autonat/types import NetworkReachability
import ../libp2p/builders
import ../libp2p/utils/future
import ./helpers

55
tests/testipaddr.nim Normal file
View File

@@ -0,0 +1,55 @@
{.used.}
# Nim-Libp2p
# Copyright (c) 2025 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
import std/options
import chronos
import ../libp2p/[utils/ipaddr], ./helpers
suite "IpAddr Utils":
teardown:
checkTrackers()
test "ipAddrMatches":
# same ip address
check ipAddrMatches(
MultiAddress.init("/ip4/127.0.0.1/tcp/4041").get(),
@[MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get()],
)
# different ip address
check not ipAddrMatches(
MultiAddress.init("/ip4/127.0.0.2/tcp/4041").get(),
@[MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get()],
)
test "ipSupport":
check ipSupport(@[MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get()]) ==
(true, false)
check ipSupport(@[MultiAddress.init("/ip6/::1/tcp/4040").get()]) == (false, true)
check ipSupport(
@[
MultiAddress.init("/ip6/::1/tcp/4040").get(),
MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get(),
]
) == (true, true)
test "isPrivate, isPublic":
check isPrivate("192.168.1.100")
check not isPublic("192.168.1.100")
check isPrivate("10.0.0.25")
check not isPublic("10.0.0.25")
check isPrivate("169.254.12.34")
check not isPublic("169.254.12.34")
check isPrivate("172.31.200.8")
check not isPublic("172.31.200.8")
check not isPrivate("1.1.1.1")
check isPublic("1.1.1.1")
check not isPrivate("185.199.108.153")
check isPublic("185.199.108.153")

View File

@@ -341,6 +341,20 @@ suite "MultiAddress test suite":
MultiAddress.init("/ip4/0.0.0.0").get().protoAddress().get() == address_v4
MultiAddress.init("/ip6/::0").get().protoAddress().get() == address_v6
test "MultiAddress getPart":
let ma = MultiAddress
.init(
"/ip4/0.0.0.0/tcp/0/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSuNEXT/unix/stdio/"
)
.get()
check:
$ma.getPart(multiCodec("ip4")).get() == "/ip4/0.0.0.0"
$ma.getPart(multiCodec("tcp")).get() == "/tcp/0"
# returns first codec match
$ma.getPart(multiCodec("p2p")).get() ==
"/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
ma.getPart(multiCodec("udp")).isErr()
test "MultiAddress getParts":
let ma = MultiAddress
.init(
@@ -421,3 +435,22 @@ suite "MultiAddress test suite":
for item in CrashesVectors:
let res = MultiAddress.init(hexToSeqByte(item))
check res.isErr()
test "areAddrsConsistent":
# same address should be consistent
check areAddrsConsistent(
MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get(),
MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get(),
)
# different addresses with same stack should be consistent
check areAddrsConsistent(
MultiAddress.init("/ip4/127.0.0.2/tcp/4041").get(),
MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get(),
)
# addresses with different stacks should not be consistent
check not areAddrsConsistent(
MultiAddress.init("/ip4/127.0.0.1/tcp/4040").get(),
MultiAddress.init("/ip4/127.0.0.1/udp/4040").get(),
)