tinydtls_sys/
lib.rs

1// SPDX-License-Identifier: EPL-1.0 OR BSD-3-CLAUSE
2/*
3 * lib.rs - Main library file for raw TinyDTLS Rust bindings.
4 * Copyright (c) 2021 The NAMIB Project Developers, all rights reserved.
5 * See the README as well as the LICENSE file for more information.
6 */
7
8// Bindgen translates the C headers, clippy's and rustfmt's recommendations are not applicable here.
9#![allow(clippy::all)]
10#![allow(non_camel_case_types)]
11#![allow(non_snake_case)]
12#![allow(non_upper_case_globals)]
13#![allow(deref_nullptr)]
14
15use std::fmt;
16
17use libc::{sockaddr, sockaddr_in, sockaddr_in6, sockaddr_storage, socklen_t};
18
19#[cfg(target_family = "windows")]
20include!(concat!(env!("OUT_DIR"), "\\bindings.rs"));
21#[cfg(not(target_family = "windows"))]
22include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
23
24// The dtls_set_handler function in tinydtls is an inline function, which is why it is not included
25// in the bindgen-generated bindings.
26// There is a workaround that could be applied to the vendored version (by setting a compiler flag
27// that forces inclusion of inline functions in generated binaries), but this would only work for
28// the vendored version and would also prevent inlining, reducing performance.
29// Therefore, we just re-implement it here.
30//
31// See https://rust-lang.github.io/rust-bindgen/faq.html#why-isnt-bindgen-generating-bindings-to-inline-functions
32#[inline]
33pub unsafe fn dtls_set_handler(ctx: *mut dtls_context_t, h: *mut dtls_handler_t) {
34    (*ctx).h = h;
35}
36
37// For backwards-compatibility, we add a Debug implementation of dtls_hello_verify_t.
38// (Automatic derive stopped working with https://github.com/rust-lang/rust/pull/104429.)
39impl fmt::Debug for dtls_hello_verify_t {
40    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
41        let Self { version, cookie_length, cookie } = self;
42        fmt.debug_struct("dtls_hello_verify_t")
43            .field("version", version)
44            .field("cookie_length", cookie_length)
45            .field("cookie", cookie)
46            .finish()
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use lazy_static::lazy_static;
54    use libc::{c_int, c_uchar, c_ushort, in6_addr, in_addr, size_t};
55    use std::collections::HashMap;
56    use std::ffi::c_void;
57    use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket};
58    use std::time::Duration;
59
60    const AF_INET: u16 = libc::AF_INET as u16;
61    const AF_INET6: u16 = libc::AF_INET6 as u16;
62
63    /// Message that the UDP echo server listens for (unencrypted) in order to determine whether it
64    /// should shut down.
65    const TERMINATE_SERVER_MESSAGE: &str = "TERMINATE SERVER";
66    /// Message that should be sent over the encrypted channel for the UDP echo client/server test
67    const ENC_MESSAGE: &str = "Encrypted Example Message";
68    /// Name of the key used for the UDP echo client/server test.
69    #[cfg(feature = "psk")]
70    const PSK_IDENTITY: &str = "testkey";
71
72    #[cfg(feature = "psk")]
73    lazy_static! {
74        /// Map for DTLS keys used in tests.
75        static ref DTLS_KEYS: HashMap<&'static str, [u8; 16]> = {
76            let mut map: HashMap<&'static str, [u8; 16]> = HashMap::new();
77            map.insert(
78                PSK_IDENTITY,
79                [
80                    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
81                ],
82            );
83            map
84        };
85    }
86
87    struct EchoTestClientState {
88        socket: UdpSocket,
89        finished: bool,
90    }
91
92    /// Converts a session pointer into a SocketAddr instance.
93    ///
94    /// # Safety
95    /// The session is assumed to be a valid pointer to a session_t struct, whose addr-field
96    /// contains a valid sockaddr value.
97    unsafe fn session_to_socketaddr(session: *mut session_t) -> SocketAddr {
98        let raw_target_addr = &session.as_ref().unwrap().addr;
99
100        // SAFETY: Value is always some kind of sockaddr, so sa_family will be set.
101        let family = raw_target_addr.sa.as_ref().sa_family;
102
103        match family {
104            AF_INET => {
105                // SAFETY: We checked the value of sa_family to ensure that this value should actually be a sockaddr_in
106                let raw_target_addr = raw_target_addr.sin.as_ref();
107                // Sockaddr fields are in network byte order, so calling to_ne_bytes() will give us the value in network byte order, no matter the system endianness.
108                let target_addr = Ipv4Addr::from(raw_target_addr.sin_addr.s_addr.to_ne_bytes());
109                SocketAddr::V4(SocketAddrV4::new(target_addr, u16::from_be(raw_target_addr.sin_port)))
110            }
111            AF_INET6 => {
112                // SAFETY: We checked the value of sa_family to ensure that this value should actually be a sockaddr_in6
113                let raw_target_addr = raw_target_addr.sin6.as_ref();
114                // Sockaddr fields are in network byte order, so calling to_ne_bytes() will give us the value in network byte order, no matter the system endianness.
115                let target_addr = Ipv6Addr::from(raw_target_addr.sin6_addr.s6_addr);
116                SocketAddr::V6(SocketAddrV6::new(
117                    target_addr,
118                    u16::from_be(raw_target_addr.sin6_port),
119                    raw_target_addr.sin6_flowinfo,
120                    raw_target_addr.sin6_scope_id,
121                ))
122            }
123            // session.addr value is either sockaddr_in or sockaddr_in6, so sa_family has to either be AF_INET or AF_INET6
124            _ => panic!("Invalid session address family"),
125        }
126    }
127
128    /// Converts a SocketAddr instance into a freshly allocated session_t struct and returns a
129    /// pointer to it.
130    ///
131    /// NOTE: The session_t is created using dtls_new_session() and has to be freed manually using
132    /// dtls_free_session().
133    /// This function returns the return value of dtls_new_session directly, therefore the pointer
134    /// can be a null pointer(!).
135    fn session_from_socketaddr(addr: &SocketAddr) -> *mut session_t {
136        match addr {
137            SocketAddr::V4(addr) => {
138                let mut raw_addr = sockaddr_in {
139                    sin_family: AF_INET,
140                    sin_port: addr.port().to_be(),
141                    sin_addr: in_addr {
142                        s_addr: u32::from_ne_bytes(addr.ip().octets()),
143                    },
144                    sin_zero: [0; 8],
145                };
146                // SAFETY: All pointers are valid, the supplied size is the size of the supplied struct.
147                unsafe {
148                    dtls_new_session(
149                        &mut raw_addr as *mut sockaddr_in as *mut sockaddr,
150                        std::mem::size_of::<sockaddr_in>() as socklen_t,
151                    )
152                }
153            }
154            SocketAddr::V6(addr) => {
155                let mut raw_addr = sockaddr_in6 {
156                    sin6_family: AF_INET6,
157                    sin6_port: addr.port().to_be(),
158                    sin6_flowinfo: addr.flowinfo(),
159                    sin6_addr: in6_addr {
160                        s6_addr: addr.ip().octets(),
161                    },
162                    sin6_scope_id: addr.scope_id(),
163                };
164                // SAFETY: All pointers are valid, the supplied size is the size of the supplied struct.
165                unsafe {
166                    dtls_new_session(
167                        &mut raw_addr as *mut sockaddr_in6 as *mut sockaddr,
168                        std::mem::size_of::<sockaddr_in6>() as socklen_t,
169                    )
170                }
171            }
172        }
173    }
174
175    /// Send callback for the UDP Echo Server Test (server side)
176    ///
177    /// # Safety
178    /// This function is intended to be set as the write/send callback for a dtls context, and
179    /// therefore expects the function arguments to match the values that tinydtls would set.
180    /// It assumes that all supplied pointers are valid, that `data` points to a memory area of at
181    /// least size `len`, and that the [app](dtls_context_t.app) field in the ctx argument is set to
182    /// a *mut UdpSocket of the socket that should be used for the server.
183    unsafe extern "C" fn echo_server_send_callback(
184        ctx: *mut dtls_context_t,
185        session: *mut session_t,
186        data: *mut uint8,
187        len: size_t,
188    ) -> i32 {
189        // SAFETY: Pointers are assumed to be valid, and data should point to a memory area of at least size len.
190        let data = std::slice::from_raw_parts(data, len);
191        let target_addr = session_to_socketaddr(session);
192
193        println!("[ECHO SEND] sending {} bytes to {}: {:?}", len, &target_addr, data);
194
195        // SAFETY: app_data is assumed to be set to a pointer to Rc<UdpSocket> by us on context creation.
196        let socket = (ctx.as_ref().unwrap().app as *mut UdpSocket).as_ref().unwrap();
197        match socket.send_to(data, target_addr) {
198            Ok(num_bytes) => num_bytes as i32,
199            Err(e) => e.raw_os_error().unwrap_or(-1),
200        }
201    }
202
203    /// Send callback for the UDP Echo Server Test (client side)
204    ///
205    /// # Safety
206    /// This function is intended to be set as the write/send callback for a dtls context, and
207    /// therefore expects the function arguments to match the values that tinydtls would set.
208    /// It assumes that all supplied pointers are valid, that `data` points to a memory area of at
209    /// least size `len`, and that the [app](dtls_context_t.app) field in the ctx argument is set to
210    /// a *mut EchoTestClientState representing the echo test client.
211    unsafe extern "C" fn echo_client_send_callback(
212        ctx: *mut dtls_context_t,
213        session: *mut session_t,
214        data: *mut uint8,
215        len: size_t,
216    ) -> i32 {
217        let data = std::slice::from_raw_parts(data, len);
218        let target_addr = session_to_socketaddr(session);
219
220        println!("[ECHO SEND] sending {} bytes to {}: {:?}", len, &target_addr, data);
221
222        let socket = &(ctx.as_ref().unwrap().app as *mut EchoTestClientState)
223            .as_ref()
224            .unwrap()
225            .socket;
226        match socket.send_to(data, target_addr) {
227            Ok(num_bytes) => num_bytes as i32,
228            Err(e) => e.raw_os_error().unwrap_or(-1),
229        }
230    }
231
232    /// PSK information callback for the UDP Echo Server Test
233    ///
234    /// # Safety
235    /// This function is intended to be set as the get_psk_info callback for a dtls context, and
236    /// therefore expects the function arguments to match the values that tinydtls would set.
237    /// It assumes that all supplied pointers are valid and that `desc`/`result` point to memory
238    /// areas of at least size `desc_len`/`result_length`.
239    #[cfg(feature = "psk")]
240    unsafe extern "C" fn echo_get_psk_info(
241        _ctx: *mut dtls_context_t,
242        _session: *const session_t,
243        type_: dtls_credentials_type_t,
244        desc: *const c_uchar,
245        desc_len: size_t,
246        result: *mut c_uchar,
247        result_length: size_t,
248    ) -> i32 {
249        let result = std::slice::from_raw_parts_mut(result, result_length);
250        let desc = std::slice::from_raw_parts(desc, desc_len);
251        match type_ {
252            dtls_credentials_type_t::DTLS_PSK_HINT | dtls_credentials_type_t::DTLS_PSK_IDENTITY => {
253                if result_length < PSK_IDENTITY.len() {
254                    panic!("Result field too small to provide PSK identity/hint value")
255                }
256                result[..PSK_IDENTITY.len()].clone_from_slice(&PSK_IDENTITY.as_bytes());
257                PSK_IDENTITY.len() as i32
258            }
259            dtls_credentials_type_t::DTLS_PSK_KEY => {
260                if result_length < DTLS_KEYS.get(PSK_IDENTITY).unwrap().len() {
261                    panic!("Result field too small to provide PSK key value")
262                }
263                result[..DTLS_KEYS
264                    .get(std::str::from_utf8(desc).expect("Invalid PSK Identity"))
265                    .unwrap()
266                    .len()]
267                    .clone_from_slice(
268                        DTLS_KEYS
269                            .get(std::str::from_utf8(desc).expect("Invalid PSK Identity"))
270                            .unwrap(),
271                    );
272                DTLS_KEYS
273                    .get(std::str::from_utf8(desc).expect("Invalid PSK Identity"))
274                    .unwrap()
275                    .len() as i32
276            }
277        }
278    }
279
280    /// Read callback for the UDP Echo Server Test (server-side).
281    ///
282    /// # Safety
283    /// This function is intended to be set as the read callback for a dtls context, and
284    /// therefore expects the function arguments to match the values that tinydtls would set.
285    /// It assumes that all supplied pointers are valid and that `data` points to a memory area of at
286    /// least size `len` containing the received and decrypted data.
287    unsafe extern "C" fn echo_server_read_callback(
288        ctx: *mut dtls_context_t,
289        session: *mut session_t,
290        data: *mut uint8,
291        len: size_t,
292    ) -> i32 {
293        println!(
294            "[ECHO SERVER] received {} bytes from {}: {:?}",
295            len,
296            session_to_socketaddr(session),
297            data
298        );
299        dtls_write(ctx, session, data, len)
300    }
301
302    /// Read callback for the UDP Echo Server Test (client-side).
303    ///
304    /// # Safety
305    /// This function is intended to be set as the read callback for a dtls context, and
306    /// therefore expects the function arguments to match the values that tinydtls would set.
307    /// It assumes that all supplied pointers are valid, that `data` points to a memory area of at
308    /// least size `len` containing the received and decrypted data, and that the [app](dtls_context_t.app)
309    /// field in the ctx argument is set to a *mut EchoTestClientState representing the echo test
310    /// client.
311    unsafe extern "C" fn echo_client_read_callback(
312        ctx: *mut dtls_context_t,
313        session: *mut session_t,
314        data: *mut uint8,
315        len: size_t,
316    ) -> i32 {
317        let data = std::slice::from_raw_parts(data, len);
318        println!(
319            "[ECHO CLIENT] received {} bytes from {}: {:?}",
320            len,
321            session_to_socketaddr(session),
322            data
323        );
324        assert_eq!(data, ENC_MESSAGE.as_bytes());
325
326        (ctx.as_ref().unwrap().app as *mut EchoTestClientState)
327            .as_mut()
328            .unwrap()
329            .finished = true;
330        let socket = &(ctx.as_ref().unwrap().app as *mut EchoTestClientState)
331            .as_ref()
332            .unwrap()
333            .socket;
334        match socket.send_to(TERMINATE_SERVER_MESSAGE.as_bytes(), session_to_socketaddr(session)) {
335            Ok(count) => count as i32,
336            Err(e) => e.raw_os_error().unwrap_or(-1),
337        }
338    }
339
340    /// Event callback for the UDP Echo Server Test (client-side)
341    ///
342    /// # Safety
343    /// This function is intended to be set as the event callback for a dtls context, and
344    /// therefore expects the function arguments to match the values that tinydtls would set.
345    /// It assumes that all supplied pointers are valid.
346    unsafe extern "C" fn echo_client_event_callback(
347        ctx: *mut dtls_context_t,
348        session: *mut session_t,
349        level: dtls_alert_level_t,
350        code: c_ushort,
351    ) -> i32 {
352        println!(
353            "[ECHO CLIENT] received Event from {} (level {:?}): {}",
354            session_to_socketaddr(session),
355            level,
356            code
357        );
358        if level == dtls_alert_level_t::DTLS_ALERT_LEVEL_FATAL && u32::from(code) != DTLS_EVENT_CONNECTED {
359            panic!("Fatal error in DTLS session")
360        }
361        match code as u32 {
362            DTLS_EVENT_CONNECTED => {
363                let mut buf = [0; ENC_MESSAGE.len()];
364                buf.clone_from_slice(ENC_MESSAGE.as_bytes());
365                dtls_write(ctx, session, buf.as_mut_ptr(), buf.len())
366            }
367            _ => 0,
368        }
369    }
370
371    /// Event callback for the UDP Echo Server Test (server-side)
372    ///
373    /// # Safety
374    /// This function is intended to be set as the event callback for a dtls context, and
375    /// therefore expects the function arguments to match the values that tinydtls would set.
376    /// It assumes that all supplied pointers are valid.
377    unsafe extern "C" fn echo_server_event_callback(
378        _ctx: *mut dtls_context_t,
379        session: *mut session_t,
380        level: dtls_alert_level_t,
381        code: c_ushort,
382    ) -> i32 {
383        println!(
384            "[ECHO SERVER] received Event from {} (level {:?}): {}",
385            session_to_socketaddr(session),
386            level,
387            code
388        );
389        if level == dtls_alert_level_t::DTLS_ALERT_LEVEL_FATAL && u32::from(code) != DTLS_EVENT_CONNECTED {
390            panic!("Fatal error in DTLS session")
391        }
392        0
393    }
394
395    /// Run the UDP DTLS echo server used for the [test_dtls_echo_client_server()] test.
396    #[cfg(feature = "psk")]
397    fn run_dtls_echo_server(mut socket: UdpSocket) {
398        let mut dtls_handlers = dtls_handler_t {
399            write: Some(echo_server_send_callback),
400            read: Some(echo_server_read_callback),
401            event: Some(echo_server_event_callback),
402            get_psk_info: Some(echo_get_psk_info),
403            get_ecdsa_key: None,
404            verify_ecdsa_key: None,
405            get_user_parameters: None,
406        };
407
408        // SAFETY: Supplied pointer is valid, dtls_new_context does not do anything with it except
409        // storing it in its app field.
410        let server_context = unsafe { dtls_new_context(&mut socket as *mut UdpSocket as *mut c_void) };
411        assert!(!server_context.is_null());
412        // SAFETY: Supplied pointers are valid (we just checked server_context, and dtls_handlers is a reference, so it must be valid).
413        unsafe { dtls_set_handler(server_context, &mut dtls_handlers) };
414
415        let mut buf: [u8; 512] = [0; 512];
416        loop {
417            let (read_bytes, peer) = socket.recv_from(&mut buf).expect("Error reading from socket");
418            if read_bytes == TERMINATE_SERVER_MESSAGE.len()
419                && &buf[0..read_bytes] == TERMINATE_SERVER_MESSAGE.as_bytes()
420            {
421                break;
422            }
423            let session = session_from_socketaddr(&peer);
424            assert!(!session.is_null());
425            // SAFETY: server_context has already been checked and is not invalidated by any called methods up to this point.
426            // We just checked that session is not null. msg and msglen are set correctly to our buffer and the length of the read data.
427            // dtls_handle_message() does not modify the session, neither do our handlers, therefore the call to dtls_free_session is valid.
428            unsafe {
429                dtls_handle_message(server_context, session, buf.as_mut_ptr(), read_bytes as c_int);
430                dtls_free_session(session);
431            }
432        }
433        // SAFETY: We have not called anything that would invalidate our context up to this point, so
434        // this pointer should be valid up until here.
435        unsafe {
436            dtls_free_context(server_context);
437        }
438    }
439
440    /// Test case that creates a basic UDP echo server over an encrypted DTLS socket and then sends
441    /// a message to it.
442    /// Based on the example described on the main page of the tinydtls documentation
443    /// (https://github.com/obgm/tinydtls/blob/develop/dtls.h#L416)
444    #[test]
445    #[cfg(feature = "psk")]
446    fn test_dtls_echo_client_server() {
447        // Binding to port 0 gives us any available free port.
448        let server_socket = UdpSocket::bind("localhost:0").expect("Could not bind UDP socket");
449        server_socket
450            .set_read_timeout(Some(Duration::from_secs(10)))
451            .expect("Could not set socket timeout");
452        server_socket
453            .set_write_timeout(Some(Duration::from_secs(10)))
454            .expect("Could not set socket timeout");
455        let server_addr = server_socket.local_addr().unwrap();
456        let server_thread = std::thread::spawn(move || run_dtls_echo_server(server_socket));
457
458        let mut dtls_handlers = dtls_handler_t {
459            write: Some(echo_client_send_callback),
460            read: Some(echo_client_read_callback),
461            event: Some(echo_client_event_callback),
462            get_psk_info: Some(echo_get_psk_info),
463            get_ecdsa_key: None,
464            verify_ecdsa_key: None,
465            get_user_parameters: None,
466        };
467
468        let client_socket = UdpSocket::bind("localhost:0").expect("Could not bind UDP socket");
469        client_socket
470            .set_read_timeout(Some(Duration::from_secs(10)))
471            .expect("Could not set socket timeout");
472        client_socket
473            .set_write_timeout(Some(Duration::from_secs(10)))
474            .expect("Could not set socket timeout");
475        let mut client_state = EchoTestClientState {
476            socket: client_socket,
477            finished: false,
478        };
479        // SAFETY: Supplied pointer is valid, dtls_new_context does not do anything with it except
480        // storing it in its app field.
481        let client_context = unsafe { dtls_new_context(&mut client_state as *mut EchoTestClientState as *mut c_void) };
482        assert!(!client_context.is_null());
483        // SAFETY: Supplied pointers are valid (we just checked client_context, and dtls_handlers is a reference, so it must be valid).
484        unsafe {
485            dtls_set_handler(client_context, &mut dtls_handlers);
486        };
487        let session = session_from_socketaddr(&server_addr);
488        assert!(!session.is_null());
489        // SAFETY: We just checked that session is not a null pointer, and we checked client_context before.
490        // dtls_set_handler also does not do anything to invalidate client_context.
491        unsafe {
492            dtls_connect(client_context, session);
493        }
494
495        let mut buf: [u8; 512] = [0; 512];
496        while !client_state.finished {
497            let (read_bytes, peer) = client_state
498                .socket
499                .recv_from(&mut buf)
500                .expect("Error reading from socket");
501            let session = session_from_socketaddr(&peer);
502            assert!(!session.is_null());
503            // SAFETY: client_context has already been checked and is not invalidated by any called methods up to this point.
504            // We just checked that session is not null. msg and msglen are set correctly to our buffer and the length of the read data.
505            // dtls_handle_message() does not modify the session, neither do our handlers, therefore the call to dtls_free_session is valid.
506            unsafe {
507                dtls_handle_message(client_context, session, buf.as_mut_ptr(), read_bytes as c_int);
508                dtls_free_session(session);
509            }
510        }
511        // SAFETY: We have not called anything that would invalidate our context up to this point, so
512        // this pointer should be valid up until here.
513        unsafe {
514            dtls_free_context(client_context);
515        }
516        server_thread.join().unwrap();
517    }
518}