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: "".}: cint var IP_TTL {.importc: "IP_TTL", header: "".}: 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 let (_, myPort) = puncher.sock.getLocalAddr() let query = Attempt(srcPort: myPort, dstIp: peerIp, dstPorts: @[peerPort]) let i = puncher.attempts.find(query) if i != -1: if msg == "ACK": echo &"handling ACK message from {peerIp}:{peerPort}" else: echo &"handling QUIC message from {peerIp}:{peerPort}" puncher.attempts[i].future.complete(peerPort) puncher.attempts.del(i) else: echo &"received unexpected packet from {peerIp}:{peerPort}" 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)