Docker-based testbed for IoT firmware connectivity. Each service mirrors or echoes incoming traffic and records every exchange to a shared SQLite store.
Built to exercise the TLS / DTLS stacks of NB-IoT and other cellular IoT modems against a controlled, instrumented echo / mirror server.
A .NET 10 host supervises an OpenVPN client and a set of echo / mirror services. Devices reach the echo and mirror services through the VPN tunnel; only the HTTP management API on port 8664 is published to the host.
- OpenVPN client: joins an APN tunnel so test devices reach the services without going through the public internet.
- Echo and mirror services: UDP, TCP, TLS, DTLS, HTTP, HTTPS. Each binds
0.0.0.0inside the container and is reachable only via the tunnel. - HTTP management API: overview, configuration, status, per-service dashboards, CSV export, CA downloads (port 8664).
- Run analytics: handshakes (TLS / DTLS / HTTPS), per-packet timing,
sequence numbers, server-side UDP probes, per-run environment snapshots,
exportable as JSON via
/runs/{id}/export.json. - Legacy cipher support: OpenSSL
SECLEVEL=1and a ClientHello-aware cert selector let constrained modems (e.g. Quectel BC660K-GL, static-RSA only) actually complete a handshake.
Internet → Docker container → OpenVPN client → VPN server
↓
Echo / mirror services (UDP / TCP / TLS / DTLS / HTTP / HTTPS)
only reachable via the VPN tunnel
↓
HTTP management API (8664), exposed externally
| Service | Port | Notes |
|---|---|---|
| UDP echo | 5001 |
Datagram reflector |
| TCP echo | 5002 |
Stream reflector |
| TLS echo | 5003 |
Stream reflector, RSA + ECC dual-stack |
| DTLS echo | 5004 |
Datagram reflector, RSA + ECC dual-stack |
| HTTP mirror | 80 |
Plain-text mirror of the request |
| HTTPS mirror | 443 |
Same, TLS-protected |
| Management | 8664 |
Dashboards, status, config, CSV / JSON export |
Copy DockerSetup/vpn_config.ovpn.example
to DockerSetup/vpn_config.ovpn and fill in the <key>, <cert>, <ca>,
and <tls-crypt> blocks supplied by your VPN provider. The real config is
gitignored.
Tested against 1NCE managed IoT connectivity
(<user>.openvpn.1nce.com); any OpenVPN-compatible provider should work as
long as the resulting tunnel reaches the device-side network.
A CA and per-service certificates must exist in DockerSetup/certs. They are
issued by the bundled CertificateProvisioner tool.
# Windows PowerShell (5.1+)
powershell -NoLogo -NoProfile -File .\DockerSetup\scripts\ensure-certs.ps1
# PowerShell 7+
pwsh ./DockerSetup/scripts/ensure-certs.ps1# macOS / Linux
bash ./DockerSetup/scripts/ensure-certs.shPass --force to re-issue the certificates.
Docker Compose (recommended):
docker-compose up -dOr Docker directly:
docker build -t iot-test-services -f DockerSetup/Dockerfile .
docker run -d \
--name iot-test-services \
--cap-add=NET_ADMIN \
--device /dev/net/tun \
-p 8664:8664 \
-v $(pwd)/DockerSetup/vpn_config.ovpn:/app/vpn_config.ovpn:ro \
iot-test-servicescurl http://localhost:8664/status # service status (JSON)
docker logs -f iot-test-services # container log
docker exec iot-test-services ip addr show tun0All runtime options live in DockerSetup/appsettings.json. The tables below highlight the keys most commonly overridden.
| Setting | Default | Notes |
|---|---|---|
ConfigPath |
/app/vpn_config.ovpn |
OpenVPN configuration file |
LogPath |
/var/log/openvpn.log |
OpenVPN log file |
InterfaceName |
tun0 |
VPN tunnel interface to monitor |
ConnectionAttempts |
30 |
Polls of the tunnel before giving up |
ConnectionAttemptDelaySeconds |
1 |
Delay between polls |
InitialDelaySeconds |
1 |
Delay before the first poll |
StopOnConnectionFailure |
false |
Stop the host if the VPN never comes up |
DumpLogOnFailure |
true |
Dump the OpenVPN log on failure or timeout |
Each service block uses the same shape: Port, BindAddress, Enabled,
plus TLS / DTLS / HTTPS variants additionally take RSA + ECC certificate
paths and a PreferEcc toggle. DTLS adds Mtu (default 256).
TLS, DTLS, and HTTPS expose EnableLegacyCiphers (default true) for
static-RSA modems.
| Section | Default port | Cert paths required |
|---|---|---|
EchoServices:Udp |
5001 |
no |
EchoServices:Tcp |
5002 |
no |
EchoServices:Tls |
5003 |
yes (RSA + ECC) |
EchoServices:Dtls |
5004 |
yes (RSA + ECC) |
EchoServices:Http |
80 |
no |
EchoServices:Https |
443 |
yes (RSA + ECC) |
| Setting | Default | Notes |
|---|---|---|
Storage:DatabasePath |
/app/data/echo.sqlite |
SQLite store for runs / packets |
Host:BindAddress |
0.0.0.0 |
Management API bind address |
Host:Port |
8664 |
Management API port |
| Setting | Default |
|---|---|
Certificates:Authority:Name |
IoT Test Services Test CA |
Certificates:Authority:RsaPublicCertPath |
/app/certs/authority/ca.crt |
Certificates:Authority:EccPublicCertPath |
/app/certs/authority/ca-ecc.crt |
From a device on the same VPN:
VPN_IP=$(docker exec iot-test-services ip -4 addr show tun0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
# UDP / TCP echo
echo "Hello UDP" | nc -u $VPN_IP 5001
echo "Hello TCP" | nc $VPN_IP 5002
# TLS / DTLS echo (OpenSSL)
echo "Hello TLS" | openssl s_client -connect $VPN_IP:5003 -quiet
echo "Hello DTLS" | openssl s_client -dtls1_2 -connect $VPN_IP:5004 -quiet
# HTTP / HTTPS mirror
curl http://$VPN_IP:80/test
curl -k https://$VPN_IP:443/testThe /config page lists the CA in PEM form. Clients that verify the server
should import it from /certificates/authority/rsa or
/certificates/authority/ecc.
Many cellular IoT modems ship TLS / DTLS stacks limited to legacy cipher
suites, often static-RSA only and without forward secrecy. The TLS and DTLS
handlers peek the ClientHello and select an RSA or ECC certificate
accordingly; EnableLegacyCiphers: true is on by default so constrained
devices (e.g. Quectel BC660K-GL) can negotiate at all.
The Mbed CE projects below talk to this testbed. The shared test library drives the echo and mirror endpoints exposed here; the per-modem EVKIT apps adapt it to a specific cellular driver and target board.
- mbed-ce-cellular-test —
reusable test library: exercises UDP / TCP / TLS / DTLS / HTTP / HTTPS
against the endpoints exposed here and reports run metadata via
/runs/{id}so dashboards line up across modems. - mbed-ce-bc660kgl-app — example app for the Quectel BC660K-GL (NB-IoT, static-RSA only).
- mbed-ce-bg770agl-app — example app for the Quectel BG770A-GL (LTE Cat-M1 / NB-IoT / NB2).
- mbed-ce-st87m01-app — example app for the ST87M01 (LTE Cat-M1, ECC-capable).
- Echo and mirror services must only be reached over the VPN tunnel. Do not
publish ports
80,443, or5001–5004directly in production. - Certificates for TLS, DTLS, and HTTPS are issued from the local
IoT Test Services Test CA. Clients that validate the server need the CA PEM from/configor the/certificates/authority/*endpoints. - Re-run
ensure-certs --forceif certificate material is compromised. - The bundled VPN config example contains no credentials; the real
vpn_config.ovpnis gitignored.
docker exec iot-test-services cat /var/log/openvpn.log # OpenVPN log
docker exec iot-test-services ip link show tun0 # Tunnel interface
docker exec iot-test-services ip route # RoutingRun locally without Docker:
dotnet restore
dotnet run --project IoTTestServicesOpenVPN must be configured separately when running outside Docker.
iot-test-service/
├── IoTTestServices.sln
├── IoTTestServices/ # .NET 10 web host (entry point)
├── IoTTestServices.Core/ # storage, packets, runs, dashboards, TLS helpers
├── IoTTestServices.Echo.Udp/ # UDP echo service
├── IoTTestServices.Echo.Tcp/ # TCP echo service
├── IoTTestServices.Echo.Tls/ # TLS echo service (RSA + ECC)
├── IoTTestServices.Echo.Dtls/ # DTLS echo service (RSA + ECC)
├── IoTTestServices.Mirror.Http/ # HTTP request mirror
├── IoTTestServices.Mirror.Https/ # HTTPS request mirror
├── IoTTestServices.Tests/ # xUnit test suite
├── Tools/
│ └── CertificateProvisioner/ # generates CA + per-service certs
└── DockerSetup/
├── Dockerfile
├── docker-compose.yml
├── appsettings.json
├── openssl-legacy.cnf
├── vpn_config.ovpn.example
├── scripts/ # ensure-certs.{ps1,sh}, dtls-selftest.sh
└── certs/ # generated TLS material (gitignored)
MIT License. See LICENSE.