punchd/puncher.nim

139 lines
5.5 KiB
Nim

import asyncdispatch, asyncnet, strformat, options
from nativesockets import Protocol
from net import IpAddress, Port, `$`, `==`
from sequtils import any
import asyncutils
type
Attempt* = ref object of RootObj
## A hole punching attempt.
##
## It is created by parsing the arguments of a request in either
## ``parseInitiateRequest`` or ``parseRespondRequest`` and must not be
## modified afterwards. By inclduing ``some(acceptFuture)``, the puncher
## tells the caller to accept connections at the local IP address and
## port ``srcPort`` before calling ``initiate``. The desired transport
## protocol can be obtained by calling ``getProcotol``. The puncher expects
## the caller to complete the future when a connections from
## ``dstIp``:``dstPort`` has been accepted.
protocol*: Protocol
srcIp*: IpAddress
srcPort*: Port
dstIp*: IpAddress
dstPorts*: seq[Port]
acceptFuture*: Option[Future[AsyncSocket]]
Puncher* = ref object of RootObj
## A hole puncher.
PunchHoleError* = object of ValueError
attempt: Attempt
## An exception indicating that the contained ``attempt`` has failed.
PunchProgressCb* = proc(extraArgs: string) {.async.}
## A callback to allow a puncher report progress.
##
## When called a status message of type ``progress``, including the given
## ``extraArgs`` has to be sent to the application.
const Timeout* = 3000
proc `==`*(a, b: Attempt): bool =
## ``==`` for hole punching attempts.
##
## Two hole punching attempts are considered equal if their ``protocol`` is
## the same, ``srcIp``, ``srcPort`` and ``dstIp`` are equal and their
## ``dstPorts`` overlap.
a.protocol == b.protocol and a.srcIp == b.srcIp and a.srcPort == b.srcPort and
a.dstIp == b.dstIp and a.dstPorts.any(proc (p: Port): bool = p in b.dstPorts)
method cleanup*(attempt: Attempt): Future[void] {.base, async.} =
## Cleans up when an attempt finished (either successful or not).
##
## Does nothing. Override for custom attempt types.
discard
method parseInitiateRequest*(puncher: Puncher, args: string): Attempt {.base.} =
## Creates a new hole punching attempt by parsing arguments of an ``initiate``
## request.
##
## ``args`` has to be the arguments after the ``TECHNIQUE`` field, i.e.
## ``IP_FROM|PORTS_FROM|IP_TO|PORTS_TO``. My throw a ``ValueError`` if
## ``args`` is invalid.
raise newException(CatchableError, "Method without implementation override")
method parseRespondRequest*(puncher: Puncher, args: string): Attempt {.base.} =
## Creates a new hole punching attempt by parsing arguments of a ``respond``
## request.
##
## ``args`` has to be the arguments after the ``TECHNIQUE`` field, i.e.
## ``IP_FROM|PORTS_FROM|IP_TO|PORTS_TO|EXTRA_ARGS``. May throw a
## ``ValueError`` if ``args`` is invalid.
raise newException(CatchableError, "Method without implementation override")
method initiate*(puncher: Puncher, attempt: Attempt, progress: PunchProgressCb):
Future[AsyncSocket] {.base, async.} =
## Initiate a hole punching attempt.
##
## ``attempt`` has to be obtained by calling ``parseInitiateRequest``.
## ``progress`` will be called when the attempt has been initiated and a
## status message of type ``progress`` has to be sent to the application. The
## returned future contains a connected socket and will fail with a
## ``PunchHoleError`` if the hole punching fails.
block: # workaround for https://github.com/nim-lang/Nim/issues/12530
raise newException(CatchableError, "Method without implementation override")
method respond*(puncher: Puncher, attempt: Attempt):
Future[AsyncSocket] {.base, async.} =
## Respond to a hole punching attempt initiated by another peer.
##
## ``attempt has to be obtained by calling ``parseRespondRequest``. The
## returned future contains a connected socket and will fail with as
## ``PunchHoleError`` if the hole punching fails.
block: # workaround for https://github.com/nim-lang/Nim/issues/12530
raise newException(CatchableError, "Method without implementation override")
# FIXME move firewall procs to dedicated module
proc makeFirewallRule(srcIp: IpAddress, srcPort: Port,
dstIp: IpAddress, dstPort: Port): string =
result = &"""-w \
-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"""
proc iptablesInsert(chain: string, rule: string) {.async.} =
let firewall_cmd = &"iptables -I {chain} {rule}"
discard await asyncExecCmd(firewall_cmd)
proc iptablesDelete(chain: string, rule: string) {.async.} =
let firewall_cmd = &"iptables -D {chain} {rule}"
discard await asyncExecCmd(firewall_cmd)
proc addFirewallRules*(attempt: Attempt) {.async.} =
for dstPort in attempt.dstPorts:
let rule = makeFirewallRule(attempt.srcIp, attempt.srcPort,
attempt.dstIp, dstPort)
try:
await iptablesInsert("INPUT", rule)
except OSError as e:
echo "cannot add firewall rule: ", e.msg
raise newException(PunchHoleError, e.msg)
proc deleteFirewallRules*(attempt: Attempt) {.async.} =
for dstPort in attempt.dstPorts:
let rule = makeFirewallRule(attempt.srcIp, attempt.srcPort,
attempt.dstIp, dstPort)
try:
await iptablesDelete("INPUT", rule)
except OSError:
# At least we tried
discard