diff --git a/punch.nim b/punch.nim deleted file mode 100644 index 7231146..0000000 --- a/punch.nim +++ /dev/null @@ -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: "".} = 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: "".} = 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: "".}: cushort - SOL_PACKET {.importc: "SOL_PACKET", header: "".}: cushort - ETH_P_ALL {.importc: "ETH_P_ALL", header: "".}: cushort - PACKET_ADD_MEMBERSHIP {.importc: "PACKET_ADD_MEMBERSHIP", header: "".}: cushort - PACKET_MR_PROMISC {.importc: "PACKET_MR_PROMISC", header: "".}: 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) - diff --git a/punchd.nim b/punchd.nim new file mode 100644 index 0000000..bbc0ec9 --- /dev/null +++ b/punchd.nim @@ -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() diff --git a/raw_socket.nim b/raw_socket.nim new file mode 100644 index 0000000..7265b97 --- /dev/null +++ b/raw_socket.nim @@ -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: "".} = 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: "".} = object + mr_ifindex: cint + mr_type: cushort + mr_alen: cushort + mr_address: array[8, cuchar] + +var + AF_PACKET {.importc: "AF_PACKET", header: "".}: cushort + SOL_PACKET {.importc: "SOL_PACKET", header: "".}: cushort + ETH_P_ALL {.importc: "ETH_P_ALL", header: "".}: cushort + PACKET_ADD_MEMBERSHIP {.importc: "PACKET_ADD_MEMBERSHIP", header: "".}: cushort + PACKET_MR_PROMISC {.importc: "PACKET_MR_PROMISC", header: "".}: 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") + + diff --git a/tcp_syni.nim b/tcp_syni.nim new file mode 100644 index 0000000..84ff610 --- /dev/null +++ b/tcp_syni.nim @@ -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: "".}: cint +var IP_TTL {.importc: "IP_TTL", header: "".}: 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")