enter punchd
This commit is contained in:
parent
9056b28627
commit
f5e07b79f4
140
punch.nim
140
punch.nim
|
@ -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)
|
||||
|
|
@ -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()
|
|
@ -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")
|
||||
|
||||
|
|
@ -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")
|
Loading…
Reference in New Issue