SkillAgentSearch skills...

Microlink

Tailscale-compatible VPN client for ESP32. Full ts2021 protocol support with WireGuard encryption, DERP relay, DISCO path discovery, and STUN. Connect your ESP32 devices to your tailnet and communicate securely with any peer. Production-ready, memory-optimized (~100KB SRAM).

Install / Use

/learn @CamM2325/Microlink

README

MicroLink v2 — ESP32 Tailscale Client

Production-ready Tailscale VPN client for the ESP32 platform with WiFi and 4G cellular support. Should work on most ESP32 variants (ESP32, ESP32-S3, ESP32-P4, etc.) — ESP32-S3 with PSRAM recommended for production.

Features

  • Full Tailscale Protocol Support

    • ts2021 coordination protocol
    • WireGuard encryption (ChaCha20-Poly1305)
    • DISCO path discovery (PING/PONG/CALL_ME_MAYBE)
    • DERP relay with dynamic region discovery (up to 32 regions)
    • STUN for public IP / NAT type discovery (IPv4 + IPv6)
    • Delta updates (PeersChanged, PeersRemoved, PeersChangedPatch)
    • MagicDNS hostname resolution (short name or FQDN)
    • Key expiry detection and auto re-registration
  • WiFi + 4G Cellular

    • WiFi primary with automatic cellular failback
    • PPP cellular data — real lwIP sockets, direct UDP, NAT hole-punching
    • AT socket bridge fallback for carriers that reject PPP auth
    • Multi-carrier: PAP (IMSI-based) and CHAP (credential-based) automatic selection
    • Seamless network rebind — switch between WiFi and cellular without destroying the VPN session (~330ms rebind, ~7s recovery)
    • Network health monitoring with automatic failback to WiFi when recovered
  • Production Ready

    • Fully async, task-based architecture (no polling loop)
    • Tested with 300+ peer tailnets (PSRAM-backed 512KB buffers)
    • NVS peer cache — DISCO probing starts immediately on reboot
    • Proactive H2 WINDOW_UPDATE for fast MapResponse downloads
    • Key expiry handling with reusable auth keys
  • Broad Hardware Support

    • Tested on ESP32-S3, ESP32-WROOM-32D, and HiLetgo ESP-32S
    • Should work on most ESP32 variants with WiFi and sufficient RAM
    • Memory-optimized: ~85-116KB SRAM static, PSRAM for large tailnet buffers
  • HTTP Config Server

    • Web UI at http://<vpn-ip>/ — system monitor, peer management, device settings
    • REST API: /api/settings, /api/peers, /api/peers/allowed, /api/monitor, /api/status, /api/restart
    • All settings persist in NVS — no rebuild needed
    • Ifdef-gated: zero cost when disabled
  • Advanced Features

    • Zero-copy WireGuard receive (raw lwIP PCB, for 30fps+ video streaming)
    • DISCO peer filtering / allowlist (reduces jitter on large tailnets)
    • Priority peer (guaranteed WG slot even when peer table is full)
    • Headscale / Ionscale compatible (configurable control plane host)
    • Credential security: all secrets in git-ignored sdkconfig

Requirements

  • ESP-IDF v5.0 or later (tested with v5.3)
  • ESP32 with WiFi (ESP32-S3 with PSRAM recommended)
  • Tailscale account with auth key (generate at https://login.tailscale.com/admin/settings/keys)
  • For cellular: ESP32-compatible 4G cellular module (e.g., SIM7600, SIM7670) + active SIM card

Hardware

Tested Boards

| Board | Type | Notes | |-------|------|-------| | ESP32-S3 with 8MB PSRAM | WiFi | Recommended for production | | Seeed Studio XIAO ESP32S3 | WiFi + Cellular | Pairs with Waveshare SIM7600X | | Waveshare ESP32-S3-Touch-AMOLED-2.06 | WiFi | Touchscreen display | | HiLetgo ESP-32S | WiFi | Budget option, no PSRAM | | ESP32-WROOM-32D / DevKitC | WiFi | Standard dev board, no PSRAM |

Tested Cellular Modules

| Module | Interface | Notes | |--------|-----------|-------| | Waveshare SIM7600G-H 4G | UART | PPP + AT socket bridge, tested with EIOT and Soracom SIMs | | LILYGO T-SIM7670G-S3 | UART | Integrated ESP32-S3 + SIM7670G |

Should Work (Untested)

MicroLink uses standard ESP-IDF APIs — any ESP32 variant with WiFi and sufficient RAM should work. ESP32-P4, ESP32-C3, ESP32-C6, etc. Boards with PSRAM are recommended for large tailnets (100+ peers).

Quick Start

1. Clone and enter an example

git clone https://github.com/CamM2325/microlink.git
cd microlink/examples/basic_connect    # or: cellular_connect, cellular_heartbeat, failover_connect

2. Configure sdkconfig

Add these settings to your sdkconfig.defaults file:

# PSRAM Configuration (required for ESP32-S3 with PSRAM)
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_TYPE_AUTO=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768

# Partition table (app needs ~1MB+)
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y

# TLS/HTTPS (required for DERP and control plane)
CONFIG_ESP_TLS_USING_MBEDTLS=y
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y

# Networking
CONFIG_LWIP_IPV4=y
CONFIG_LWIP_IP4_FRAG=y
CONFIG_LWIP_IP4_REASSEMBLY=y

# Stack size
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192

3. Configure credentials

cp sdkconfig.credentials.example sdkconfig.credentials
# Edit sdkconfig.credentials with your WiFi SSID/password, Tailscale auth key, etc.

Or run idf.py menuconfig → MicroLink V2 → Credentials to set them interactively.

Credentials are stored in sdkconfig (which is gitignored) so they are never accidentally committed to version control.

4. Build and flash

source ~/esp/esp-idf/export.sh
idf.py build
idf.py -p /dev/ttyACM0 flash monitor

5. Test

From any device on your tailnet:

tailscale ping esp32-microlink

You should see:

pong from esp32-microlink (100.x.x.x) via DERP(dfw) in 150ms

Memory Footprint

Static Memory (measured with idf.py size, ESP-IDF v5.3, ESP32-S3)

| Build | SRAM (static) | Flash | IRAM | Free SRAM | |-------|---------------|-------|------|-----------| | WiFi + Web UI (basic_connect) | 116 KB | 950 KB | 16 KB | 226 KB | | WiFi + Cellular failover (failover_connect) | 123 KB | 950 KB | 16 KB | 219 KB | | Cellular only (cellular_connect) | 85 KB | 758 KB | 16 KB | 256 KB |

Runtime Memory (allocated from heap at startup)

| Resource | Size | Location | |----------|------|----------| | Task stacks (coord + derp_tx + net_io + wg_mgr) | 42 KB | SRAM | | H2 receive buffer | 512 KB (configurable) | PSRAM | | JSON parse buffer | 512 KB (configurable) | PSRAM | | NVS peer cache (64 peers) | ~6 KB | PSRAM | | HTTP config server | ~7 KB | SRAM (ifdef-gated) | | Per WG peer | ~200 bytes | SRAM |

ESP32-S3 with PSRAM (Recommended)

MapResponse buffers (H2 + JSON) are allocated from PSRAM only during coordination polling, then freed. Peak PSRAM usage is ~1MB (~12% of 8MB). Leaves 200KB+ SRAM free for your application.

ESP32 without PSRAM

Boards without PSRAM can reduce H2/JSON buffers to 64KB via menuconfig (sufficient for ~30 peers). Total SRAM usage: ~140KB. Suitable for simple sensor reporting, heartbeats, and small data payloads. Not recommended for large tailnets or memory-heavy applications.

# sdkconfig.defaults for ESP32 without PSRAM
CONFIG_ML_H2_BUFFER_SIZE_KB=64
CONFIG_ML_JSON_BUFFER_SIZE_KB=64
CONFIG_ML_MAX_PEERS=8

Examples

| Example | Description | Hardware | |---------|-------------|----------| | basic_connect | WiFi → Tailscale → UDP echo + web config | Any ESP32 with WiFi | | cellular_connect | 4G cellular → Tailscale → bidirectional UDP | XIAO + Waveshare SIM7600 | | cellular_heartbeat | Periodic heartbeat over 4G cellular | XIAO + Waveshare SIM7600 | | failover_connect | WiFi primary + cellular fallback | XIAO + Waveshare SIM7600 |

Architecture

MicroLink v2 uses a fully async, task-based architecture. All protocol operations run concurrently in dedicated FreeRTOS tasks with queue-based IPC — no polling loop needed.

┌─────────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐
│ coord_task  │  │ derp_tx  │  │ net_io   │  │ wg_mgr  │
│ (Tailscale  │  │ (DERP    │  │ (select  │  │ (WG +   │
│  control)   │  │  relay)  │  │  loop)   │  │  DISCO) │
└──────┬──────┘  └────┬─────┘  └────┬─────┘  └────┬────┘
       │              │             │              │
       └──────────────┴─────────────┴──────────────┘
                          │
                    Queue-based IPC
                          │
                ┌─────────┴─────────┐
                │   WiFi / Cellular  │
                │  (ml_net_switch)   │
                └───────────────────┘

Key differences from v1:

  • No microlink_update() polling — all tasks run independently
  • Queue-based IPC instead of shared state + mutexes (no deadlocks)
  • Dedicated DERP TX task (non-blocking sends)
  • select() loop in net_io for multiplexed packet routing

Task Stack Sizes

| Task | Stack | Purpose | |------|-------|---------| | coord_task | 12 KB | Tailscale control plane (TLS, H2, JSON parsing) | | derp_tx | 14 KB | DERP relay send (TLS overhead) | | net_io | 8 KB | UDP packet routing (select loop) | | wg_mgr | 8 KB | WireGuard + DISCO + STUN |

API Reference

Initialization

// Get a default configuration
microlink_config_t config = {
    .auth_key = "tskey-auth-...",
    .device_name = "my-sensor",
    .enable_derp = true,
    .enable_disco = true,
    .enable_stun = true,
    .max_peers = 16,
};

// Initialize (creates tasks, does NOT connect yet)
microlink_t *ml = microlink_init(&config);

// Start connecting (WiFi must be up)
microlink_start(ml);

Connection Status

// Check connection state
microlink_state_t state = microlink_get_state(ml);
bool connected = microlink_is_connected(ml);

// Get our VPN IP
uint32_t vpn_ip = microlink_get_vpn_ip(ml);
char ip_str[16];
microlink_ip_to_str(vpn_ip, ip_str);
// ip_str = "100.x.y.z"

Connection states: ML_STATE_IDLEML_STATE_WIFI_WAITML_STATE_CONNECTINGML_STATE_REGISTERINGML_STATE_CONNECTED

UDP Communication

// Create UDP socket bound to VPN IP
microlink_udp_socket_t *sock = microlink_udp_create(ml, 
View on GitHub
GitHub Stars90
CategoryCustomer
Updated1h ago
Forks11

Languages

C

Security Score

85/100

Audited on Mar 31, 2026

No findings