mirror of
https://github.com/status-im/nim-eth.git
synced 2025-01-10 06:15:44 +00:00
2c236f6495
Currently only setting `--styleCheck:hint` as there are some dependency fixes required and the compiler seems to trip over the findnode MessageKind, findnode Message field and the findNode proc. Also over protocol.Protocol usage.
271 lines
10 KiB
Nim
271 lines
10 KiB
Nim
# Copyright (c) 2021 Status Research & Development GmbH
|
|
# Licensed and distributed under either of
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
{.push raises: [Defect].}
|
|
|
|
import
|
|
std/[tables, options, sugar],
|
|
chronos, bearssl, chronicles,
|
|
../keys,
|
|
./utp_socket,
|
|
./packets
|
|
|
|
logScope:
|
|
topics = "utp_router"
|
|
|
|
export utp_socket
|
|
|
|
type
|
|
# New remote client connection callback
|
|
# ``server`` - UtpProtocol object.
|
|
# ``client`` - accepted client utp socket.
|
|
AcceptConnectionCallback*[A] = proc(server: UtpRouter[A],
|
|
client: UtpSocket[A]): Future[void] {.gcsafe, raises: [Defect].}
|
|
|
|
# Callback to act as firewall for incoming peers. Should return true if peer
|
|
# is allowed to connect.
|
|
AllowConnectionCallback*[A] = proc(r: UtpRouter[A], remoteAddress: A,
|
|
connectionId: uint16): bool {.gcsafe, raises: [Defect], noSideEffect.}
|
|
|
|
# Object responsible for creating and maintaining table of utp sockets.
|
|
# Caller should use `processIncomingBytes` proc to feed it with incoming byte
|
|
# packets. Based on this input, proper utp sockets will be created, closed,
|
|
# or will receive data.
|
|
UtpRouter*[A] = ref object
|
|
sockets: Table[UtpSocketKey[A], UtpSocket[A]]
|
|
socketConfig: SocketConfig
|
|
acceptConnection: AcceptConnectionCallback[A]
|
|
closed: bool
|
|
sendCb*: SendCallback[A]
|
|
allowConnection*: AllowConnectionCallback[A]
|
|
rng*: ref BrHmacDrbgContext
|
|
|
|
const
|
|
# Maximal number of tries to generate unique socket while establishing
|
|
# outgoing connection.
|
|
maxSocketGenerationTries = 1000
|
|
|
|
# This should probably be in standard lib, it allows lazy composition of options
|
|
# i.e one can write: O1 orElse O2 orElse O3, and chain will be evaluated to
|
|
# first option which isSome()
|
|
template orElse[A](a: Option[A], b: Option[A]): Option[A] =
|
|
if (a.isSome()):
|
|
a
|
|
else:
|
|
b
|
|
|
|
proc getUtpSocket[A](s: UtpRouter[A], k: UtpSocketKey[A]): Option[UtpSocket[A]] =
|
|
let s = s.sockets.getOrDefault(k)
|
|
if s == nil:
|
|
none[UtpSocket[A]]()
|
|
else:
|
|
some(s)
|
|
|
|
proc deRegisterUtpSocket[A](s: UtpRouter[A], socket: UtpSocket[A]) =
|
|
s.sockets.del(socket.socketKey)
|
|
|
|
iterator allSockets[A](s: UtpRouter[A]): UtpSocket[A] =
|
|
for socket in s.sockets.values():
|
|
yield socket
|
|
|
|
proc len*[A](s: UtpRouter[A]): int =
|
|
## returns number of active sockets
|
|
len(s.sockets)
|
|
|
|
proc registerUtpSocket[A](p: UtpRouter, s: UtpSocket[A]) =
|
|
## Register socket, overwriting already existing one
|
|
p.sockets[s.socketKey] = s
|
|
# Install deregister handler, so when socket gets closed, in will be promptly
|
|
# removed from open sockets table
|
|
s.registerCloseCallback(proc () = p.deRegisterUtpSocket(s))
|
|
|
|
proc registerIfAbsent[A](p: UtpRouter, s: UtpSocket[A]): bool =
|
|
## Registers socket only if it's not already existing in the active sockets
|
|
## table. Returns true if socket has been succesfuly registered.
|
|
if p.sockets.hasKey(s.socketKey):
|
|
false
|
|
else:
|
|
p.registerUtpSocket(s)
|
|
true
|
|
|
|
proc new*[A](
|
|
T: type UtpRouter[A],
|
|
acceptConnectionCb: AcceptConnectionCallback[A],
|
|
allowConnectionCb: AllowConnectionCallback[A],
|
|
socketConfig: SocketConfig = SocketConfig.init(),
|
|
rng = newRng()): UtpRouter[A] =
|
|
doAssert(not(isNil(acceptConnectionCb)))
|
|
UtpRouter[A](
|
|
sockets: initTable[UtpSocketKey[A], UtpSocket[A]](),
|
|
acceptConnection: acceptConnectionCb,
|
|
allowConnection: allowConnectionCb,
|
|
socketConfig: socketConfig,
|
|
rng: rng
|
|
)
|
|
|
|
proc new*[A](
|
|
T: type UtpRouter[A],
|
|
acceptConnectionCb: AcceptConnectionCallback[A],
|
|
socketConfig: SocketConfig = SocketConfig.init(),
|
|
rng = newRng()): UtpRouter[A] =
|
|
UtpRouter[A].new(acceptConnectionCb, nil, socketConfig, rng)
|
|
|
|
# There are different possiblites how connection was established, and we need to
|
|
# check every case
|
|
proc getSocketOnReset[A](r: UtpRouter[A], sender: A, id: uint16): Option[UtpSocket[A]] =
|
|
# id is our recv id
|
|
let recvKey = UtpSocketKey[A].init(sender, id)
|
|
|
|
# id is our send id, and we did nitiate the connection, our recv id is id - 1
|
|
let sendInitKey = UtpSocketKey[A].init(sender, id - 1)
|
|
|
|
# id is our send id, and we did not initiate the connection, so our recv id is id + 1
|
|
let sendNoInitKey = UtpSocketKey[A].init(sender, id + 1)
|
|
|
|
r.getUtpSocket(recvKey)
|
|
.orElse(r.getUtpSocket(sendInitKey).filter(s => s.connectionIdSnd == id))
|
|
.orElse(r.getUtpSocket(sendNoInitKey).filter(s => s.connectionIdSnd == id))
|
|
|
|
proc shouldAllowConnection[A](r: UtpRouter[A], remoteAddress: A, connectionId: uint16): bool =
|
|
if r.allowConnection == nil:
|
|
# if the callback is not configured it means all incoming connections are allowed
|
|
true
|
|
else:
|
|
r.allowConnection(r, remoteAddress, connectionId)
|
|
|
|
proc processPacket[A](r: UtpRouter[A], p: Packet, sender: A) {.async.}=
|
|
notice "Received packet ", packet = p
|
|
|
|
case p.header.pType
|
|
of ST_RESET:
|
|
let maybeSocket = r.getSocketOnReset(sender, p.header.connectionId)
|
|
if maybeSocket.isSome():
|
|
notice "Received rst packet on known connection closing"
|
|
let socket = maybeSocket.unsafeGet()
|
|
# reference implementation acutally changes the socket state to reset state unless
|
|
# user explicitly closed socket before. The only difference between reset and destroy
|
|
# state is that socket in destroy state is ultimatly deleted from active connection
|
|
# list but socket in reset state lingers there until user of library closes it
|
|
# explictly.
|
|
socket.destroy()
|
|
else:
|
|
notice "Received rst packet for not known connection"
|
|
of ST_SYN:
|
|
# Syn packet are special, and we need to add 1 to header connectionId
|
|
let socketKey = UtpSocketKey[A].init(sender, p.header.connectionId + 1)
|
|
let maybeSocket = r.getUtpSocket(socketKey)
|
|
if (maybeSocket.isSome()):
|
|
notice "Ignoring SYN for already existing connection"
|
|
else:
|
|
if (r.shouldAllowConnection(sender, p.header.connectionId)):
|
|
notice "Received SYN for not known connection. Initiating incoming connection"
|
|
# Initial ackNr is set to incoming packer seqNr
|
|
let incomingSocket = newIncomingSocket[A](sender, r.sendCb, r.socketConfig ,p.header.connectionId, p.header.seqNr, r.rng[])
|
|
r.registerUtpSocket(incomingSocket)
|
|
await incomingSocket.startIncomingSocket()
|
|
# Based on configuration, socket is passed to upper layer either in SynRecv
|
|
# or Connected state
|
|
asyncSpawn r.acceptConnection(r, incomingSocket)
|
|
else:
|
|
notice "Connection declined"
|
|
else:
|
|
let socketKey = UtpSocketKey[A].init(sender, p.header.connectionId)
|
|
let maybeSocket = r.getUtpSocket(socketKey)
|
|
if (maybeSocket.isSome()):
|
|
let socket = maybeSocket.unsafeGet()
|
|
await socket.processPacket(p)
|
|
else:
|
|
# TODO add keeping track of recently send reset packets and do not send reset
|
|
# to peers which we recently send reset to.
|
|
notice "Recevied FIN/DATA/ACK on not known socket sending reset"
|
|
let rstPacket = resetPacket(randUint16(r.rng[]), p.header.connectionId, p.header.seqNr)
|
|
await r.sendCb(sender, encodePacket(rstPacket))
|
|
|
|
proc processIncomingBytes*[A](r: UtpRouter[A], bytes: seq[byte], sender: A) {.async.} =
|
|
if (not r.closed):
|
|
let dec = decodePacket(bytes)
|
|
if (dec.isOk()):
|
|
await processPacket[A](r, dec.get(), sender)
|
|
else:
|
|
warn "failed to decode packet from address", address = sender
|
|
|
|
proc generateNewUniqueSocket[A](r: UtpRouter[A], address: A): Option[UtpSocket[A]] =
|
|
## Tries to generate unique socket, gives up after maxSocketGenerationTries tries
|
|
var tryCount = 0
|
|
|
|
while tryCount < maxSocketGenerationTries:
|
|
let rcvId = randUint16(r.rng[])
|
|
let socket = newOutgoingSocket[A](address, r.sendCb, r.socketConfig, rcvId, r.rng[])
|
|
|
|
if r.registerIfAbsent(socket):
|
|
return some(socket)
|
|
|
|
inc tryCount
|
|
|
|
return none[UtpSocket[A]]()
|
|
|
|
proc connect[A](s: UtpSocket[A]): Future[ConnectionResult[A]] {.async.}=
|
|
let startFut = s.startOutgoingSocket()
|
|
|
|
startFut.cancelCallback = proc(udata: pointer) {.gcsafe.} =
|
|
# if for some reason future will be cancelled, destory socket to clear it from
|
|
# active socket list
|
|
s.destroy()
|
|
|
|
try:
|
|
await startFut
|
|
return ok(s)
|
|
except ConnectionError:
|
|
s.destroy()
|
|
return err(OutgoingConnectionError(kind: ConnectionTimedOut))
|
|
except CatchableError as e:
|
|
s.destroy()
|
|
# this may only happen if user provided callback will for some reason fail
|
|
return err(OutgoingConnectionError(kind: ErrorWhileSendingSyn, error: e))
|
|
|
|
# Connect to provided address
|
|
# Reference implementation: https://github.com/bittorrent/libutp/blob/master/utp_internal.cpp#L2732
|
|
proc connectTo*[A](r: UtpRouter[A], address: A): Future[ConnectionResult[A]] {.async.} =
|
|
let maybeSocket = r.generateNewUniqueSocket(address)
|
|
|
|
if (maybeSocket.isNone()):
|
|
return err(OutgoingConnectionError(kind: SocketAlreadyExists))
|
|
else:
|
|
let socket = maybeSocket.unsafeGet()
|
|
return await socket.connect()
|
|
|
|
# Connect to provided address with provided connection id, if socket with this id
|
|
# and address already exsits return error
|
|
proc connectTo*[A](r: UtpRouter[A], address: A, connectionId: uint16): Future[ConnectionResult[A]] {.async.} =
|
|
let socket = newOutgoingSocket[A](address, r.sendCb, r.socketConfig, connectionId, r.rng[])
|
|
|
|
if (r.registerIfAbsent(socket)):
|
|
return await socket.connect()
|
|
else:
|
|
return err(OutgoingConnectionError(kind: SocketAlreadyExists))
|
|
|
|
proc shutdown*[A](r: UtpRouter[A]) =
|
|
# stop processing any new packets and close all sockets in background without
|
|
# notifing remote peers
|
|
r.closed = true
|
|
for s in r.allSockets():
|
|
s.destroy()
|
|
|
|
proc shutdownWait*[A](r: UtpRouter[A]) {.async.} =
|
|
var activeSockets: seq[UtpSocket[A]] = @[]
|
|
# stop processing any new packets and close all sockets without
|
|
# notifing remote peers
|
|
r.closed = true
|
|
|
|
# we need to make copy as calling socket.destroyWait() removes socket from the table
|
|
# and iterator throws error. Antother option would be to wait until number of opensockets
|
|
# go to 0
|
|
for s in r.allSockets():
|
|
activeSockets.add(s)
|
|
|
|
for s in activeSockets:
|
|
yield s.destroyWait()
|