Skip to content

Commit 870214e

Browse files
fix(endpoint): route shared-socket QUIC packets by destination CID (#89)
1 parent 3813b84 commit 870214e

9 files changed

Lines changed: 270 additions & 66 deletions

File tree

generate_lsquic_ffi.nim

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,29 @@ from os import parentDir, `/`
77

88
import boringssl
99

10-
proc dropGeneratedCEnumsImpl(opirOutput: JsonNode): JsonNode =
10+
const preludeTypes = ["struct_lsquic_cid", "lsquic_cid_t"]
11+
12+
proc dropPreludeTypesAndGeneratedCEnumsImpl(opirOutput: JsonNode): JsonNode =
1113
var resp = newJArray()
1214
for node in opirOutput:
1315
# enums are generated manually to avoid issue described in
1416
# https://github.com/PMunch/futhark/issues/152
1517
if node{"kind"}.getStr("") == "enum":
1618
continue
19+
20+
# Futhark incorrectly maps the struct alignment on lsquic_cid_t to field alignment,
21+
# so the prelude supplies the correct native layout instead.
22+
if node{"name"}.getStr("") in preludeTypes:
23+
continue
24+
1725
resp.add node
1826
resp
1927

2028
importc:
2129
outputPath currentSourcePath.parentDir / "tmp_lsquic_ffi.nim"
2230
path currentSourcePath.parentDir / "libs/lsquic/include"
2331
addopircallback proc(opirOutput: JsonNode): JsonNode {.closure.} =
24-
dropGeneratedCEnumsImpl(opirOutput)
32+
dropPreludeTypesAndGeneratedCEnumsImpl(opirOutput)
2533
rename FILE, CFile # Rename `FILE` that STB uses to `CFile` which is the Nim equivalent
2634
rename struct_sockaddr, SockAddr # Rename `struct_sockaddr` for chronos SockAddr
2735
"lsquic.h"

lsquic/context/client.nim

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ method dial*(
109109
return err("could not dial: " & $remote)
110110

111111
quicClientConn.lsquicConn = conn
112+
ctx.trackConnectionCid(conn)
112113
ctx.processWhenReady()
113114

114115
ok(quicClientConn)
@@ -121,6 +122,7 @@ proc new*(T: typedesc[ClientContext], tlsConfig: TLSConfig): Result[T, string] =
121122
ctx.tlsConfig = tlsConfig
122123
ctx.running = true
123124
ctx.setupSSLContext()
125+
ctx.initCidTracking()
124126

125127
lsquic_engine_init_settings(addr ctx.settings, 0)
126128
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] =
155157
ea_get_ssl_ctx: getSSLCtx,
156158
ea_packets_out: sendPacketsOut,
157159
)
160+
ctx.api.ea_new_scids = addCids
161+
ctx.api.ea_live_scids = addCids
162+
ctx.api.ea_old_scids = removeCids
163+
ctx.api.ea_cids_update_ctx = cast[pointer](ctx)
158164

159165
ctx.engine = lsquic_engine_new(0, addr ctx.api)
160166
if ctx.engine.isNil:

lsquic/context/context.nim

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-License-Identifier: Apache-2.0 OR MIT
22
# Copyright (c) Status Research & Development GmbH
33

4-
import std/deques
4+
import std/[deques, hashes, sets, strutils]
55
import boringssl
66
import chronos
77
import chronos/osdefs
@@ -12,17 +12,96 @@ import
1212
let SSL_CTX_ID = SSL_CTX_get_ex_new_index(0, nil, nil, nil, nil) # Yes, this is global
1313
doAssert SSL_CTX_ID >= 0, "could not generate global ssl_ctx id"
1414

15-
type QuicContext* = ref object of RootObj
16-
settings*: struct_lsquic_engine_settings
17-
api*: struct_lsquic_engine_api
18-
engine*: ptr struct_lsquic_engine
19-
stream_if*: struct_lsquic_stream_if
20-
tlsConfig*: TLSConfig
21-
tickTimeout*: Timeout
22-
sslCtx*: ptr SSL_CTX
23-
fd*: cint
24-
processing: bool
25-
running*: bool
15+
type
16+
CidKey* = object
17+
len*: uint8
18+
bytes*: array[MAX_CID_LEN, uint8]
19+
20+
LsquicCidArray = UncheckedArray[lsquic_cid_t]
21+
22+
QuicContext* = ref object of RootObj
23+
settings*: struct_lsquic_engine_settings
24+
api*: struct_lsquic_engine_api
25+
engine*: ptr struct_lsquic_engine
26+
stream_if*: struct_lsquic_stream_if
27+
tlsConfig*: TLSConfig
28+
tickTimeout*: Timeout
29+
sslCtx*: ptr SSL_CTX
30+
fd*: cint
31+
processing: bool
32+
running*: bool
33+
ownedCids: HashSet[CidKey]
34+
35+
func hash*(cid: CidKey): Hash =
36+
var h = hash(cid.len)
37+
for i in 0 ..< cid.len.int:
38+
h = h !& hash(cid.bytes[i])
39+
!$h
40+
41+
func shortLog*(cid: CidKey): string =
42+
var ret = $cid.len & ":"
43+
for i in 0 ..< min(cid.len.int, 8):
44+
ret.add(toHex(cid.bytes[i], 2))
45+
ret
46+
47+
chronicles.formatIt(CidKey):
48+
shortLog(it)
49+
50+
func toCidKey(cid: lsquic_cid_t, key: var CidKey): bool =
51+
if cid.len == 0 or cid.len.int > MAX_CID_LEN:
52+
return false
53+
54+
key = CidKey(len: cid.len)
55+
for i in 0 ..< cid.len.int:
56+
key.bytes[i] = cid.buf[i]
57+
true
58+
59+
proc initCidTracking*(ctx: QuicContext) {.raises: [].} =
60+
ctx.ownedCids = initHashSet[CidKey]()
61+
62+
proc addCids*(
63+
ctx: pointer, _: ptr pointer, cids: ptr lsquic_cid_t, nCids: cuint
64+
) {.cdecl, raises: [].} =
65+
let quicCtx = cast[QuicContext](ctx)
66+
if quicCtx.isNil or cids.isNil:
67+
return
68+
69+
let cidsArr = cast[ptr LsquicCidArray](cids)
70+
for i in 0 ..< nCids.int:
71+
var key: CidKey
72+
if toCidKey(cidsArr[i], key):
73+
quicCtx.ownedCids.incl(key)
74+
trace "Registered CID", cid = key, cidCount = quicCtx.ownedCids.len
75+
76+
proc removeCids*(
77+
ctx: pointer, _: ptr pointer, cids: ptr lsquic_cid_t, nCids: cuint
78+
) {.cdecl, raises: [].} =
79+
let quicCtx = cast[QuicContext](ctx)
80+
if quicCtx.isNil or cids.isNil:
81+
return
82+
83+
let cidsArr = cast[ptr LsquicCidArray](cids)
84+
for i in 0 ..< nCids.int:
85+
var key: CidKey
86+
if toCidKey(cidsArr[i], key):
87+
quicCtx.ownedCids.excl(key)
88+
trace "Removed CID", cid = key, cidCount = quicCtx.ownedCids.len
89+
90+
proc trackConnectionCid*(ctx: QuicContext, conn: ptr lsquic_conn_t) {.raises: [].} =
91+
if ctx.isNil or conn.isNil:
92+
return
93+
94+
let cid = lsquic_conn_id(conn)
95+
if cid.isNil:
96+
return
97+
98+
var key: CidKey
99+
if toCidKey(cid[], key):
100+
ctx.ownedCids.incl(key)
101+
trace "Tracked connection CID", cid = key, cidCount = ctx.ownedCids.len
102+
103+
proc ownsCid*(ctx: QuicContext, cid: CidKey): bool {.raises: [].} =
104+
not ctx.isNil and cid in ctx.ownedCids
26105

27106
proc isRunning*(ctx: QuicContext): bool {.raises: [].} =
28107
not ctx.isNil and ctx.running and not ctx.engine.isNil

lsquic/context/io.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ../helpers/[openarray, sequninit, transportaddr]
99
import std/[nativesockets, net]
1010

1111
when not defined(windows):
12+
import chronicles
1213
import posix
1314

1415
when defined(windows):
@@ -144,6 +145,7 @@ proc sendPacketsOut*(
144145

145146
let res = sendmsg(SocketHandle(quicCtx.fd), msg.addr, 0)
146147
if res < 0:
148+
trace "sendmsg failed", sent, nspecs
147149
break
148150

149151
sent.inc

lsquic/context/server.nim

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ proc onNewConn(
3333
onClose: proc() =
3434
discard,
3535
)
36-
GC_ref(quicConn) # Keep it pinned until on_conn_closed is called
3736
let serverCtx = cast[ServerContext](stream_if_ctx)
37+
serverCtx.trackConnectionCid(conn)
38+
GC_ref(quicConn) # Keep it pinned until on_conn_closed is called
3839
serverCtx.incoming.putNoWait(quicConn)
3940
cast[ptr lsquic_conn_ctx_t](quicConn)
4041

@@ -58,6 +59,7 @@ proc new*(T: typedesc[ServerContext], tlsConfig: TLSConfig): Result[T, string] =
5859
ctx.running = true
5960
ctx.incoming = newAsyncQueue[QuicConnection]()
6061
ctx.setupSSLContext()
62+
ctx.initCidTracking()
6163

6264
lsquic_engine_init_settings(addr ctx.settings, LSENG_SERVER)
6365
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] =
9294
ea_get_ssl_ctx: getSSLCtx,
9395
ea_packets_out: sendPacketsOut,
9496
)
97+
ctx.api.ea_new_scids = addCids
98+
ctx.api.ea_live_scids = addCids
99+
ctx.api.ea_old_scids = removeCids
100+
ctx.api.ea_cids_update_ctx = cast[pointer](ctx)
95101

96102
ctx.engine = lsquic_engine_new(LSENG_SERVER, addr ctx.api)
97103
if ctx.engine.isNil:

lsquic/endpoint.nim

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright (c) Status Research & Development GmbH
33

44
import chronos, chronicles, results
5-
import ./[errors, connection, tlsconfig, datagram, connectionmanager]
5+
import ./[errors, connection, tlsconfig, datagram, connectionmanager, lsquic_ffi]
66
import ./context/[server, client, context, io]
77

88
type
@@ -39,17 +39,79 @@ proc createClientContext(
3939
context.fd = fd
4040
context
4141

42+
proc scidLen(endpoint: QuicEndpoint): cuint {.raises: [].} =
43+
if not endpoint.serverContext.isNil and
44+
endpoint.serverContext.settings.es_scid_len != 0:
45+
return endpoint.serverContext.settings.es_scid_len
46+
if not endpoint.clientContext.isNil and
47+
endpoint.clientContext.settings.es_scid_len != 0:
48+
return endpoint.clientContext.settings.es_scid_len
49+
LSQUIC_DF_SCID_LEN.cuint
50+
51+
proc packetDcid(
52+
endpoint: QuicEndpoint, packet: seq[byte], cid: var CidKey
53+
): bool {.raises: [].} =
54+
if packet.len == 0:
55+
return false
56+
57+
var cidLen: uint8
58+
let offset = lsquic_dcid_from_packet(
59+
unsafeAddr packet[0], packet.len.csize_t, endpoint.scidLen(), addr cidLen
60+
)
61+
if offset < 0:
62+
return false
63+
64+
let start = offset.int
65+
if cidLen == 0 or cidLen.int > MAX_CID_LEN or start + cidLen.int > packet.len:
66+
return false
67+
68+
cid = CidKey(len: cidLen)
69+
for i in 0 ..< cidLen.int:
70+
cid.bytes[i] = packet[start + i]
71+
true
72+
73+
func isIetfInitial(packet: seq[byte]): bool {.raises: [].} =
74+
if packet.len == 0:
75+
return false
76+
(packet[0] and 0xC0'u8) == 0xC0'u8 and (packet[0] and 0x30'u8) == 0
77+
4278
proc receiveDatagram(
4379
endpoint: QuicEndpoint, data: seq[byte], local, remote: TransportAddress
4480
) {.raises: [].} =
4581
if endpoint.isNil or endpoint.stopped:
4682
return
4783

48-
# Endpoints can have both contexts; each engine needs to see the packet.
49-
if not endpoint.clientContext.isNil:
84+
let
85+
hasClientContext = not endpoint.clientContext.isNil
86+
hasServerContext = not endpoint.serverContext.isNil
87+
88+
var cid: CidKey
89+
if endpoint.packetDcid(data, cid):
90+
if hasClientContext and endpoint.clientContext.ownsCid(cid):
91+
trace "Routing datagram to client context", cid
92+
endpoint.clientContext.receive(Datagram(data: data), local, remote)
93+
return
94+
95+
if hasServerContext and endpoint.serverContext.ownsCid(cid):
96+
trace "Routing datagram to server context", cid
97+
endpoint.serverContext.receive(Datagram(data: data), local, remote)
98+
return
99+
100+
if hasClientContext and not hasServerContext:
50101
endpoint.clientContext.receive(Datagram(data: data), local, remote)
51-
if not endpoint.serverContext.isNil:
102+
return
103+
104+
if hasServerContext and not hasClientContext:
105+
endpoint.serverContext.receive(Datagram(data: data), local, remote)
106+
return
107+
108+
if hasServerContext and data.isIetfInitial():
109+
trace "Routing initial datagram with unknown CID to server context",
110+
bytes = data.len, local, remote
52111
endpoint.serverContext.receive(Datagram(data: data), local, remote)
112+
return
113+
114+
trace "Dropping datagram with unknown CID", bytes = data.len, local, remote
53115

54116
proc receiveFromUdp(
55117
endpoint: QuicEndpoint, udp: DatagramTransport, remote: TransportAddress

0 commit comments

Comments
 (0)