Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dtls connection using mbedtls #10

Merged
merged 46 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f1707f5
feat: dtls connection using mbedtls
lchenut Mar 8, 2024
db96253
Merge remote-tracking branch 'origin/master' into dtls-protocol
lchenut Apr 12, 2024
bef7f8e
refactor: change according to the stun protocol rework
lchenut Apr 12, 2024
a4e6ab9
Merge remote-tracking branch 'origin/master' into dtls-protocol
lchenut May 24, 2024
2da7046
chore: rename init proc into new
lchenut May 30, 2024
d1b9fda
docs: adds object field comments
lchenut May 31, 2024
6652be6
Merge branch 'master' into dtls-protocol
diegomrsantos Jun 17, 2024
66ca741
chore: split dtls.nim into two files & renaming
lchenut Jun 20, 2024
bf240b1
chore: remove useless code
lchenut Jun 21, 2024
66a2aa7
chore: remove TODOs as they were addressed with a Stun refactorization
lchenut Jun 21, 2024
24e42a9
fix: oversight on dtls.new
lchenut Jun 21, 2024
ba9f04a
feat: add dtls test
lchenut Jun 21, 2024
416ff7b
chore: added license & used pragma on testdtls
lchenut Jun 21, 2024
f8c1b2f
fix: remove usage of deprecated TrackerCounter
lchenut Jun 28, 2024
874cff7
fix: trackers counter
lchenut Jun 28, 2024
bf2e53c
Merge remote-tracking branch 'origin/master' into dtls-protocol
lchenut Jul 19, 2024
3eb4b23
fix:
lchenut Jul 19, 2024
0f144ce
chore: renaming test
lchenut Jul 31, 2024
33372bc
docs: update DtlsConn comment
lchenut Jul 31, 2024
59fd302
fix: remove code duplicate
lchenut Jul 31, 2024
ffa8a51
chore: update comment
lchenut Jul 31, 2024
d003d20
chore: remove duplication mbedtls initialization code in accept/conne…
lchenut Jul 31, 2024
a9ec658
feat: add exception management to dtls_transport
lchenut Aug 1, 2024
f49ecea
fix: check address family before handshake
lchenut Aug 2, 2024
afd80aa
fix: exhaustive case
lchenut Aug 2, 2024
45cc272
fix: do not create dtlsConn if the address family is not IP
lchenut Aug 2, 2024
7cf9423
chore: remove entropy from MbedTLSCtx
lchenut Aug 2, 2024
59f76a0
chore: remove asyncspawn of cleanupdtlsconn
lchenut Aug 7, 2024
c0769c0
chore: ctx is no longer public
lchenut Aug 7, 2024
6a894ac
test: add a test with more than 2 nodes
lchenut Aug 7, 2024
f8bb4b8
chore: started is now useful
lchenut Aug 7, 2024
d7a707c
chore: update Dtls.stop
lchenut Aug 7, 2024
df2737a
chore: removed unecessary todos
lchenut Aug 7, 2024
36700ef
docs: add comments on DtlsConn.read and getters
lchenut Aug 7, 2024
ac80c9c
feat: add tracker for dtls connection and transport
lchenut Aug 7, 2024
2c327c5
chore: privatize local and remote certificate
lchenut Aug 7, 2024
6975f76
style: use nph
lchenut Aug 9, 2024
c89590f
fix: remove laddr from dtls_conn (not used)
lchenut Aug 9, 2024
8f51516
style: sort imports
lchenut Aug 9, 2024
54f4523
chore: clean Dtls.stop
lchenut Aug 9, 2024
ab02a68
fix: remote address is no longer exposed
lchenut Aug 9, 2024
c5681ed
fix: raddr change oversight
lchenut Aug 9, 2024
a8692af
chore: change `verify` name
lchenut Aug 9, 2024
e056631
chore: changed `sendFuture: Future[void]` into `dataToSend: seq[byte]`
lchenut Aug 13, 2024
5d7b428
chore: avoid sequence copy
lchenut Aug 13, 2024
a90d85f
chore: change assert message
lchenut Aug 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
375 changes: 375 additions & 0 deletions webrtc/dtls/dtls.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
# Nim-WebRTC
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

import times, deques, tables, sequtils
import chronos, chronicles
import ./utils, ../errors,
../stun/[stun_connection, stun_transport]

import mbedtls/ssl
import mbedtls/ssl_cookie
import mbedtls/ssl_cache
import mbedtls/pk
import mbedtls/md
import mbedtls/entropy
import mbedtls/ctr_drbg
import mbedtls/rsa
import mbedtls/x509
import mbedtls/x509_crt
import mbedtls/bignum
import mbedtls/error
import mbedtls/net_sockets
import mbedtls/timing

logScope:
topics = "webrtc dtls"

# Implementation of a DTLS client and a DTLS Server by using the Mbed-TLS library.
# Multiple things here are unintuitive partly because of the callbacks
# used by Mbed-TLS and that those callbacks cannot be async.
#
# TODO:
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved
# - Check the viability of the add/pop first/last of the asyncqueue with the limit.
# There might be some errors (or crashes) with some edge cases with the no wait option
# - Not critical - Check how to make a better use of MBEDTLS_ERR_SSL_WANT_WRITE
# - Not critical - May be interesting to split Dtls and DtlsConn into two files
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved

# This limit is arbitrary, it could be interesting to make it configurable.
const PendingHandshakeLimit = 1024

# -- DtlsConn --
# A Dtls connection to a specific IP address recovered by the receiving part of
# the Udp "connection"

type
DtlsConn* = ref object
conn: StunConn # The wrapper protocol Stun Connection
laddr: TransportAddress # Local address
raddr*: TransportAddress # Remote address
dataRecv: seq[byte] # data received which will be read by SCTP
sendFuture: Future[void]
# This future is set by synchronous Mbed-TLS callbacks and waited, if set, once
# the synchronous functions ends

# Close connection management
closed: bool
closeEvent: AsyncEvent

# Local and Remote certificate, needed by wrapped protocol DataChannel
# and by libp2p
localCert: seq[byte]
remoteCert: seq[byte]

# Mbed-TLS contexts
ssl: mbedtls_ssl_context
config: mbedtls_ssl_config
cookie: mbedtls_ssl_cookie_ctx
cache: mbedtls_ssl_cache_context
timer: mbedtls_timing_delay_context

ctr_drbg: mbedtls_ctr_drbg_context
entropy: mbedtls_entropy_context
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved

proc new(T: type DtlsConn, conn: StunConn, laddr: TransportAddress): T =
## Initialize a Dtls Connection
##
var self = T(conn: conn, laddr: laddr)
self.raddr = conn.raddr
self.closed = false
self.closeEvent = newAsyncEvent()
return self

proc join*(self: DtlsConn) {.async: (raises: [CancelledError]).} =
## Wait for the Dtls Connection to be closed
##
await self.closeEvent.wait()

proc dtlsHandshake(
self: DtlsConn,
isServer: bool
) {.async: (raises: [CancelledError, WebRtcError].} =
var shouldRead = isServer
while self.ssl.private_state != MBEDTLS_SSL_HANDSHAKE_OVER:
if shouldRead:
if isServer:
case self.raddr.family
of AddressFamily.IPv4:
mb_ssl_set_client_transport_id(self.ssl, self.raddr.address_v4)
of AddressFamily.IPv6:
mb_ssl_set_client_transport_id(self.ssl, self.raddr.address_v6)
else:
raise newException(WebRtcError, "DTLS - Remote address isn't an IP address")
self.dataRecv = await self.conn.read()
self.sendFuture = nil
let res = mb_ssl_handshake_step(self.ssl)
if not self.sendFuture.isNil():
await self.sendFuture
shouldRead = false
if res == MBEDTLS_ERR_SSL_WANT_WRITE:
continue
elif res == MBEDTLS_ERR_SSL_WANT_READ:
shouldRead = true
continue
elif res == MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED:
mb_ssl_session_reset(self.ssl)
shouldRead = isServer
continue
elif res != 0:
raise newException(WebRtcError, "DTLS - " & $(res.mbedtls_high_level_strerr()))

proc close*(self: DtlsConn) {.async: (raises: [CancelledError]).} =
## Close a Dtls Connection
##
if self.closed:
debug "Try to close an already closed DtlsConn"
return

self.closed = true
self.sendFuture = nil
# TODO: proc mbedtls_ssl_close_notify => template mb_ssl_close_notify in nim-mbedtls
let x = mbedtls_ssl_close_notify(addr self.ssl)
if not self.sendFuture.isNil():
await self.sendFuture
self.closeEvent.fire()

proc write*(self: DtlsConn, msg: seq[byte]) {.async.} =
## Write a message using mbedtls_ssl_write
##
# Mbed-TLS will wrap the message properly and call `dtlsSend` callback.
# `dtlsSend` will write the message on the higher Stun connection.
if self.closed:
debug "Try to write on an already closed DtlsConn"
return
var buf = msg
try:
self.sendFuture = nil
let write = mb_ssl_write(self.ssl, buf)
if not self.sendFuture.isNil():
let sendFuture = self.sendFuture
await sendFuture
trace "Dtls write", msgLen = msg.len(), actuallyWrote = write
except MbedTLSError as exc:
trace "Dtls write error", errorMsg = exc.msg
raise exc

proc read*(self: DtlsConn): Future[seq[byte]] {.async.} =
if self.closed:
debug "Try to read on an already closed DtlsConn"
return
var res = newSeq[byte](8192)
while true:
self.dataRecv = await self.conn.read()
# TODO: Find a clear way to use the template `mb_ssl_read` without
# messing up things with exception
let length = mbedtls_ssl_read(addr self.ssl, cast[ptr byte](addr res[0]), res.len().uint)
if length == MBEDTLS_ERR_SSL_WANT_READ:
continue
if length < 0:
raise newException(WebRtcError, "DTLS - " & $(length.cint.mbedtls_high_level_strerr()))
res.setLen(length)
return res

# -- Dtls --

type
Dtls* = ref object of RootObj
connections: Table[TransportAddress, DtlsConn]
transport: Stun
laddr: TransportAddress
started: bool
ctr_drbg: mbedtls_ctr_drbg_context
entropy: mbedtls_entropy_context

serverPrivKey: mbedtls_pk_context
serverCert: mbedtls_x509_crt
localCert: seq[byte]

proc updateOrAdd(aq: AsyncQueue[(TransportAddress, seq[byte])],
raddr: TransportAddress, buf: seq[byte]) =
for kv in aq.mitems():
if kv[0] == raddr:
kv[1] = buf
return
aq.addLastNoWait((raddr, buf))

proc new*(T: type Dtls, transport: Stun, laddr: TransportAddress): T =
var self = T()

self.connections = initTable[TransportAddress, DtlsConn]()
self.conn = conn
self.laddr = laddr
self.started = true

mb_ctr_drbg_init(self.ctr_drbg)
mb_entropy_init(self.entropy)
mb_ctr_drbg_seed(self.ctr_drbg, mbedtls_entropy_func, self.entropy, nil, 0)

self.serverPrivKey = self.ctr_drbg.generateKey()
self.serverCert = self.ctr_drbg.generateCertificate(self.serverPrivKey)
self.localCert = newSeq[byte](self.serverCert.raw.len)
copyMem(addr self.localCert[0], self.serverCert.raw.p, self.serverCert.raw.len)

proc stop*(self: Dtls) {.async.} =
if not self.started:
warn "Already stopped"
return

await allFutures(toSeq(self.connections.values()).mapIt(it.close()))
self.started = false

# -- Remote / Local certificate getter --

proc remoteCertificate*(conn: DtlsConn): seq[byte] =
conn.remoteCert

proc localCertificate*(conn: DtlsConn): seq[byte] =
conn.localCert

proc localCertificate*(self: Dtls): seq[byte] =
self.localCert

# -- MbedTLS Callbacks --

proc verify(ctx: pointer, pcert: ptr mbedtls_x509_crt,
state: cint, pflags: ptr uint32): cint {.cdecl.} =
# verify is the procedure called by mbedtls when receiving the remote
# certificate. It's usually used to verify the validity of the certificate.
# We use this procedure to store the remote certificate as it's mandatory
# to have it for the Prologue of the Noise protocol, aswell as the localCertificate.
var self = cast[DtlsConn](ctx)
let cert = pcert[]

self.remoteCert = newSeq[byte](cert.raw.len)
copyMem(addr self.remoteCert[0], cert.raw.p, cert.raw.len)
return 0

proc dtlsSend(ctx: pointer, buf: ptr byte, len: uint): cint {.cdecl.} =
# dtlsSend is the procedure called by mbedtls when data needs to be sent.
# As the StunConn's write proc is asynchronous and dtlsSend cannot be async,
# we store the future of this write and await it after the end of the
# function (see write or dtlsHanshake for example).
var self = cast[DtlsConn](ctx)
var toWrite = newSeq[byte](len)
if len > 0:
copyMem(addr toWrite[0], buf, len)
trace "dtls send", len
self.sendFuture = self.conn.write(self.raddr, toWrite)
result = len.cint

proc dtlsRecv(ctx: pointer, buf: ptr byte, len: uint): cint {.cdecl.} =
# dtlsRecv is the procedure called by mbedtls when data needs to be received.
# As we cannot asynchronously await for data to be received, we use a data received
# queue. If this queue is empty, we return `MBEDTLS_ERR_SSL_WANT_READ` for us to await
# when the mbedtls proc resumed (see read or dtlsHandshake for example)
let self = cast[DtlsConn](ctx)
if self.dataRecv.len() == 0:
return MBEDTLS_ERR_SSL_WANT_READ

copyMem(buf, addr self.dataRecv[0], self.dataRecv.len())
result = self.dataRecv.len().cint
self.dataRecv = @[]
trace "dtls receive", len, result

# -- Dtls Accept / Connect procedures --

proc cleanupDtlsConn(self: Dtls, conn: DtlsConn) {.async.} =
# Waiting for a connection to be closed to remove it from the table
await conn.join()
self.connections.del(conn.raddr)

proc accept*(self: Dtls): Future[DtlsConn] {.async.} =
## Accept a Dtls Connection
##
var res = DtlsConn.new(await self.transport.accept(), self.laddr)

mb_ssl_init(res.ssl)
mb_ssl_config_init(res.config)
mb_ssl_cookie_init(res.cookie)
mb_ssl_cache_init(res.cache)

res.ctr_drbg = self.ctr_drbg
res.entropy = self.entropy

var pkey = self.serverPrivKey
var srvcert = self.serverCert
res.localCert = self.localCert

mb_ssl_config_defaults(
res.config,
MBEDTLS_SSL_IS_SERVER,
MBEDTLS_SSL_TRANSPORT_DATAGRAM,
MBEDTLS_SSL_PRESET_DEFAULT
)
mb_ssl_conf_rng(res.config, mbedtls_ctr_drbg_random, res.ctr_drbg)
mb_ssl_conf_read_timeout(res.config, 10000) # in milliseconds
mb_ssl_conf_ca_chain(res.config, srvcert.next, nil)
mb_ssl_conf_own_cert(res.config, srvcert, pkey)
mb_ssl_cookie_setup(res.cookie, mbedtls_ctr_drbg_random, res.ctr_drbg)
mb_ssl_conf_dtls_cookies(res.config, addr res.cookie)
mb_ssl_set_timer_cb(res.ssl, res.timer)
mb_ssl_setup(res.ssl, res.config)
mb_ssl_session_reset(res.ssl)
mb_ssl_set_verify(res.ssl, verify, res)
mb_ssl_conf_authmode(res.config, MBEDTLS_SSL_VERIFY_OPTIONAL)
mb_ssl_set_bio(res.ssl, cast[pointer](res), dtlsSend, dtlsRecv, nil)
while true:
try:
self.connections[res.raddr] = res
await res.dtlsHandshake(true)
asyncSpawn self.removeConnection(res)
break
except WebRtcError as exc:
trace "Handshake fails, try accept another connection",
remoteAddress = res.raddr, error = exc.msg
self.connections.del(res.raddr)
res.conn = await self.transport.accept()
return res

proc connect*(self: Dtls, raddr: TransportAddress): Future[DtlsConn] {.async.} =
## Connect to a remote address, creating a Dtls Connection
var res = DtlsConn.new(await self.transport.connect(raddr), self.laddr)

mb_ssl_init(res.ssl)
mb_ssl_config_init(res.config)

res.ctr_drbg = self.ctr_drbg
res.entropy = self.entropy

var pkey = res.ctr_drbg.generateKey()
var srvcert = res.ctr_drbg.generateCertificate(pkey)
res.localCert = newSeq[byte](srvcert.raw.len)
copyMem(addr res.localCert[0], srvcert.raw.p, srvcert.raw.len)

mb_ctr_drbg_init(res.ctr_drbg)
mb_entropy_init(res.entropy)
mb_ctr_drbg_seed(res.ctr_drbg, mbedtls_entropy_func, res.entropy, nil, 0)

mb_ssl_config_defaults(res.config,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_DATAGRAM,
MBEDTLS_SSL_PRESET_DEFAULT)
mb_ssl_conf_rng(res.config, mbedtls_ctr_drbg_random, res.ctr_drbg)
mb_ssl_conf_read_timeout(res.config, 10000) # in milliseconds
mb_ssl_conf_ca_chain(res.config, srvcert.next, nil)
mb_ssl_set_timer_cb(res.ssl, res.timer)
mb_ssl_setup(res.ssl, res.config)
mb_ssl_set_verify(res.ssl, verify, res)
mb_ssl_conf_authmode(res.config, MBEDTLS_SSL_VERIFY_OPTIONAL)
mb_ssl_set_bio(res.ssl, cast[pointer](res), dtlsSend, dtlsRecv, nil)

try:
self.connections[raddr] = res
await res.dtlsHandshake(false)
asyncSpawn self.removeConnection(res)
except WebRtcError as exc:
trace "Handshake fails", remoteAddress = raddr, error = exc.msg
self.connections.del(raddr)
raise exc

return res
Loading
Loading