mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-03-19 10:43:35 +00:00
feat: autonat support
This commit is contained in:
parent
a1631569d1
commit
a816201f50
@ -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,
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
432
codex/nat.nim
432
codex/nat.nim
@ -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
363
codex/nat/port_mapping.nim
Normal 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)
|
||||
153
codex/nat/reachabilitymanager.nim
Normal file
153
codex/nat/reachabilitymanager.nim
Normal 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
68
codex/nat/utils.nim
Normal 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"
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
@ -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
2
vendor/lrucache.nim
vendored
@ -1 +1 @@
|
||||
Subproject commit ba57736921b2972163b673fc706e7659e7c5cbd6
|
||||
Subproject commit 8767ade0b76ea5b5d4ce24a52d0c58a6ebeb66cd
|
||||
2
vendor/nim-bearssl
vendored
2
vendor/nim-bearssl
vendored
@ -1 +1 @@
|
||||
Subproject commit f08d72203f9e110c099c6f393e1c0640fcbe176f
|
||||
Subproject commit 667b40440a53a58e9f922e29e20818720c62d9ac
|
||||
2
vendor/nim-blscurve
vendored
2
vendor/nim-blscurve
vendored
@ -1 +1 @@
|
||||
Subproject commit f4d0de2eece20380541fbf73d4b8bf57dc214b3b
|
||||
Subproject commit de2d3c79264bba18dbea469c8c5c4b3bb3c8bc55
|
||||
2
vendor/nim-chronos
vendored
2
vendor/nim-chronos
vendored
@ -1 +1 @@
|
||||
Subproject commit 0646c444fce7c7ed08ef6f2c9a7abfd172ffe655
|
||||
Subproject commit c04576d829b8a0a1b12baaa8bc92037501b3a4a0
|
||||
2
vendor/nim-contract-abi
vendored
2
vendor/nim-contract-abi
vendored
@ -1 +1 @@
|
||||
Subproject commit 0a7b4cecce725bcb11ad8648035a92704a8854d3
|
||||
Subproject commit 842f48910be4f388bcbf8abf1f02aba1d5e2ee64
|
||||
2
vendor/nim-eth
vendored
2
vendor/nim-eth
vendored
@ -1 +1 @@
|
||||
Subproject commit d9135e6c3c5d6d819afdfb566aa8d958756b73a8
|
||||
Subproject commit dcfbc4291d39b59563828c3e32be4d51a2f25931
|
||||
2
vendor/nim-ethers
vendored
2
vendor/nim-ethers
vendored
@ -1 +1 @@
|
||||
Subproject commit 965b8cd752544df96b5effecbbd27a8f56a25d62
|
||||
Subproject commit 30871c7b1d5784e36c51223bd36ef6f1fffcc030
|
||||
2
vendor/nim-faststreams
vendored
2
vendor/nim-faststreams
vendored
@ -1 +1 @@
|
||||
Subproject commit ce27581a3e881f782f482cb66dc5b07a02bd615e
|
||||
Subproject commit cf8d4d22636b8e514caf17e49f9c786ac56b0e85
|
||||
2
vendor/nim-http-utils
vendored
2
vendor/nim-http-utils
vendored
@ -1 +1 @@
|
||||
Subproject commit c53852d9e24205b6363bba517fa8ee7bde823691
|
||||
Subproject commit 8bb1acbaa4b86eb866145b0d468eff64a57d1897
|
||||
2
vendor/nim-json-rpc
vendored
2
vendor/nim-json-rpc
vendored
@ -1 +1 @@
|
||||
Subproject commit b6e40a776fa2d00b97a9366761fb7da18f31ae5c
|
||||
Subproject commit cbe8edf69d743a787b76b1cd25bfc4eae89927f7
|
||||
2
vendor/nim-json-serialization
vendored
2
vendor/nim-json-serialization
vendored
@ -1 +1 @@
|
||||
Subproject commit a6dcf03e04e179127a5fcb7e495d19a821d56c17
|
||||
Subproject commit 6eadb6e939ffa7882ff5437033c11a9464d3385c
|
||||
2
vendor/nim-leveldbstatic
vendored
2
vendor/nim-leveldbstatic
vendored
@ -1 +1 @@
|
||||
Subproject commit 5a0cd8de6b2363827c43cafd3ed346ecee427e1e
|
||||
Subproject commit 378ef63e261e3b5834a3567404edc3ce838498b3
|
||||
2
vendor/nim-libbacktrace
vendored
2
vendor/nim-libbacktrace
vendored
@ -1 +1 @@
|
||||
Subproject commit 99bc2ba16bc2d44f9a97e706304f64744d913d7f
|
||||
Subproject commit 6da0cda88ab7780bd5fd342327adb91ab84692aa
|
||||
2
vendor/nim-libp2p
vendored
2
vendor/nim-libp2p
vendored
@ -1 +1 @@
|
||||
Subproject commit e82080f7b1aa61c6d35fa5311b873f41eff4bb52
|
||||
Subproject commit 7eaf79fefe45b03a4281e3505d5fcc97b32df39c
|
||||
2
vendor/nim-metrics
vendored
2
vendor/nim-metrics
vendored
@ -1 +1 @@
|
||||
Subproject commit 9b9afee96357ad82dabf4563cf292f89b50423df
|
||||
Subproject commit cacfdc12454a0804c65112b9f4f50d1375208dcd
|
||||
2
vendor/nim-nat-traversal
vendored
2
vendor/nim-nat-traversal
vendored
@ -1 +1 @@
|
||||
Subproject commit 860e18c37667b5dd005b94c63264560c35d88004
|
||||
Subproject commit 6508ce75060878dfcdfa21f94721672c69a1823b
|
||||
2
vendor/nim-ngtcp2
vendored
2
vendor/nim-ngtcp2
vendored
@ -1 +1 @@
|
||||
Subproject commit 791eb859145f9f268eb23eb9cbe777bdd7699c4d
|
||||
Subproject commit 6834f4756b6af58356ac9c4fef3d71db3c3ae5fe
|
||||
2
vendor/nim-nitro
vendored
2
vendor/nim-nitro
vendored
@ -1 +1 @@
|
||||
Subproject commit 5ccdeb46e06dcf5cef80d0acbb80ee8a17d596e7
|
||||
Subproject commit e3719433d5ace25947c468787c805969642b3913
|
||||
2
vendor/nim-presto
vendored
2
vendor/nim-presto
vendored
@ -1 +1 @@
|
||||
Subproject commit 62225bfa7ce703a99e04680bfc3498e69b52897f
|
||||
Subproject commit 92b1c7ff141e6920e1f8a98a14c35c1fa098e3be
|
||||
2
vendor/nim-protobuf-serialization
vendored
2
vendor/nim-protobuf-serialization
vendored
@ -1 +1 @@
|
||||
Subproject commit 4d74e157cdf1bdcd0ffd41519ebde740c4b80447
|
||||
Subproject commit 5a31137a82c2b6a989c9ed979bb636c7a49f570e
|
||||
2
vendor/nim-quic
vendored
2
vendor/nim-quic
vendored
@ -1 +1 @@
|
||||
Subproject commit 525842aeca6111fd5035568d0f59aa2b338cc29d
|
||||
Subproject commit ddcb31ffb74b5460ab37fd13547eca90594248bc
|
||||
2
vendor/nim-serde
vendored
2
vendor/nim-serde
vendored
@ -1 +1 @@
|
||||
Subproject commit 649ae60e05ec432738d41eb8d613c5d7f434c4a3
|
||||
Subproject commit 5ced7c88b97d99c582285ce796957fb71fd42434
|
||||
2
vendor/nim-serialization
vendored
2
vendor/nim-serialization
vendored
@ -1 +1 @@
|
||||
Subproject commit b0f2fa32960ea532a184394b0f27be37bd80248b
|
||||
Subproject commit 2086c99608b4bf472e1ef5fe063710f280243396
|
||||
2
vendor/nim-sqlite3-abi
vendored
2
vendor/nim-sqlite3-abi
vendored
@ -1 +1 @@
|
||||
Subproject commit 6797c31836bff377bf50f1ac7bf8122449bf99ba
|
||||
Subproject commit 05bbff1af4e8fe2d972ba4b0667b89ca94d3ebba
|
||||
2
vendor/nim-stew
vendored
2
vendor/nim-stew
vendored
@ -1 +1 @@
|
||||
Subproject commit b66168735d6f3841c5239c3169d3fe5fe98b1257
|
||||
Subproject commit a6e198132097fb544d04959aeb3b839e1408f942
|
||||
2
vendor/nim-taskpools
vendored
2
vendor/nim-taskpools
vendored
@ -1 +1 @@
|
||||
Subproject commit 97f76faef6ba64bc77d9808c27ec5e9917e7cfde
|
||||
Subproject commit 66585e2e960b7695e48ea60377fb3aeac96406e8
|
||||
2
vendor/nim-testutils
vendored
2
vendor/nim-testutils
vendored
@ -1 +1 @@
|
||||
Subproject commit e4d37dc1652d5c63afb89907efb5a5e812261797
|
||||
Subproject commit 4d37244f9f5e1acd8592a4ceb5c3fc47bc160181
|
||||
2
vendor/nim-toml-serialization
vendored
2
vendor/nim-toml-serialization
vendored
@ -1 +1 @@
|
||||
Subproject commit b5b387e6fb2a7cc75d54a269b07cc6218361bd46
|
||||
Subproject commit fea85b27f0badcf617033ca1bc05444b5fd8aa7a
|
||||
2
vendor/nim-websock
vendored
2
vendor/nim-websock
vendored
@ -1 +1 @@
|
||||
Subproject commit 35ae76f1559e835c80f9c1a3943bf995d3dd9eb5
|
||||
Subproject commit ebe308a79a7b440a11dfbe74f352be86a3883508
|
||||
2
vendor/nim-zlib
vendored
2
vendor/nim-zlib
vendored
@ -1 +1 @@
|
||||
Subproject commit c71efff5fd1721362b3363dc7d0e2a4c0dbc6453
|
||||
Subproject commit 91cf360b1aeb2e0c753ff8bac6de22a41c5ed8cd
|
||||
2
vendor/stint
vendored
2
vendor/stint
vendored
@ -1 +1 @@
|
||||
Subproject commit 470b7892561b5179ab20bd389a69217d6213fe58
|
||||
Subproject commit 5c5e01cef089a261474b7abfe246b37447aaa8ed
|
||||
Loading…
x
Reference in New Issue
Block a user