quicp2p/puncher.nim

148 lines
5.7 KiB
Nim
Raw Normal View History

2020-11-17 22:50:00 +01:00
import asyncdispatch, asyncnet, net, port_prediction, strformat
2020-11-17 20:40:30 +01:00
2020-11-18 09:31:09 +01:00
from nativesockets import
SockAddr,
SockAddr_storage,
SockLen,
getSockOptInt,
setSockOptInt
from sequtils import any, map
2020-11-17 20:40:30 +01:00
type
Attempt* = object
2020-11-17 20:40:30 +01:00
## A hole punching attempt.
socks*: seq[AsyncSocket]
dstIp*: IpAddress
dstPorts*: seq[Port]
future*: Future[(AsyncSocket, Port)]
2020-11-17 20:40:30 +01:00
Puncher* = ref object
socks*: seq[AsyncSocket]
natProps*: NatProperties
attempts*: seq[Attempt]
2020-11-17 20:40:30 +01:00
PunchHoleError* = object of ValueError
2020-11-18 09:31:09 +01:00
var IPPROTO_IP {.importc: "IPPROTO_IP", header: "<netinet/in.h>".}: cint
var IP_TTL {.importc: "IP_TTL", header: "<netinet/in.h>".}: cint
2020-11-17 20:40:30 +01:00
const Timeout = 3000
const InitiatorMaxOutPacketCount = 1000
const ResponderMaxOutPacketCount = 70
const MaxOutPacketCount = max(InitiatorMaxOutPacketCount,
ResponderMaxOutPacketCount)
proc srcPort(sock: AsyncSocket): Port =
result = sock.getLocalAddr[1]
2020-11-17 20:40:30 +01:00
proc `==`(a, b: Attempt): bool =
## ``==`` for hole punching attempts.
##
## Two hole punching attempts are considered equal if their ``dstIp`` is
## equal and their ``dstPorts`` overlap.
a.dstIp == b.dstIp and a.dstPorts.any(proc (p: Port): bool = p in b.dstPorts)
proc initPuncher*(sock: AsyncSocket, probedSrcPorts: seq[Port]): Puncher =
# TODO: determine IP_TTL
let (_, primarySrcPort) = sock.getLocalAddr()
let natProps = getNatProperties(primarySrcPort, probedSrcPorts)
result = Puncher(socks: @[sock], natProps: natProps)
if result.natProps.natType == SymmetricRandom:
# our NAT is of the evil symmetric type with random port allocation. We are
# trying to help the other peer by allocating a lot of auxillary sockets
# for punching more holes
result.socks.setLen(MaxOutPacketCount)
for i in 1 .. MaxOutPacketCount - 1:
result.socks[i] = newAsyncSocket(sockType = SOCK_DGRAM,
protocol = IPPROTO_UDP, buffered = false)
result.socks[i].bindAddr(Port(0))
2020-11-17 20:40:30 +01:00
proc primarySrcPort*(puncher: Puncher): Port =
puncher.socks[0].srcPort
2020-11-17 20:40:30 +01:00
proc punch(puncher: Puncher, peerIp: IpAddress, peerPort: Port,
peerProbedPorts: seq[Port], isInitiating: bool, msg: string):
Future[Attempt] {.async.} =
let punchFuture = newFuture[(AsyncSocket, Port)]("punch")
let peerNatProps = getNatProperties(peerPort, peerProbedPorts)
let maxOutPacketCount = if isInitiating:
InitiatorMaxOutPacketCount
else:
ResponderMaxOutPacketCount
var sockCount = 1
if puncher.natProps.natType == SymmetricRandom:
if peerNatProps.natType == SymmetricRandom:
# If both peers are behind a SymmetricRandom NAT we give up.
raise newException(PunchHoleError,
"both peers behind symmetric NAT with random port allocation")
sockCount = maxOutPacketCount
let predictedDstPorts = predictPortRange(peerNatProps, maxOutPacketCount.uint16)
result = Attempt(dstIp: peerIp, dstPorts: predictedDstPorts,
future: punchFuture)
if puncher.attempts.contains(result):
2020-11-17 20:40:30 +01:00
raise newException(PunchHoleError,
"hole punching to given destination already active")
puncher.attempts.add(result)
let srcPorts = puncher.socks[0 .. sockCount - 1].map(srcPort)
echo &"sending msg {msg} to {peerIp}, srcPorts: {srcPorts}, dstPorts: {result.dstPorts}"
2020-11-17 20:40:30 +01:00
var peerAddr: Sockaddr_storage
var peerSockLen: SockLen
try:
2020-11-18 09:31:09 +01:00
var defaultTTL: int
for i in 0 .. sockCount - 1:
let sock = puncher.socks[i]
if isInitiating:
defaultTTL = sock.getFd.getSockOptInt(IPPROTO_IP, IP_TTL)
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(sock.getFd.AsyncFD, msg.cstring, msg.len,
cast[ptr SockAddr](addr peerAddr), peerSockLen)
if isInitiating:
sock.getFd.setSockOptInt(IPPROTO_IP, IP_TTL, defaultTTL)
2020-11-17 20:40:30 +01:00
except OSError as e:
raise newException(PunchHoleError, e.msg)
proc initiate*(puncher: Puncher, peerIp: IpAddress, peerPort: Port,
peerProbedPorts: seq[Port]): Future[Attempt] =
2020-11-18 09:31:09 +01:00
punch(puncher, peerIp, peerPort, peerProbedPorts, true, "SYN")
2020-11-17 20:40:30 +01:00
proc respond*(puncher: Puncher, peerIp: IpAddress, peerPort: Port,
peerProbedPorts: seq[Port]): Future[Attempt] =
2020-11-18 09:31:09 +01:00
punch(puncher, peerIp, peerPort, peerProbedPorts, false, "ACK")
2020-11-17 20:40:30 +01:00
proc finalize*(attempt: Attempt): Future[(AsyncSocket, 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, sock: AsyncSocket,
peerIp: IpAddress, peerPort: Port) =
2020-11-17 20:40:30 +01:00
## 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 query = Attempt(dstIp: peerIp, dstPorts: @[peerPort])
2020-11-17 20:40:30 +01:00
let i = puncher.attempts.find(query)
if i != -1:
2020-11-18 17:12:18 +01:00
if msg == "ACK":
echo &"handling ACK message from {peerIp}:{peerPort}"
else:
echo &"handling QUIC message from {peerIp}:{peerPort}"
puncher.attempts[i].future.complete((sock, peerPort))
2020-11-17 20:40:30 +01:00
puncher.attempts.del(i)
2020-11-18 18:24:19 +01:00
else:
echo &"received unexpected packet from {peerIp}:{peerPort}"
2020-11-17 20:40:30 +01:00
proc handleMsg*(puncher: Puncher, msg: string, sock: AsyncSocket,
2020-11-17 20:40:30 +01:00
peerAddr: SockAddr | Sockaddr_storage, peerSockLen: SockLen) =
var peerIp: IpAddress
var peerPort: Port
fromSockAddr(peerAddr, peerSockLen, peerIp, peerPort)
handleMsg(puncher, msg, sock, peerIp, peerPort)