Skip to content

Commit 1e55e2d

Browse files
committed
feat: upgrade dimpl to 0.4 and add STUN server interop testing
- Upgrade dimpl from 0.3 to 0.4 to access public protocol_version() API - Replace Debug-based protocol version detection hack with proper API call - Add dtls_scan.sh script for scanning public STUN servers' DTLS support - Add StunServerInteropTests for real-world DTLS handshake validation
1 parent 7cf6f3c commit 1e55e2d

5 files changed

Lines changed: 279 additions & 22 deletions

File tree

native/Cargo.lock

Lines changed: 16 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

native/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repository = "https://github.com/HMBSbige/DTLS"
99
crate-type = ["cdylib"]
1010

1111
[dependencies]
12-
dimpl = { version = "0.3" }
12+
dimpl = { version = "0.4" }
1313

1414
[profile.release]
1515
lto = true

native/src/dtls/ffi.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,11 @@ fn write_outgoing_to_buffer(s: &mut DtlsSession, buf: &mut [u8]) -> Result<usize
103103
Ok(offset)
104104
}
105105

106-
// HACK: remove when dimpl exposes protocol_version()
107-
// dimpl 0.3.0 does not provide a public API to query the negotiated protocol
108-
// version. We infer it from the Debug output which contains the Inner enum
109-
// variant name (e.g. "Client12", "Server12", "Client13", "Server13").
110-
fn detect_protocol_version(dtls: &dimpl::Dtls) -> u16 {
111-
let dbg = format!("{:?}", dtls);
112-
if dbg.contains("Client12") || dbg.contains("Server12") {
113-
0x0303
114-
} else if dbg.contains("Client13") || dbg.contains("Server13") {
115-
0x0304
116-
} else {
117-
panic!("detect_protocol_version called in unexpected state: {dbg}");
106+
fn protocol_version_to_u16(ver: dimpl::ProtocolVersion) -> u16 {
107+
match ver {
108+
dimpl::ProtocolVersion::DTLS1_2 => 0x0303,
109+
dimpl::ProtocolVersion::DTLS1_3 => 0x0304,
110+
_ => 0,
118111
}
119112
}
120113

@@ -133,8 +126,10 @@ fn drain_output(s: &mut DtlsSession) -> Result<(), dimpl::Error> {
133126
dimpl::Output::Packet(data) => s.outgoing_pkts.push_back(data.to_vec()),
134127
dimpl::Output::Connected => {
135128
s.handshake_complete = true;
136-
if s.protocol_version == 0 {
137-
s.protocol_version = detect_protocol_version(&s.dtls);
129+
if s.protocol_version == 0
130+
&& let Some(ver) = s.dtls.protocol_version()
131+
{
132+
s.protocol_version = protocol_version_to_u16(ver);
138133
}
139134
}
140135
dimpl::Output::ApplicationData(data) => s.app_data.push_back(data.to_vec()),
@@ -154,6 +149,7 @@ fn drain_output(s: &mut DtlsSession) -> Result<(), dimpl::Error> {
154149
break;
155150
}
156151
dimpl::Output::KeyingMaterial(..) => {}
152+
_ => {}
157153
}
158154
}
159155
Ok(())

scripts/dtls_scan.sh

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/bin/bash
2+
# DTLS STUN server scanner using wolfSSL
3+
#
4+
# - Fetches public STUN server list
5+
# - Tests DTLS 1.2 (each ECDHE-ECDSA-* cipher individually) and DTLS 1.3
6+
# - All cipher probes + cert verification run in parallel
7+
# - Certificate trust verified via wolfSSL (no openssl dependency)
8+
#
9+
# Requires: WOLFSSL_HOME env var
10+
11+
set -euo pipefail
12+
13+
HOST_LIST_URL="https://raw.githubusercontent.com/pradt2/always-online-stun/refs/heads/master/valid_hosts.txt"
14+
PORT=5349
15+
TIMEOUT=6
16+
ATTEMPTS=3
17+
PARALLEL=20
18+
19+
ECDSA_CIPHERS_12=(
20+
ECDHE-ECDSA-AES128-GCM-SHA256
21+
ECDHE-ECDSA-AES256-GCM-SHA384
22+
ECDHE-ECDSA-CHACHA20-POLY1305
23+
ECDHE-ECDSA-AES128-SHA256
24+
ECDHE-ECDSA-AES256-SHA384
25+
ECDHE-ECDSA-AES128-SHA
26+
ECDHE-ECDSA-AES256-SHA
27+
)
28+
ALL_ECDSA_12=$(IFS=:; echo "${ECDSA_CIPHERS_12[*]}")
29+
30+
# ── Detect system CA bundle ──────────────────────────────
31+
32+
CA_BUNDLE=""
33+
for p in /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-bundle.crt \
34+
/etc/pki/tls/certs/ca-bundle.crt /etc/ssl/cert.pem; do
35+
[[ -f "$p" ]] && CA_BUNDLE="$p" && break
36+
done
37+
38+
# ── Resolve wolfSSL client ───────────────────────────────
39+
40+
CLIENT="$WOLFSSL_HOME/build/examples/client/client"
41+
42+
# ── Self-dispatch: --scan-host <host> <result_dir> ───────
43+
44+
if [[ "${1:-}" == "--scan-host" ]]; then
45+
host=$2
46+
result_dir=$3
47+
CLIENT="$WOLFSSL_HOME/build/examples/client/client"
48+
49+
# Step 1: quick gate — can this host do DTLS at all?
50+
probe_out=$(cd "$WOLFSSL_HOME" && timeout "$TIMEOUT" "$CLIENT" \
51+
-u -v 3 -h "$host" -p "$PORT" -d -x -S "$host" \
52+
-l "$ALL_ECDSA_12" 2>&1) || true
53+
if ! echo "$probe_out" | grep -q "SSL version is DTLSv1.2"; then
54+
probe_out=$(cd "$WOLFSSL_HOME" && timeout "$TIMEOUT" "$CLIENT" \
55+
-u -v 4 -h "$host" -p "$PORT" -d -x -S "$host" 2>&1) || true
56+
if ! echo "$probe_out" | grep -q "SSL version is DTLSv1.3"; then
57+
echo "FAIL $host:$PORT"
58+
exit 0
59+
fi
60+
fi
61+
62+
# Step 2: parallel probes — all ciphers + DTLS 1.3 + cert verification
63+
tmpdir=$(mktemp -d)
64+
trap 'rm -rf "$tmpdir"' EXIT
65+
66+
# DTLS 1.2: each cipher in parallel
67+
for cipher in "${ECDSA_CIPHERS_12[@]}"; do
68+
(
69+
for _ in $(seq 1 "$ATTEMPTS"); do
70+
out=$(cd "$WOLFSSL_HOME" && timeout "$TIMEOUT" "$CLIENT" \
71+
-u -v 3 -h "$host" -p "$PORT" -d -x -S "$host" \
72+
-l "$cipher" 2>&1) || true
73+
if echo "$out" | grep -q "SSL version is DTLSv1.2"; then
74+
echo "$cipher" > "$tmpdir/c12_$cipher"
75+
break
76+
fi
77+
done
78+
) &
79+
done
80+
81+
# DTLS 1.3 probe
82+
(
83+
for _ in $(seq 1 "$ATTEMPTS"); do
84+
out=$(cd "$WOLFSSL_HOME" && timeout "$TIMEOUT" "$CLIENT" \
85+
-u -v 4 -h "$host" -p "$PORT" -d -x -S "$host" 2>&1) || true
86+
if echo "$out" | grep -q "SSL version is DTLSv1.3"; then
87+
echo "$out" | grep "SSL cipher suite is" | sed 's/.*SSL cipher suite is //' \
88+
> "$tmpdir/dtls13"
89+
break
90+
fi
91+
done
92+
) &
93+
94+
# Certificate verification via wolfSSL (no -d flag, use -A for CA bundle)
95+
if [[ -n "$CA_BUNDLE" ]]; then
96+
(
97+
out=$(cd "$WOLFSSL_HOME" && timeout "$TIMEOUT" "$CLIENT" \
98+
-u -v 3 -h "$host" -p "$PORT" -x -S "$host" \
99+
-A "$CA_BUNDLE" -l "$ALL_ECDSA_12" 2>&1)
100+
# shellcheck disable=SC2181
101+
if [[ $? -eq 0 ]] && echo "$out" | grep -q "SSL version is DTLSv1"; then
102+
echo "TRUSTED" > "$tmpdir/cert_status"
103+
else
104+
echo "UNTRUSTED" > "$tmpdir/cert_status"
105+
fi
106+
) &
107+
fi
108+
109+
wait
110+
111+
# Step 3: collect results
112+
supported_12=()
113+
for cipher in "${ECDSA_CIPHERS_12[@]}"; do
114+
[[ -f "$tmpdir/c12_$cipher" ]] && supported_12+=("$cipher")
115+
done
116+
117+
dtls13_cipher=""
118+
[[ -f "$tmpdir/dtls13" ]] && dtls13_cipher=$(cat "$tmpdir/dtls13")
119+
120+
cert_status="N/A"
121+
[[ -f "$tmpdir/cert_status" ]] && cert_status=$(cat "$tmpdir/cert_status")
122+
123+
# Write structured result file
124+
{
125+
echo "cert=$cert_status"
126+
if [[ -n "$dtls13_cipher" ]]; then
127+
echo "1.3=$dtls13_cipher"
128+
fi
129+
for c in "${supported_12[@]}"; do
130+
echo "1.2=$c"
131+
done
132+
} > "$result_dir/$host"
133+
echo "OK $host:$PORT cert=$cert_status 1.2=${#supported_12[@]} ciphers 1.3=${dtls13_cipher:-none}"
134+
exit 0
135+
fi
136+
137+
# ── Main ─────────────────────────────────────────────────
138+
139+
if [[ -z "${WOLFSSL_HOME:-}" ]]; then
140+
echo "ERROR: WOLFSSL_HOME is not set"
141+
echo "Usage: WOLFSSL_HOME=/path/to/wolfssl bash $0"
142+
exit 1
143+
fi
144+
145+
CLIENT="$WOLFSSL_HOME/build/examples/client/client"
146+
if ! command -v "$CLIENT" &>/dev/null && [[ ! -x "$CLIENT" ]]; then
147+
echo "ERROR: wolfSSL client binary not found at $CLIENT"
148+
echo "Build: cd \$WOLFSSL_HOME && cmake -B build -DWOLFSSL_DTLS=yes -DWOLFSSL_DTLS13=yes && cmake --build build"
149+
exit 1
150+
fi
151+
152+
if [[ -n "$CA_BUNDLE" ]]; then
153+
echo "CA bundle: $CA_BUNDLE"
154+
else
155+
echo "WARNING: No system CA bundle found — certificate verification will show N/A"
156+
fi
157+
158+
echo "Fetching STUN server list..."
159+
HOSTS=$(curl -sfL "$HOST_LIST_URL" | sed 's/:.*//')
160+
HOST_COUNT=$(echo "$HOSTS" | wc -l | tr -d ' ')
161+
echo "Got $HOST_COUNT hosts. Scanning port $PORT (ECDHE-ECDSA, ${ATTEMPTS} attempts, ${PARALLEL} parallel)..."
162+
echo ""
163+
164+
RESULT_DIR=$(mktemp -d)
165+
trap 'rm -rf "$RESULT_DIR"' EXIT
166+
167+
echo "$HOSTS" | xargs -P"$PARALLEL" -I{} bash "$0" --scan-host {} "$RESULT_DIR"
168+
169+
# ── Display results ──────────────────────────────────────
170+
171+
echo ""
172+
echo "================================================================================"
173+
echo " DTLS Scan Results — ECDHE-ECDSA, port $PORT, wolfSSL"
174+
echo "================================================================================"
175+
echo ""
176+
177+
OK_COUNT=0
178+
if ls "$RESULT_DIR"/* 1>/dev/null 2>&1; then
179+
for f in $(ls "$RESULT_DIR"/* 2>/dev/null | sort); do
180+
host=$(basename "$f")
181+
cert_status="" dtls13="" dtls12_ciphers=()
182+
while IFS='=' read -r key val; do
183+
case "$key" in
184+
cert) cert_status="$val" ;;
185+
1.3) dtls13="$val" ;;
186+
1.2) dtls12_ciphers+=("$val") ;;
187+
esac
188+
done < "$f"
189+
190+
echo " $host:$PORT"
191+
echo " Cert: $cert_status"
192+
if [[ ${#dtls12_ciphers[@]} -gt 0 ]]; then
193+
echo " DTLS 1.2: ${#dtls12_ciphers[@]} ciphers"
194+
for c in "${dtls12_ciphers[@]}"; do
195+
echo " - $c"
196+
done
197+
else
198+
echo " DTLS 1.2: not supported"
199+
fi
200+
if [[ -n "$dtls13" ]]; then
201+
echo " DTLS 1.3: $dtls13"
202+
else
203+
echo " DTLS 1.3: not supported"
204+
fi
205+
echo ""
206+
OK_COUNT=$((OK_COUNT + 1))
207+
done
208+
fi
209+
210+
echo "Total: $OK_COUNT / $HOST_COUNT servers support DTLS with ECDHE-ECDSA on port $PORT"
211+
echo ""
212+
echo "CERT: TRUSTED = verified against system CA, UNTRUSTED = self-signed/unknown, N/A = no CA bundle"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using DTLS.Dtls;
2+
using System.Net;
3+
using System.Net.Sockets;
4+
using System.Security.Authentication;
5+
6+
namespace DTLS.Tests;
7+
8+
public class StunServerInteropTests
9+
{
10+
private const int StunPort = 5349;
11+
12+
[Theory]
13+
[InlineData("stun.wirecloud.de")]
14+
[InlineData("stun.hot-chilli.net")]
15+
public async Task Client_DtlsHandshake_WithStunServer(string stunHost)
16+
{
17+
Assert.SkipUnless
18+
(
19+
!string.Equals(Environment.GetEnvironmentVariable("CI"), "true", StringComparison.OrdinalIgnoreCase),
20+
"STUN interop tests are skipped when CI=true."
21+
);
22+
23+
IPAddress[] addresses = await Dns.GetHostAddressesAsync(stunHost, AddressFamily.InterNetwork, TestContext.Current.CancellationToken);
24+
25+
using UdpClient udp = new();
26+
UdpDatagramTransport transport = new(udp, new IPEndPoint(addresses.First(), StunPort));
27+
28+
await using DtlsTransport client = await DtlsTransport.CreateClientAsync
29+
(
30+
transport,
31+
new DtlsClientOptions { ServerName = stunHost }
32+
);
33+
34+
await client.HandshakeAsync(TestContext.Current.CancellationToken);
35+
36+
Assert.False(client.Session.IsHandshaking);
37+
Assert.True(client.Session.Protocol is SslProtocols.Tls12 or SslProtocols.Tls13);
38+
Assert.NotNull(client.Session.RemoteCertificate);
39+
}
40+
}

0 commit comments

Comments
 (0)