feat: autonat support

This commit is contained in:
Adam Uhlíř 2025-10-24 15:04:20 +02:00
parent a1631569d1
commit a816201f50
No known key found for this signature in database
GPG Key ID: 0CBD7AA7B5A72FED
40 changed files with 657 additions and 577 deletions

View File

@ -37,7 +37,7 @@ import ./utils/addrutils
import ./namespaces
import ./codextypes
import ./logutils
import ./nat
import ./nat/reachabilitymanager
logScope:
topics = "codex node"
@ -50,6 +50,7 @@ type
repoStore: RepoStore
maintenance: BlockMaintainer
taskpool: Taskpool
reachabilityManager: ReachabilityManager
isStarted: bool
CodexPrivateKey* = libp2p.PrivateKey # alias
@ -75,12 +76,16 @@ proc start*(s: CodexServer) {.async.} =
await s.codexNode.switch.start()
let (announceAddrs, discoveryAddrs) = nattedAddress(
s.config.nat, s.codexNode.switch.peerInfo.addrs, s.config.discoveryPort
)
s.reachabilityManager.getAnnounceRecords = some proc() =
s.codexNode.switch.peerInfo.addrs
s.reachabilityManager.getDiscoveryRecords = some proc() =
s.codexNode.discovery.dhtRecord.data.addresses.mapIt(it.address)
s.reachabilityManager.updateAnnounceRecords = some proc(records: seq[MultiAddress]) =
s.codexNode.discovery.updateAnnounceRecord(records)
s.reachabilityManager.updateDiscoveryRecords = some proc(records: seq[MultiAddress]) =
s.codexNode.discovery.updateDhtRecord(records)
s.codexNode.discovery.updateAnnounceRecord(announceAddrs)
s.codexNode.discovery.updateDhtRecord(discoveryAddrs)
await s.reachabilityManager.start(s.codexNode.switch, s.config.bootstrapNodes)
await s.codexNode.start()
@ -98,6 +103,7 @@ proc stop*(s: CodexServer) {.async.} =
var futures =
@[
s.reachabilityManager.stop(),
s.codexNode.switch.stop(),
s.codexNode.stop(),
s.codexNode.discovery.stop(),
@ -141,6 +147,9 @@ proc new*(
T: type CodexServer, config: CodexConf, privateKey: CodexPrivateKey
): CodexServer =
## create CodexServer including setting up datastore, repostore, etc
let reachabilityManager = ReachabilityManager.new(config.portMappingStrategy)
let switch = SwitchBuilder
.new()
.withPrivateKey(privateKey)
@ -152,6 +161,11 @@ proc new*(
.withAgentVersion(config.agentString)
.withSignedPeerRecord(true)
.withTcpTransport({ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay})
# Adds AutoNAT server support - ability to respond to other peers ask about their reachability status
.withAutonat()
# Adds AutoNAT client support - to discover the node's rechability
.withServices(@[reachabilityManager.getAutonatService()])
.build()
var
@ -275,4 +289,5 @@ proc new*(
repoStore: repoStore,
maintenance: maintenance,
taskPool: taskPool,
reachabilityManager: reachabilityManager,
)

View File

@ -41,12 +41,11 @@ import ./logutils
import ./stores
import ./units
import ./utils
import ./nat
import ./utils/natutils
import ./nat/port_mapping
from ./blockexchange/engine/pendingblocks import DefaultBlockRetries
export units, net, codextypes, logutils, completeCmdArg, parseCmdArg, NatConfig
export units, net, codextypes, logutils, completeCmdArg, parseCmdArg
export
DefaultQuotaBytes, DefaultBlockTtl, DefaultBlockInterval, DefaultNumBlocksPerInterval,
@ -144,14 +143,14 @@ type
name: "listen-addrs"
.}: seq[MultiAddress]
nat* {.
forcePortMapping* {.
desc:
"Specify method to use for determining public address. " &
"Must be one of: any, none, upnp, pmp, extip:<IP>",
defaultValue: defaultNatConfig(),
"Overide automatic detection to specific upnp mode. " &
"Must be one of: any, none, upnp, pmp",
defaultValue: PortMappingStrategy.Any,
defaultValueDesc: "any",
name: "nat"
.}: NatConfig
name: "force-port-mapping"
.}: PortMappingStrategy
discoveryPort* {.
desc: "Discovery (UDP) port",
@ -280,9 +279,6 @@ type
func defaultAddress*(conf: CodexConf): IpAddress =
result = static parseIpAddress("127.0.0.1")
func defaultNatConfig*(): NatConfig =
result = NatConfig(hasExtIp: false, nat: NatStrategy.NatAny)
proc getCodexVersion(): string =
let tag = strip(staticExec("git describe --tags --abbrev=0"))
if tag.isEmptyOrWhitespace:
@ -355,35 +351,20 @@ proc parseCmdArg*(T: type SignedPeerRecord, uri: string): T =
quit QuitFailure
return res.get()
func parse*(T: type NatConfig, p: string): Result[NatConfig, string] =
func parseCmdArg*(T: type PortMappingStrategy, p: string): T {.raises: [ValueError].} =
case p.toLowerAscii
of "any":
return ok(NatConfig(hasExtIp: false, nat: NatStrategy.NatAny))
PortMappingStrategy.Any
of "none":
return ok(NatConfig(hasExtIp: false, nat: NatStrategy.NatNone))
PortMappingStrategy.None
of "upnp":
return ok(NatConfig(hasExtIp: false, nat: NatStrategy.NatUpnp))
PortMappingStrategy.Upnp
of "pmp":
return ok(NatConfig(hasExtIp: false, nat: NatStrategy.NatPmp))
PortMappingStrategy.Pmp
else:
if p.startsWith("extip:"):
try:
let ip = parseIpAddress(p[6 ..^ 1])
return ok(NatConfig(hasExtIp: true, extIp: ip))
except ValueError:
let error = "Not a valid IP address: " & p[6 ..^ 1]
return err(error)
else:
return err("Not a valid NAT option: " & p)
raise newException(ValueError, "Not a valid NAT option: " & p)
proc parseCmdArg*(T: type NatConfig, p: string): T =
let res = NatConfig.parse(p)
if res.isErr:
fatal "Cannot parse the NAT config.", error = res.error(), input = p
quit QuitFailure
return res.get()
proc completeCmdArg*(T: type NatConfig, val: string): seq[string] =
proc completeCmdArg*(T: type PortMappingStrategy, val: string): seq[string] =
return @[]
func parse*(T: type NBytes, p: string): Result[NBytes, string] =
@ -463,11 +444,11 @@ proc readValue*(
val = dur
proc readValue*(
r: var TomlReader, val: var NatConfig
r: var TomlReader, val: var PortMappingStrategy
) {.raises: [SerializationError].} =
val =
try:
parseCmdArg(NatConfig, r.readValue(string))
parseCmdArg(PortMappingStrategy, r.readValue(string))
except CatchableError as err:
raise newException(SerializationError, err.msg)

View File

@ -1,432 +0,0 @@
# Copyright (c) 2019-2023 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.
{.push raises: [].}
import
std/[options, os, strutils, times, net, atomics],
stew/[objects],
nat_traversal/[miniupnpc, natpmp],
json_serialization/std/net,
results
import pkg/chronos
import pkg/chronicles
import pkg/libp2p
import ./utils
import ./utils/natutils
import ./utils/addrutils
const
UPNP_TIMEOUT = 200 # ms
PORT_MAPPING_INTERVAL = 20 * 60 # seconds
NATPMP_LIFETIME = 60 * 60 # in seconds, must be longer than PORT_MAPPING_INTERVAL
type PortMappings* = object
internalTcpPort: Port
externalTcpPort: Port
internalUdpPort: Port
externalUdpPort: Port
description: string
type PortMappingArgs =
tuple[strategy: NatStrategy, tcpPort, udpPort: Port, description: string]
type NatConfig* = object
case hasExtIp*: bool
of true: extIp*: IpAddress
of false: nat*: NatStrategy
var
upnp {.threadvar.}: Miniupnp
npmp {.threadvar.}: NatPmp
strategy = NatStrategy.NatNone
natClosed: Atomic[bool]
extIp: Option[IpAddress]
activeMappings: seq[PortMappings]
natThreads: seq[Thread[PortMappingArgs]] = @[]
logScope:
topics = "nat"
type PrefSrcStatus = enum
NoRoutingInfo
PrefSrcIsPublic
PrefSrcIsPrivate
BindAddressIsPublic
BindAddressIsPrivate
## Also does threadvar initialisation.
## Must be called before redirectPorts() in each thread.
proc getExternalIP*(natStrategy: NatStrategy, quiet = false): Option[IpAddress] =
var externalIP: IpAddress
if natStrategy == NatStrategy.NatAny or natStrategy == NatStrategy.NatUpnp:
if upnp == nil:
upnp = newMiniupnp()
upnp.discoverDelay = UPNP_TIMEOUT
let dres = upnp.discover()
if dres.isErr:
debug "UPnP", msg = dres.error
else:
var
msg: cstring
canContinue = true
case upnp.selectIGD()
of IGDNotFound:
msg = "Internet Gateway Device not found. Giving up."
canContinue = false
of IGDFound:
msg = "Internet Gateway Device found."
of IGDNotConnected:
msg = "Internet Gateway Device found but it's not connected. Trying anyway."
of NotAnIGD:
msg =
"Some device found, but it's not recognised as an Internet Gateway Device. Trying anyway."
of IGDIpNotRoutable:
msg =
"Internet Gateway Device found and is connected, but with a reserved or non-routable IP. Trying anyway."
if not quiet:
debug "UPnP", msg
if canContinue:
let ires = upnp.externalIPAddress()
if ires.isErr:
debug "UPnP", msg = ires.error
else:
# if we got this far, UPnP is working and we don't need to try NAT-PMP
try:
externalIP = parseIpAddress(ires.value)
strategy = NatStrategy.NatUpnp
return some(externalIP)
except ValueError as e:
error "parseIpAddress() exception", err = e.msg
return
if natStrategy == NatStrategy.NatAny or natStrategy == NatStrategy.NatPmp:
if npmp == nil:
npmp = newNatPmp()
let nres = npmp.init()
if nres.isErr:
debug "NAT-PMP", msg = nres.error
else:
let nires = npmp.externalIPAddress()
if nires.isErr:
debug "NAT-PMP", msg = nires.error
else:
try:
externalIP = parseIpAddress($(nires.value))
strategy = NatStrategy.NatPmp
return some(externalIP)
except ValueError as e:
error "parseIpAddress() exception", err = e.msg
return
# This queries the routing table to get the "preferred source" attribute and
# checks if it's a public IP. If so, then it's our public IP.
#
# Further more, we check if the bind address (user provided, or a "0.0.0.0"
# default) is a public IP. That's a long shot, because code paths involving a
# user-provided bind address are not supposed to get here.
proc getRoutePrefSrc(bindIp: IpAddress): (Option[IpAddress], PrefSrcStatus) =
let bindAddress = initTAddress(bindIp, Port(0))
if bindAddress.isAnyLocal():
let ip = getRouteIpv4()
if ip.isErr():
# No route was found, log error and continue without IP.
error "No routable IP address found, check your network connection",
error = ip.error
return (none(IpAddress), NoRoutingInfo)
elif ip.get().isGlobalUnicast():
return (some(ip.get()), PrefSrcIsPublic)
else:
return (none(IpAddress), PrefSrcIsPrivate)
elif bindAddress.isGlobalUnicast():
return (some(bindIp), BindAddressIsPublic)
else:
return (none(IpAddress), BindAddressIsPrivate)
# Try to detect a public IP assigned to this host, before trying NAT traversal.
proc getPublicRoutePrefSrcOrExternalIP*(
natStrategy: NatStrategy, bindIp: IpAddress, quiet = true
): Option[IpAddress] =
let (prefSrcIp, prefSrcStatus) = getRoutePrefSrc(bindIp)
case prefSrcStatus
of NoRoutingInfo, PrefSrcIsPublic, BindAddressIsPublic:
return prefSrcIp
of PrefSrcIsPrivate, BindAddressIsPrivate:
let extIp = getExternalIP(natStrategy, quiet)
if extIp.isSome:
return some(extIp.get)
proc doPortMapping(
strategy: NatStrategy, tcpPort, udpPort: Port, description: string
): Option[(Port, Port)] {.gcsafe.} =
var
extTcpPort: Port
extUdpPort: Port
if strategy == NatStrategy.NatUpnp:
for t in [(tcpPort, UPNPProtocol.TCP), (udpPort, UPNPProtocol.UDP)]:
let
(port, protocol) = t
pmres = upnp.addPortMapping(
externalPort = $port,
protocol = protocol,
internalHost = upnp.lanAddr,
internalPort = $port,
desc = description,
leaseDuration = 0,
)
if pmres.isErr:
error "UPnP port mapping", msg = pmres.error, port
return
else:
# let's check it
let cres =
upnp.getSpecificPortMapping(externalPort = $port, protocol = protocol)
if cres.isErr:
warn "UPnP port mapping check failed. Assuming the check itself is broken and the port mapping was done.",
msg = cres.error
info "UPnP: added port mapping",
externalPort = port, internalPort = port, protocol = protocol
case protocol
of UPNPProtocol.TCP:
extTcpPort = port
of UPNPProtocol.UDP:
extUdpPort = port
elif strategy == NatStrategy.NatPmp:
for t in [(tcpPort, NatPmpProtocol.TCP), (udpPort, NatPmpProtocol.UDP)]:
let
(port, protocol) = t
pmres = npmp.addPortMapping(
eport = port.cushort,
iport = port.cushort,
protocol = protocol,
lifetime = NATPMP_LIFETIME,
)
if pmres.isErr:
error "NAT-PMP port mapping", msg = pmres.error, port
return
else:
let extPort = Port(pmres.value)
info "NAT-PMP: added port mapping",
externalPort = extPort, internalPort = port, protocol = protocol
case protocol
of NatPmpProtocol.TCP:
extTcpPort = extPort
of NatPmpProtocol.UDP:
extUdpPort = extPort
return some((extTcpPort, extUdpPort))
proc repeatPortMapping(args: PortMappingArgs) {.thread, raises: [ValueError].} =
ignoreSignalsInThread()
let
(strategy, tcpPort, udpPort, description) = args
interval = initDuration(seconds = PORT_MAPPING_INTERVAL)
sleepDuration = 1_000 # in ms, also the maximum delay after pressing Ctrl-C
var lastUpdate = now()
# We can't use copies of Miniupnp and NatPmp objects in this thread, because they share
# C pointers with other instances that have already been garbage collected, so
# we use threadvars instead and initialise them again with getExternalIP(),
# even though we don't need the external IP's value.
let ipres = getExternalIP(strategy, quiet = true)
if ipres.isSome:
while natClosed.load() == false:
let
# we're being silly here with this channel polling because we can't
# select on Nim channels like on Go ones
currTime = now()
if currTime >= (lastUpdate + interval):
discard doPortMapping(strategy, tcpPort, udpPort, description)
lastUpdate = currTime
sleep(sleepDuration)
proc stopNatThreads() {.noconv.} =
# stop the thread
debug "Stopping NAT port mapping renewal threads"
try:
natClosed.store(true)
joinThreads(natThreads)
except Exception as exc:
warn "Failed to stop NAT port mapping renewal thread", exc = exc.msg
# delete our port mappings
# FIXME: if the initial port mapping failed because it already existed for the
# required external port, we should not delete it. It might have been set up
# by another program.
# In Windows, a new thread is created for the signal handler, so we need to
# initialise our threadvars again.
let ipres = getExternalIP(strategy, quiet = true)
if ipres.isSome:
if strategy == NatStrategy.NatUpnp:
for entry in activeMappings:
for t in [
(entry.externalTcpPort, entry.internalTcpPort, UPNPProtocol.TCP),
(entry.externalUdpPort, entry.internalUdpPort, UPNPProtocol.UDP),
]:
let
(eport, iport, protocol) = t
pmres = upnp.deletePortMapping(externalPort = $eport, protocol = protocol)
if pmres.isErr:
error "UPnP port mapping deletion", msg = pmres.error
else:
debug "UPnP: deleted port mapping",
externalPort = eport, internalPort = iport, protocol = protocol
elif strategy == NatStrategy.NatPmp:
for entry in activeMappings:
for t in [
(entry.externalTcpPort, entry.internalTcpPort, NatPmpProtocol.TCP),
(entry.externalUdpPort, entry.internalUdpPort, NatPmpProtocol.UDP),
]:
let
(eport, iport, protocol) = t
pmres = npmp.deletePortMapping(
eport = eport.cushort, iport = iport.cushort, protocol = protocol
)
if pmres.isErr:
error "NAT-PMP port mapping deletion", msg = pmres.error
else:
debug "NAT-PMP: deleted port mapping",
externalPort = eport, internalPort = iport, protocol = protocol
proc redirectPorts*(
strategy: NatStrategy, tcpPort, udpPort: Port, description: string
): Option[(Port, Port)] =
result = doPortMapping(strategy, tcpPort, udpPort, description)
if result.isSome:
let (externalTcpPort, externalUdpPort) = result.get()
# needed by NAT-PMP on port mapping deletion
# Port mapping works. Let's launch a thread that repeats it, in case the
# NAT-PMP lease expires or the router is rebooted and forgets all about
# these mappings.
activeMappings.add(
PortMappings(
internalTcpPort: tcpPort,
externalTcpPort: externalTcpPort,
internalUdpPort: udpPort,
externalUdpPort: externalUdpPort,
description: description,
)
)
try:
natThreads.add(Thread[PortMappingArgs]())
natThreads[^1].createThread(
repeatPortMapping, (strategy, externalTcpPort, externalUdpPort, description)
)
# atexit() in disguise
if natThreads.len == 1:
# we should register the thread termination function only once
addQuitProc(stopNatThreads)
except Exception as exc:
warn "Failed to create NAT port mapping renewal thread", exc = exc.msg
proc setupNat*(
natStrategy: NatStrategy, tcpPort, udpPort: Port, clientId: string
): tuple[ip: Option[IpAddress], tcpPort, udpPort: Option[Port]] =
## Setup NAT port mapping and get external IP address.
## If any of this fails, we don't return any IP address but do return the
## original ports as best effort.
## TODO: Allow for tcp or udp port mapping to be optional.
if extIp.isNone:
extIp = getExternalIP(natStrategy)
if extIp.isSome:
let ip = extIp.get
let extPorts = (
{.gcsafe.}:
redirectPorts(
strategy, tcpPort = tcpPort, udpPort = udpPort, description = clientId
)
)
if extPorts.isSome:
let (extTcpPort, extUdpPort) = extPorts.get()
(ip: some(ip), tcpPort: some(extTcpPort), udpPort: some(extUdpPort))
else:
warn "UPnP/NAT-PMP available but port forwarding failed"
(ip: none(IpAddress), tcpPort: some(tcpPort), udpPort: some(udpPort))
else:
warn "UPnP/NAT-PMP not available"
(ip: none(IpAddress), tcpPort: some(tcpPort), udpPort: some(udpPort))
proc setupAddress*(
natConfig: NatConfig, bindIp: IpAddress, tcpPort, udpPort: Port, clientId: string
): tuple[ip: Option[IpAddress], tcpPort, udpPort: Option[Port]] {.gcsafe.} =
## Set-up of the external address via any of the ways as configured in
## `NatConfig`. In case all fails an error is logged and the bind ports are
## selected also as external ports, as best effort and in hope that the
## external IP can be figured out by other means at a later stage.
## TODO: Allow for tcp or udp bind ports to be optional.
if natConfig.hasExtIp:
# any required port redirection must be done by hand
return (some(natConfig.extIp), some(tcpPort), some(udpPort))
case natConfig.nat
of NatStrategy.NatAny:
let (prefSrcIp, prefSrcStatus) = getRoutePrefSrc(bindIp)
case prefSrcStatus
of NoRoutingInfo, PrefSrcIsPublic, BindAddressIsPublic:
return (prefSrcIp, some(tcpPort), some(udpPort))
of PrefSrcIsPrivate, BindAddressIsPrivate:
return setupNat(natConfig.nat, tcpPort, udpPort, clientId)
of NatStrategy.NatNone:
let (prefSrcIp, prefSrcStatus) = getRoutePrefSrc(bindIp)
case prefSrcStatus
of NoRoutingInfo, PrefSrcIsPublic, BindAddressIsPublic:
return (prefSrcIp, some(tcpPort), some(udpPort))
of PrefSrcIsPrivate:
error "No public IP address found. Should not use --nat:none option"
return (none(IpAddress), some(tcpPort), some(udpPort))
of BindAddressIsPrivate:
error "Bind IP is not a public IP address. Should not use --nat:none option"
return (none(IpAddress), some(tcpPort), some(udpPort))
of NatStrategy.NatUpnp, NatStrategy.NatPmp:
return setupNat(natConfig.nat, tcpPort, udpPort, clientId)
proc nattedAddress*(
natConfig: NatConfig, addrs: seq[MultiAddress], udpPort: Port
): tuple[libp2p, discovery: seq[MultiAddress]] =
## Takes a NAT configuration, sequence of multiaddresses and UDP port and returns:
## - Modified multiaddresses with NAT-mapped addresses for libp2p
## - Discovery addresses with NAT-mapped UDP ports
var discoveryAddrs = newSeq[MultiAddress](0)
let newAddrs = addrs.mapIt:
block:
# Extract IP address and port from the multiaddress
let (ipPart, port) = getAddressAndPort(it)
if ipPart.isSome and port.isSome:
# Try to setup NAT mapping for the address
let (newIP, tcp, udp) =
setupAddress(natConfig, ipPart.get, port.get, udpPort, "codex")
if newIP.isSome:
# NAT mapping successful - add discovery address with mapped UDP port
discoveryAddrs.add(getMultiAddrWithIPAndUDPPort(newIP.get, udp.get))
# Remap original address with NAT IP and TCP port
it.remapAddr(ip = newIP, port = tcp)
else:
# NAT mapping failed - use original address
echo "Failed to get external IP, using original address", it
discoveryAddrs.add(getMultiAddrWithIPAndUDPPort(ipPart.get, udpPort))
it
else:
# Invalid multiaddress format - return as is
it
(newAddrs, discoveryAddrs)

363
codex/nat/port_mapping.nim Normal file
View File

@ -0,0 +1,363 @@
# Copyright (c) 2019-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.
{.push raises: [].}
import std/[options, os, strutils, times, net, atomics]
import pkg/stew/objects
import pkg/nat_traversal/[miniupnpc, natpmp]
import pkg/json_serialization/std/net
import pkg/results
import pkg/questionable
import pkg/questionable/results
import pkg/chronos
import pkg/chronicles
import pkg/libp2p
import ../utils
logScope:
topics = "codex nat port-mapping"
const
UPNP_TIMEOUT = 200 # ms
RENEWAL_INTERVAL = 20 * 60 # seconds
Pmp_LIFETIME = 60 * 60 # in seconds, must be longer than RENEWAL_INTERVAL
MAPPING_DESCRIPTION = "codex"
type PortMappingStrategy* = enum
Any
Upnp
Pmp
None
type MappingPort* = ref object of RootObj
value*: Port
type TcpPort* = ref object of MappingPort
type UdpPort* = ref object of MappingPort
proc newTcpMappingPort*(value: Port): TcpPort =
TcpPort(value: value)
proc newUdpMappingPort*(value: Port): UdpPort =
UdpPort(value: value)
type PortMapping = tuple[internalPort: MappingPort, externalPort: Option[MappingPort]]
type RenewelThreadArgs =
tuple[strategy: PortMappingStrategy, portMapping: seq[PortMapping]]
var
upnp {.threadvar.}: Miniupnp
npmp {.threadvar.}: NatPmp
mappings: seq[PortMapping]
portMappingExiting: Atomic[bool]
renewalThread: Thread[RenewelThreadArgs]
proc initUpnp(): bool =
logScope:
protocol = "upnp"
if upnp != nil:
warn "UPnP already initialized!"
return true
upnp = newMiniupnp()
upnp.discoverDelay = UPNP_TIMEOUT
if err =? upnp.discover().errorOption:
warn "UPnP error discoverning Internet Gateway Devices", msg = err
upnp = nil
return false
case upnp.selectIGD()
of IGDNotFound:
info "UPnP Internet Gateway Device not found. Giving up."
upnp = nil
# As UPnP is not supported on our network we won't be using it --> lets erase it.
of IGDFound:
info "UPnP Internet Gateway Device found."
of IGDNotConnected:
info "UPnP Internet Gateway Device found but it's not connected. Trying anyway."
of NotAnIGD:
info "Some device found, but it's not recognised as an Internet Gateway Device. Trying anyway."
of IGDIpNotRoutable:
info "UPnP Internet Gateway Device found and is connected, but with a reserved or non-routable IP. Trying anyway."
return true
proc initNpmp(): bool =
logScope:
protocol = "npmp"
if npmp != nil:
warn "NAT-PMP already initialized!"
return true
npmp = newNatPmp()
if err =? npmp.init().errorOption:
warn "Error initialization of NAT-PMP", msg = err
npmp = nil
return false
if err =? npmp.externalIPAddress().errorOption:
warn "Fetching of external IP failed.", msg = err
npmp = nil
return false
info "NAT-PMP initialized"
return true
## Try to initilize all the port mapping protocols and returns
## the protocol that will be used.
proc initProtocols(strategy: PortMappingStrategy): PortMappingStrategy =
if strategy == PortMappingStrategy.Any or strategy == PortMappingStrategy.Upnp:
if initUpnp():
return PortMappingStrategy.Upnp
if strategy == PortMappingStrategy.Any or strategy == PortMappingStrategy.Pmp:
if initNpmp():
return PortMappingStrategy.Pmp
return PortMappingStrategy.None
proc upnpPortMapping(
internalPort: MappingPort, externalPort: MappingPort
): ?!MappingPort {.gcsafe.} =
let protocol = if (internalPort is TcpPort): UPNPProtocol.TCP else: UPNPProtocol.UDP
logScope:
protocol = "upnp"
externalPort = externalPort.value
internalPort = internalPort.value
protocol = protocol
let pmres = upnp.addPortMapping(
externalPort = $(externalPort.value),
protocol = protocol,
internalHost = upnp.lanAddr,
internalPort = $(internalPort.value),
desc = MAPPING_DESCRIPTION,
leaseDuration = 0,
)
if pmres.isErr:
error "UPnP port mapping", msg = pmres.error
return failure(pmres.error)
# let's check it
let cres = upnp.getSpecificPortMapping(
externalPort = $(externalPort.value), protocol = protocol
)
if cres.isErr:
warn "UPnP port mapping check failed. Assuming the check itself is broken and the port mapping was done.",
msg = cres.error
info "UPnP added port mapping"
return success(externalPort)
proc npmpPortMapping(
internalPort: MappingPort, externalPort: MappingPort
): ?!MappingPort {.gcsafe.} =
let protocol =
if (internalPort is TcpPort): NatPmpProtocol.TCP else: NatPmpProtocol.UDP
logScope:
protocol = "npmp"
externalPort = externalPort.value
internalPort = internalPort.value
protocol = protocol
without extPort =?
npmp.addPortMapping(
eport = externalPort.value,
iport = internalPort.value,
protocol = protocol,
lifetime = Pmp_LIFETIME,
), err:
error "NAT-PMP port mapping error", msg = err.msg
return failure(err.msg)
info "NAT-PMP: added port mapping"
if internalPort is TcpPort:
return success(newTcpMappingPort(extPort))
else:
return success(newUdpMappingPort(extPort))
## Create port mapping that will try to utilize the same port number
## of the internal port for the external port mapping.
##
## TODO: Add support for trying mapping of random external port.
proc doPortMapping(port: MappingPort): ?!MappingPort {.gcsafe.} =
if upnp != nil:
return upnpPortMapping(port, port)
if npmp != nil:
return npmpPortMapping(port, port)
return failure("No active startegy")
proc doPortMapping(
internalPort: MappingPort, externalPort: MappingPort
): ?!MappingPort {.gcsafe.} =
if upnp != nil:
return upnpPortMapping(internalPort, externalPort)
if npmp != nil:
return npmpPortMapping(internalPort, externalPort)
return failure("No active startegy")
## Gets external IP provided by the port mapping protocols
## Port mapping needs to be succesfully started first using `startPortMapping()`
proc getExternalIP*(): ?IpAddress =
if upnp == nil and npmp == nil:
warn "No available port-mapping protocol"
return IpAddress.none
if upnp != nil:
let ires = upnp.externalIPAddress
if ires.isOk():
info "Got externa IP address: " & ires.value, ip = ires.value
return parseIpAddress(ires.value).some
else:
debug "Getting external IP address using UPnP failed",
msg = ires.error, protocol = "upnp"
if npmp != nil:
let nires = npmp.externalIPAddress()
if nires.isErr:
debug "Getting external IP address using NAT-PMP failed", msg = nires.error
else:
try:
info "Got externa IP address: " & $(nires.value),
ip =$ (nires.value), protocol = "npmp"
return parseIpAddress($(nires.value)).some
except ValueError as e:
error "parseIpAddress() exception", err = e.msg
return IpAddress.none
proc startPortMapping*(
strategy: PortMappingStrategy, internalPorts: seq[MappingPort]
): ?!seq[PortMapping] =
if strategy == PortMappingStrategy.None:
return failure("No port mapping strategy requested")
if internalPorts.len == 0:
return failure("No internal ports to be mapped were supplied")
strategy = initProtocols(strategy)
if strategy == PortMappingStrategy.None:
return failure("No available port mapping protocols on the network")
portMapping = newSeqOfCap[PortMappings](internalPorts.len)
for port in internalPorts:
without mappedPort =? doPortMapping(port), err:
warn "Failed to map port", port = port.value, msg = err.msg
portMapping.add((internalPort: port, externalPort: MappingPort.none))
portMapping.add((internalPort: port, externalPort: mappedPort.some))
startRenewalThread(strategy)
return success(externalPorts)
proc stopPortMapping*() =
if upnp == nil or npmp == nil:
debug "Port mapping is not running, nothing to stop"
return
info "Stopping port mapping renewal threads"
try:
portMappingExiting.store(true)
renewalThread.join()
except CatchableError as exc:
warn "Failed to stop port mapping renewal thread", exc = exc.msg
for mapping in portMapping:
if upnp != nil:
let protocol =
if (internalPort is TcpPort): UPNPProtocol.TCP else: UPNPProtocol.UDP
if err =?
upnp.deletePortMapping(
externalPort = $(mapping.externalPort.value), protocol = protocol
).errorOption:
error "UPnP port mapping deletion error", msg = err.msg
else:
debug "UPnP: deleted port mapping",
externalPort = mapping.externalPort,
internalPort = mapping.internalPort,
protocol = protocol
if npnp != nil:
let protocol =
if (internalPort is TcpPort): NatPmpProtocol.TCP else: NatPmpProtocol.UDP
if err =?
npmp.deletePortMapping(
eport = mapping.externalPort.value,
iport = mapping.internalPort.value,
protocol = protocol,
).errorOption:
error "NAT-PMP port mapping deletion error", msg = err.msg
else:
debug "NAT-PMP: deleted port mapping",
externalPort = mapping.externalPort,
internalPort = mapping.internalPort,
protocol = protocol
proc startRenewalThread(
strategy: PortMappingStrategy,
internalPorts: seq[MappingPort],
externalPorts: seq[?MappingPort],
) =
try:
renewalThread = Thread[RenewelThreadArgs]()
renewalThread.createThread(renewPortMapping, (strategy, portMapping))
except CatchableError as exc:
warn "Failed to create NAT port mapping renewal thread", exc = exc.msg
proc renewPortMapping(args: RenewelThreadArgs) {.thread, raises: [ValueError].} =
ignoreSignalsInThread()
let
(strategy, portMappings) = args
interval = initDuration(seconds = RENEWAL_INTERVAL)
sleepDuration = 1_000 # in ms, also the maximum delay after pressing Ctrl-C
var lastUpdate = now()
# We can't use copies of Miniupnp and Pmp objects in this thread, because they share
# C pointers with other instances that have already been garbage collected, so
# we use threadvars instead and initialise them again with initProtocols(),
# even though we don't need the external IP's value.
if initProtocols(strategy) == PortMappingStrategy.None:
error "Could not initiate protocols in renewal thread"
return
while portMappingExiting.load() == false:
if now() >= (lastUpdate + interval):
for mapping in portMappings:
if externalPort =? mapping.externalPort:
without renewedExternalPort =?
doPortMapping(mapping.internalPort, externalPort), err:
error "Error while renewal of port mapping", msg = err.msg
if renewedExternalPort.value != externalPort.value:
error "The renewed external port is not the same as the originally mapped"
lastUpdate = now()
sleep(sleepDuration)

View File

@ -0,0 +1,153 @@
import std/sequtils
import pkg/chronos
import pkg/chronicles
import pkg/questionable
import pkg/questionable/results
import pkg/libp2p
import pkg/libp2p/protocols/connectivity/autonat/client
import pkg/libp2p/protocols/connectivity/autonat/service
import ../rng as random
import ./port_mapping
const AutonatCheckInterval = Opt.some(chronos.seconds(30))
logScope:
topics = "codex nat reachabilitymanager"
type
ReachabilityManager* = ref object of RootObj
networkReachability*: NetworkReachability
portMappingStrategy: PortMappingStrategy
getAnnounceRecords*: ?GetRecords
getDiscoveryRecords*: ?GetRecords
updateAnnounceRecords*: ?UpdateRecords
updateDiscoveryRecords*: ?UpdateRecords
started = false
GetRecords* = proc(): seq[MultiAddress] {.raises: [].}
UpdateRecords* = proc(records: seq[MultiAddress]) {.raises: [].}
proc new*(
T: typedesc[ReachabilityManager], portMappingStrategy: PortMappingStrategy
): T =
return T(portMappingStrategy: portMappingStrategy)
proc getReachabilityHandler(manager: ReachabilityManager): StatusAndConfidenceHandler =
let statusAndConfidenceHandler = proc(
networkReachability: NetworkReachability, confidenceOpt: Opt[float]
): Future[void] {.gcsafe, async: (raises: [CancelledError]).} =
if not started:
warn "ReachabilityManager was not started, but we are already getting reachability updates! Ignoring..."
return
without confidence =? confidenceOpt:
debug "Node reachability reported without confidence"
return
if manager.networkReachability == networkReachability:
debug "Node reachability reported without change",
networkReachability = networkReachability
return
info "Node reachability status changed",
networkReachability = networkReachability, confidence = confidenceOpt
manager.networkReachability = networkReachability
if networkReachability == NetworkReachability.Unreachable:
# Lets first start to expose port using port mapping protocols like NAT-PMP or UPnP
if manager.startPortMapping():
return # We exposed ports so we should be good!
info "No more options to become reachable"
return statusAndConfidenceHandler
proc startPortMapping(self: ReachabilityManager): bool =
try:
let announceRecords = self.getAnnounceRecords()
let discoveryRecords = self.getDiscoveryRecords()
let portsToBeMapped =
(announceRecords & discoveryRecords).mapIt(getAddressAndPort(it)).mapIt(it.port)
without mappedPorts =? startPortMapping(
manager.portMappingStrategy, portsToBeMapped
), err:
warn "Could not start port mapping", msg = err
return false
if mappedPorts.any(
proc(x: ?MappingPort): bool =
isNone(x)
):
warn "Some ports were not mapped - not using port mapping then"
return false
info "Started port mapping"
let announceMappedRecords = zip(
announceRecords, mappedPorts[0 .. announceRecords.len - 1]
)
.mapIt(getMultiAddr(getAddressAndPort(it[0]).ip, it[1].value))
self.updateAnnounceRecords(announceMappedRecords)
let discoveryMappedRecords = zip(
discoveryRecords, mappedPorts[announceRecords.len, ^1]
)
.mapIt(getMultiAddr(getAddressAndPort(it[0]).ip, it[1].value))
self.updateDiscoveryRecords(discoveryMappedRecords)
return true
except ValueError as exc:
error "Error while starting port mapping", msg = exc.msg
return false
proc start*(
self: ReachabilityManager, switch: Switch, bootNodes: seq[SignedPeerRecord]
): Future[void] {.async: (raises: [CancelledError]).} =
doAssert self.getAnnounceRecords.isSome, "getAnnounceRecords is not set"
doAssert self.getDiscoveryRecords.isSome, "getDiscoveryRecords is not set"
doAssert self.updateAnnounceRecords.isSome, "updateAnnounceRecords is not set"
doAssert self.updateDiscoveryRecords.isSome, "updateDiscoveryRecords is not set"
self.started = true
## Until more robust way of NAT-traversal helper peers discovery is implemented
## we will start with simple populating the libp2p peerstore with bootstrap nodes
## https://github.com/codex-storage/nim-codex/issues/1320
for peer in bootNodes:
try:
await switch.connect(peer.data.peerId, peer.data.addresses.mapIt(it.address))
except CancelledError as exc:
raise exc
except CatchableError as exc:
info "Failed to dial bootstrap nodes", err = exc.msg
proc stop*(): Future[void] {.async: (raises: [CancelledError]).} =
stopPortMapping()
self.started = false
proc getAutonatService*(self: ReachabilityManager): Service =
## AutonatService request other peers to dial us back
## flagging us as Reachable or NotReachable.
## We use minimum confidence 0.1 (confidence is calculated as numOfReplies/maxQueueSize) as
## that will give an answer already for response from one peer.
## As we use bootnodes for this in initial setup, it is possible we might
## get only one peer to ask about our reachability and it is crucial to get at least some reply.
## This should be changed once proactive NAT-traversal helper peers discovery is implemented.
let autonatService = AutonatService.new(
autonatClient = AutonatClient.new(),
rng = random.Rng.instance(),
scheduleInterval = AutonatCheckInterval,
askNewConnectedPeers = true,
numPeersToAsk = 5,
maxQueueSize = 10,
minConfidence = 0.1,
)
autonatService.statusAndConfidenceHandler(self.getReachabilityHandler())
return Service(autonatService)

68
codex/nat/utils.nim Normal file
View File

@ -0,0 +1,68 @@
## Nim-Codex
## Copyright (c) 2022 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/strutils
import std/options
import std/net
import pkg/libp2p
import pkg/questionable
import pkg/questionable/results
import ./port_mapping
proc getAddressAndPort*(
ma: MultiAddress
): tuple[ip: IpAddress, port: MappingPort] {.raises: [ValueError].} =
try:
# Try IPv4 first
let ipv4Result = ma[multiCodec("ip4")]
let ip =
if ipv4Result.isOk:
let ipBytes = ipv4Result.get().protoArgument().expect("Invalid IPv4 format")
let ipArray = [ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3]]
IpAddress(family: IPv4, address_v4: ipArray)
else:
# Try IPv6 if IPv4 not found
let ipv6Result = ma[multiCodec("ip6")]
if ipv6Result.isOk:
let ipBytes = ipv6Result.get().protoArgument().expect("Invalid IPv6 format")
var ipArray: array[16, byte]
for i in 0 .. 15:
ipArray[i] = ipBytes[i]
IpAddress(family: IPv6, address_v6: ipArray)
else:
raise newException(ValueError, "Unknown IP family")
# Get TCP Port
let tcpPortResult = ma[multiCodec("tcp")]
if tcpPortResult.isOk:
let tcpPortBytes =
tcpPortResult.get().protoArgument().expect("Invalid port format")
let tcpPort = newTcpMappingPort(Port(fromBytesBE(uint16, tcpPortBytes)))
return (ip: ip, port: tcpPort)
# Get UDP Port
let udpPortResult = ma[multiCodec("udp")]
if udpPortResult.isOk:
let udpPortBytes =
udpPortResult.get().protoArgument().expect("Invalid port format")
let udpPort = newUdpMappingPort(Port(fromBytesBE(uint16, udpPortBytes)))
return (ip: ip, port: udpPort)
raise newException(ValueError, "No TCP/UDP port specified")
except Exception:
raise newException(ValueError, "Invalid multiaddr")
proc getMultiAddr*(ip: IpAddress, port: MappingPort): MultiAddress =
let ipFamily = if ip.family == IpAddressFamily.IPv4: "/ip4/" else: "/ip6/"
let portType = if (internalPort is TcpPort): "/tcp/" else: "/udp/"
return MultiAddress.init(ipFamily & $ip & portType & $(port.value)).expect(
"valid multiaddr"
)

View File

@ -12,6 +12,7 @@
import std/net
import std/strutils
import std/options
import std/net
import pkg/libp2p
import pkg/stew/endians2
@ -42,7 +43,7 @@ func remapAddr*(
proc getMultiAddrWithIPAndUDPPort*(ip: IpAddress, port: Port): MultiAddress =
## Creates a MultiAddress with the specified IP address and UDP port
##
##
## Parameters:
## - ip: A valid IP address (IPv4 or IPv6)
## - port: The UDP port number

View File

@ -1,66 +0,0 @@
{.push raises: [].}
import std/[net, tables, hashes], pkg/results, chronos, chronicles
import pkg/libp2p
type NatStrategy* = enum
NatAny
NatUpnp
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
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)
if route.source.isUnspecified():
err("No best ipv4 route found")
else:
let ip =
try:
route.source.address()
except ValueError as e:
# This should not occur really.
error "Address conversion error", exception = e.name, msg = e.msg
return err("Invalid IP address")
ok(ip)

View File

@ -219,11 +219,8 @@ proc generateNodes*(
if config.enableBootstrap:
waitFor switch.peerInfo.update()
let (announceAddrs, discoveryAddrs) = nattedAddress(
NatConfig(hasExtIp: false, nat: NatNone),
switch.peerInfo.addrs,
bindPort.Port,
)
let (announceAddrs, discoveryAddrs) =
nattedAddress(switch.peerInfo.addrs, bindPort.Port)
blockDiscovery.updateAnnounceRecord(announceAddrs)
blockDiscovery.updateDhtRecord(discoveryAddrs)
if blockDiscovery.dhtRecord.isSome:

2
vendor/lrucache.nim vendored

@ -1 +1 @@
Subproject commit ba57736921b2972163b673fc706e7659e7c5cbd6
Subproject commit 8767ade0b76ea5b5d4ce24a52d0c58a6ebeb66cd

2
vendor/nim-bearssl vendored

@ -1 +1 @@
Subproject commit f08d72203f9e110c099c6f393e1c0640fcbe176f
Subproject commit 667b40440a53a58e9f922e29e20818720c62d9ac

2
vendor/nim-blscurve vendored

@ -1 +1 @@
Subproject commit f4d0de2eece20380541fbf73d4b8bf57dc214b3b
Subproject commit de2d3c79264bba18dbea469c8c5c4b3bb3c8bc55

2
vendor/nim-chronos vendored

@ -1 +1 @@
Subproject commit 0646c444fce7c7ed08ef6f2c9a7abfd172ffe655
Subproject commit c04576d829b8a0a1b12baaa8bc92037501b3a4a0

@ -1 +1 @@
Subproject commit 0a7b4cecce725bcb11ad8648035a92704a8854d3
Subproject commit 842f48910be4f388bcbf8abf1f02aba1d5e2ee64

2
vendor/nim-eth vendored

@ -1 +1 @@
Subproject commit d9135e6c3c5d6d819afdfb566aa8d958756b73a8
Subproject commit dcfbc4291d39b59563828c3e32be4d51a2f25931

2
vendor/nim-ethers vendored

@ -1 +1 @@
Subproject commit 965b8cd752544df96b5effecbbd27a8f56a25d62
Subproject commit 30871c7b1d5784e36c51223bd36ef6f1fffcc030

@ -1 +1 @@
Subproject commit ce27581a3e881f782f482cb66dc5b07a02bd615e
Subproject commit cf8d4d22636b8e514caf17e49f9c786ac56b0e85

@ -1 +1 @@
Subproject commit c53852d9e24205b6363bba517fa8ee7bde823691
Subproject commit 8bb1acbaa4b86eb866145b0d468eff64a57d1897

2
vendor/nim-json-rpc vendored

@ -1 +1 @@
Subproject commit b6e40a776fa2d00b97a9366761fb7da18f31ae5c
Subproject commit cbe8edf69d743a787b76b1cd25bfc4eae89927f7

@ -1 +1 @@
Subproject commit a6dcf03e04e179127a5fcb7e495d19a821d56c17
Subproject commit 6eadb6e939ffa7882ff5437033c11a9464d3385c

@ -1 +1 @@
Subproject commit 5a0cd8de6b2363827c43cafd3ed346ecee427e1e
Subproject commit 378ef63e261e3b5834a3567404edc3ce838498b3

@ -1 +1 @@
Subproject commit 99bc2ba16bc2d44f9a97e706304f64744d913d7f
Subproject commit 6da0cda88ab7780bd5fd342327adb91ab84692aa

2
vendor/nim-libp2p vendored

@ -1 +1 @@
Subproject commit e82080f7b1aa61c6d35fa5311b873f41eff4bb52
Subproject commit 7eaf79fefe45b03a4281e3505d5fcc97b32df39c

2
vendor/nim-metrics vendored

@ -1 +1 @@
Subproject commit 9b9afee96357ad82dabf4563cf292f89b50423df
Subproject commit cacfdc12454a0804c65112b9f4f50d1375208dcd

@ -1 +1 @@
Subproject commit 860e18c37667b5dd005b94c63264560c35d88004
Subproject commit 6508ce75060878dfcdfa21f94721672c69a1823b

2
vendor/nim-ngtcp2 vendored

@ -1 +1 @@
Subproject commit 791eb859145f9f268eb23eb9cbe777bdd7699c4d
Subproject commit 6834f4756b6af58356ac9c4fef3d71db3c3ae5fe

2
vendor/nim-nitro vendored

@ -1 +1 @@
Subproject commit 5ccdeb46e06dcf5cef80d0acbb80ee8a17d596e7
Subproject commit e3719433d5ace25947c468787c805969642b3913

2
vendor/nim-presto vendored

@ -1 +1 @@
Subproject commit 62225bfa7ce703a99e04680bfc3498e69b52897f
Subproject commit 92b1c7ff141e6920e1f8a98a14c35c1fa098e3be

@ -1 +1 @@
Subproject commit 4d74e157cdf1bdcd0ffd41519ebde740c4b80447
Subproject commit 5a31137a82c2b6a989c9ed979bb636c7a49f570e

2
vendor/nim-quic vendored

@ -1 +1 @@
Subproject commit 525842aeca6111fd5035568d0f59aa2b338cc29d
Subproject commit ddcb31ffb74b5460ab37fd13547eca90594248bc

2
vendor/nim-serde vendored

@ -1 +1 @@
Subproject commit 649ae60e05ec432738d41eb8d613c5d7f434c4a3
Subproject commit 5ced7c88b97d99c582285ce796957fb71fd42434

@ -1 +1 @@
Subproject commit b0f2fa32960ea532a184394b0f27be37bd80248b
Subproject commit 2086c99608b4bf472e1ef5fe063710f280243396

@ -1 +1 @@
Subproject commit 6797c31836bff377bf50f1ac7bf8122449bf99ba
Subproject commit 05bbff1af4e8fe2d972ba4b0667b89ca94d3ebba

2
vendor/nim-stew vendored

@ -1 +1 @@
Subproject commit b66168735d6f3841c5239c3169d3fe5fe98b1257
Subproject commit a6e198132097fb544d04959aeb3b839e1408f942

@ -1 +1 @@
Subproject commit 97f76faef6ba64bc77d9808c27ec5e9917e7cfde
Subproject commit 66585e2e960b7695e48ea60377fb3aeac96406e8

@ -1 +1 @@
Subproject commit e4d37dc1652d5c63afb89907efb5a5e812261797
Subproject commit 4d37244f9f5e1acd8592a4ceb5c3fc47bc160181

@ -1 +1 @@
Subproject commit b5b387e6fb2a7cc75d54a269b07cc6218361bd46
Subproject commit fea85b27f0badcf617033ca1bc05444b5fd8aa7a

2
vendor/nim-websock vendored

@ -1 +1 @@
Subproject commit 35ae76f1559e835c80f9c1a3943bf995d3dd9eb5
Subproject commit ebe308a79a7b440a11dfbe74f352be86a3883508

2
vendor/nim-zlib vendored

@ -1 +1 @@
Subproject commit c71efff5fd1721362b3363dc7d0e2a4c0dbc6453
Subproject commit 91cf360b1aeb2e0c753ff8bac6de22a41c5ed8cd

2
vendor/stint vendored

@ -1 +1 @@
Subproject commit 470b7892561b5179ab20bd389a69217d6213fe58
Subproject commit 5c5e01cef089a261474b7abfe246b37447aaa8ed