punchd/tcp_syni.nim

189 lines
7.3 KiB
Nim

from nativesockets import Protocol, setSockOptInt
from net import IpAddress, Port, `$`, `==`
from random import randomize, rand
from strutils import join
import asyncdispatch, asyncnet, strformat
import ip_packet
import message
import network_interface
import options
import port_prediction
import puncher
import raw_socket
import utils
export puncher
type
TcpSyniPuncher* = ref object of Puncher
InitiateRequest = object
srcIp: IpAddress
srcPorts: seq[Port]
dstIp: IpAddress
dstPorts: seq[Port]
RespondRequest = object
dstIp: IpAddress
dstPorts: seq[Port]
srcIp: IpAddress
srcPorts: seq[Port]
seqNums: seq[uint32]
TcpSyniInitiateAttempt = ref object of Attempt
TcpSyniRespondAttempt = ref object of Attempt
seqNums: seq[uint32]
var IPPROTO_IP {.importc: "IPPROTO_IP", header: "<netinet/in.h>".}: cint
var IP_TTL {.importc: "IP_TTL", header: "<netinet/in.h>".}: cint
proc captureSeqNumbers(attempt: Attempt, cb: PunchProgressCb) {.async.} =
# FIXME: timeout?
let iface = getNetworkInterface(attempt.srcIp)
let captureFd = setupEthernetCapturingSocket(iface)
var seqNums = newSeq[uint32]()
while seqNums.len < attempt.dstPorts.len:
let packet = await captureFd.recv(4000)
if packet == "":
break
let parsed = parseEthernetPacket(packet)
if parsed.protocol == tcp and
parsed.ipAddrSrc == attempt.srcIp and
parsed.tcpPortSrc.int == attempt.srcPort.int and
parsed.ipAddrDst == attempt.dstIp and
parsed.tcpFlags == {SYN}:
for port in attempt.dstPorts:
if parsed.tcpPortDst.int == port.int:
seqNums.add(parsed.tcpSeqNumber)
break
closeSocket(captureFd)
await cb(seqNums.join(","))
proc captureAndResendAck(attempt: Attempt) {.async.} =
let iface = getNetworkInterface(attempt.srcIp)
let captureFd = setupEthernetCapturingSocket(iface)
let injectFd = setupTcpInjectingSocket()
block loops:
while true:
let packet = await captureFd.recv(4000)
if packet == "":
break
var parsed = parseEthernetPacket(packet)
if parsed.protocol == tcp and
parsed.ipAddrSrc == attempt.srcIp and
parsed.tcpPortSrc.int == attempt.srcPort.int and
parsed.ipAddrDst == attempt.dstIp and
parsed.tcpFlags == {ACK}:
for port in attempt.dstPorts:
if parsed.tcpPortDst.int == port.int:
parsed.ipTTL = 64
echo &"[{parsed.ipAddrSrc}:{parsed.tcpPortSrc.int} -> {parsed.ipAddrDst}:{parsed.tcpPortDst}, SEQ {parsed.tcpSeqNumber}] resending ACK with TTL {parsed.ipTTL}"
await injectFd.injectTcpPacket(parsed)
break loops
closeSocket(captureFd)
closeSocket(injectFd)
proc connect(srcIp: IpAddress, srcPort: Port, dstIp: IpAddress, dstPort: Port,
future: Future[AsyncSocket]) {.async.} =
let sock = newAsyncSocket()
sock.setSockOpt(OptReuseAddr, true)
sock.getFd.setSockOptInt(IPPROTO_IP, IP_TTL, 2)
echo &"connect {srcIp}:{srcPort} -> {dstIp}:{dstPort}"
sock.bindAddr(srcPort, $srcIp)
try:
await sock.connect($dstIp, dstPort)
sock.getFd.setSockOptInt(IPPROTO_IP, IP_TTL, 64)
future.complete(sock)
except OSError as e:
echo &"connection {srcIP}:{srcPort.int} -> {dstIp}:{dstPort.int} failed: ", e.msg
sock.close()
proc injectSynPackets(attempt: TcpSyniRespondAttempt) {.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)
for seqNum in attempt.seqNums:
let synIn = IpPacket(protocol: tcp, ipAddrSrc: attempt.dstIp,
ipAddrDst: attempt.srcIp, ipTTL: 64,
tcpPortSrc: dstPort,
tcpPortDst: attempt.srcPort,
tcpSeqNumber: seqNum, tcpAckNumber: 0,
tcpFlags: {SYN}, tcpWindowSize: 1452 * 10)
echo &"[{synIn.ipAddrSrc}:{synIn.tcpPortSrc} -> {synIn.ipAddrDst}:{synIn.tcpPortDst}, SEQ {synIn.tcpSeqNumber}] injecting incoming SYN"
await injectFd.injectTcpPacket(synIn)
closeSocket(injectFd)
proc initTcpSyniPuncher*(): TcpSyniPuncher =
randomize()
TcpSyniPuncher()
method cleanup*(attempt: TcpSyniInitiateAttempt) {.async.} =
await deleteFirewallRules(attempt)
method cleanup*(attempt: TcpSyniRespondAttempt) {.async.} =
discard
method getProtocol*(puncher: TcpSyniPuncher): Protocol =
IPPROTO_TCP
method parseInitiateRequest*(puncher: TcpSyniPuncher, args: string): Attempt =
let parsed = parseMessage[InitiateRequest](args)
let localIp = getPrimaryIPAddr(parsed.dstIp)
let predictedDstPorts = predictPortRange(parsed.dstPorts)
TcpSyniInitiateAttempt(srcIp: localIp, srcPort: parsed.srcPorts[0],
dstIp: parsed.dstIp, dstPorts: predictedDstPorts,
acceptFuture: none(Future[AsyncSocket]))
method parseRespondRequest*(puncher: TcpSyniPuncher, args: string): Attempt =
let parsed = parseMessage[RespondRequest](args)
let localIp = getPrimaryIPAddr(parsed.dstIp)
let predictedDstPorts = predictPortRange(parsed.dstPorts)
let acceptFuture = newFuture[AsyncSocket]("parseRespondRequest")
TcpSyniRespondAttempt(srcIp: localIp, srcPort: parsed.srcPorts[0],
dstIp: parsed.dstIp, dstPorts: predictedDstPorts,
acceptFuture: some(acceptFuture),
seqNums: parsed.seqNums)
method initiate*(puncher: TcpSyniPuncher, attempt: Attempt,
progress: PunchProgressCb): Future[AsyncSocket] {.async.} =
assert(attempt of TcpSyniInitiateAttempt, "unexpected attempt type")
assert(attempt.acceptFuture.isNone(), "expected attempt without acceptFuture")
await addFirewallRules(attempt)
asyncCheck captureSeqNumbers(attempt, progress)
asyncCheck captureAndResendAck(attempt)
try:
let connectFuture = newFuture[AsyncSocket]("connect")
for dstPort in attempt.dstPorts:
asyncCheck connect(attempt.srcIp, attempt.srcPort, attempt.dstIp,
dstPort, connectFuture)
await connectFuture or sleepAsync(Timeout)
await deleteFirewallRules(attempt)
if connectFuture.finished():
result = connectFuture.read()
else:
raise newException(PunchHoleError, "timeout")
except OSError as e:
raise newException(PunchHoleError, e.msg)
method respond*(puncher: TcpSyniPuncher, attempt: Attempt):
Future[AsyncSocket] {.async.} =
assert(attempt of TcpSyniRespondAttempt, "unexpected attempt type")
assert(attempt.acceptFuture.isSome(), "expected attempt with acceptFuture")
try:
let acceptFuture = attempt.acceptFuture.get()
await injectSynPackets(TcpSyniRespondAttempt(attempt))
await acceptFuture or sleepAsync(Timeout)
if acceptFuture.finished():
result = acceptFuture.read()
else:
raise newException(PunchHoleError, "timeout")
except OSError as e:
raise newException(PunchHoleError, e.msg)