first try implementing the NUTSS (b) approach

This commit is contained in:
Christian Ulrich 2020-10-14 20:52:20 +02:00
parent 9fac0cec0e
commit 3f27102e20
No known key found for this signature in database
GPG Key ID: 8241BE099775A097
3 changed files with 187 additions and 4 deletions

View File

@ -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,20 +63,35 @@ 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)

98
tcp_nutss_initiator.nim Normal file
View File

@ -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)

50
tcp_nutss_responder.nim Normal file
View File

@ -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)