From 3f27102e20e4f8a47f7e6ef4a42144f6dbbd6b65 Mon Sep 17 00:00:00 2001 From: Christian Ulrich Date: Wed, 14 Oct 2020 20:52:20 +0200 Subject: [PATCH] first try implementing the NUTSS (b) approach --- punchd.nim | 43 ++++++++++++++++-- tcp_nutss_initiator.nim | 98 +++++++++++++++++++++++++++++++++++++++++ tcp_nutss_responder.nim | 50 +++++++++++++++++++++ 3 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 tcp_nutss_initiator.nim create mode 100644 tcp_nutss_responder.nim diff --git a/punchd.nim b/punchd.nim index b0f26b6..3f806c6 100644 --- a/punchd.nim +++ b/punchd.nim @@ -5,6 +5,8 @@ import asyncutils import message import tcp_syni_connect import tcp_syni_accept +import tcp_nutss_initiator +import tcp_nutss_responder from strutils import format, join from nativesockets import setSockOptInt @@ -14,6 +16,8 @@ type unixSocket: AsyncSocket tcpSyniCP: TcpSyniConnectPuncher tcpSyniAP: TcpSyniAcceptPuncher + tcpNutssInitiator: TcpNutssInitiator + tcpNutssResponder: TcpNutssResponder Sigint = object of CatchableError @@ -31,6 +35,18 @@ type srcPorts: seq[Port] seqNums: seq[uint32] + TcpNutssInitiate = object + srcIp: IpAddress + srcPorts: seq[Port] + dstIp: IpAddress + dstPorts: seq[Port] + + TcpNutssRespond = object + dstIp: IpAddress + dstPorts: seq[Port] + srcIp: IpAddress + srcPorts: seq[Port] + const PunchdSocket = "/tmp/punchd.socket" proc handleSigint() {.noconv.} = @@ -47,19 +63,34 @@ proc handleRequest(punchd: Punchd, line: string, case args[0]: of "tcp-syni-connect": let req = parseMessage[TcpSyniConnect](args[2]) - proc handleSeqNumbers(seqNumbers: seq[uint32]) {.async.} = + proc progress(seqNumbers: seq[uint32]) {.async.} = echo "progress! seqNumbers: ", seqNumbers let content = @["tcp-syni-accept", $req.srcIp, req.srcPorts.join(","), $req.dstIp, req.dstPorts.join(","), seqNumbers.join(",")].join("|") await unixSock.send(&"progress|{id}|{content}\n") sock = await punchd.tcpSyniCP.connect(req.srcPorts[0], req.dstIp, - req.dstPorts, handleSeqNumbers) + req.dstPorts, progress) of "tcp-syni-accept": let req = parseMessage[TcpSyniAccept](args[2]) sock = await punchd.tcpSyniAP.accept(req.srcPorts[0], req.dstIp, req.dstPorts, req.seqNums) + + of "tcp-nutss-initiate": + let req = parseMessage[TcpNutssInitiate](args[2]) + proc progress() {.async.} = + echo "progress!" + let content = @["tcp-nutss-respond", $req.srcIp, req.srcPorts.join(","), + $req.dstIp, req.dstPorts.join(",")].join("|") + await unixSock.send(&"progress|{id}|{content}\n") + sock = await punchd.tcpNutssInitiator.initiate(req.srcPorts[0], req.dstIp, + req.dstPorts, progress) + + of "tcp-nutss-respond": + let req = parseMessage[TcpNutssRespond](args[2]) + sock = await punchd.tcpNutssResponder.respond(req.srcPorts[0], req.dstIp, + req.dstPorts) else: raise newException(ValueError, "invalid request") @@ -99,14 +130,18 @@ proc main() = fpOthersRead, fpOthersWrite}) let punchd = Punchd(unixSocket: unixSocket, tcpSyniCP: initTcpSyniConnectPuncher(), - tcpSyniAP: initTcpSyniAcceptPuncher()) + tcpSyniAP: initTcpSyniAcceptPuncher(), + tcpNutssInitiator: initTcpNutssInitiator(), + tcpNutssResponder: initTcpNutssResponder()) asyncCheck handleUsers(punchd) try: runForever() except Sigint: waitFor punchd.tcpSyniCP.cleanup() - waitFor punchd.tcpSyniAP.cleanup + waitFor punchd.tcpSyniAP.cleanup() + waitFor punchd.tcpNutssInitiator.cleanup() + waitFor punchd.tcpNutssResponder.cleanup() punchd.unixSocket.close() removeFile(PunchdSocket) diff --git a/tcp_nutss_initiator.nim b/tcp_nutss_initiator.nim new file mode 100644 index 0000000..cb6da62 --- /dev/null +++ b/tcp_nutss_initiator.nim @@ -0,0 +1,98 @@ +import asyncdispatch, asyncnet, strformat +from net import IpAddress, Port, `$`, `==`, parseIpAddress +from random import randomize, rand +from sequtils import any +import ip_packet +import port_prediction +import puncher +import raw_socket +import utils + +export PunchHoleError + +type + Attempt = object + srcIp: IpAddress + srcPort: Port + dstIp: IpAddress + dstPorts: seq[Port] + future: Future[AsyncSocket] + + TcpNutssInitiator* = Puncher[Attempt] + + PunchProgressCb* = proc() {.async.} + +proc cleanup*(puncher: TcpNutssInitiator) {.async.} = + while puncher.attempts.len() != 0: + await puncher.attempts.pop().deleteFirewallRules() # FIXME: needed? + +proc initTcpNutssInitiator*(): TcpNutssInitiator = + randomize() + TcpNutssInitiator() + +proc injectSynPackets(attempt: Attempt) {.async.} = + let injectFd = setupTcpInjectingSocket() + for dstPort in attempt.dstPorts: + let synOut = IpPacket(protocol: tcp, ipAddrSrc: attempt.srcIp, + ipAddrDst: attempt.dstIp, ipTTL: 2, + tcpPortSrc: attempt.srcPort, tcpPortDst: dstPort, + tcpSeqNumber: rand(uint32), tcpAckNumber: 0, + tcpFlags: {SYN}, tcpWindowSize: 1452 * 10) + echo &"[{synOut.ipAddrSrc}:{synOut.tcpPortSrc} -> {synOut.ipAddrDst}:{synOut.tcpPortDst}, SEQ {synOut.tcpSeqNumber}] injecting outgoing SYN" + await injectFd.injectTcpPacket(synOut) + +proc accept(puncher: TcpNutssInitiator, ip: IpAddress, port: Port) {.async.} = + let sock = newAsyncSocket() + #sock.setSockOpt(OptReuseAddr, true) + sock.bindAddr(port, $ip) + sock.listen() + while true: + let acceptFuture = sock.accept() + await acceptFuture or sleepAsync(Timeout) + if acceptFuture.finished(): + let peer = acceptFuture.read() + let (peerAddr, peerPort) = peer.getPeerAddr() + let peerIp = parseIpAddress(peerAddr) + let i = puncher.findAttempt(ip, port, peerIp, @[peerPort]) + if i == -1: + echo "Accepted connection, but no attempt found. Discarding." + peer.close() + continue + else: + let attempt = puncher.attempts[i] + attempt.future.complete(peer) + let attempts = puncher.findAttemptsByLocalAddr(ip, port) + if attempts.len() <= 1: + break + sock.close() + +proc initiate*(puncher: TcpNutssInitiator, srcPort: Port, dstIp: IpAddress, + dstPorts: seq[Port], progressCb: PunchProgressCb): + Future[AsyncSocket] {.async.} = + let localIp = getPrimaryIPAddr(dstIp) + let existingAttempts = puncher.findAttemptsByLocalAddr(localIp, srcPort) + if existingAttempts.len() == 0: + echo &"initiating connection to {dstIp}:{dstPorts[0].int}" + asyncCheck puncher.accept(localIp, srcPort) + else: + for a in existingAttempts: + if a.dstIp == dstIp and + a.dstPorts.any(proc (p: Port): bool = p in dstPorts): + raise newException(PunchHoleError, "hole punching for given parameters already active") + try: + let attempt = Attempt(srcIp: localIp, srcPort: srcPort, dstIp: dstIp, + dstPorts: predictPortRange(dstPorts), + future: newFuture[AsyncSocket]("initiate")) + puncher.attempts.add(attempt) + #await attempt.addFirewallRules() + await attempt.injectSynPackets() + await progressCb() + await attempt.future or sleepAsync(Timeout) + #await attempt.deleteFirewallRules() + puncher.attempts.del(puncher.attempts.find(attempt)) + if attempt.future.finished(): + result = attempt.future.read() + else: + raise newException(PunchHoleError, "timeout") + except OSError as e: + raise newException(PunchHoleError, e.msg) diff --git a/tcp_nutss_responder.nim b/tcp_nutss_responder.nim new file mode 100644 index 0000000..280ab8f --- /dev/null +++ b/tcp_nutss_responder.nim @@ -0,0 +1,50 @@ +import asyncdispatch, asyncnet, strformat +from net import IpAddress, Port, `$`, `==` +import puncher +import utils + +export PunchHoleError + +type + Attempt = object + srcIp: IpAddress + srcPort: Port + dstIp: IpAddress + dstPorts: seq[Port] + + TcpNutssResponder* = Puncher[Attempt] + +proc cleanup*(puncher: TcpNutssResponder) {.async.} = + while puncher.attempts.len() != 0: + await puncher.attempts.pop().deleteFirewallRules() # FIXME: needed? + +proc initTcpNutssResponder*(): TcpNutssResponder = + TcpNutssResponder() + +proc connect(srcIp: IpAddress, srcPort: Port, dstIp: IpAddress, dstPort: Port, + future: Future[AsyncSocket]) {.async.} = + let sock = newAsyncSocket() + sock.setSockOpt(OptReuseAddr, true) + echo &"connect {srcIp}:{srcPort} -> {dstIp}:{dstPort}" + sock.bindAddr(srcPort, $srcIp) + try: + await sock.connect($dstIp, dstPort) + future.complete(sock) + except OSError as e: + echo &"connection {srcIP}:{srcPort.int} -> {dstIp}:{dstPort.int} failed: ", e.msg + sock.close() + +proc respond*(puncher: TcpNutssResponder, srcPort: Port, dstIp: IpAddress, + dstPorts: seq[Port]): Future[AsyncSocket] {.async.} = + let localIp = getPrimaryIPAddr(dstIp) + try: + let connectFuture = newFuture[AsyncSocket]("respond") + for dstPort in dstPorts: + asyncCheck connect(localIp, srcPort, dstIp, dstPort, connectFuture) + await connectFuture or sleepAsync(Timeout) + if connectFuture.finished(): + result = connectFuture.read() + else: + raise newException(PunchHoleError, "timeout") + except OSError as e: + raise newException(PunchHoleError, e.msg)