enter punchd

This commit is contained in:
Christian Ulrich 2020-07-07 19:39:28 +02:00
parent 9056b28627
commit f5e07b79f4
No known key found for this signature in database
GPG Key ID: 8241BE099775A097
4 changed files with 265 additions and 140 deletions

140
punch.nim
View File

@ -1,140 +0,0 @@
import
asyncdispatch,
asyncutils,
network_interface,
packet_info,
strformat
from nativesockets import SOCK_RAW, bindAddr, htons
from posix import ioctl, setsockopt, SockAddr, SockLen, SocketHandle
export NetworkInterfaceError
type
Sockaddr_ll {.importc: "struct sockaddr_ll", pure, final,
header: "<linux/if_packet.h>".} = object
sll_family: cushort # Always AF_PACKET
sll_protocol: cushort # Physical-layer protocol
sll_ifindex: cint # Interface number
sll_hatype: cushort # ARP hardware type
sll_pkttype: cuchar # Packet type
sll_halen: cuchar # Length of address
sll_addr: array[8, cuchar] # Physical-layer address
Packet_mreq {.importc: "struct packet_mreq", pure, final,
header: "<linux/if_packet.h>".} = object
mr_ifindex: cint
mr_type: cushort
mr_alen: cushort
mr_address: array[8, cuchar]
PunchHoleError* = object of ValueError
var
AF_PACKET {.importc: "AF_PACKET", header: "<sys/socket.h>".}: cushort
SOL_PACKET {.importc: "SOL_PACKET", header: "<sys/socket.h>".}: cushort
ETH_P_ALL {.importc: "ETH_P_ALL", header: "<linux/if_ether.h>".}: cushort
PACKET_ADD_MEMBERSHIP {.importc: "PACKET_ADD_MEMBERSHIP", header: "<linux/if_packet.h>".}: cushort
PACKET_MR_PROMISC {.importc: "PACKET_MR_PROMISC", header: "<linux/if_packet.h>".}: cushort
proc setupRawSocket(iface: NetworkInterface): AsyncFD =
# Create a raw packet socket. For accessing outgoing packets we need to use
# ETH_P_ALL which is needed in network byte order, see packet(7) man page.
result = createAsyncNativeSocket(AF_PACKET.cint,
SOCK_RAW.cint,
htons(ETH_P_ALL).cint)
# Limit capturing of packets to the desired network interface, see
# netdevice(7) man page
echo "interface: ", iface.name, ", index: ", iface.index
var sa = Sockaddr_ll(sll_family: AF_PACKET,
sll_protocol: htons(ETH_P_ALL),
sll_ifindex: iface.index)
if bindAddr(result.SocketHandle,
cast[ptr SockAddr](addr sa),
sizeof(Sockaddr_ll).SockLen) != 0:
raise newException(PunchHoleError, "cannot bind to interface")
# Enable promiscuous mode, see netdevice(7) man page
var req = Packet_mreq(mr_ifindex: iface.index, mr_type: PACKET_MR_PROMISC)
if setsockopt(result.SocketHandle,
SOL_PACKET.cint,
PACKET_ADD_MEMBERSHIP.cint,
addr req,
sizeof(req).SockLen) != 0:
raise newException(PunchHoleError, "cannot enable promiscuous mode")
proc captureSequenceNumber(rawFd: AsyncFD,
clientAddress: string,
clientPort: Port,
serverAddress: string,
serverPort: Port): Future[uint32] {.async.} =
while true:
let packet = await rawFd.recv(4000)
if packet == "":
break
echo "packet len: ", packet.len
let packetInfo = fromString(packet)
if packetInfo.protocol == tcp and
packetInfo.tcpIpSrc == clientAddress and
packetInfo.tcpPortSrc.int == clientPort.int and
packetInfo.tcpIpDst == serverAddress and
packetInfo.tcpPortDst.int == serverPort.int:
return packetInfo.tcpSeqNumber
proc addFirewallRule(clientAddress: string,
clientPort: Port,
serverAddress: string,
serverPort: Port) {.async.} =
let firewall_cmd = fmt"""iptables -A INPUT \
-d {clientAddress} \
-p icmp \
--icmp-type time-exceeded \
-m conntrack \
--ctstate RELATED \
--ctproto tcp \
--ctorigsrc {clientAddress} \
--ctorigsrcport {clientPort.int} \
--ctorigdst {serverAddress} \
--ctorigdstport {serverPort.int} \
-j DROP"""
let exitcode = await asyncExecCmd(firewall_cmd)
if exitcode != 0:
raise newException(PunchHoleError, "cannot add firewall rule")
proc deleteFirewallRule(clientAddress: string,
clientPort: Port,
serverAddress: string,
serverPort: Port) {.async.} =
let firewall_cmd = fmt"""iptables -D INPUT \
-d {clientAddress} \
-p icmp \
--icmp-type time-exceeded \
-m conntrack \
--ctstate RELATED \
--ctproto tcp \
--ctorigsrc {clientAddress} \
--ctorigsrcport {clientPort.int} \
--ctorigdst {serverAddress} \
--ctorigdstport {serverPort.int} \
-j DROP"""
let exitcode = await asyncExecCmd(firewall_cmd)
if exitcode != 0:
raise newException(PunchHoleError, "cannot delete firewall rule")
proc punchHoleAsClient*(clientAddress: string,
clientPort: Port,
serverAddress: string,
serverPort: Port) {.async.} =
let iface = fromIpAddress(clientAddress)
let rawFd = setupRawSocket(iface)
let seqNumber = await captureSequenceNumber(rawFd,
clientAddress,
clientPort,
serverAddress,
serverPort)
echo "captured sequence number: ", seqNumber
await addFirewallRule(clientAddress, clientPort, serverAddress, serverPort)
await deleteFirewallRule(clientAddress, clientPort, serverAddress, serverPort)
closeSocket(rawFd)

84
punchd.nim Normal file
View File

@ -0,0 +1,84 @@
import asyncdispatch, asyncnet, os, strformat, strutils
from nativesockets import Domain, SockType, Protocol
from net import IpAddress, Port, `$`
import asyncutils
import message
import tcp_syni
from strutils import format, join
from nativesockets import setSockOptInt
type
# Requests
TcpSyniConnect = object
srcIp: IpAddress
srcPorts: array[3, Port]
dstIp: IpAddress
dstPorts: array[3, Port]
TcpSyniAccept = object
dstIp: IpAddress
dstPorts: array[3, Port]
srcIp: IpAddress
srcPorts: array[3, Port]
seqNums: array[10, uint32]
proc handleRequest(line: string, unixSock: AsyncSocket) {.async.} =
var id: string
var sock: AsyncSocket
var puncher: TcpSyniPuncher
try:
let args = line.parseArgs(3)
id = args[1]
case args[0]:
of "tcp-syni-connect":
let req = parseMessage[TcpSyniConnect](args[2])
proc handleSeqNumbers(seqNumbers: seq[uint32]) {.async.} =
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")
puncher = await initPuncher(req.srcPorts[0], req.dstIp, req.dstPorts)
sock = await puncher.connect(handleSeqNumbers)
of "tcp-syni-accept":
let req = parseMessage[TcpSyniAccept](args[2])
puncher = await initPuncher(req.srcPorts[0], req.dstIp, req.dstPorts,
@(req.seqNums))
sock = await puncher.accept()
else:
raise newException(ValueError, "invalid request")
let unixFd = unixSock.getFd.AsyncFD
await unixFd.asyncSendMsg(&"ok|{id}\n", @[fromFd(sock.getFd.AsyncFD)])
await puncher.cleanup
except PunchHoleError as e:
await unixSock.send(&"error|{id}|{e.msg}\n")
await puncher.cleanup
except ValueError:
unixSock.close
proc handleRequests(userSock: AsyncSocket) {.async.} =
while true:
let line = await userSock.recvLine(maxLength = 400)
if line.len == 0:
break
asyncCheck handleRequest(line, userSock)
proc handleUsers(sock: AsyncSocket) {.async.} =
while true:
let user = await sock.accept()
asyncCheck handleRequests(user)
proc main() =
removeFile("/tmp/punchd.socket")
let unixSocket = newAsyncSocket(AF_UNIX, SOCK_STREAM, IPPROTO_IP)
unixSocket.bindUnix("/tmp/punchd.socket")
asyncCheck handleUsers(unixSocket)
runForever()
when isMainModule:
main()

60
raw_socket.nim Normal file
View File

@ -0,0 +1,60 @@
import asyncdispatch
from nativesockets import SOCK_RAW, bindAddr, htons
from posix import setsockopt, SockAddr, SockLen, SocketHandle
import network_interface
type
RawSocketError* = object of CatchableError
Sockaddr_ll {.importc: "struct sockaddr_ll", pure, final,
header: "<linux/if_packet.h>".} = object
sll_family: cushort # Always AF_PACKET
sll_protocol: cushort # Physical-layer protocol
sll_ifindex: cint # Interface number
sll_hatype: cushort # ARP hardware type
sll_pkttype: cuchar # Packet type
sll_halen: cuchar # Length of address
sll_addr: array[8, cuchar] # Physical-layer address
Packet_mreq {.importc: "struct packet_mreq", pure, final,
header: "<linux/if_packet.h>".} = object
mr_ifindex: cint
mr_type: cushort
mr_alen: cushort
mr_address: array[8, cuchar]
var
AF_PACKET {.importc: "AF_PACKET", header: "<sys/socket.h>".}: cushort
SOL_PACKET {.importc: "SOL_PACKET", header: "<sys/socket.h>".}: cushort
ETH_P_ALL {.importc: "ETH_P_ALL", header: "<linux/if_ether.h>".}: cushort
PACKET_ADD_MEMBERSHIP {.importc: "PACKET_ADD_MEMBERSHIP", header: "<linux/if_packet.h>".}: cushort
PACKET_MR_PROMISC {.importc: "PACKET_MR_PROMISC", header: "<linux/if_packet.h>".}: cushort
proc setupRawSocket*(iface: NetworkInterface): AsyncFD =
# Create a raw packet socket. For accessing outgoing packets we need to use
# ETH_P_ALL which is needed in network byte order, see packet(7) man page.
result = createAsyncNativeSocket(AF_PACKET.cint,
SOCK_RAW.cint,
htons(ETH_P_ALL).cint)
# Limit capturing of packets to the desired network interface, see
# netdevice(7) man page
echo "interface: ", iface.name, ", index: ", iface.index
var sa = Sockaddr_ll(sll_family: AF_PACKET,
sll_protocol: htons(ETH_P_ALL),
sll_ifindex: iface.index)
if bindAddr(result.SocketHandle,
cast[ptr SockAddr](addr sa),
sizeof(Sockaddr_ll).SockLen) != 0:
raise newException(RawSocketError, "cannot bind to interface")
# Enable promiscuous mode, see netdevice(7) man page
var req = Packet_mreq(mr_ifindex: iface.index, mr_type: PACKET_MR_PROMISC)
if setsockopt(result.SocketHandle,
SOL_PACKET.cint,
PACKET_ADD_MEMBERSHIP.cint,
addr req,
sizeof(req).SockLen) != 0:
raise newException(RawSocketError, "cannot enable promiscuous mode")

121
tcp_syni.nim Normal file
View File

@ -0,0 +1,121 @@
import asyncdispatch, asyncnet, strformat
from net import IpAddress, Port, `$`, getPrimaryIPAddr
from nativesockets import setSockOptInt
import asyncutils
import network_interface
import packet_info
import raw_socket
var IPPROTO_IP {.importc: "IPPROTO_IP", header: "<netinet/in.h>".}: cint
var IP_TTL {.importc: "IP_TTL", header: "<netinet/in.h>".}: cint
type
TcpSyniPuncher* = object
srcIp: IpAddress
srcPort: Port
dstIp: IpAddress
dstPorts: array[10, Port]
seqNums: seq[uint32]
PunchProgressCb* = proc (seqNums: seq[uint32]) {.async.}
PunchHoleError* = object of ValueError
proc addFirewallRule(srcIp: IpAddress, srcPort: Port,
dstIp: IpAddress, dstPort: Port) {.async.} =
let firewall_cmd = fmt"""iptables -A INPUT \
-d {srcIp} \
-p icmp \
--icmp-type time-exceeded \
-m conntrack \
--ctstate RELATED \
--ctproto tcp \
--ctorigsrc {srcIp} \
--ctorigsrcport {srcPort.int} \
--ctorigdst {dstIp} \
--ctorigdstport {dstPort.int} \
-j DROP"""
let exitcode = await asyncExecCmd(firewall_cmd)
if exitcode != 0:
raise newException(PunchHoleError, "cannot add firewall rule")
proc delFirewallRule(srcIp: IpAddress, srcPort: Port,
dstIp: IpAddress, dstPort: Port) {.async.} =
let firewall_cmd = fmt"""iptables -D INPUT \
-d {srcIp} \
-p icmp \
--icmp-type time-exceeded \
-m conntrack \
--ctstate RELATED \
--ctproto tcp \
--ctorigsrc {srcIp} \
--ctorigsrcport {srcPort.int} \
--ctorigdst {dstIp} \
--ctorigdstport {dstPort.int} \
-j DROP"""
let exitcode = await asyncExecCmd(firewall_cmd)
if exitcode != 0:
raise newException(PunchHoleError, "cannot delete firewall rule")
proc captureSeqNumbers(puncher: TcpSyniPuncher, rawFd: AsyncFD,
cb: PunchProgressCb) {.async.} =
# FIXME: timeout?
var seqNums = newSeq[uint32]()
while seqNums.len < puncher.dstPorts.len:
let packet = await rawFd.recv(4000)
if packet == "":
break
echo "packet len: ", packet.len
let packetInfo = fromString(packet)
if packetInfo.protocol == tcp and
packetInfo.tcpIpSrc == $puncher.srcIp and
packetInfo.tcpPortSrc.int == puncher.srcPort.int and
packetInfo.tcpIpDst == $puncher.dstIp:
for i, port in puncher.dstPorts.pairs:
if packetInfo.tcpPortDst.int == port.int:
seqNums.add(packetInfo.tcpSeqNumber)
break
await cb(seqNums)
proc initPuncher*(srcPort: Port, dstIp: IpAddress, dstPorts: array[3, Port],
seqNums: seq[uint32] = @[]): Future[TcpSyniPuncher] {.async.} =
let localIp = getPrimaryIPAddr(dstIp)
# TODO: do real port prediction
var predictedDstPorts: array[10, Port]
let basePort = min(dstPorts[1].uint16, uint16.high - 9)
for i in 0.uint16 .. 9.uint16:
predictedDstPorts[i] = Port(basePort + i)
result = TcpSyniPuncher(srcIp: localIp, srcPort: srcPort, dstIp: dstIp,
dstPorts: predictedDstPorts, seqNums: seqNums)
for dstPort in result.dstPorts:
await addFirewallRule(result.srcIp, result.srcPort, result.dstIp, dstPort)
proc cleanup*(puncher: TcpSyniPuncher) {.async.} =
for dstPort in puncher.dstPorts:
await delFirewallRule(puncher.srcIp, puncher.srcPort, puncher.dstIp, dstPort)
proc doConnect(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)
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
discard
proc connect*(puncher: TcpSyniPuncher,
progressCb: PunchProgressCb): Future[AsyncSocket] =
result = newFuture[AsyncSocket]("tcp_syni.connect")
let iface = fromIpAddress($puncher.srcIp)
let rawFd = setupRawSocket(iface)
asyncCheck puncher.captureSeqNumbers(rawFd, progressCb)
for dstPort in puncher.dstPorts:
asyncCheck doConnect(puncher.srcIp, puncher.srcPort, puncher.dstIp,
dstPort, result)
proc accept*(puncher: TcpSyniPuncher): Future[AsyncSocket] =
result = newFuture[AsyncSocket]("tcp_syni.accept")