quicp2p/puncher.nim

108 lines
3.9 KiB
Nim

import asyncdispatch, asyncnet, net, port_prediction, strformat
from nativesockets import
SockAddr,
SockAddr_storage,
SockLen,
getSockOptInt,
setSockOptInt
from sequtils import any
type
Attempt* = object
## A hole punching attempt.
srcPort*: Port
dstIp*: IpAddress
dstPorts*: seq[Port]
future*: Future[Port]
Puncher* = ref object
sock: AsyncSocket
attempts: seq[Attempt]
PunchHoleError* = object of ValueError
var IPPROTO_IP {.importc: "IPPROTO_IP", header: "<netinet/in.h>".}: cint
var IP_TTL {.importc: "IP_TTL", header: "<netinet/in.h>".}: cint
const Timeout = 3000
proc `==`(a, b: Attempt): bool =
## ``==`` for hole punching attempts.
##
## Two hole punching attempts are considered equal if their ``srcPort`` and
## ``dstIp`` are equal and their ``dstPorts`` overlap.
a.srcPort == b.srcPort and a.dstIp == b.dstIp and
a.dstPorts.any(proc (p: Port): bool = p in b.dstPorts)
proc initPuncher*(sock: AsyncSocket): Puncher =
Puncher(sock: sock)
proc punch(puncher: Puncher, peerIp: IpAddress, peerPort: Port,
peerProbedPorts: seq[Port], lowTTL: bool, msg: string):
Future[Attempt] {.async.} =
let punchFuture = newFuture[Port]("punch")
let predictedDstPorts = predictPortRange(peerPort, peerProbedPorts)
let (_, myPort) = puncher.sock.getLocalAddr()
result = Attempt(srcPort: myPort, dstIp: peerIp, dstPorts: predictedDstPorts,
future: punchFuture)
if puncher.attempts.contains(result):
raise newException(PunchHoleError,
"hole punching for given parameters already active")
puncher.attempts.add(result)
echo &"sending msg {msg} to {peerIp}, predicted ports: {result.dstPorts}"
var peerAddr: Sockaddr_storage
var peerSockLen: SockLen
try:
var defaultTTL: int
if lowTTL:
defaultTTL = puncher.sock.getFd.getSockOptInt(IPPROTO_IP, IP_TTL)
puncher.sock.getFd.setSockOptInt(IPPROTO_IP, IP_TTL, 2)
for dstPort in result.dstPorts:
toSockAddr(result.dstIp, dstPort, peerAddr, peerSockLen)
# TODO: replace asyncdispatch.sendTo with asyncnet.sendTo (Nim 1.4 required)
await sendTo(puncher.sock.getFd().AsyncFD, msg.cstring, msg.len,
cast[ptr SockAddr](addr peerAddr), peerSockLen)
if lowTTL:
puncher.sock.getFd.setSockOptInt(IPPROTO_IP, IP_TTL, defaultTTL)
except OSError as e:
raise newException(PunchHoleError, e.msg)
proc initiate*(puncher: Puncher, peerIp: IpAddress, peerPort: Port,
peerProbedPorts: seq[Port]): Future[Attempt] =
punch(puncher, peerIp, peerPort, peerProbedPorts, true, "SYN")
proc respond*(puncher: Puncher, peerIp: IpAddress, peerPort: Port,
peerProbedPorts: seq[Port]): Future[Attempt] =
punch(puncher, peerIp, peerPort, peerProbedPorts, false, "ACK")
proc finalize*(attempt: Attempt): Future[Port] {.async.} =
await attempt.future or sleepAsync(Timeout)
if attempt.future.finished:
result = attempt.future.read()
else:
raise newException(PunchHoleError, "timeout")
proc handleMsg*(puncher: Puncher, msg: string, peerIp: IpAddress,
peerPort: Port) =
## Handles an incoming UDP message which may complete the Futures returned by
## ``initiate`` and ``respond``.
if msg == "SYN":
# We received a SYN packet. We ignore it because we expected it to be
# filtered by our NAT.
return
echo &"handling ACK message from {peerIp}:{peerPort}"
let (_, myPort) = puncher.sock.getLocalAddr()
let query = Attempt(srcPort: myPort, dstIp: peerIp, dstPorts: @[peerPort])
let i = puncher.attempts.find(query)
if i != -1:
puncher.attempts[i].future.complete(peerPort)
puncher.attempts.del(i)
proc handleMsg*(puncher: Puncher, msg: string,
peerAddr: SockAddr | Sockaddr_storage, peerSockLen: SockLen) =
var peerIp: IpAddress
var peerPort: Port
fromSockAddr(peerAddr, peerSockLen, peerIp, peerPort)
handleMsg(puncher, msg, peerIp, peerPort)