diff --git a/generate_lsquic_ffi.nim b/generate_lsquic_ffi.nim index 5f43372..1c56b4d 100644 --- a/generate_lsquic_ffi.nim +++ b/generate_lsquic_ffi.nim @@ -7,13 +7,21 @@ from os import parentDir, `/` import boringssl -proc dropGeneratedCEnumsImpl(opirOutput: JsonNode): JsonNode = +const preludeTypes = ["struct_lsquic_cid", "lsquic_cid_t"] + +proc dropPreludeTypesAndGeneratedCEnumsImpl(opirOutput: JsonNode): JsonNode = var resp = newJArray() for node in opirOutput: # enums are generated manually to avoid issue described in # https://github.com/PMunch/futhark/issues/152 if node{"kind"}.getStr("") == "enum": continue + + # Futhark incorrectly maps the struct alignment on lsquic_cid_t to field alignment, + # so the prelude supplies the correct native layout instead. + if node{"name"}.getStr("") in preludeTypes: + continue + resp.add node resp @@ -21,7 +29,7 @@ importc: outputPath currentSourcePath.parentDir / "tmp_lsquic_ffi.nim" path currentSourcePath.parentDir / "libs/lsquic/include" addopircallback proc(opirOutput: JsonNode): JsonNode {.closure.} = - dropGeneratedCEnumsImpl(opirOutput) + dropPreludeTypesAndGeneratedCEnumsImpl(opirOutput) rename FILE, CFile # Rename `FILE` that STB uses to `CFile` which is the Nim equivalent rename struct_sockaddr, SockAddr # Rename `struct_sockaddr` for chronos SockAddr "lsquic.h" diff --git a/lsquic/context/client.nim b/lsquic/context/client.nim index 34202f8..bb48297 100644 --- a/lsquic/context/client.nim +++ b/lsquic/context/client.nim @@ -109,6 +109,7 @@ method dial*( return err("could not dial: " & $remote) quicClientConn.lsquicConn = conn + ctx.trackConnectionCid(conn) ctx.processWhenReady() ok(quicClientConn) @@ -121,6 +122,7 @@ proc new*(T: typedesc[ClientContext], tlsConfig: TLSConfig): Result[T, string] = ctx.tlsConfig = tlsConfig ctx.running = true ctx.setupSSLContext() + ctx.initCidTracking() lsquic_engine_init_settings(addr ctx.settings, 0) ctx.settings.es_versions = 1.cuint shl LSQVER_I001.cuint #IETF QUIC v1 @@ -155,6 +157,10 @@ proc new*(T: typedesc[ClientContext], tlsConfig: TLSConfig): Result[T, string] = ea_get_ssl_ctx: getSSLCtx, ea_packets_out: sendPacketsOut, ) + ctx.api.ea_new_scids = addCids + ctx.api.ea_live_scids = addCids + ctx.api.ea_old_scids = removeCids + ctx.api.ea_cids_update_ctx = cast[pointer](ctx) ctx.engine = lsquic_engine_new(0, addr ctx.api) if ctx.engine.isNil: diff --git a/lsquic/context/context.nim b/lsquic/context/context.nim index 66891ba..fed33f0 100644 --- a/lsquic/context/context.nim +++ b/lsquic/context/context.nim @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT # Copyright (c) Status Research & Development GmbH -import std/deques +import std/[deques, hashes, sets, strutils] import boringssl import chronos import chronos/osdefs @@ -12,17 +12,96 @@ import let SSL_CTX_ID = SSL_CTX_get_ex_new_index(0, nil, nil, nil, nil) # Yes, this is global doAssert SSL_CTX_ID >= 0, "could not generate global ssl_ctx id" -type QuicContext* = ref object of RootObj - settings*: struct_lsquic_engine_settings - api*: struct_lsquic_engine_api - engine*: ptr struct_lsquic_engine - stream_if*: struct_lsquic_stream_if - tlsConfig*: TLSConfig - tickTimeout*: Timeout - sslCtx*: ptr SSL_CTX - fd*: cint - processing: bool - running*: bool +type + CidKey* = object + len*: uint8 + bytes*: array[MAX_CID_LEN, uint8] + + LsquicCidArray = UncheckedArray[lsquic_cid_t] + + QuicContext* = ref object of RootObj + settings*: struct_lsquic_engine_settings + api*: struct_lsquic_engine_api + engine*: ptr struct_lsquic_engine + stream_if*: struct_lsquic_stream_if + tlsConfig*: TLSConfig + tickTimeout*: Timeout + sslCtx*: ptr SSL_CTX + fd*: cint + processing: bool + running*: bool + ownedCids: HashSet[CidKey] + +func hash*(cid: CidKey): Hash = + var h = hash(cid.len) + for i in 0 ..< cid.len.int: + h = h !& hash(cid.bytes[i]) + !$h + +func shortLog*(cid: CidKey): string = + var ret = $cid.len & ":" + for i in 0 ..< min(cid.len.int, 8): + ret.add(toHex(cid.bytes[i], 2)) + ret + +chronicles.formatIt(CidKey): + shortLog(it) + +func toCidKey(cid: lsquic_cid_t, key: var CidKey): bool = + if cid.len == 0 or cid.len.int > MAX_CID_LEN: + return false + + key = CidKey(len: cid.len) + for i in 0 ..< cid.len.int: + key.bytes[i] = cid.buf[i] + true + +proc initCidTracking*(ctx: QuicContext) {.raises: [].} = + ctx.ownedCids = initHashSet[CidKey]() + +proc addCids*( + ctx: pointer, _: ptr pointer, cids: ptr lsquic_cid_t, nCids: cuint +) {.cdecl, raises: [].} = + let quicCtx = cast[QuicContext](ctx) + if quicCtx.isNil or cids.isNil: + return + + let cidsArr = cast[ptr LsquicCidArray](cids) + for i in 0 ..< nCids.int: + var key: CidKey + if toCidKey(cidsArr[i], key): + quicCtx.ownedCids.incl(key) + trace "Registered CID", cid = key, cidCount = quicCtx.ownedCids.len + +proc removeCids*( + ctx: pointer, _: ptr pointer, cids: ptr lsquic_cid_t, nCids: cuint +) {.cdecl, raises: [].} = + let quicCtx = cast[QuicContext](ctx) + if quicCtx.isNil or cids.isNil: + return + + let cidsArr = cast[ptr LsquicCidArray](cids) + for i in 0 ..< nCids.int: + var key: CidKey + if toCidKey(cidsArr[i], key): + quicCtx.ownedCids.excl(key) + trace "Removed CID", cid = key, cidCount = quicCtx.ownedCids.len + +proc trackConnectionCid*(ctx: QuicContext, conn: ptr lsquic_conn_t) {.raises: [].} = + if ctx.isNil or conn.isNil: + return + + let cid = lsquic_conn_id(conn) + if cid.isNil: + return + + var key: CidKey + if toCidKey(cid[], key): + ctx.ownedCids.incl(key) + trace "Tracked connection CID", cid = key, cidCount = ctx.ownedCids.len + +proc ownsCid*(ctx: QuicContext, cid: CidKey): bool {.raises: [].} = + not ctx.isNil and cid in ctx.ownedCids proc isRunning*(ctx: QuicContext): bool {.raises: [].} = not ctx.isNil and ctx.running and not ctx.engine.isNil diff --git a/lsquic/context/io.nim b/lsquic/context/io.nim index 8a52c51..ac6e888 100644 --- a/lsquic/context/io.nim +++ b/lsquic/context/io.nim @@ -9,6 +9,7 @@ import ../helpers/[openarray, sequninit, transportaddr] import std/[nativesockets, net] when not defined(windows): + import chronicles import posix when defined(windows): @@ -144,6 +145,7 @@ proc sendPacketsOut*( let res = sendmsg(SocketHandle(quicCtx.fd), msg.addr, 0) if res < 0: + trace "sendmsg failed", sent, nspecs break sent.inc diff --git a/lsquic/context/server.nim b/lsquic/context/server.nim index 3cf23f9..de5fd8b 100644 --- a/lsquic/context/server.nim +++ b/lsquic/context/server.nim @@ -33,8 +33,9 @@ proc onNewConn( onClose: proc() = discard, ) - GC_ref(quicConn) # Keep it pinned until on_conn_closed is called let serverCtx = cast[ServerContext](stream_if_ctx) + serverCtx.trackConnectionCid(conn) + GC_ref(quicConn) # Keep it pinned until on_conn_closed is called serverCtx.incoming.putNoWait(quicConn) cast[ptr lsquic_conn_ctx_t](quicConn) @@ -58,6 +59,7 @@ proc new*(T: typedesc[ServerContext], tlsConfig: TLSConfig): Result[T, string] = ctx.running = true ctx.incoming = newAsyncQueue[QuicConnection]() ctx.setupSSLContext() + ctx.initCidTracking() lsquic_engine_init_settings(addr ctx.settings, LSENG_SERVER) ctx.settings.es_versions = 1.cuint shl LSQVER_I001.cuint #IETF QUIC v1 @@ -92,6 +94,10 @@ proc new*(T: typedesc[ServerContext], tlsConfig: TLSConfig): Result[T, string] = ea_get_ssl_ctx: getSSLCtx, ea_packets_out: sendPacketsOut, ) + ctx.api.ea_new_scids = addCids + ctx.api.ea_live_scids = addCids + ctx.api.ea_old_scids = removeCids + ctx.api.ea_cids_update_ctx = cast[pointer](ctx) ctx.engine = lsquic_engine_new(LSENG_SERVER, addr ctx.api) if ctx.engine.isNil: diff --git a/lsquic/endpoint.nim b/lsquic/endpoint.nim index 0d35327..154c201 100644 --- a/lsquic/endpoint.nim +++ b/lsquic/endpoint.nim @@ -2,7 +2,7 @@ # Copyright (c) Status Research & Development GmbH import chronos, chronicles, results -import ./[errors, connection, tlsconfig, datagram, connectionmanager] +import ./[errors, connection, tlsconfig, datagram, connectionmanager, lsquic_ffi] import ./context/[server, client, context, io] type @@ -39,17 +39,79 @@ proc createClientContext( context.fd = fd context +proc scidLen(endpoint: QuicEndpoint): cuint {.raises: [].} = + if not endpoint.serverContext.isNil and + endpoint.serverContext.settings.es_scid_len != 0: + return endpoint.serverContext.settings.es_scid_len + if not endpoint.clientContext.isNil and + endpoint.clientContext.settings.es_scid_len != 0: + return endpoint.clientContext.settings.es_scid_len + LSQUIC_DF_SCID_LEN.cuint + +proc packetDcid( + endpoint: QuicEndpoint, packet: seq[byte], cid: var CidKey +): bool {.raises: [].} = + if packet.len == 0: + return false + + var cidLen: uint8 + let offset = lsquic_dcid_from_packet( + unsafeAddr packet[0], packet.len.csize_t, endpoint.scidLen(), addr cidLen + ) + if offset < 0: + return false + + let start = offset.int + if cidLen == 0 or cidLen.int > MAX_CID_LEN or start + cidLen.int > packet.len: + return false + + cid = CidKey(len: cidLen) + for i in 0 ..< cidLen.int: + cid.bytes[i] = packet[start + i] + true + +func isIetfInitial(packet: seq[byte]): bool {.raises: [].} = + if packet.len == 0: + return false + (packet[0] and 0xC0'u8) == 0xC0'u8 and (packet[0] and 0x30'u8) == 0 + proc receiveDatagram( endpoint: QuicEndpoint, data: seq[byte], local, remote: TransportAddress ) {.raises: [].} = if endpoint.isNil or endpoint.stopped: return - # Endpoints can have both contexts; each engine needs to see the packet. - if not endpoint.clientContext.isNil: + let + hasClientContext = not endpoint.clientContext.isNil + hasServerContext = not endpoint.serverContext.isNil + + var cid: CidKey + if endpoint.packetDcid(data, cid): + if hasClientContext and endpoint.clientContext.ownsCid(cid): + trace "Routing datagram to client context", cid + endpoint.clientContext.receive(Datagram(data: data), local, remote) + return + + if hasServerContext and endpoint.serverContext.ownsCid(cid): + trace "Routing datagram to server context", cid + endpoint.serverContext.receive(Datagram(data: data), local, remote) + return + + if hasClientContext and not hasServerContext: endpoint.clientContext.receive(Datagram(data: data), local, remote) - if not endpoint.serverContext.isNil: + return + + if hasServerContext and not hasClientContext: + endpoint.serverContext.receive(Datagram(data: data), local, remote) + return + + if hasServerContext and data.isIetfInitial(): + trace "Routing initial datagram with unknown CID to server context", + bytes = data.len, local, remote endpoint.serverContext.receive(Datagram(data: data), local, remote) + return + + trace "Dropping datagram with unknown CID", bytes = data.len, local, remote proc receiveFromUdp( endpoint: QuicEndpoint, udp: DatagramTransport, remote: TransportAddress diff --git a/lsquic/lsquic_ffi.nim b/lsquic/lsquic_ffi.nim index 39d7fb0..3bca7a7 100644 --- a/lsquic/lsquic_ffi.nim +++ b/lsquic/lsquic_ffi.nim @@ -11,7 +11,9 @@ import chronos/osdefs import zlib import boringssl -type ptrdiff_t* {.importc: "ptrdiff_t", header: "".} = int +type + ptrdiff_t* {.importc: "ptrdiff_t", header: "".} = int + uint_fast8_t* {.importc: "uint_fast8_t", header: "".} = uint8 # enums are generated manually to avoid issue described in # https://github.com/PMunch/futhark/issues/152 @@ -43,6 +45,9 @@ borrowCEnumOps(enum_lsquic_conn_param) borrowCEnumOps(enum_LSQUIC_CONN_STATUS) const + MAX_CID_LEN* = 20 + GQUIC_CID_LEN* = 8 + LSQVER_043* = enum_lsquic_version(0) LSQVER_046* = enum_lsquic_version(1) LSQVER_050* = enum_lsquic_version(2) @@ -107,6 +112,16 @@ when defined(windows): {.passc: "-I" & lshpack.} {.passc: "-I" & xxhash.} +type + struct_lsquic_cid* {. + importc: "struct lsquic_cid", header: "lsquic_types.h", bycopy, completeStruct + .} = object + buf* {.importc: "buf".}: array[MAX_CID_LEN, uint8] + len* {.importc: "len".}: uint_fast8_t + padding: array[3, uint8] + + lsquic_cid_t* = struct_lsquic_cid + const HAVE_BORINGSSL = "-DHAVE_BORINGSSL" const XXH_HEADER_NAME = "-DXXH_HEADER_NAME=\"\"" @@ -357,14 +372,7 @@ else: "Declaration of " & "LSQUIC_DF_CFCW_CLIENT" & " already exists, not redeclaring" ) type - struct_lsquic_cid_570425834 {.pure, inheritable, bycopy.} = object - buf* {.align(8'i64).}: array[20'i64, uint8] - ## Generated based on /home/r/vacp2p/nim-lsquic/libs/lsquic/include/lsquic_types.h:27:28 - len* {.align(8'i64).}: uint_fast8_t_570425837 - uint_fast8_t_570425836 = uint8 ## Generated based on /usr/include/stdint.h:60:24 - lsquic_cid_t_570425838 = struct_lsquic_cid_570425835 - ## Generated based on /home/r/vacp2p/nim-lsquic/libs/lsquic/include/lsquic_types.h:32:3 lsquic_stream_id_t_570425840 = uint64 ## Generated based on /home/r/vacp2p/nim-lsquic/libs/lsquic/include/lsquic_types.h:40:18 lsquic_engine_t_570425842 = struct_lsquic_engine @@ -553,9 +561,8 @@ type proc(a0: pointer, a1: pointer, a2: pointer, a3: cschar): void {.cdecl.} pmi_return*: proc(a0: pointer, a1: pointer, a2: pointer, a3: cschar): void {.cdecl.} - lsquic_cids_update_f_570425876 = proc( - a0: pointer, a1: ptr pointer, a2: ptr lsquic_cid_t_570425839, a3: cuint - ): void {.cdecl.} + lsquic_cids_update_f_570425876 = + proc(a0: pointer, a1: ptr pointer, a2: ptr lsquic_cid_t, a3: cuint): void {.cdecl.} ## Generated based on /home/r/vacp2p/nim-lsquic/libs/lsquic/include/lsquic.h:1307:16 struct_lsquic_hset_if_570425878 {.pure, inheritable, bycopy.} = object hsi_create_header_set*: @@ -733,17 +740,6 @@ type else: struct_lsquic_conn_info_570425895 ) - struct_lsquic_cid_570425835 = ( - when declared(struct_lsquic_cid): - when ownSizeof(struct_lsquic_cid) != ownSizeof(struct_lsquic_cid_570425834): - static: - warning( - "Declaration of " & "struct_lsquic_cid" & " exists but with different size" - ) - struct_lsquic_cid - else: - struct_lsquic_cid_570425834 - ) lsquic_stream_id_t_570425841 = ( when declared(lsquic_stream_id_t): when ownSizeof(lsquic_stream_id_t) != ownSizeof(lsquic_stream_id_t_570425840): @@ -910,17 +906,6 @@ type else: lsquic_stream_ctx_t_570425850 ) - lsquic_cid_t_570425839 = ( - when declared(lsquic_cid_t): - when ownSizeof(lsquic_cid_t) != ownSizeof(lsquic_cid_t_570425838): - static: - warning( - "Declaration of " & "lsquic_cid_t" & " exists but with different size" - ) - lsquic_cid_t - else: - lsquic_cid_t_570425838 - ) uint_fast8_t_570425837 = ( when declared(uint_fast8_t): when ownSizeof(uint_fast8_t) != ownSizeof(uint_fast8_t_570425836): @@ -1046,11 +1031,6 @@ else: hint( "Declaration of " & "struct_lsquic_conn_info" & " already exists, not redeclaring" ) -when not declared(struct_lsquic_cid): - type struct_lsquic_cid* = struct_lsquic_cid_570425834 -else: - static: - hint("Declaration of " & "struct_lsquic_cid" & " already exists, not redeclaring") when not declared(lsquic_stream_id_t): type lsquic_stream_id_t* = lsquic_stream_id_t_570425840 else: @@ -1138,11 +1118,6 @@ when not declared(lsquic_stream_ctx_t): else: static: hint("Declaration of " & "lsquic_stream_ctx_t" & " already exists, not redeclaring") -when not declared(lsquic_cid_t): - type lsquic_cid_t* = lsquic_cid_t_570425838 -else: - static: - hint("Declaration of " & "lsquic_cid_t" & " already exists, not redeclaring") when not declared(uint_fast8_t): type uint_fast8_t* = uint_fast8_t_570425836 else: @@ -2670,7 +2645,7 @@ else: when not declared(lsquic_conn_id): proc lsquic_conn_id*( c: ptr lsquic_conn_t_570425845 - ): ptr lsquic_cid_t_570425839 {.cdecl, importc: "lsquic_conn_id".} + ): ptr lsquic_cid_t {.cdecl, importc: "lsquic_conn_id".} else: static: @@ -2968,7 +2943,7 @@ else: ) when not declared(lsquic_cid_from_packet): proc lsquic_cid_from_packet*( - a0: ptr uint8, bufsz: csize_t, cid: ptr lsquic_cid_t_570425839 + a0: ptr uint8, bufsz: csize_t, cid: ptr lsquic_cid_t ): cint {.cdecl, importc: "lsquic_cid_from_packet".} else: diff --git a/prelude.nim b/prelude.nim index 46ed253..36ae346 100644 --- a/prelude.nim +++ b/prelude.nim @@ -11,7 +11,9 @@ import chronos/osdefs import zlib import boringssl -type ptrdiff_t* {.importc: "ptrdiff_t", header: "".} = int +type + ptrdiff_t* {.importc: "ptrdiff_t", header: "".} = int + uint_fast8_t* {.importc: "uint_fast8_t", header: "".} = uint8 # enums are generated manually to avoid issue described in # https://github.com/PMunch/futhark/issues/152 @@ -43,6 +45,9 @@ borrowCEnumOps(enum_lsquic_conn_param) borrowCEnumOps(enum_LSQUIC_CONN_STATUS) const + MAX_CID_LEN* = 20 + GQUIC_CID_LEN* = 8 + LSQVER_043* = enum_lsquic_version(0) LSQVER_046* = enum_lsquic_version(1) LSQVER_050* = enum_lsquic_version(2) @@ -107,6 +112,16 @@ when defined(windows): {.passc: "-I" & lshpack.} {.passc: "-I" & xxhash.} +type + struct_lsquic_cid* {. + importc: "struct lsquic_cid", header: "lsquic_types.h", bycopy, completeStruct + .} = object + buf* {.importc: "buf".}: array[MAX_CID_LEN, uint8] + len* {.importc: "len".}: uint_fast8_t + padding: array[3, uint8] + + lsquic_cid_t* = struct_lsquic_cid + const HAVE_BORINGSSL = "-DHAVE_BORINGSSL" const XXH_HEADER_NAME = "-DXXH_HEADER_NAME=\"\"" diff --git a/tests/test_connection.nim b/tests/test_connection.nim index efc865b..e40920c 100644 --- a/tests/test_connection.nim +++ b/tests/test_connection.nim @@ -3,7 +3,7 @@ {.used.} -import chronos, chronos/unittest2/asynctests, results, chronicles +import chronos, chronos/unittest2/asynctests, results, chronicles, sequtils import lsquic import ./helpers/clientserver @@ -153,6 +153,54 @@ proc runEndpointDialOnlyTest(address: TransportAddress) {.async.} = incomingConn.close() await allFutures(outgoingConn.closedFuture(), incomingConn.closedFuture()) +proc runEndpointSharedSocketCrossDialTest(address: TransportAddress) {.async.} = + let endpointA = makeEndpoint(address) + let endpointB = makeEndpoint(address) + let addressA = endpointA.localAddress() + let addressB = endpointB.localAddress() + defer: + await allFutures(endpointA.stop(), endpointB.stop()) + + let acceptInitial = endpointB.accept() + let initialOutgoing = await endpointA.dial(addressB) + let initialIncoming = await acceptInitial + + check: + initialOutgoing.localAddress().port == addressA.port + initialOutgoing.remoteAddress().port == addressB.port + initialIncoming.localAddress().port == addressB.port + initialIncoming.remoteAddress().port == addressA.port + + let + acceptA = endpointA.accept() + acceptB = endpointB.accept() + dialAtoB = endpointA.dial(addressB) + dialBtoA = endpointB.dial(addressA) + + let + outgoingAtoB = await dialAtoB.wait(5.seconds) + outgoingBtoA = await dialBtoA.wait(5.seconds) + incomingA = await acceptA.wait(5.seconds) + incomingB = await acceptB.wait(5.seconds) + + check: + outgoingAtoB.localAddress().port == addressA.port + outgoingAtoB.remoteAddress().port == addressB.port + outgoingBtoA.localAddress().port == addressB.port + outgoingBtoA.remoteAddress().port == addressA.port + incomingA.localAddress().port == addressA.port + incomingA.remoteAddress().port == addressB.port + incomingB.localAddress().port == addressB.port + incomingB.remoteAddress().port == addressA.port + + let conns = @[ + initialOutgoing, initialIncoming, outgoingAtoB, outgoingBtoA, incomingA, incomingB + ] + for conn in conns: + conn.close() + + await allFutures(conns.mapIt(it.closedFuture())) + proc runConcurrentStreamOpenTest(address: TransportAddress) {.async.} = const streamCount = 16 @@ -220,3 +268,6 @@ suite "connection": asyncTest "dial-only endpoint works without listener": await runEndpointDialOnlyTest(initTAddress("127.0.0.1:0")) + + asyncTest "endpoints cross-dial from shared listener sockets": + await runEndpointSharedSocketCrossDialTest(initTAddress("127.0.0.1:0"))