feat: picotls integration (#55)

This commit is contained in:
richΛrd
2025-03-05 12:16:42 -04:00
committed by GitHub
parent 040473413a
commit ada59a24c9
50 changed files with 864 additions and 460 deletions

View File

@@ -9,7 +9,7 @@ on:
jobs:
test:
timeout-minutes: 30
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
@@ -23,18 +23,26 @@ jobs:
cpu: i386
shell: bash
- os: macos
runner: macos-latest
runner: macos-13
cpu: amd64
shell: bash
- os: macos
runner: macos-14
cpu: arm64
shell: bash
- os: windows
runner: windows-latest
cpu: amd64
shell: msys2 {0}
nim:
- branch: version-1-6
- branch: version-2-0
- ref: version-1-6
- ref: version-2-0
name: '${{ matrix.platform.os }}-${{ matrix.platform.cpu }} (Nim ${{ matrix.nim.branch }})'
defaults:
run:
shell: ${{ matrix.platform.shell }}
name: '${{ matrix.platform.os }}-${{ matrix.platform.cpu }} (Nim ${{ matrix.nim.ref }})'
runs-on: ${{ matrix.platform.runner }}
steps:
- name: Checkout
@@ -46,7 +54,12 @@ jobs:
os: ${{ matrix.platform.os }}
cpu: ${{ matrix.platform.cpu }}
shell: ${{ matrix.platform.shell }}
nim_branch: ${{ matrix.nim.branch }}
nim_ref: ${{ matrix.nim.ref }}
- name: Install deps (windows)
if : ${{ matrix.platform.os == 'windows'}}
run: |
pacman -S --noconfirm base-devel gcc mingw-w64-x86_64-openssl
- name: Install dependencies
run: nimble install -y

3
.gitignore vendored
View File

@@ -1,3 +0,0 @@
*
!*/
!*.*

View File

@@ -8,6 +8,6 @@ requires "nim >= 1.6.0"
requires "stew#head"
requires "chronos >= 4.0.3 & < 5.0.0"
requires "nimcrypto >= 0.6.0 & < 0.7.0"
requires "ngtcp2#6834f4756b6af58356ac9c4fef3d71db3c3ae5fe"
requires "ngtcp2 >= 0.35.0"
requires "unittest2"
requires "chronicles >= 0.10.2"

View File

@@ -1,10 +1,13 @@
import pkg/chronos
import chronos
import results
import ./listener
import ./connection
import ./udp/datagram
import ./errors
import ./transport/tlsbackend
export Listener
export accept
export Connection
export Stream
export openStream
@@ -18,20 +21,76 @@ export drop
export close
export waitClosed
export errors
export destroy
export CertificateVerifier
export certificateVerifierCB
export CustomCertificateVerifier
export InsecureCertificateVerifier
export init
proc listen*(address: TransportAddress): Listener =
newListener(address)
type TLSConfig* = object
certificate: seq[byte]
key: seq[byte]
certificateVerifier: Opt[CertificateVerifier]
proc accept*(listener: Listener): Future[Connection] {.async.} =
result = await listener.waitForIncoming()
type Quic = ref object of RootObj
tlsConfig: TLSConfig
proc dial*(address: TransportAddress): Future[Connection] {.async.} =
type QuicClient* = object of Quic
type QuicServer* = object of Quic
proc init*(
t: typedesc[TLSConfig],
certificate: seq[byte] = @[],
key: seq[byte] = @[],
certificateVerifier: Opt[CertificateVerifier] = Opt.none(CertificateVerifier),
): TLSConfig {.gcsafe, raises: [QuicConfigError].} =
# In a config, certificate and keys are optional, but if using them, both must
# be specified at the same time
if certificate.len != 0 or key.len != 0:
if certificate.len == 0:
raise newException(QuicConfigError, "certificate is required in TLSConfig")
if key.len == 0:
raise newException(QuicConfigError, "key is required in TLSConfig")
return TLSConfig(
certificate: certificate, key: key, certificateVerifier: certificateVerifier
)
proc init*(
t: typedesc[QuicServer], tlsConfig: TLSConfig
): QuicServer {.raises: [QuicConfigError].} =
if tlsConfig.certificate.len == 0:
raise newException(QuicConfigError, "tlsConfig does not contain a certificate")
return QuicServer(tlsConfig: tlsConfig)
proc init*(t: typedesc[QuicClient], tlsConfig: TLSConfig): QuicClient {.raises: [].} =
return QuicClient(tlsConfig: tlsConfig)
proc listen*(
self: QuicServer, address: TransportAddress
): Listener {.raises: [QuicError, TransportOsError].} =
let tlsBackend = newServerTLSBackend(
self.tlsConfig.certificate, self.tlsConfig.key, self.tlsConfig.certificateVerifier
)
return newListener(tlsBackend, address)
proc dial*(
self: QuicClient, address: TransportAddress
): Future[Connection] {.async: (raises: [QuicError, TransportOsError]).} =
let tlsBackend = newClientTLSBackend(
self.tlsConfig.certificate, self.tlsConfig.key, self.tlsConfig.certificateVerifier
)
var connection: Connection
proc onReceive(udp: DatagramTransport, remote: TransportAddress) {.async.} =
let datagram = Datagram(data: udp.getMessage())
connection.receive(datagram)
let udp = newDatagramTransport(onReceive)
connection = newOutgoingConnection(udp, address)
connection = newOutgoingConnection(tlsBackend, udp, address)
connection.startHandshake()
result = connection
return connection

View File

@@ -1,5 +1,5 @@
import pkg/chronos
import pkg/stew/results
import results
import ./udp/datagram
import ./errors

View File

@@ -5,6 +5,7 @@ import ./transport/connectionid
import ./transport/stream
import ./transport/quicconnection
import ./transport/quicclientserver
import ./transport/tlsbackend
import ./helpers/asyncloop
export Stream, close, read, write
@@ -22,7 +23,9 @@ type
closed: AsyncEvent
IncomingConnection = ref object of Connection
OutgoingConnection = ref object of Connection
tlsBackend: Opt[TLSBackend]
proc ids*(connection: Connection): seq[ConnectionId] =
connection.quic.ids
@@ -43,9 +46,19 @@ proc drop*(connection: Connection) {.async.} =
proc close*(connection: Connection) {.async.} =
await connection.quic.close()
if connection is OutgoingConnection:
let outConn = OutgoingConnection(connection)
if outConn.tlsBackend.isSome:
outConn.tlsBackend.get().destroy()
outConn.tlsBackend = Opt.none(TLSBackend)
proc waitClosed*(connection: Connection) {.async.} =
await connection.closed.wait()
if connection is OutgoingConnection:
let outConn = OutgoingConnection(connection)
if outConn.tlsBackend.isSome:
outConn.tlsBackend.get().destroy()
outConn.tlsBackend = Opt.none(TLSBackend)
proc startSending(connection: Connection, remote: TransportAddress) =
trace "Starting sending loop"
@@ -53,9 +66,9 @@ proc startSending(connection: Connection, remote: TransportAddress) =
try:
trace "Getting datagram"
let datagram = await connection.quic.outgoing.get()
trace "Sending datagraom"
trace "Sending datagram"
await connection.udp.sendTo(remote, datagram.data)
trace "Sent datagraom"
trace "Sent datagram"
except TransportError as e:
trace "Failed to send datagram", errorMsg = e.msg
trace "Failing connection loop future with error"
@@ -94,10 +107,10 @@ proc disconnect(connection: Connection) {.async.} =
trace "Fired closed event"
proc newIncomingConnection*(
udp: DatagramTransport, remote: TransportAddress
tlsBackend: TLSBackend, udp: DatagramTransport, remote: TransportAddress
): Connection =
let datagram = Datagram(data: udp.getMessage())
let quic = newQuicServerConnection(udp.localAddress, remote, datagram)
let quic = newQuicServerConnection(tlsBackend, udp.localAddress, remote, datagram)
let closed = newAsyncEvent()
let connection = IncomingConnection(udp: udp, quic: quic, closed: closed)
proc onDisconnect() {.async.} =
@@ -111,11 +124,13 @@ proc newIncomingConnection*(
connection
proc newOutgoingConnection*(
udp: DatagramTransport, remote: TransportAddress
tlsBackend: TLSBackend, udp: DatagramTransport, remote: TransportAddress
): Connection =
let quic = newQuicClientConnection(udp.localAddress, remote)
let quic = newQuicClientConnection(tlsBackend, udp.localAddress, remote)
let closed = newAsyncEvent()
let connection = OutgoingConnection(udp: udp, quic: quic, closed: closed)
let connection = OutgoingConnection(
udp: udp, quic: quic, closed: closed, tlsBackend: Opt.some(tlsBackend)
)
proc onDisconnect() {.async.} =
trace "Calling onDisconnect for newOutgoingConnection"
await connection.disconnect()
@@ -126,7 +141,7 @@ proc newOutgoingConnection*(
connection.startSending(remote)
connection
proc startHandshake*(connection: Connection) =
proc startHandshake*(connection: Connection) {.gcsafe.} =
connection.quic.send()
proc receive*(connection: Connection, datagram: Datagram) =

View File

@@ -1,6 +1,7 @@
type
QuicError* = object of IOError
QuicDefect* = object of Defect
QuicConfigError* = object of CatchableError
template errorAsDefect*(body): untyped =
try:

View File

@@ -3,8 +3,10 @@ import ./basics
import ./connection
import ./transport/connectionid
import ./transport/parsedatagram
import ./transport/tlsbackend
type Listener* = ref object
tlsBackend: TLSBackend
udp: DatagramTransport
incoming: AsyncQueue[Connection]
connections: Table[ConnectionId, Connection]
@@ -42,23 +44,30 @@ proc getOrCreateConnection*(
var connection: Connection
let destination = parseDatagram(udp.getMessage()).destination
if not listener.hasConnection(destination):
connection = newIncomingConnection(udp, remote)
connection = newIncomingConnection(listener.tlsBackend, udp, remote)
listener.addConnection(connection, destination)
else:
connection = listener.getConnection(destination)
connection
proc newListener*(address: TransportAddress): Listener =
proc newListener*(tlsBackend: TLSBackend, address: TransportAddress): Listener =
let listener = Listener(incoming: newAsyncQueue[Connection]())
proc onReceive(udp: DatagramTransport, remote: TransportAddress) {.async.} =
let connection = listener.getOrCreateConnection(udp, remote)
connection.receive(Datagram(data: udp.getMessage()))
listener.tlsBackend = tlsBackend
listener.udp = newDatagramTransport(onReceive, local = address)
listener
proc waitForIncoming*(listener: Listener): Future[Connection] {.async.} =
result = await listener.incoming.get()
await listener.incoming.get()
proc accept*(listener: Listener): Future[Connection] {.async.} =
result = await listener.waitForIncoming()
proc stop*(listener: Listener) {.async.} =
await listener.udp.closeWait()
proc destroy*(listener: Listener) =
listener.tlsBackend.destroy()

View File

@@ -15,8 +15,6 @@ type
proc newClosedConnection*(): ClosedConnection =
ClosedConnection()
{.push locks: "unknown".}
method ids(state: ClosedConnection): seq[ConnectionId] =
@[]
@@ -38,5 +36,3 @@ method drop(state: ClosedConnection) {.async.} =
trace "Dropping ClosedConnection state"
discard
trace "Dropped ClosedConnection state"
{.pop.}

View File

@@ -21,13 +21,9 @@ proc sendFinalDatagram(state: ClosingConnection) =
except AsyncQueueFullError:
raise newException(QuicError, "Outgoing queue is full")
{.push locks: "unknown".}
method enter(state: ClosingConnection, connection: QuicConnection) =
procCall enter(DrainingConnection(state), connection)
state.sendFinalDatagram()
method receive(state: ClosingConnection, datagram: Datagram) =
state.sendFinalDatagram()
{.pop.}

View File

@@ -24,8 +24,6 @@ proc callDisconnect(connection: QuicConnection) {.async.} =
await disconnect()
trace "Called disconnect proc on QuicConnection"
{.push locks: "unknown".}
method ids*(state: DisconnectingConnection): seq[ConnectionId] =
state.ids
@@ -68,5 +66,3 @@ method drop(state: DisconnectingConnection) {.async.} =
return
connection.switch(newClosedConnection())
trace "dropped DisconnectingConnection state"
{.pop.}

View File

@@ -4,10 +4,12 @@ import ../../../basics
import ../../quicconnection
import ../../connectionid
import ../../stream
import ../../tlsbackend
import ../native/connection
import ../native/streams
import ../native/client
import ../native/server
import ../native/errors
import ./closingstate
import ./drainingstate
import ./disconnectingstate
@@ -24,42 +26,59 @@ type OpenConnection* = ref object of ConnectionState
proc newOpenConnection*(ngtcp2Connection: Ngtcp2Connection): OpenConnection =
OpenConnection(ngtcp2Connection: ngtcp2Connection, streams: OpenStreams.new)
proc openClientConnection*(local, remote: TransportAddress): OpenConnection =
newOpenConnection(newNgtcp2Client(local, remote))
proc openClientConnection*(
tlsBackend: TLSBackend, local, remote: TransportAddress
): OpenConnection =
let ngtcp2Conn = newNgtcp2Client(tlsBackend.picoTLS, local, remote)
newOpenConnection(ngtcp2Conn)
proc openServerConnection*(
local, remote: TransportAddress, datagram: Datagram
tlsBackend: TLSBackend, local, remote: TransportAddress, datagram: Datagram
): OpenConnection =
newOpenConnection(newNgtcp2Server(local, remote, datagram.data))
newOpenConnection(newNgtcp2Server(tlsBackend.picoTLS, local, remote, datagram.data))
{.push locks: "unknown".}
method close(state: OpenConnection) {.async.}
method enter(state: OpenConnection, connection: QuicConnection) =
trace "Entering OpenConnection state"
procCall enter(ConnectionState(state), connection)
state.quicConnection = Opt.some(connection)
# Workaround weird bug
proc onNewId(id: ConnectionId) =
var onNewId = proc(id: ConnectionId) =
if isNil(connection.onNewId):
return
connection.onNewId(id)
state.ngtcp2Connection.onNewId = Opt.some(onNewId)
proc onRemoveId(id: ConnectionId) =
var onRemoveId = proc(id: ConnectionId) =
if isNil(connection.onRemoveId):
return
connection.onRemoveId(id)
state.ngtcp2Connection.onNewId = Opt.some(onNewId)
state.ngtcp2Connection.onRemoveId = Opt.some(onRemoveId)
state.ngtcp2Connection.onSend = proc(datagram: Datagram) =
errorAsDefect:
connection.outgoing.putNoWait(datagram)
state.ngtcp2Connection.onIncomingStream = proc(stream: Stream) =
state.streams.add(stream)
connection.incoming.putNoWait(stream)
state.ngtcp2Connection.onHandshakeDone = proc() =
connection.handshake.fire()
state.ngtcp2Connection.onTimeout = proc() {.gcsafe, raises: [].} =
try:
waitFor connection.close()
except QuicError:
# TODO: handle
discard
except CatchableError:
# TODO: handle
discard
trace "Entered OpenConnection state"
method leave(state: OpenConnection) =
@@ -77,15 +96,24 @@ method send(state: OpenConnection) =
state.ngtcp2Connection.send()
method receive(state: OpenConnection, datagram: Datagram) =
state.ngtcp2Connection.receive(datagram)
let quicConnection = state.quicConnection.valueOr:
return
if state.ngtcp2Connection.isDraining:
let duration = state.ngtcp2Connection.closingDuration()
let ids = state.ids
let draining = newDrainingConnection(ids, duration)
quicConnection.switch(draining)
asyncSpawn draining.close()
var isDraining = false
try:
state.ngtcp2Connection.receive(datagram)
except Ngtcp2Error as e:
trace "ngtcp2 error on receive", code = $e.msg
isDraining = state.ngtcp2Connection.isDraining
# TODO:
# if not isDraining:
# raise newException(QuicError, "could not receive - code:" & $e.msg)
finally:
let quicConnection = state.quicConnection.valueOr:
return
if isDraining:
let duration = state.ngtcp2Connection.closingDuration()
let ids = state.ids
let draining = newDrainingConnection(ids, duration)
quicConnection.switch(draining)
asyncSpawn draining.close()
method openStream(
state: OpenConnection, unidirectional: bool

View File

@@ -4,6 +4,8 @@ import ./native/client
import ./native/handshake
import ./native/streams
import ./native/parsedatagram
import ./native/picotls
import ./native/certificateverifier
export parseDatagram
export Ngtcp2Connection
@@ -15,3 +17,10 @@ export handshake
export ids
export openStream
export destroy
export PicoTLSContext
export PicoTLSConnection
export init
export destroy
export newConnection
export certificateverifier

View File

@@ -0,0 +1,7 @@
import ./certificateverifier/certificateverifier
import ./certificateverifier/custom
import ./certificateverifier/insecure
export certificateverifier
export custom
export insecure

View File

@@ -0,0 +1,11 @@
import ngtcp2
type CertificateVerifier* = ref object of RootObj
method destroy*(t: CertificateVerifier) {.base, gcsafe.} =
doAssert false, "override this method"
method getPtlsVerifyCertificateT*(
t: CertificateVerifier
): ptr ptls_verify_certificate_t {.base, gcsafe.} =
doAssert false, "override this method"

View File

@@ -0,0 +1,70 @@
import ngtcp2
import sequtils
import ./certificateverifier
import ../pointers
import ../../../../helpers/openarray
type
certificateVerifierCB* =
proc(derCertificates: seq[seq[byte]]): bool {.gcsafe, noSideEffect.}
customPTLSVerifyCertificateT = object of ptls_verify_certificate_t
customCertVerifier: certificateVerifierCB
CustomCertificateVerifier* = ref object of CertificateVerifier
verifier: ptr customPTLSVerifyCertificateT
proc validateCertificate(
self: ptr ptls_verify_certificate_t,
tls: ptr ptls_t,
server_name: cstring,
verify_sign: proc(
verify_ctx: pointer, algo: uint16, data: ptls_iovec_t, sign: ptls_iovec_t
): cint {.cdecl.},
verify_data: ptr pointer,
certs: ptr ptls_iovec_t,
num_certs: csize_t,
): cint {.cdecl.} =
let certVerifier = cast[ptr customPTLSVerifyCertificateT](self)
if certVerifier.customCertVerifier.isNil:
doAssert false, "custom cert verifier was not setup"
var derCertificates = newSeq[seq[byte]](num_certs)
for i in 0 ..< int(num_certs):
let cert = certs + i
derCertificates[i] = toSeq(toOpenArray(cert.base, cert.len))
if certVerifier.customCertVerifier(derCertificates):
return 0
else:
return PTLS_ALERT_BAD_CERTIFICATE
proc init*(
t: typedesc[CustomCertificateVerifier], certVerifierCB: certificateVerifierCB
): CustomCertificateVerifier {.gcsafe.} =
let response = CustomCertificateVerifier()
response.verifier = create(customPTLSVerifyCertificateT)
var algos = cast[ptr UncheckedArray[uint16]](alloc(uint16.sizeof * 5))
algos[0] = PTLS_SIGNATURE_RSA_PSS_RSAE_SHA256
algos[1] = PTLS_SIGNATURE_ECDSA_SECP256R1_SHA256
algos[2] = PTLS_SIGNATURE_RSA_PKCS1_SHA256
algos[3] = PTLS_SIGNATURE_RSA_PKCS1_SHA1
algos[4] = high(uint16)
response.verifier.cb = validateCertificate
response.verifier.algos = cast[ptr uint16](algos)
response.verifier.customCertVerifier = certVerifierCB
return response
method destroy*(t: CustomCertificateVerifier) {.gcsafe.} =
if t.verifier.isNil:
return
let algosPtr = cast[pointer](t.verifier.algos)
dealloc(algosPtr)
dealloc(t.verifier)
t.verifier = nil
method getPtlsVerifyCertificateT*(
t: CustomCertificateVerifier
): ptr ptls_verify_certificate_t =
return t.verifier

View File

@@ -0,0 +1,17 @@
import ngtcp2
import ./certificateverifier
type InsecureCertificateVerifier* = ref object of CertificateVerifier
proc init*(t: typedesc[InsecureCertificateVerifier]): InsecureCertificateVerifier {.gcsafe.} =
return InsecureCertificateVerifier()
method destroy*(t: InsecureCertificateVerifier) {.gcsafe.} =
discard
method getPtlsVerifyCertificateT*(
t: InsecureCertificateVerifier
): ptr ptls_verify_certificate_t =
# picotls will check against null to determine whether a certificate verifier
# was setup or not
return nil

View File

@@ -1,75 +1,31 @@
import pkg/ngtcp2
import pkg/nimcrypto
import ngtcp2
import ../../../errors
import ../../version
import ../../../basics
import ../../../helpers/openarray
import ../../connectionid
import ./ids
import ./encryption
import ./keys
import ./settings
import ./cryptodata
import ./connection
import ./path
import ./picotls
import ./rand
import ./streams
import ./timestamp
import ./handshake
proc onClientInitial(connection: ptr ngtcp2_conn, user_data: pointer): cint {.cdecl.} =
connection.install0RttKey()
connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_INITIAL)
proc onReceiveCryptoData(
connection: ptr ngtcp2_conn,
level: ngtcp2_encryption_level,
offset: uint64,
data: ptr uint8,
datalen: uint,
userData: pointer,
): cint {.cdecl.} =
if level == NGTCP2_ENCRYPTION_LEVEL_INITIAL:
connection.installHandshakeKeys()
if level == NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE:
connection.handleCryptoData(toOpenArray(data, datalen))
connection.install1RttKeys()
connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE)
ngtcp2_conn_tls_handshake_completed(connection)
proc onReceiveRetry(
connection: ptr ngtcp2_conn, hd: ptr ngtcp2_pkt_hd, userData: pointer
): cint {.cdecl.} =
return 0
proc onRand(dest: ptr uint8, destLen: uint, rand_ctx: ptr ngtcp2_rand_ctx) {.cdecl.} =
doAssert destLen.int == randomBytes(dest, destLen.int)
proc onDeleteCryptoAeadCtx(
conn: ptr ngtcp2_conn, aead_ctx: ptr ngtcp2_crypto_aead_ctx, userData: pointer
) {.cdecl.} =
discard
proc onDeleteCryptoCipherCtx(
conn: ptr ngtcp2_conn, cipher_ctx: ptr ngtcp2_crypto_cipher_ctx, userData: pointer
) {.cdecl.} =
discard
proc onGetPathChallengeData(
conn: ptr ngtcp2_conn, data: ptr uint8, userData: pointer
): cint {.cdecl.} =
let bytesWritten = randomBytes(data, NGTCP2_PATH_CHALLENGE_DATALEN)
if bytesWritten != NGTCP2_PATH_CHALLENGE_DATALEN:
return NGTCP2_ERR_CALLBACK_FAILURE
return 0
proc newNgtcp2Client*(local, remote: TransportAddress): Ngtcp2Connection =
proc newNgtcp2Client*(
tlsContext: PicoTLSContext, local, remote: TransportAddress
): Ngtcp2Connection =
var callbacks: ngtcp2_callbacks
callbacks.client_initial = onClientInitial
callbacks.recv_crypto_data = onReceiveCryptoData
callbacks.recv_retry = onReceiveRetry
callbacks.client_initial = ngtcp2_crypto_client_initial_cb
callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb
callbacks.recv_retry = ngtcp2_crypto_recv_retry_cb
callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb
callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb
callbacks.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb
callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb
callbacks.rand = onRand
callbacks.delete_crypto_aead_ctx = onDeleteCryptoAeadCtx
callbacks.delete_crypto_cipher_ctx = onDeleteCryptoCipherCtx
callbacks.get_path_challenge_data = onGetPathChallengeData
installConnectionIdCallback(callbacks)
installEncryptionCallbacks(callbacks)
@@ -83,24 +39,61 @@ proc newNgtcp2Client*(local, remote: TransportAddress): Ngtcp2Connection =
let destination = randomConnectionId().toCid
let path = newPath(local, remote)
result = newConnection(path)
let nConn = newConnection(path)
var conn: ptr ngtcp2_conn
var ret = ngtcp2_conn_client_new_versioned(
addr conn,
unsafeAddr destination,
unsafeAddr source,
path.toPathPtr,
CurrentQuicVersion,
NGTCP2_CALLBACKS_V1,
addr callbacks,
NGTCP2_SETTINGS_V2,
unsafeAddr settings,
NGTCP2_TRANSPORT_PARAMS_V1,
unsafeAddr transportParams,
nil,
addr nConn[],
)
if ret != 0:
raise newException(QuicError, "could not create new client versioned conn: " & $ret)
doAssert 0 ==
ngtcp2_conn_client_new_versioned(
addr conn,
unsafeAddr destination,
unsafeAddr source,
path.toPathPtr,
CurrentQuicVersion,
NGTCP2_CALLBACKS_V1,
addr callbacks,
NGTCP2_SETTINGS_V2,
unsafeAddr settings,
NGTCP2_TRANSPORT_PARAMS_V1,
unsafeAddr transportParams,
nil,
addr result[],
)
let cptls: ptr ngtcp2_crypto_picotls_ctx = create(ngtcp2_crypto_picotls_ctx)
result.conn = Opt.some(conn)
ngtcp2_crypto_picotls_ctx_init(cptls)
var tls = tlsContext.newConnection(false)
cptls.ptls = tls.conn
var addExtensions = cast[ptr UncheckedArray[ptls_raw_extension_t]](alloc(
ptls_raw_extension_t.sizeof * 2
))
addExtensions[0] = ptls_raw_extension_t(type_field: high(uint16))
addExtensions[1] = ptls_raw_extension_t(type_field: high(uint16))
cptls.handshake_properties = ptls_handshake_properties_t(
additional_extensions: cast[ptr ptls_raw_extension_t](addExtensions)
)
ngtcp2_conn_set_tls_native_handle(conn, cptls)
var connref = create(ngtcp2_crypto_conn_ref)
connref.user_data = conn
connref.get_conn = proc(
connRef: ptr ngtcp2_crypto_conn_ref
): ptr ngtcp2_conn {.cdecl.} =
cast[ptr ngtcp2_conn](connRef.user_data)
var dataPtr = ptls_get_data_ptr(tls.conn)
dataPtr[] = connref
ret = ngtcp2_crypto_picotls_configure_client_session(cptls, conn)
if ret != 0:
raise newException(QuicError, "could not configure client session: " & $ret)
nConn.conn = Opt.some(conn)
nConn.tlsConn = tls
nConn.cptls = cptls
nConn.connref = connref
nConn

View File

@@ -1,5 +1,6 @@
import std/sequtils
import pkg/ngtcp2
import ngtcp2
import chronicles
import ../../../basics
import ../../../udp/congestion
import ../../../helpers/openarray
@@ -7,18 +8,27 @@ import ../../stream
import ../../timeout
import ../../connectionid
import ./path
import ./picotls
import ./errors as ngtcp2errors
import ./timestamp
import ./pointers
logScope:
topics = "ngtcp2 conn"
type
Ngtcp2Connection* = ref object
conn*: Opt[ptr ngtcp2_conn]
tlsConn*: PicoTLSConnection
cptls*: ptr ngtcp2_crypto_picotls_ctx
connref*: ptr ngtcp2_crypto_conn_ref
path*: Path
buffer*: array[4096, byte]
flowing*: AsyncEvent
timeout*: Timeout
onSend*: proc(datagram: Datagram) {.gcsafe, raises: [].}
onTimeout*: proc() {.raises: [].}
onIncomingStream*: proc(stream: Stream)
onHandshakeDone*: proc()
onNewId*: Opt[proc(id: ConnectionId)]
@@ -31,6 +41,14 @@ proc destroy*(connection: Ngtcp2Connection) =
return
connection.timeout.stop()
ngtcp2_conn_del(conn)
ngtcp2_crypto_picotls_deconfigure_session(connection.cptls)
connection.tlsConn.destroy()
dealloc(connection.cptls.handshake_properties.additional_extensions)
dealloc(connection.connref)
dealloc(connection.cptls)
connection.cptls = nil
connection.connref = nil
connection.tlsConn = nil
connection.conn = Opt.none(ptr ngtcp2_conn)
connection.onSend = nil
connection.onIncomingStream = nil
@@ -40,6 +58,8 @@ proc destroy*(connection: Ngtcp2Connection) =
proc handleTimeout(connection: Ngtcp2Connection) {.gcsafe, raises: [].}
proc executeOnTimeout(connection: Ngtcp2Connection) {.async.}
proc newConnection*(path: Path): Ngtcp2Connection =
let connection = Ngtcp2Connection()
connection.path = path
@@ -49,6 +69,9 @@ proc newConnection*(path: Path): Ngtcp2Connection =
connection.handleTimeout()
)
connection.flowing.fire()
asyncSpawn connection.executeOnTimeout()
connection
proc ids*(connection: Ngtcp2Connection): seq[ConnectionId] =
@@ -63,7 +86,7 @@ proc ids*(connection: Ngtcp2Connection): seq[ConnectionId] =
proc updateTimeout*(connection: Ngtcp2Connection) =
let conn = connection.conn.valueOr:
raise newException(Ngtcp2ConnectionClosed, "connection no longer exists")
trace "updateTimeout"
let expiry = ngtcp2_conn_get_expiry(conn)
if expiry != uint64.high:
connection.timeout.set(Moment.init(expiry.int64, 1.nanoseconds))
@@ -88,8 +111,8 @@ proc trySend(
addr packetInfo,
addr connection.buffer[0],
connection.buffer.len.uint,
written,
0,
cast[ptr ngtcp2_ssize](written),
NGTCP2_WRITE_STREAM_FLAG_NONE,
streamId,
messagePtr,
messageLen,
@@ -116,6 +139,7 @@ proc send(
messagePtr: ptr byte,
messageLen: uint,
): Future[int] {.async.} =
let written = addr result
var datagram = trySend(connection, streamId, messagePtr, messageLen, written)
while datagram.data.len == 0:
@@ -154,10 +178,7 @@ proc tryReceive(connection: Ngtcp2Connection, datagram: openArray[byte], ecn: EC
proc receive*(
connection: Ngtcp2Connection, datagram: openArray[byte], ecn = ecnNonCapable
) =
try:
connection.tryReceive(datagram, ecn)
except Ngtcp2Error:
return
connection.tryReceive(datagram, ecn)
connection.send()
connection.flowing.fire()
@@ -169,13 +190,20 @@ proc handleTimeout(connection: Ngtcp2Connection) =
return
errorAsDefect:
checkResult ngtcp2_conn_handle_expiry(conn, now())
let ret = ngtcp2_conn_handle_expiry(conn, now())
trace "handleExpiry", ret
checkResult ret
connection.send()
proc close*(connection: Ngtcp2Connection): Datagram =
let conn = connection.conn.valueOr:
raise newException(Ngtcp2ConnectionClosed, "connection no longer exists")
if (
ngtcp2_conn_in_closing_period(conn) == 1 or ngtcp2_conn_in_draining_period(conn) == 1
):
return
var ccerr: ngtcp2_ccerr
ngtcp2_ccerr_default(addr ccerr)
@@ -195,6 +223,14 @@ proc close*(connection: Ngtcp2Connection): Datagram =
let ecn = ECN(packetInfo.ecn)
Datagram(data: data, ecn: ecn)
# TODO: should stop all event loops
proc executeOnTimeout(connection: Ngtcp2Connection) {.async.} =
trace "Waiting expiration"
await connection.timeout.expired()
trace "Timeout expired"
#TODO should we call connection.onTimeout()
proc closingDuration*(connection: Ngtcp2Connection): Duration =
let conn = connection.conn.valueOr:
raise newException(Ngtcp2ConnectionClosed, "connection no longer exists")

View File

@@ -1,13 +0,0 @@
import pkg/ngtcp2
import ./params
proc submitCryptoData*(connection: ptr ngtcp2_conn, level: ngtcp2_encryption_level) =
var cryptoData = connection.encodeTransportParameters()
doAssert 0 ==
ngtcp2_conn_submit_crypto_data(
connection, level, addr cryptoData[0], cryptoData.len.uint
)
proc handleCryptoData*(connection: ptr ngtcp2_conn, data: openArray[byte]) =
let parameters = decodeTransportParameters(data)
connection.setRemoteTransportParameters(parameters)

View File

@@ -1,61 +1,7 @@
import pkg/ngtcp2
import ./pointers
const aeadlen* = 16
proc dummyEncrypt(
dest: ptr uint8,
aead: ptr ngtcp2_crypto_aead,
aead_ctx: ptr ngtcp2_crypto_aead_ctx,
plaintext: ptr uint8,
plaintextlen: uint,
nonce: ptr uint8,
noncelen: uint,
ad: ptr uint8,
adlen: uint,
): cint {.cdecl.} =
moveMem(dest, plaintext, plaintextlen)
zeroMem(dest + plaintextlen.int, aeadlen)
proc dummyDecrypt(
dest: ptr uint8,
aead: ptr ngtcp2_crypto_aead,
aead_ctx: ptr ngtcp2_crypto_aead_ctx,
ciphertext: ptr uint8,
ciphertextlen: uint,
nonce: ptr uint8,
noncelen: uint,
ad: ptr uint8,
adlen: uint,
): cint {.cdecl.} =
moveMem(dest, ciphertext, ciphertextlen - aeadlen)
proc dummyHpMask(
dest: ptr uint8,
hp: ptr ngtcp2_crypto_cipher,
hpContext: ptr ngtcp2_crypto_cipher_ctx,
sample: ptr uint8,
): cint {.cdecl.} =
var NGTCP2_FAKE_HP_MASK = "\x00\x00\x00\x00\x00"
copyMem(dest, addr NGTCP2_FAKE_HP_MASK[0], NGTCP2_FAKE_HP_MASK.len)
proc dummyUpdateKey(
conn: ptr ngtcp2_conn,
rx_secret: ptr uint8,
tx_secret: ptr uint8,
rx_aead_ctx: ptr ngtcp2_crypto_aead_ctx,
rx_iv: ptr uint8,
tx_aead_ctx: ptr ngtcp2_crypto_aead_ctx,
tx_iv: ptr uint8,
current_rx_secret: ptr uint8,
current_tx_secret: ptr uint8,
secretlen: uint,
user_data: pointer,
): cint {.cdecl.} =
discard
import ngtcp2
proc installEncryptionCallbacks*(callbacks: var ngtcp2_callbacks) =
callbacks.encrypt = dummyEncrypt
callbacks.decrypt = dummyDecrypt
callbacks.hp_mask = dummyHpMask
callbacks.update_key = dummyUpdateKey
callbacks.encrypt = ngtcp2_crypto_encrypt_cb
callbacks.decrypt = ngtcp2_crypto_decrypt_cb
callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb
callbacks.update_key = ngtcp2_crypto_update_key_cb

View File

@@ -1,4 +1,4 @@
import pkg/ngtcp2
import ngtcp2
import ../../../errors
type

View File

@@ -1,4 +1,4 @@
import pkg/ngtcp2
import ngtcp2
import ./connection
proc onHandshakeDone(connection: ptr ngtcp2_conn, userData: pointer): cint {.cdecl.} =

View File

@@ -1,4 +1,4 @@
import pkg/ngtcp2
import ngtcp2
import ../../../basics
import ../../../helpers/openarray
import ../../connectionid
@@ -18,13 +18,19 @@ proc getNewConnectionId(
conn: ptr ngtcp2_conn,
id: ptr ngtcp2_cid,
token: ptr uint8,
cidlen: uint,
cidlen: csize_t,
userData: pointer,
): cint {.cdecl.} =
let newId = randomConnectionId(cidlen.int)
id[] = newId.toCid
zeroMem(token, NGTCP2_STATELESS_RESET_TOKENLEN)
# TODO: should ngtcp2_crypto_generate_stateless_reset_token so
# we can signal the other peer that the connection is no longer valid?
# ngtcp2_crypto_generate_stateless_reset_token(
# token, some_static_secret_data, config.static_secret.size(), cid) !=
# 0)
let
connection = cast[Ngtcp2Connection](userData)
onNewId = connection.onNewId.valueOr:
@@ -44,3 +50,4 @@ proc removeConnectionId(
proc installConnectionIdCallback*(callbacks: var ngtcp2_callbacks) =
callbacks.get_new_connection_id = getNewConnectionId
callbacks.remove_connection_id = removeConnectionId

View File

@@ -1,70 +0,0 @@
import pkg/ngtcp2
import ../../../helpers/openarray
import ./encryption
type
Secret = seq[byte]
AuthenticatedEncryptionWithAssociatedData = object
context: ngtcp2_crypto_aead_ctx
HeaderProtection = object
context: ngtcp2_crypto_cipher_ctx
Key = object
aead: AuthenticatedEncryptionWithAssociatedData
hp: HeaderProtection
iv: seq[byte]
CryptoContext = ngtcp2_crypto_ctx
proc dummyCryptoContext(): CryptoContext =
var ctx = CryptoContext()
ctx.max_encryption = 1000
ctx.aead.max_overhead = aeadlen
return ctx
proc dummyKey(): Key =
Key(iv: cast[seq[byte]]("dummykey"))
proc dummySecret(): Secret =
cast[seq[byte]]("dummysecret")
proc install0RttKey*(connection: ptr ngtcp2_conn) =
let context = dummyCryptoContext()
connection.ngtcp2_conn_set_initial_crypto_ctx(unsafeAddr context)
let key = dummyKey()
doAssert 0 ==
connection.ngtcp2_conn_install_initial_key(
key.aead.context.unsafeAddr, key.iv.toUnsafePtr, key.hp.context.unsafeAddr,
key.aead.context.unsafeAddr, key.iv.toUnsafePtr, key.hp.context.unsafeAddr,
key.iv.len.uint,
)
proc installHandshakeKeys*(connection: ptr ngtcp2_conn) =
let context = dummyCryptoContext()
connection.ngtcp2_conn_set_crypto_ctx(unsafeAddr context)
let rx, tx = dummyKey()
doAssert 0 ==
ngtcp2_conn_install_rx_handshake_key(
connection, rx.aead.context.unsafeAddr, rx.iv.toUnsafePtr, rx.iv.len.uint,
rx.hp.context.unsafeAddr,
)
doAssert 0 ==
ngtcp2_conn_install_tx_handshake_key(
connection, tx.aead.context.unsafeAddr, tx.iv.toUnsafePtr, tx.iv.len.uint,
tx.hp.context.unsafeAddr,
)
proc install1RttKeys*(connection: ptr ngtcp2_conn) =
let secret = dummySecret()
let rx, tx = dummyKey()
doAssert 0 ==
ngtcp2_conn_install_rx_key(
connection, secret.toUnsafePtr, secret.len.uint, rx.aead.context.unsafeAddr,
rx.iv.toUnsafePtr, rx.iv.len.uint, rx.hp.context.unsafeAddr,
)
doAssert 0 ==
ngtcp2_conn_install_tx_key(
connection, secret.toUnsafePtr, secret.len.uint, tx.aead.context.unsafeAddr,
tx.iv.toUnsafePtr, tx.iv.len.uint, tx.hp.context.unsafeAddr,
)

View File

@@ -1,4 +1,4 @@
import pkg/ngtcp2
import ngtcp2
import ./errors
type TransportParameters* = ngtcp2_transport_params

View File

@@ -1,25 +1,30 @@
import std/nativesockets
import pkg/ngtcp2
import ngtcp2
import ../../../basics
type Path* = ref object
storage: ngtcp2_path_storage
path: ngtcp2_path
localAddress: Sockaddr_storage
localAddrLen: SockLen
remoteAddress: Sockaddr_storage
remoteAddrLen: SockLen
proc toPathPtr*(path: Path): ptr ngtcp2_path =
addr path.storage.path
addr path.path
proc newPath*(local, remote: TransportAddress): Path =
var localAddress, remoteAddress: Sockaddr_storage
var localLength, remoteLength: SockLen
local.toSAddr(localAddress, localLength)
remote.toSAddr(remoteAddress, remoteLength)
var path = Path()
ngtcp2_path_storage_init(
addr path.storage,
cast[ptr SockAddr](addr localAddress),
localLength,
cast[ptr SockAddr](addr remoteAddress),
remoteLength,
nil,
var p = Path()
local.toSAddr(p.localAddress, p.localAddrLen)
remote.toSAddr(p.remoteAddress, p.remoteAddrLen)
p.path = ngtcp2_path(
local: ngtcp2_addr(
addr_field: cast[ptr ngtcp2_sockaddr](p.localAddress.addr),
addr_len: p.localAddrLen,
),
remote: ngtcp2_addr(
addr_field: cast[ptr ngtcp2_sockaddr](p.remoteAddress.addr),
addr_len: p.remoteAddrLen,
),
user_data: nil
)
path
p

View File

@@ -0,0 +1,110 @@
import ngtcp2
import results
import ../../../errors
import tables
import ./certificateverifier
type
PicoTLSContext* = ref object
context*: ptr ptls_context_t
signCert: ptr ptls_openssl_sign_certificate_t
certVerifier: Opt[CertificateVerifier]
PicoTLSConnection* = ref object
conn*: ptr ptls_t
proc loadCertificate(ctx: ptr ptls_context_t, certificate: seq[byte]) =
var buf = create(ptls_cred_buffer_t)
defer:
dealloc(buf)
buf.off = 0
buf.owns_base = 0
buf.len = uint(len(certificate))
buf.base = newString(buf.len).cstring
copyMem(buf.base[0].unsafeAddr, certificate[0].unsafeAddr, buf.len)
let ret = ptls_load_certificates_from_memory(ctx, buf)
if ret != 0:
raise newException(QuicError, "could not load certificate: " & $ret)
proc loadPrivateKey(signCert: ptr ptls_openssl_sign_certificate_t, key: seq[byte]) =
let ret =
ptls_openssl_init_sign_certificate_with_mem_key(signCert, key[0].unsafeAddr, key.len.cint)
if ret != 0:
raise newException(QuicError, "could not load private key: " & $ret)
proc init*(
t: typedesc[PicoTLSContext],
certificate: seq[byte],
key: seq[byte],
certVerifier: Opt[CertificateVerifier],
requiresClientAuthentication: bool,
): PicoTLSContext =
var ctx = create(ptls_context_t)
ctx.random_bytes = ptls_openssl_random_bytes
ctx.get_time = addr ptls_get_time
ctx.key_exchanges =
cast[ptr ptr ptls_key_exchange_algorithm_t](addr ptls_openssl_key_exchanges)
ctx.cipher_suites = cast[ptr ptr ptls_cipher_suite_t](addr ptls_openssl_cipher_suites)
if certVerifier.isSome:
if requiresClientAuthentication:
ctx.require_client_authentication = 1
try:
ctx.verify_certificate = certVerifier.get().getPtlsVerifyCertificateT()
except:
doAssert false, "checked with if"
else:
ctx.verify_certificate = nil
var signCert: ptr ptls_openssl_sign_certificate_t = nil
if len(key) != 0 and len(certificate) != 0:
signCert = create(ptls_openssl_sign_certificate_t)
loadPrivateKey(signCert, key)
loadCertificate(ctx, certificate)
ctx.sign_certificate = addr signCert.super
return PicoTLSContext(context: ctx, signCert: signCert, certVerifier: certVerifier)
proc cfree(p: pointer) {.importc: "free", header: "<stdlib.h>".}
proc destroy*(p: PicoTLSContext) =
if p.context == nil:
return
if not p.signCert.isNil:
ptls_openssl_dispose_sign_certificate(p.signCert)
let arr = cast[ptr UncheckedArray[ptls_iovec_t]](p.context.certificates.list)
for i in 0 ..< p.context.certificates.count:#
cfree(arr[i].base)
cfree(p.context.certificates.list)
dealloc(p.signCert)
p.context.certificates.list = nil
p.context.certificates.count = 0
p.signCert = nil
if p.certVerifier.isSome:
try:
p.certVerifier.get().destroy()
except:
doAssert false, "checked with if"
p.certVerifier = Opt.none(CertificateVerifier)
dealloc(p.context)
p.context = nil
proc newConnection*(p: PicoTLSContext, isServer: bool): PicoTLSConnection =
return PicoTLSConnection(
conn:
if isServer:
ptls_server_new(p.context)
else:
ptls_client_new(p.context)
)
proc destroy*(p: PicoTLSConnection) =
if p.conn == nil:
return
ptls_free(p.conn)
p.conn = nil

View File

@@ -1,2 +1,2 @@
proc `+`*[T](p: ptr T, a: int): ptr T =
cast[ptr T](cast[ByteAddress](p) + ByteAddress(a))
cast[ptr T](cast[uint](p) + uint(a))

View File

@@ -0,0 +1,8 @@
import ngtcp2
import nimcrypto
proc onRand*(
dest: ptr uint8, destLen: csize_t, rand_ctx: ptr ngtcp2_rand_ctx
) {.cdecl.} =
# TODO: external source of randomness?
doAssert destLen.int == randomBytes(dest, destLen.int)

View File

@@ -1,76 +1,33 @@
import pkg/ngtcp2
import pkg/nimcrypto
import ../../../basics
import ../../../helpers/openarray
import ../../../errors
import ../../packets
import ../../version
import ./encryption
import ./ids
import ./keys
import ./settings
import ./cryptodata
import ./connection
import ./path
import ./picotls
import ./rand
import ./streams
import ./timestamp
import ./handshake
import ./parsedatagram
proc onReceiveClientInitial(
connection: ptr ngtcp2_conn, dcid: ptr ngtcp2_cid, userData: pointer
): cint {.cdecl.} =
connection.install0RttKey()
proc onReceiveCryptoData(
connection: ptr ngtcp2_conn,
level: ngtcp2_encryption_level,
offset: uint64,
data: ptr uint8,
datalen: uint,
userData: pointer,
): cint {.cdecl.} =
if level == NGTCP2_ENCRYPTION_LEVEL_INITIAL:
connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_INITIAL)
connection.installHandshakeKeys()
connection.handleCryptoData(toOpenArray(data, datalen))
connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE)
connection.install1RttKeys()
if level == NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE:
connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_1RTT)
ngtcp2_conn_tls_handshake_completed(connection)
proc onRand(dest: ptr uint8, destLen: uint, rand_ctx: ptr ngtcp2_rand_ctx) {.cdecl.} =
doAssert destLen.int == randomBytes(dest, destLen.int)
proc onDeleteCryptoAeadCtx(
conn: ptr ngtcp2_conn, aead_ctx: ptr ngtcp2_crypto_aead_ctx, userData: pointer
) {.cdecl.} =
discard
proc onDeleteCryptoCipherCtx(
conn: ptr ngtcp2_conn, cipher_ctx: ptr ngtcp2_crypto_cipher_ctx, userData: pointer
) {.cdecl.} =
discard
proc onGetPathChallengeData(
conn: ptr ngtcp2_conn, data: ptr uint8, userData: pointer
): cint {.cdecl.} =
let bytesWritten = randomBytes(data, NGTCP2_PATH_CHALLENGE_DATALEN)
if bytesWritten != NGTCP2_PATH_CHALLENGE_DATALEN:
return NGTCP2_ERR_CALLBACK_FAILURE
return 0
proc newNgtcp2Server*(
local, remote: TransportAddress, source, destination: ngtcp2_cid
tlsContext: PicoTLSContext,
local, remote: TransportAddress,
source, destination: ngtcp2_cid,
): Ngtcp2Connection =
var callbacks: ngtcp2_callbacks
callbacks.recv_client_initial = onReceiveClientInitial
callbacks.recv_crypto_data = onReceiveCryptoData
callbacks.recv_client_initial = ngtcp2_crypto_recv_client_initial_cb
callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb
callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb
callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb
callbacks.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb
callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb
callbacks.rand = onRand
callbacks.delete_crypto_aead_ctx = onDeleteCryptoAeadCtx
callbacks.delete_crypto_cipher_ctx = onDeleteCryptoCipherCtx
callbacks.get_path_challenge_data = onGetPathChallengeData
installConnectionIdCallback(callbacks)
installEncryptionCallbacks(callbacks)
@@ -86,34 +43,71 @@ proc newNgtcp2Server*(
let id = randomConnectionId().toCid
let path = newPath(local, remote)
result = newConnection(path)
let nConn = newConnection(path)
var conn: ptr ngtcp2_conn
var ret = ngtcp2_conn_server_new_versioned(
addr conn,
unsafeAddr source,
unsafeAddr id,
path.toPathPtr,
CurrentQuicVersion,
NGTCP2_CALLBACKS_V1,
addr callbacks,
NGTCP2_SETTINGS_V2,
addr settings,
NGTCP2_TRANSPORT_PARAMS_V1,
addr transportParams,
nil,
addr nConn[],
)
if ret != 0:
raise newException(QuicError, "could not create new server versioned conn: " & $ret)
doAssert 0 ==
ngtcp2_conn_server_new_versioned(
addr conn,
unsafeAddr source,
unsafeAddr id,
path.toPathPtr,
CurrentQuicVersion,
NGTCP2_CALLBACKS_V1,
addr callbacks,
NGTCP2_SETTINGS_V2,
addr settings,
NGTCP2_TRANSPORT_PARAMS_V1,
addr transportParams,
nil,
addr result[],
)
let cptls: ptr ngtcp2_crypto_picotls_ctx = create(ngtcp2_crypto_picotls_ctx)
result.conn = Opt.some(conn)
ngtcp2_crypto_picotls_ctx_init(cptls)
var tls = tlsContext.newConnection(true)
cptls.ptls = tls.conn
var addExtensions = cast[ptr UncheckedArray[ptls_raw_extension_t]](alloc(
ptls_raw_extension_t.sizeof * 2
))
addExtensions[0] = ptls_raw_extension_t(type_field: high(uint16))
addExtensions[1] = ptls_raw_extension_t(type_field: high(uint16))
cptls.handshake_properties = ptls_handshake_properties_t(
additional_extensions: cast[ptr ptls_raw_extension_t](addExtensions)
)
ngtcp2_conn_set_tls_native_handle(conn, cptls)
var connref = create(ngtcp2_crypto_conn_ref)
connref.user_data = conn
connref.get_conn = proc(
connRef: ptr ngtcp2_crypto_conn_ref
): ptr ngtcp2_conn {.cdecl.} =
cast[ptr ngtcp2_conn](connRef.user_data)
var dataPtr = ptls_get_data_ptr(tls.conn)
dataPtr[] = connref
ret = ngtcp2_crypto_picotls_configure_server_session(cptls)
if ret != 0:
raise newException(QuicError, "could not configure server session: " & $ret)
nConn.conn = Opt.some(conn)
nConn.tlsConn = tls
nConn.cptls = cptls
nConn.connref = connref
nConn
proc extractIds(datagram: openArray[byte]): tuple[source, dest: ngtcp2_cid] =
let info = parseDatagram(datagram)
(source: info.source.toCid, dest: info.destination.toCid)
proc newNgtcp2Server*(
local, remote: TransportAddress, datagram: openArray[byte]
tlsContext: PicoTLSContext, local, remote: TransportAddress, datagram: openArray[byte]
): Ngtcp2Connection =
let (source, destination) = extractIds(datagram)
newNgtcp2Server(local, remote, source, destination)
newNgtcp2Server(tlsContext, local, remote, source, destination)

View File

@@ -1,4 +1,4 @@
import pkg/ngtcp2
import ngtcp2
proc defaultSettings*(): ngtcp2_settings =
ngtcp2_settings_default_versioned(NGTCP2_SETTINGS_V2, addr result)

View File

@@ -1,8 +1,9 @@
import pkg/ngtcp2
import ngtcp2
import ../../../helpers/openarray
import ../../stream
import ../stream/openstate
import ./connection
import chronicles
proc newStream(connection: Ngtcp2Connection, id: int64): Stream =
newStream(id, newOpenStream(connection))
@@ -29,6 +30,7 @@ proc onStreamClose(
user_data: pointer,
stream_user_data: pointer,
): cint {.cdecl.} =
trace "onStreamClose"
let openStream = cast[OpenStream](stream_user_data)
if openStream != nil:
openStream.onClose()
@@ -39,16 +41,43 @@ proc onReceiveStreamData(
stream_id: int64,
offset: uint64,
data: ptr uint8,
datalen: uint,
datalen: csize_t,
user_data: pointer,
stream_user_data: pointer,
): cint {.cdecl.} =
trace "onReceiveStreamData"
let state = cast[OpenStream](stream_user_data)
var bytes = newSeqUninitialized[byte](datalen)
copyMem(bytes.toUnsafePtr, data, datalen)
state.receive(bytes)
proc onStreamReset(
connection: ptr ngtcp2_conn,
stream_id: int64,
final_size: uint64,
app_error_code: uint64,
user_data: pointer,
stream_user_data: pointer,
): cint {.cdecl.} =
trace "onStreamReset"
let openStream = cast[OpenStream](stream_user_data)
if openStream != nil:
openStream.onClose()
return 0
proc onStreamStopSending(
conn: ptr ngtcp2_conn,
stream_id: int64,
app_error_code: uint64,
user_data: pointer,
stream_user_data: pointer,
): cint {.cdecl.} =
trace "onStreamStopSending"
return 0
proc installStreamCallbacks*(callbacks: var ngtcp2_callbacks) =
callbacks.stream_open = onStreamOpen
callbacks.stream_close = onStreamClose
callbacks.recv_stream_data = onReceiveStreamData
callbacks.stream_reset = onStreamReset
callbacks.stream_stop_sending = onStreamStopSending

View File

@@ -1,5 +1,9 @@
import ../../../basics
import ../../stream
import chronicles
logScope:
topics = "closed state"
type
ClosedStream* = ref object of StreamState
@@ -15,9 +19,11 @@ method enter*(state: ClosedStream, stream: Stream) =
stream.closed.fire()
method read*(state: ClosedStream): Future[seq[byte]] {.async.} =
trace "cant read, stream is closed"
raise newException(ClosedStreamError, "stream is closed")
method write*(state: ClosedStream, bytes: seq[byte]) {.async.} =
trace "cant write, stream is closed"
raise newException(ClosedStreamError, "stream is closed")
method close*(state: ClosedStream) {.async.} =

View File

@@ -1,11 +1,16 @@
import ../basics
import ./tlsbackend
import ./quicconnection
import ./ngtcp2/connection/openstate
proc newQuicClientConnection*(local, remote: TransportAddress): QuicConnection =
newQuicConnection(openClientConnection(local, remote))
proc newQuicClientConnection*(
tlsBackend: TLSBackend, local, remote: TransportAddress
): QuicConnection =
let openConn = openClientConnection(tlsBackend, local, remote)
newQuicConnection(openConn)
proc newQuicServerConnection*(
local, remote: TransportAddress, datagram: Datagram
tlsBackend: TLSBackend, local, remote: TransportAddress, datagram: Datagram
): QuicConnection =
newQuicConnection(openServerConnection(local, remote, datagram))
let openConn = openServerConnection(tlsBackend, local, remote, datagram)
newQuicConnection(openConn)

View File

@@ -12,7 +12,7 @@ type
StreamError* = object of QuicError
{.push locks: "unknown", raises: [QuicError].}
{.push raises: [QuicError].}
method enter*(state: StreamState, stream: Stream) {.base.} =
doAssert not state.entered # states are not reentrant

View File

@@ -1,4 +1,5 @@
import ../basics
import chronicles
type Timeout* = ref object
timer: Opt[TimerCallback]
@@ -6,6 +7,7 @@ type Timeout* = ref object
expired: AsyncEvent
proc setTimer(timeout: Timeout, moment: Moment) =
trace "setTimer"
proc onTimeout(_: pointer) =
timeout.expired.fire()
timeout.onExpiry()
@@ -16,6 +18,7 @@ const skip = proc() =
discard
proc newTimeout*(onExpiry: proc() {.gcsafe, raises: [].} = skip): Timeout =
trace "newTimeout"
Timeout(onExpiry: onExpiry, expired: newAsyncEvent())
proc stop*(timeout: Timeout) =
@@ -32,3 +35,4 @@ proc set*(timeout: Timeout, duration: Duration) =
proc expired*(timeout: Timeout) {.async.} =
await timeout.expired.wait()
trace "expired"

View File

@@ -0,0 +1,45 @@
import results
import ngtcp2
import ./ngtcp2/native
import ../errors
export CertificateVerifier
export certificateVerifierCB
export CustomCertificateVerifier
export InsecureCertificateVerifier
export init
export destroy
type TLSBackend* = ref object
picoTLS*: PicoTLSContext
proc newServerTLSBackend*(
certificate: seq[byte],
key: seq[byte],
certificateVerifier: Opt[CertificateVerifier],
): TLSBackend {.raises: [QuicError].} =
let picotlsCtx = PicoTLSContext.init(
certificate, key, certificateVerifier, certificateVerifier.isSome
)
let ret = ngtcp2_crypto_picotls_configure_server_context(picotlsCtx.context)
if ret != 0:
raise newException(QuicError, "could not configure server context: " & $ret)
return TLSBackend(picoTLS: picotlsCtx)
proc newClientTLSBackend*(
certificate: seq[byte],
key: seq[byte],
certificateVerifier: Opt[CertificateVerifier],
): TLSBackend {.raises: [QuicError].} =
let picotlsCtx = PicoTLSContext.init(certificate, key, certificateVerifier, false)
let ret = ngtcp2_crypto_picotls_configure_client_context(picotlsCtx.context)
if ret != 0:
raise newException(QuicError, "could not configure client context: " & $ret)
return TLSBackend(picoTLS: picotlsCtx)
proc destroy*(self: TLSBackend) =
if self.picoTLS.isNil:
return
self.picoTLS.destroy()
self.picoTLS = nil

View File

@@ -1,4 +1,3 @@
from pkg/ngtcp2 import NGTCP2_PROTO_VER_V2
#from pkg/ngtcp2 import NGTCP2_PROTO_VER_V2
const DraftVersion29 = 0xFF00001D'u32
const CurrentQuicVersion* = cast[uint32](NGTCP2_PROTO_VER_V2)
const CurrentQuicVersion* = cast[uint32](0x6b3343cfu) # TODO: figure out how to export NGTCP2_PROTO_VER_V2 with futhark

View File

@@ -0,0 +1,15 @@
import sequtils
import os
const certificateStr =
staticRead(parentDir(currentSourcePath()) / "testCertificate.pem")
const privateKeyStr = staticRead(parentDir(currentSourcePath()) / "testPrivateKey.pem")
proc strToSeq(val: string): seq[byte] =
toSeq(val.toOpenArrayByte(0, val.high))
proc testCertificate*(): seq[byte] =
strToSeq(certificateStr)
proc testPrivateKey*(): seq[byte] =
strToSeq(privateKeyStr)

View File

@@ -1,8 +1,8 @@
import std/random
import pkg/chronos
import pkg/quic/transport/quicconnection
import pkg/quic/transport/quicclientserver
import pkg/quic/transport/[quicconnection, quicclientserver, tlsbackend]
import pkg/quic/helpers/asyncloop
import ./certificate
import ./addresses
proc networkLoop*(source, destination: QuicConnection) {.async.} =
@@ -37,11 +37,17 @@ proc simulateLossyNetwork*(a, b: QuicConnection) {.async.} =
await allFutures(loop1.cancelAndWait(), loop2.cancelAndWait())
proc setupConnection*(): Future[tuple[client, server: QuicConnection]] {.async.} =
let client = newQuicClientConnection(zeroAddress, zeroAddress)
client.send()
let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress)
client.send() # Start Handshake
let datagram = await client.outgoing.get()
let server = newQuicServerConnection(zeroAddress, zeroAddress, datagram)
server.receive(datagram)
let serverTLSBackend = newServerTLSBackend(
testCertificate(), testPrivateKey(), Opt.none(CertificateVerifier)
)
let server =
newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, datagram)
result = (client, server)
proc performHandshake*(): Future[tuple[client, server: QuicConnection]] {.async.} =

View File

@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIB0jCCAXegAwIBAgIVAJUFhCQrlzzkFZbZkyXNRwCXSvYWMAwGCCqGSM49BAMC
BQAwFDESMBAGA1UEAwwJbGlicDJwLmlvMCAXDTc1MDEwMTAwMDAwMFoYDzQwOTYw
MTAxMDAwMDAwWjAUMRIwEAYDVQQDDAlsaWJwMnAuaW8wWTATBgcqhkjOPQIBBggq
hkjOPQMBBwNCAAThQpOzDszrSDCf9wqR1+0WOt6BHVwv3Q3R+8quylRwLbR2D8ov
PoXfWCzoACju9j2wBj4WHgb69Cpp9FiW7qx0o4GhMIGeMIGABgorBgEEAYOiWgEB
BHIwbwQlCAISIQIz55rVQB21ks44HQ9G/0YJgvDB5v5/6Bl+MkQq5k3A4gRGMEQC
IBLxYkAwI5H5GMzWPpjhOt3pJQ9gi7ICqCejEIFSuvf1AiA2kZZ+twfvyIYV7AAv
InM3WOWZlgY4a7TDRm8C974pHwAwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw
DAYIKoZIzj0EAwIFAANHADBEAiBV/KbtQ5SIAu373/8j/PzLnewCHUzXWTd3zjN9
nzkGIgIgd1IJKDqnotzvCKw4WYrAmgTMkINJwlaQf278ranRaEY=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJTCwOljI3BRUBE3VXVy4sN16sh62IliV7X9y3uEPh4FoAoGCCqGSM49
AwEHoUQDQgAE4UKTsw7M60gwn/cKkdftFjregR1cL90N0fvKrspUcC20dg/KLz6F
31gs6AAo7vY9sAY+Fh4G+vQqafRYlu6sdA==
-----END EC PRIVATE KEY-----

View File

@@ -1,17 +1,23 @@
import pkg/chronos
import pkg/chronos/unittest2/asynctests
import pkg/quic
import chronos
import chronos/unittest2/asynctests
import quic
import quic/transport/tlsbackend
import ../helpers/certificate
suite "api":
setup:
var listener = listen(initTAddress("127.0.0.1:0"))
let serverTLSConfig = TLSConfig.init(testCertificate(), testPrivateKey())
var server = QuicServer.init(serverTLSConfig)
let clientTLSConfig = TLSConfig.init()
var client = QuicClient.init(clientTLSConfig)
var listener = server.listen(initTAddress("127.0.0.1:0"))
let address = listener.localAddress
teardown:
waitFor listener.stop()
asyncTest "opens and drops connections":
let dialing = dial(address)
let dialing = client.dial(address)
let accepting = listener.accept()
let outgoing = await dialing
@@ -26,7 +32,7 @@ suite "api":
await incoming.drop()
asyncTest "opens and closes streams":
let dialing = dial(address)
let dialing = client.dial(address)
let accepting = listener.accept()
let outgoing = await dialing
@@ -42,7 +48,7 @@ suite "api":
await incoming.drop()
asyncTest "waits until peer closes connection":
let dialing = dial(address)
let dialing = client.dial(address)
let accepting = listener.accept()
let outgoing = await dialing
@@ -53,11 +59,11 @@ suite "api":
asyncTest "accepts multiple incoming connections":
let accepting1 = listener.accept()
let outgoing1 = await dial(address)
let outgoing1 = await client.dial(address)
let incoming1 = await accepting1
let accepting2 = listener.accept()
let outgoing2 = await dial(address)
let outgoing2 = await client.dial(address)
let incoming2 = await accepting2
check incoming1 != incoming2
@@ -70,7 +76,7 @@ suite "api":
asyncTest "writes to and reads from streams":
let message = @[1'u8, 2'u8, 3'u8]
let outgoing = await dial(address)
let outgoing = await client.dial(address)
defer:
await outgoing.drop()

View File

@@ -1,6 +1,9 @@
import pkg/chronos
import pkg/chronos/unittest2/asynctests
import pkg/quic/connection
import chronos
import chronos/unittest2/asynctests
import results
import quic/connection
import quic/transport/tlsbackend
import ../helpers/udp
suite "connections":
@@ -9,8 +12,8 @@ suite "connections":
asyncTest "handles error when writing to udp transport by closing connection":
let udp = newDatagramTransport()
let connection = newOutgoingConnection(udp, address)
let tlsBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let connection = newOutgoingConnection(tlsBackend, udp, address)
await udp.closeWait()
connection.startHandshake()

View File

@@ -1,27 +1,39 @@
import results
import pkg/unittest2
import ../helpers/async
import pkg/quic
import pkg/chronos
import ../helpers/certificate
suite "examples from Readme":
test "outgoing and incoming connections":
let message = cast[seq[byte]]("some message")
proc outgoing() {.async.} =
let connection = await dial(initTAddress("127.0.0.1:12345"))
let cb = proc(derCertificates: seq[seq[byte]]): bool {.gcsafe.} =
# TODO: implement custom certificate validation
return derCertificates.len > 0
let customCertVerif: CertificateVerifier = CustomCertificateVerifier.init(cb)
let tlsConfig = TLSConfig.init(certificateVerifier = Opt.some(customCertVerif))
let client = QuicClient.init(tlsConfig)
let connection = await client.dial(initTAddress("127.0.0.1:12345"))
let stream = await connection.openStream()
let message = cast[seq[byte]]("some message")
await stream.write(message)
await stream.close()
await connection.waitClosed()
await connection.close()
proc incoming() {.async.} =
let listener = listen(initTAddress("127.0.0.1:12345"))
let tlsConfig = TLSConfig.init(testCertificate(), testPrivateKey())
let server = QuicServer.init(tlsConfig)
let listener = server.listen(initTAddress("127.0.0.1:12345"))
let connection = await listener.accept()
let stream = await connection.incomingStream()
let message = await stream.read()
let readMessage = await stream.read()
await stream.close()
await connection.close()
await connection.waitClosed()
await listener.stop()
check message == cast[seq[byte]]("some message")
listener.destroy()
check readMessage == message
waitFor allSucceeded(incoming(), outgoing())

View File

@@ -1,12 +1,15 @@
import pkg/chronos
import pkg/chronos/unittest2/asynctests
import pkg/quic
import pkg/quic/listener
import chronos
import chronos/unittest2/asynctests
import quic
import quic/listener
import quic/transport/tlsbackend
import ../helpers/udp
import ../helpers/certificate
suite "listener":
setup:
var listener = newListener(initTAddress("127.0.0.1:0"))
let tlsBackend = newServerTLSBackend(testCertificate(), testPrivateKey(), Opt.none(CertificateVerifier))
var listener = newListener(tlsBackend, initTAddress("127.0.0.1:0"))
let address = listener.localAddress
check address.port != Port(0)

View File

@@ -1,11 +1,9 @@
import pkg/unittest2
import pkg/ngtcp2
import pkg/stew/results
import pkg/quic/errors
import pkg/quic/transport/ngtcp2/native/connection
import pkg/quic/transport/ngtcp2/native/client
import pkg/quic/transport/ngtcp2/native/params
import pkg/quic/transport/ngtcp2/native/settings
import unittest2
import ngtcp2
import results
import quic/errors
import quic/transport/tlsbackend
import quic/transport/ngtcp2/native/[connection, client, params, settings]
import ../helpers/addresses
suite "ngtcp2 transport parameters":
@@ -17,7 +15,15 @@ suite "ngtcp2 transport parameters":
test "encoding and decoding":
let encoded = encodeTransportParameters(transport_params)
let decoded = decodeTransportParameters(encoded)
check decoded == transport_params
check:
transport_params.initial_max_streams_uni == decoded.initial_max_streams_uni
transport_params.initial_max_stream_data_uni == decoded.initial_max_stream_data_uni
transport_params.initial_max_streams_bidi == decoded.initial_max_streams_bidi
transport_params.initial_max_stream_data_bidi_local ==
decoded.initial_max_stream_data_bidi_local
transport_params.initial_max_stream_data_bidi_remote ==
decoded.initial_max_stream_data_bidi_remote
transport_params.initial_max_data == decoded.initial_max_data
test "raises when decoding fails":
var encoded = encodeTransportParameters(transport_params)
@@ -27,7 +33,8 @@ suite "ngtcp2 transport parameters":
discard decodeTransportParameters(encoded)
test "raises when setting remote parameters fails":
let connection = newNgtcp2Client(zeroAddress, zeroAddress)
let tlsBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let connection = newNgtcp2Client(tlsBackend.picoTLS, zeroAddress, zeroAddress)
defer:
connection.destroy()
transport_params.active_connection_id_limit = 0

View File

@@ -1,16 +1,17 @@
import pkg/chronos
import pkg/chronos/unittest2/asynctests
import pkg/quic/errors
import pkg/quic/transport/quicconnection
import pkg/quic/transport/quicclientserver
import pkg/quic/udp/datagram
import pkg/quic/transport/connectionid
import chronos
import chronos/unittest2/asynctests
import quic/errors
import quic/transport/[quicconnection, quicclientserver, tlsbackend]
import quic/udp/datagram
import quic/transport/connectionid
import ../helpers/simulation
import ../helpers/addresses
import ../helpers/certificate
suite "quic connection":
asyncTest "sends outgoing datagrams":
let client = newQuicClientConnection(zeroAddress, zeroAddress)
let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress)
defer:
await client.drop()
client.send()
@@ -18,14 +19,17 @@ suite "quic connection":
check datagram.len > 0
asyncTest "processes received datagrams":
let client = newQuicClientConnection(zeroAddress, zeroAddress)
let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress)
defer:
await client.drop()
client.send()
let datagram = await client.outgoing.get()
let server = newQuicServerConnection(zeroAddress, zeroAddress, datagram)
let serverTLSBackend = newServerTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let server =
newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, datagram)
defer:
await server.drop()
@@ -35,7 +39,10 @@ suite "quic connection":
let invalid = Datagram(data: @[0'u8])
expect QuicError:
discard newQuicServerConnection(zeroAddress, zeroAddress, invalid)
let serverTLSBackend =
newServerTLSBackend(@[], @[], Opt.none(CertificateVerifier))
discard
newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, invalid)
asyncTest "performs handshake":
let (client, server) = await performHandshake()
@@ -60,11 +67,16 @@ suite "quic connection":
check server.ids != client.ids
asyncTest "notifies about id changes":
let client = newQuicClientConnection(zeroAddress, zeroAddress)
let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress)
client.send()
let datagram = await client.outgoing.get()
let server = newQuicServerConnection(zeroAddress, zeroAddress, datagram)
let serverTLSBackend = newServerTLSBackend(
testCertificate(), testPrivateKey(), Opt.none(CertificateVerifier)
)
let server =
newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, datagram)
var newId: ConnectionId
server.onNewId = proc(id: ConnectionId) =
newId = id
@@ -80,7 +92,8 @@ suite "quic connection":
await server.drop
asyncTest "raises ConnectionError when closed":
let connection = newQuicClientConnection(zeroAddress, zeroAddress)
let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let connection = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress)
await connection.drop()
expect ConnectionError:
@@ -93,7 +106,8 @@ suite "quic connection":
discard await connection.openStream()
asyncTest "has empty list of ids when closed":
let connection = newQuicClientConnection(zeroAddress, zeroAddress)
let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier))
let connection = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress)
await connection.drop()
check connection.ids.len == 0

View File

@@ -33,13 +33,6 @@ suite "streams":
let stream = await client.openStream()
await stream.close()
asyncTest "writes to stream":
let stream = await client.openStream()
let message = @[1'u8, 2'u8, 3'u8]
await stream.write(message)
check client.outgoing.anyIt(it.data.contains(message))
asyncTest "writes zero-length message":
let stream = await client.openStream()
await stream.write(@[])