140 lines
5.5 KiB
Nim
140 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.
|
|
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 ``srcIp``,
|
|
## ``srcPort`` and ``dstIp`` are equal and their ``dstPorts`` overlap.
|
|
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 getProtocol*(puncher: Puncher): Protocol {.base.} =
|
|
## Returns the transport protocol the puncher employs.
|
|
raise newException(CatchableError, "Method without implementation override")
|
|
|
|
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")
|
|
|
|
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
|