2020-07-06 15:10:10 +02:00
|
|
|
import asyncdispatch, asyncnet, os, strformat, strutils, tables
|
|
|
|
from nativeSockets import Domain, SockType, Protocol
|
|
|
|
from net import IpAddress, Port, isIpAddress, `$`
|
|
|
|
import asyncutils
|
|
|
|
import ../../message
|
|
|
|
import random
|
|
|
|
|
2020-08-16 12:41:07 +02:00
|
|
|
const AcceptPort = Port(2000)
|
|
|
|
|
2020-07-06 15:10:10 +02:00
|
|
|
type
|
|
|
|
PunchdResponse = Future[tuple[msgContent: string, sock: AsyncSocket]]
|
|
|
|
PunchdProgressCb = proc (future: PunchdResponse, msgContent: string) {.async.}
|
|
|
|
|
|
|
|
OutgoingPunchdMessage = object
|
|
|
|
future: PunchdResponse
|
|
|
|
progressCb: PunchdProgressCb
|
|
|
|
|
|
|
|
PunchdConnection = object
|
|
|
|
sock: AsyncSocket
|
|
|
|
outMessages: TableRef[string, OutgoingPunchdMessage]
|
|
|
|
inConnections: FutureStream[AsyncSocket]
|
|
|
|
|
|
|
|
ServerConnection = object
|
|
|
|
sock: AsyncSocket
|
|
|
|
outMessages: TableRef[string, Future[string]]
|
|
|
|
peerNotifications: FutureStream[string]
|
2020-07-21 19:53:21 +02:00
|
|
|
probedIp: IpAddress
|
|
|
|
probedPorts: seq[Port]
|
2020-07-06 15:10:10 +02:00
|
|
|
|
|
|
|
# Punchd messages
|
|
|
|
ProgressTcpSyniConnect* = object
|
|
|
|
command: string
|
|
|
|
args: string
|
|
|
|
|
|
|
|
# Server messages
|
2020-07-26 17:48:47 +02:00
|
|
|
OkGetPeerinfo* = object
|
2020-07-06 15:10:10 +02:00
|
|
|
ip: string
|
|
|
|
ports: array[3, uint16]
|
2020-07-26 17:46:17 +02:00
|
|
|
OkGetEndpoint* = object
|
2020-07-06 15:10:10 +02:00
|
|
|
ip: IpAddress
|
|
|
|
port: Port
|
|
|
|
NotifyPeer* = object
|
|
|
|
sender: string
|
|
|
|
recipient: string
|
|
|
|
command: string
|
|
|
|
srcIp: IpAddress
|
|
|
|
srcPorts: array[3, Port]
|
|
|
|
dstIp: IpAddress
|
|
|
|
dstPorts: array[3, Port]
|
2020-07-21 11:10:56 +02:00
|
|
|
seqNumbers: seq[uint32]
|
2020-07-06 15:10:10 +02:00
|
|
|
|
|
|
|
# Exceptions
|
|
|
|
PunchdError = object of ValueError # FIXME: not used yet
|
|
|
|
ServerError = object of ValueError
|
|
|
|
|
|
|
|
proc usage() =
|
|
|
|
echo &"usage: {paramStr(0)} SERVER_HOSTNAME SERVER_PORT PEER_ID [OTHER_PEER_ID]"
|
|
|
|
|
2020-07-26 17:46:17 +02:00
|
|
|
proc handleServerMessages(conn: ServerConnection) {.async.} =
|
2020-07-06 15:10:10 +02:00
|
|
|
while true:
|
2020-07-26 17:46:17 +02:00
|
|
|
let line = await conn.sock.recvLine(maxLength = 400)
|
2020-07-06 15:10:10 +02:00
|
|
|
let args = line.parseArgs(3, 1)
|
|
|
|
case args[0]:
|
|
|
|
of "ok":
|
2020-07-26 17:46:17 +02:00
|
|
|
let future = conn.outMessages[args[1]]
|
|
|
|
conn.outMessages.del(args[1])
|
2020-07-06 15:10:10 +02:00
|
|
|
future.complete(args[2])
|
|
|
|
of "error":
|
2020-07-26 17:46:17 +02:00
|
|
|
let future = conn.outMessages[args[1]]
|
|
|
|
conn.outMessages.del(args[1])
|
2020-07-06 15:10:10 +02:00
|
|
|
future.fail(newException(ServerError, args[2]))
|
|
|
|
of "notify-peer":
|
2020-07-26 17:46:17 +02:00
|
|
|
asyncCheck conn.peerNotifications.write(line.substr(args[0].len + 1))
|
2020-07-06 15:10:10 +02:00
|
|
|
else:
|
|
|
|
raise newException(ValueError, "invalid server message")
|
|
|
|
|
2020-07-26 17:46:17 +02:00
|
|
|
proc handlePunchdMessages(conn: PunchdConnection) {.async.} =
|
2020-07-06 15:10:10 +02:00
|
|
|
while true:
|
2020-07-26 17:46:17 +02:00
|
|
|
let fd = conn.sock.getFd.AsyncFD
|
2020-07-06 15:10:10 +02:00
|
|
|
let resp = await fd.asyncRecvMsg(size = 400, cmsgSize = sizeof(AsyncFD))
|
2020-07-20 10:08:28 +02:00
|
|
|
let line = resp.data.strip(leading = false, trailing = true, chars = {'\n'})
|
|
|
|
let args = line.parseArgs(3, 1)
|
2020-07-06 15:10:10 +02:00
|
|
|
case args[0]:
|
|
|
|
of "ok":
|
2020-07-26 17:46:17 +02:00
|
|
|
let outMsg = conn.outMessages[args[1]]
|
|
|
|
conn.outMessages.del(args[1])
|
2020-07-06 15:10:10 +02:00
|
|
|
if resp.cmsgs.len != 1:
|
|
|
|
raise newException(ValueError, "invalid punchd message")
|
|
|
|
let sock = newAsyncSocket(resp.cmsgs[0].getFd)
|
|
|
|
register(sock.getFd.AsyncFD)
|
|
|
|
outMsg.future.complete((args[2], sock))
|
|
|
|
of "error":
|
2020-07-26 17:46:17 +02:00
|
|
|
let outMsg = conn.outMessages[args[1]]
|
|
|
|
conn.outMessages.del(args[1])
|
2020-07-06 15:10:10 +02:00
|
|
|
outMsg.future.fail(newException(ServerError, args[2]))
|
|
|
|
of "progress":
|
2020-07-26 17:46:17 +02:00
|
|
|
let outMsg = conn.outMessages[args[1]]
|
2020-07-06 15:10:10 +02:00
|
|
|
asyncCheck outMsg.progressCb(outMsg.future, args[2])
|
|
|
|
else:
|
|
|
|
raise newException(ValueError, "invalid punchd message")
|
|
|
|
|
|
|
|
proc sendRequest(connection: PunchdConnection, command: string, content: string,
|
|
|
|
progressCb: PunchdProgressCb = nil): PunchdResponse =
|
|
|
|
result = newFuture[PunchdResponse.T]("sendRequest")
|
|
|
|
let id = $rand(uint32)
|
|
|
|
asyncCheck connection.sock.send(&"{command}|{id}|{content}\n")
|
|
|
|
let outMsg = OutgoingPunchdMessage(future: result, progressCb: progressCb)
|
|
|
|
echo "id = ", id
|
|
|
|
connection.outMessages[id] = outMsg
|
|
|
|
|
|
|
|
proc sendRequest(connection: ServerConnection, command: string,
|
|
|
|
content: string): Future[string] =
|
|
|
|
result = newFuture[string]("sendRequest")
|
|
|
|
let id = $rand(uint32)
|
2020-07-26 17:46:17 +02:00
|
|
|
var request: string
|
|
|
|
if content.len != 0:
|
|
|
|
request = &"{command}|{id}|{content}\n"
|
|
|
|
else:
|
|
|
|
request = &"{command}|{id}\n"
|
|
|
|
asyncCheck connection.sock.send(request)
|
2020-07-06 15:10:10 +02:00
|
|
|
connection.outMessages[id] = result
|
|
|
|
|
|
|
|
proc acceptConnection(punchdConn: PunchdConnection, command: string,
|
|
|
|
msgContent: string) {.async.} =
|
|
|
|
echo "accepting connection ", msgContent
|
|
|
|
let resp = await punchdConn.sendRequest(command, msgContent)
|
|
|
|
asyncCheck punchdConn.inConnections.write(resp.sock)
|
|
|
|
|
|
|
|
proc handlePeerNotifications(serverConn: ServerConnection,
|
|
|
|
punchdConn: PunchdConnection,
|
|
|
|
peerId: string) {.async.} =
|
|
|
|
while true:
|
|
|
|
let (hasData, data) = await serverConn.peerNotifications.read
|
|
|
|
if not hasData:
|
|
|
|
break
|
|
|
|
try:
|
|
|
|
let msg = parseMessage[NotifyPeer](data)
|
|
|
|
# FIXME: check if we want to receive messages from the sender
|
|
|
|
echo "received message from ", msg.sender
|
|
|
|
let srcPorts = msg.srcPorts.join(",")
|
|
|
|
let dstPorts = msg.dstPorts.join(",")
|
|
|
|
let seqNumbers = msg.seqNumbers.join(",")
|
|
|
|
let req = &"{msg.srcIp}|{srcPorts}|{msg.dstIp}|{dstPorts}|{seqNumbers}"
|
|
|
|
asyncCheck acceptConnection(punchdConn, msg.command, req)
|
|
|
|
except ValueError as e:
|
|
|
|
echo e.msg
|
|
|
|
discard
|
|
|
|
|
|
|
|
proc punchHole(punchdConn: PunchdConnection, serverConn: ServerConnection,
|
2020-07-26 17:46:17 +02:00
|
|
|
peerId: string, otherPeerId: string):
|
|
|
|
Future[AsyncSocket] {.async.} =
|
2020-07-26 17:48:47 +02:00
|
|
|
let sResp = await serverConn.sendRequest("get-peerinfo", otherPeerId)
|
|
|
|
let peerInfo = parseMessage[OkGetPeerinfo](sResp)
|
2020-07-06 15:10:10 +02:00
|
|
|
proc progressCb(future: PunchdResponse, msgContent: string) {.async.} =
|
|
|
|
try:
|
|
|
|
let parsedResp = parseMessage[ProgressTcpSyniConnect](msgContent)
|
|
|
|
let req = &"{peerId}|{otherPeerId}|{parsedResp.command}|{parsedResp.args}"
|
|
|
|
discard await serverConn.sendRequest("notify-peer", req)
|
|
|
|
except ServerError as e:
|
|
|
|
future.fail(e)
|
|
|
|
except ValueError as e:
|
|
|
|
future.fail(e)
|
2020-07-21 19:53:21 +02:00
|
|
|
let myPorts = (@[Port(1234)] & serverConn.probedPorts).join(",")
|
2020-07-06 15:10:10 +02:00
|
|
|
let peerPorts = peerInfo.ports.join(",")
|
2020-07-26 17:46:17 +02:00
|
|
|
let req = &"{serverConn.probedIp}|{myPorts}|{peerInfo.ip}|{peerPorts}"
|
2020-07-06 15:10:10 +02:00
|
|
|
let pResp = await punchdConn.sendRequest("tcp-syni-connect", req, progressCb)
|
|
|
|
result = pResp.sock
|
|
|
|
|
2020-07-21 19:53:21 +02:00
|
|
|
proc initServerConnection(serverHostname: string, serverPort: Port,
|
2020-07-26 17:46:17 +02:00
|
|
|
probingPort: Port): Future[ServerConnection] {.async.} =
|
2020-07-29 20:15:01 +02:00
|
|
|
var failCount = 0
|
|
|
|
while result.probedPorts.len < 2:
|
2020-07-26 17:46:17 +02:00
|
|
|
# FIXME: error handling
|
|
|
|
let sock = newAsyncSocket()
|
2020-07-29 20:15:01 +02:00
|
|
|
try:
|
|
|
|
sock.bindAddr(probingPort)
|
|
|
|
except OSError as e:
|
|
|
|
if failCount == 3:
|
|
|
|
raise e
|
|
|
|
failCount.inc
|
|
|
|
await sleepAsync(1000)
|
|
|
|
continue
|
2020-07-26 17:46:17 +02:00
|
|
|
await sock.connect(serverHostname, serverPort)
|
|
|
|
let id = rand(uint32)
|
|
|
|
await sock.send(&"get-endpoint|{id}\n")
|
|
|
|
let line = await sock.recvLine(maxLength = 400)
|
|
|
|
let args = line.parseArgs(3)
|
|
|
|
assert(args[0] == "ok")
|
|
|
|
assert(args[1] == $id)
|
|
|
|
let endpoint = parseMessage[OkGetEndpoint](args[2])
|
2020-08-16 12:41:07 +02:00
|
|
|
echo "endpoint: ", endpoint
|
2020-07-26 17:46:17 +02:00
|
|
|
result.probedIp = endpoint.ip
|
|
|
|
result.probedPorts.add(endpoint.port)
|
|
|
|
let emptyLine = await sock.recvLine(maxLength = 400)
|
|
|
|
assert(emptyLine.len == 0)
|
|
|
|
sock.close()
|
|
|
|
|
2020-07-21 19:53:21 +02:00
|
|
|
result.sock = await asyncnet.dial(serverHostname, serverPort)
|
|
|
|
result.outMessages = newTable[string, Future[string]]()
|
|
|
|
result.peerNotifications = newFutureStream[string]("initServerConnection")
|
|
|
|
|
2020-07-06 15:10:10 +02:00
|
|
|
proc runApp(serverHostname: string, serverPort: Port, peerId: string,
|
|
|
|
otherPeerId: string = "") {.async.} =
|
|
|
|
randomize() # initialize random number generator
|
|
|
|
var punchdConn = PunchdConnection()
|
|
|
|
punchdConn.sock = newAsyncSocket(AF_UNIX, SOCK_STREAM, IPPROTO_IP)
|
|
|
|
punchdConn.outMessages = newTable[string, OutgoingPunchdMessage]()
|
|
|
|
punchdConn.inConnections = newFutureStream[AsyncSocket]("runApp")
|
|
|
|
await punchdConn.sock.connectUnix("/tmp/punchd.socket")
|
|
|
|
asyncCheck handlePunchdMessages(punchdConn)
|
|
|
|
if otherPeerId.len == 0:
|
|
|
|
# register and wait for connections
|
2020-07-21 19:53:21 +02:00
|
|
|
let serverConn = await initServerConnection(serverHostname, serverPort,
|
2020-08-16 12:41:07 +02:00
|
|
|
AcceptPort)
|
2020-07-21 19:53:21 +02:00
|
|
|
asyncCheck handleServerMessages(serverConn)
|
|
|
|
asyncCheck handlePeerNotifications(serverConn, punchdConn, peerId)
|
2020-08-16 12:41:07 +02:00
|
|
|
let myPorts = (@[AcceptPort] & serverConn.probedPorts).join(",")
|
2020-07-21 19:53:21 +02:00
|
|
|
let req = &"{peerId}|{serverConn.probedIp}|{myPorts}"
|
|
|
|
echo "registering: ", req
|
2020-07-06 15:10:10 +02:00
|
|
|
discard await serverConn.sendRequest("register", req)
|
|
|
|
while true:
|
|
|
|
let (hasSock, sock) = await punchdConn.inConnections.read
|
|
|
|
if not hasSock:
|
|
|
|
break
|
|
|
|
echo "got connection"
|
|
|
|
let msg = await sock.recv(1000)
|
|
|
|
echo "received message: ", msg
|
|
|
|
await sock.send("pong")
|
|
|
|
|
|
|
|
else:
|
|
|
|
# initiate a new connection
|
2020-07-26 17:46:17 +02:00
|
|
|
var serverConn = await initServerConnection(serverHostname, serverPort,
|
|
|
|
Port(1234))
|
2020-07-21 19:53:21 +02:00
|
|
|
asyncCheck handleServerMessages(serverConn)
|
2020-07-26 17:46:17 +02:00
|
|
|
let sock = await punchHole(punchdConn, serverConn, peerId, otherPeerId)
|
2020-07-06 15:10:10 +02:00
|
|
|
await sock.send("ping")
|
|
|
|
let msg = await sock.recv(1000)
|
|
|
|
echo "received message: ", msg
|
|
|
|
|
|
|
|
proc main() =
|
|
|
|
if paramCount() < 3 or paramCount() > 4:
|
|
|
|
usage()
|
|
|
|
quit(1)
|
|
|
|
let portNumber = paramStr(2).parseUInt
|
|
|
|
if portNumber > uint16.high:
|
|
|
|
usage()
|
|
|
|
quit(1)
|
|
|
|
if paramCount() == 4:
|
|
|
|
waitFor runApp(paramStr(1), Port(portNumber), paramStr(3), paramStr(4))
|
|
|
|
else:
|
|
|
|
waitFor runApp(paramStr(1), Port(portNumber), paramStr(3))
|
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
main()
|