Skip to content
OpenIPCPublic

About

Minimal dependency-free UDP/TCP tunnel for embedded Linux targets.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

OpenIPC logo

OpenIPC Camex

English | Русский | 简体中文

camex CI Release License: MIT Code size

Minimal dependency-free UDP/TCP tunnel for embedded Linux targets.

Ships as a userspace daemon and an optional standalone kernel module.


Features

  • Client/server architecture over UDP or TCP
  • Auto-config mode — client fetches tunnel parameters from the server
  • Protocol v2 — Noise handshake (NKpsk0, X25519, ChaCha20-Poly1305, BLAKE2b), fresh keys per session and direction, always encrypted
  • No external dependencies — only libc required
  • Cross-platform builds — x86_64, ARM, AArch64, MIPS (via CI)
  • Custom kernel module — camex.ko replaces tun.ko with no external deps
  • Compact codebase — ~3,500 lines total, easy to audit

Quick Start

Keys

The server has a key pair; every client needs the server's public key and a pre-shared key (the global one, or its own psk= from the server config).

(umask 077; camex --genkey > server.key)   # server_key=… (secret) and public_key=…

Server

sudo ./camex --mode server --port 5800 --config /etc/camex/camex.conf \
  --server-key-file server.key --psk secret

Client (auto mode)

sudo ./camex --mode client --auto --name 0203A104B5AE \
  --server-host vpn.example.org --port 5800 \
  --server-pubkey <public_key> --psk secret

Client (manual mode)

sudo ./camex --mode client \
  --local-cidr 10.0.0.2/24 --gateway-ip 10.0.0.1 \
  --server-host vpn.example.org --port 5800 \
  --server-pubkey <public_key> --psk secret

Android

The app connects in auto mode: the server sends the address, routes and MTU.

Main screen Settings
Main screen Settings
  • Server host / port — the camex server.
  • Client name — the client ID, i.e. the [client ID] block on the server.
  • Transport — UDP or TCP, as on the server.
  • Server public key and Pre-shared key — --server-pubkey --psk; the key must match the client's psk= (or the global key).
  • Route all traffic through the tunnel — off: only the tunnel network and the server's routes go through the tunnel.
  • Advanced — MTU, keepalive and server timeout (screenshot).

Binary Sizes

Platform Size (stripped) UPX compressed
x86_64 see CI see CI
armhf see CI see CI
aarch64 see CI see CI
mipsel see CI see CI

Sizes are updated by CI on each release. See the latest build artifacts or releases.

Requirements

Requirement Details
Linux kernel 3.10 or later
TUN support CONFIG_TUN=y (/dev/net/tun) or camex.ko loaded (/dev/camex)
Privileges root or CAP_NET_ADMIN + CAP_NET_RAW (non-root with camex.ko: also access to /dev/camex, see Kernel Module)
C compiler C99-compatible (GCC or Clang)

Build

Userspace app

make

Kernel module (optional)

# Prerequisites: kernel headers
make kmod

Both

make && make kmod

Cross-compile

make CROSS_COMPILE=mipsel-linux-

Install kernel module

sudo make kmod-install

Clean

make clean          # remove userspace build artifacts
make kmod-clean     # remove kernel module build artifacts
make distclean      # clean everything

TUN Driver Support

The app auto-detects the available TUN backend at startup. It tries /dev/net/tun (tun.ko) first; if that fails or TUNSETIFF is unavailable, it falls back to /dev/camex (camex.ko). Both backends work identically from the protocol perspective.

Override auto-detection with --tun-dev <path>:

sudo ./camex --mode client ... --tun-dev /dev/camex
Feature tun.ko (/dev/net/tun) camex.ko (/dev/camex)
Interface name dynamic (tun0, tun1, …) always camex
Requires TUNSETIFF ioctl Yes No
External dependency CONFIG_TUN=y or tun.ko loaded camex.ko loaded
IPv4 Yes Yes
IPv6 Yes No (IPv4 only)
Multiple instances Yes No (one)
Bare IPv4 packets Yes (with IFF_NO_PI) Yes

Userspace App

Modes

Mode Description
client Creates a TUN interface and connects to the server
server Listens for many clients and forwards tunnel packets

Usage

Client

sudo ./camex \
  --mode client \
  --auto \
  --name 0203A104B5AE \
  --server-host vpn.example.org \
  --port 5800 \
  --server-pubkey <public_key> --psk secret

Manual client mode:

sudo ./camex \
  --mode client \
  --local-cidr 10.0.0.2/24 \
  --gateway-ip 10.0.0.1 \
  --server-host vpn.example.org \
  --port 5800 \
  --route-cidr 192.168.100.0/24 \
  --server-pubkey <public_key> --psk secret

With explicit TUN backend:

sudo ./camex \
  --mode client \
  --local-cidr 10.0.0.2/24 \
  --gateway-ip 10.0.0.1 \
  --server-host vpn.example.org \
  --port 5800 \
  --server-pubkey <public_key> --psk secret \
  --tun-dev /dev/camex

Server

sudo ./camex \
  --mode server \
  --port 5800 \
  --bind-ip 0.0.0.0 \
  --config /etc/camex/camex.conf \
  --server-key-file /etc/camex/server.key \
  --psk secret

Options

Option Short Description
--mode <mode> -M Operation mode: client or server (required)
--auto -a Fetch tunnel parameters from the server
--name <id> -n Client ID (auto mode; optional in manual mode)
--local-cidr <cidr> -l Tunnel address in address/prefix form
--gateway-ip <addr> -g Client gateway inside the tunnel
--server-host <addr> -s Server hostname or IP for client mode
--port <port> -p Server port for client or listen port for server
--bind-ip <addr> -b Server bind address (default: 0.0.0.0)
--config <path> -f Server config file path
--route-cidr <cidr> -c Extra route to install on the client; repeatable
--mtu <size> -t Tunnel MTU, 576–9000 (default: 1500)
--psk <key> -k Pre-shared key: hex:<64 hex digits>, base64:<44 chars>, or a passphrase (stretched with Argon2id). Required on clients
--server-pubkey <key> Client: the server's public key (from --genkey)
--server-key-file <path> Server: file with its private key (or server_key= in [server])
--genkey Print a new server key pair (server_key=, public_key=) and exit
--encrypt -e Accepted for compatibility; protocol v2 is always encrypted
--tun-dev <path> -T TUN device path (default: auto-detect /dev/net/tun then /dev/camex)
--pid-file <path> -P Write PID to file on startup
--bind-dev <iface> -d Bind socket to a specific network interface (SO_BINDTODEVICE)
--transport <proto> -R Transport protocol: udp (default) or tcp
--version -v Print version and exit
--help -h Print usage and exit

Examples

Ready-to-use configuration files are available in the examples/ directory:

File Description
examples/camex.conf Server-side configuration file format reference
examples/camex.service systemd service unit for camex daemon
examples/README.md Usage instructions for all examples

Server Configuration

The server reads a flat key=value config file with one client record per block. See examples/camex.conf for the full format.

sudo ./camex --mode server --port 5800 --config /etc/camex/camex.conf

Auto-mode clients get their addresses from the config file. A client whose ID has no [client ID] block gets [defaults], or, when no address is configured, one from the auto pool 10.200.0.2–10.200.0.254 (addresses in use and those of [client ID] blocks are skipped; addresses of expired clients are reused). To serve only the IDs listed in the file, set allow_unknown_clients=no in the [server] block.

After a change to the file, kill -HUP the server. A running auto-mode client picks up a changed address, gateway, MTU or routes with its next handshake (a key renewal, at most 10 minutes later, or a reconnect): it reopens its tunnel with the new settings and replaces its routes, without a restart.

Security Model

camex is a small tunnel for cameras, not a general-purpose VPN. Know what it protects before exposing it to an untrusted network.

Protocol v2 (camex 3.x)

  • Handshake. Noise_NKpsk0_25519_ChaChaPoly_BLAKE2b: the client knows the server's public key, both sides know the client's pre-shared key. Every session gets fresh keys, separate for each direction (forward secrecy: a leaked key does not decrypt recorded sessions).
  • Packets. ChaCha20-Poly1305: the payload is encrypted, the 17-byte header (type, session index, counter) is authenticated but readable. Packets that fail authentication are dropped. Keys are renewed every 10 minutes.
  • Keys. The pre-shared key is a raw key (hex:, base64:) or a passphrase stretched with Argon2id. A captured handshake does not let anyone test passphrase guesses offline: that needs the server's private key.
  • Per-client keys. A psk= in a [client ID] block gives that client its own key; it can register only with it, and the global key no longer works for that ID (unless both passphrases are the same, which makes them one key). See Notes for the exact rules.
  • Source addresses. A client may send only from its own tunnel address; packets with another source address are dropped.
  • Replay protection. Data packets have a 2048-packet window per session. While a client has a live session, a handshake with an older timestamp is rejected; a replayed handshake can never take a working session over, since a new session replaces the old one only with the client's first packet on it (a client whose clock went back after a reboot still reconnects). Packets of an ended session cannot be replayed into a new one: its keys are different.
  • Roaming. A session is found by its index, not by the client's address: after a NAT rebinding the client keeps its session.
  • Config reload. After SIGHUP, a live session whose key was removed or changed, or now belongs to another client, ends.

What it does not protect

  • The server's private key lets its holder impersonate the server to clients that know the pre-shared key: keep server_key secret.
  • Metadata. Packet sizes, timing, endpoints and the cleartext header (packet type, session index, counter) are visible.
  • There is no unencrypted mode; for an open tunnel without encryption see OpenIPC VTesse.
  • Inside the tunnel traffic is not filtered: any client can reach the server and other clients on the tunnel network. Restrict that with a firewall on the server's tunnel interface (tun0 or similar with /dev/net/tun, camex with the kernel module; the server logs its name at startup).

Denial of service limits

  • TCP connections that have not completed a handshake are limited (4 per source address, 32 in total) and closed after 10 s. A handshake carries a MAC keyed with the server's public key: packets from anyone who does not know it are dropped after one hash. The server processes at most 200 handshakes per second.
  • Log messages that a peer can trigger (dropped packets, rejected registrations) are rate-limited; the number suppressed is logged.
  • The auto pool hands out 10.200.0.2–.254 and never gives one address to two live clients (an expired client's address is handed out again; a client that reconnects, also after a server restart, gets its previous address back while it is free); allow_unknown_clients=no serves only auto-mode IDs listed in the config file (manual clients are bound by their key and address, see Notes).

Recommended deployment

  • Keep the server key file readable by the server only (umask 077); give the clients public_key only.
  • Prefer raw keys (hex:/base64:) or long random passphrases.
  • Give each camera its own psk= in a [client ID] block, and set allow_unknown_clients=no when all auto-mode cameras are listed.
  • Run the server with only the capabilities it needs (CAP_NET_ADMIN; with the kernel module also file access to /dev/camex, see below), and firewall the tunnel interface.

Kernel module permissions

/dev/camex is created 0600, and open() requires CAP_NET_ADMIN; see Key Design Decisions. The module does no encryption: encryption is done by the userspace app.

Android app

The app uses VpnService, so Android shows the VPN key icon while the tunnel is up; there is no way around that with VpnService (a client without TUN is tracked in #21). Only the tunnel network and the routes the server sends go through the tunnel; a 0.0.0.0/0 route from the server is ignored unless Route all traffic through the tunnel is enabled in the settings. That switch covers IPv4 only: IPv6 traffic does not go through the tunnel.

Kernel Module

Architecture

Userspace                          Kernel
──────────────────                 ──────────────────────────────────────────
                                   ┌─────────────────────────────────┐
  write(fd, ipv4_pkt)  ──────────► │  copy_from_user()               │
                                   │  netif_rx(skb)                  │
                                   │       │                         │
                                   │       ▼                         │
                                   │  IP stack / routing             │
                                   │       │                         │
                                   │       ▼                         │
                                   │  camex_net_xmit()               │
                                   │  enqueue → rx_queue             │
                                   │  wake_up(rx_wait)               │
  read(fd, buf)        ◄────────── │  copy_to_user()                 │
                                   └─────────────────────────────────┘
  poll/select/epoll    ◄────────── wait_queue (rx_wait)

  /dev/camex  ←────────────────────────────────────── net interface "camex"

Two independent kernel objects — a character device (miscdevice) and a network interface (net_device) — share a single camex_priv instance. Communication between them flows through the rx_queue protected by a spinlock.

Key Design Decisions

1. No dependency on tun.ko

The upstream /dev/net/tun is a separate module (drivers/net/tun.c) that must be either loaded or compiled into the kernel. camex registers its own net_device and miscdevice directly, using only core kernel subsystems (netdev, misc, skbuff) that are always present and cannot be unloaded or disabled.

2. miscdevice instead of manual chrdev registration

Using misc_register() automatically:

  • allocates a minor number dynamically (no conflicts);
  • creates /dev/camex via udev/devtmpfs without any udev rules;
  • sets permissions to 0600 (root only).

open() additionally requires CAP_NET_ADMIN: the device injects packets into the network stack and exposes routed traffic, which is a network-admin operation, just like configuring a tun device.

A daemon that runs as a non-root user with CAP_NET_ADMIN + CAP_NET_RAW also needs file access to the node, which 0600 does not give it. Grant it to a dedicated group with a udev rule (the capability check still applies):

# /etc/udev/rules.d/90-camex.rules
KERNEL=="camex", GROUP="camex", MODE="0660"

or, without udev, after each insmod: chgrp camex /dev/camex && chmod 0660 /dev/camex.

3. Packet queue protected by spinlock + irqsave

camex_net_xmit() is called from softirq context (not from process context), so spin_lock_irqsave is used instead of a mutex to protect the queue. This prevents deadlocks when called from interrupt context.

4. Blocking read() via wait_event_interruptible

Instead of busy-waiting or timers, the standard kernel mechanism is used: the process sleeps on a wait_queue, and camex_net_xmit() wakes it up via wake_up_interruptible(). Signals correctly interrupt the wait (-ERESTARTSYS).

5. IPv4-only with explicit validation on write()

On write(), the first byte of the buffer is checked before copy_from_user via get_user(). If the IP version is not 4, -EINVAL is returned without allocating an skb, preventing garbage or non-IPv4 packets from entering the stack.

6. Single user (EBUSY)

g_priv->attached is an atomic_t taken with atomic_cmpxchg(). If /dev/camex is already open, a subsequent open() immediately returns -EBUSY, and two concurrent open() calls can never both succeed. On close() the queue is flushed and the slot released under the same lock that camex_net_xmit() takes, so no packet is left for the next open().

7. ARPHRD_NONE + IFF_NOARP

The interface is declared as having no link-layer address. The kernel does not attempt to resolve MAC addresses (ARP) and does not prepend Ethernet headers. Userspace sends and receives bare IPv4 datagrams starting at the version byte 0x45....

8. Compatibility via #define shims

All #if LINUX_VERSION_CODE guards are grouped into a single #define block at the top of the file. The driver body uses only macro names (CAMEX_ALLOC_NETDEV, CAMEX_POLL_T, CAMEX_SKB_PUT, etc.) with no conditional compilation inside functions. This simplifies auditing and maintenance.

Kernel Compatibility

Range Status Adapted API
3.10 – 3.16 ✅ alloc_netdev(sz, name, setup) — 3 arguments
3.17 – 3.19 ✅ alloc_netdev + NET_NAME_PREDICTABLE — 4 arguments
4.0 – 4.9 ✅ min_mtu/max_mtu fields absent from net_device
4.10 – 4.15 ✅ min_mtu/max_mtu fields introduced
4.16 – 4.19 ✅ poll returns __poll_t; EPOLL* constants
4.20+ ✅ skb_put() returns void* instead of unsigned char*
5.x ✅ no changes required
6.0 – 6.6 LTS ✅ no changes required

Note: Kernels 3.0 – 3.9 are theoretically covered by the same shims but are extremely rare in practice (RHEL 6, early embedded).

Building and Installation

Prerequisites

# Debian / Ubuntu
sudo apt install linux-headers-$(uname -r) build-essential

# RHEL / CentOS 7
sudo yum install kernel-devel-$(uname -r) gcc make

# Fedora / RHEL 8+
sudo dnf install kernel-devel gcc make

# Alpine Linux
sudo apk add linux-headers build-base

Compile

make kmod

Load the module

sudo insmod camex.ko

Verify:

dmesg | tail -5
# camex: loaded — /dev/camex <-> net:camex  (kernel 5.15)

ls -la /dev/camex
# crw------- 1 root root 10, 58 ...  /dev/camex

ip link show camex
# camex: <POINTOPOINT,NOARP> mtu 1500 ...

Persistent load at boot

# Install the module into the kernel tree
sudo make kmod-install     # copies camex.ko and runs depmod -a

# Enable autoload
echo "camex" | sudo tee /etc/modules-load.d/camex.conf

Unload

sudo rmmod camex

Unloading is blocked while /dev/camex is held open by any process (rmmod returns EBUSY).

Character Device API

Operation Behaviour
open() Needs CAP_NET_ADMIN; one fd at a time, EBUSY otherwise
read() Returns one IPv4 packet. Blocks when queue is empty
read() + O_NONBLOCK Returns EAGAIN when queue is empty
write() Injects one IPv4 packet into the stack. Minimum 20 bytes
write() non-IPv4 Returns EINVAL
poll() POLLIN when a packet is available; POLLOUT always
close() Flushes the queue, clears carrier, releases the slot

Error codes

Code Cause
EPERM open() without CAP_NET_ADMIN
EBUSY /dev/camex is already open by another process
ENODEV Module is being unloaded
EMSGSIZE read() buffer smaller than packet length (packet dropped)
EINVAL write(): size < 20 or > 65535 bytes, or not IPv4
EAGAIN read() with O_NONBLOCK, queue is empty
ENOMEM Failed to allocate an skb

Limitations

Functional

  • IPv4 only. IPv6 is intentionally excluded. A write() with an IPv6 packet returns -EINVAL.

  • Single process at a time. Only one file descriptor may hold /dev/camex open. A second open() returns EBUSY. For multi-process schemes, multiplex via a Unix socket in userspace.

  • Single device instance. Exactly one camex interface and one /dev/camex device are created. Supporting multiple instances requires extending the code with a g_priv[] array and name suffixes.

  • TUN (Layer 3) only. TAP (Layer 2, Ethernet frames) is not implemented. The interface has no MAC address (ARPHRD_NONE). ARP does not work.

  • No ioctl compatibility with /dev/net/tun. The commands TUNSETIFF, TUNGETIFF, TUNSETPERSIST, and other ioctls from the upstream driver are not implemented. Programs that use ioctl(fd, TUNSETIFF, ...) will not work with camex without modification.

  • MTU is fixed at 1500 bytes. Changing it via ip link set camex mtu N is not blocked by the kernel, but camex does not enforce the MTU on write().

Queue and Memory

  • Queue depth is 64 packets. When full, new packets are silently dropped and tx_dropped is incremented (visible in ip -s link). Adjust the QUEUE_LIMIT constant in the source.

  • Each queued packet is stored as an sk_buff. Every sk_buff consumes ~200 bytes of header plus packet data. At 64 × 1500 bytes the maximum queue memory footprint is approximately 96–110 KB.

  • A read() with a too-small buffer destroys the packet. If the buffer is smaller than the packet length, the packet is lost and EMSGSIZE is returned. Always read with a buffer of at least 1500 bytes (or the interface MTU).

Thread Safety

  • Concurrent read() calls from multiple threads of the same process are not serialised at the fd level — protect access with a mutex in userspace.

  • Concurrent write() calls from multiple threads are safe: each call independently allocates its own skb.

Networking

  • No masquerading/NAT out of the box. To route traffic from camex to the internet, configure iptables -t nat -A POSTROUTING manually.

  • netif_rx() vs netif_receive_skb(). The driver uses netif_rx() (the older softirq-deferred path). On high-throughput systems, netif_receive_skb() offers lower latency but requires process or softirq context — the substitution is trivial if needed.

Comparison with tun.ko

Feature camex tun.ko
External module dependency No Requires tun.ko
IPv4 Yes Yes
IPv6 No (intentional) Yes
TAP (L2) No Yes
Multiple device instances No (one) Yes (dynamic)
Multiple open file descriptors No (one) Yes
ioctl TUNSETIFF / TUNGETIFF No Yes
Persistent device No Yes
poll / select / epoll Yes Yes
O_NONBLOCK Yes Yes
Statistics (ip -s link) Yes Yes
Kernel 3.x support Yes Yes
Code size ~490 lines ~3500 lines

Internal Specifics

Compatibility Shims

All #if LINUX_VERSION_CODE guards are placed in a dedicated #define block at the top of camex-k.c. The driver body uses only macro names (CAMEX_ALLOC_NETDEV, CAMEX_POLL_T, CAMEX_SKB_PUT, etc.) with no version conditionals inside functions. This keeps the logic readable and easy to audit.

Registration and Deregistration Order

On insmod:

  1. alloc_netdev + camex_priv initialisation
  2. register_netdev → interface appears in ip link
  3. misc_register → /dev/camex appears in the filesystem

On rmmod:

  1. misc_deregister → /dev/camex disappears (new open() calls blocked)
  2. g_priv = NULL → protects against use-after-free in xmit path
  3. unregister_netdev + free_netdev

The reverse order guarantees that by the time memory is freed, no new accesses can arrive through either path (fd or netdev).

Carrier and Link State

  • netif_carrier_off on init — the interface shows NO-CARRIER until userspace opens /dev/camex.
  • netif_carrier_on on open() — carrier becomes active.
  • netif_carrier_off on close() — carrier is cleared; routes through the interface become unreachable.

GFP_ATOMIC in xmit

camex_net_xmit() is called from softirq context where sleeping is forbidden. Therefore kmalloc for pkt_node uses GFP_ATOMIC. If memory is unavailable in this context the packet is dropped (tx_dropped++).

Versioning & Release Process

Single source of truth

The only place to change the software version is src/version.h:

#define CAMEX_VERSION "X.Y.Z"

All other version references (Android, CI, git tags) must match this value. The CI pipeline enforces this automatically: a release build will fail if the git tag does not match CAMEX_VERSION.

Release checklist

  1. Bump src/version.h — update CAMEX_VERSION to the new version.
  2. Sync Android versions — run scripts/sync-android-version.sh (or make apk which calls it automatically). This updates:
    • android/app/build.gradle.kts — versionName + versionCode
    • android/app/src/main/AndroidManifest.xml — android:versionName + android:versionCode
  3. Commit: git commit -m "chore: bump version to X.Y.Z"
  4. Tag: git tag -a vX.Y.Z -m "Release vX.Y.Z"
  5. Push: git push origin master && git push origin vX.Y.Z
  6. CI creates the GitHub Release automatically with artifacts named camex-X.Y.Z-<platform>.

Version numbering rules

  • versionName (Android) = CAMEX_VERSION (e.g., "2.3.6")
  • versionCode (Android) = MAJOR * 10000 + MINOR * 100 + PATCH (e.g., 20306)
  • Git tag = v + CAMEX_VERSION (e.g., v2.3.6)
  • GitHub Release title = camex vX.Y.Z

Manual CI trigger

If a release needs to be triggered manually (e.g., re-uploading artifacts), use the workflow_dispatch event in the Actions tab with the version input set to the tag name (e.g., v2.3.6).

Notes

  • Each client should use a unique tunnel IP.
  • The server relays packets based on the inner IPv4 destination address.
  • Client sockets are outbound only; no listening port is opened on the client side.
  • In auto mode, -l, -g, and -c are supplied by the server.
  • examples/camex.conf shows the server-side parameter database format.
  • Protocol v2 works over both UDP and TCP transport; every session starts with a handshake (--server-key-file on the server, --server-pubkey and --psk on the client). camex 3.x does not talk to camex 2.x.
  • The server supports per-client PSKs: add psk=<key> inside a [client ID] block in the config file. Keys are bound to client IDs:
    • a client that has its own key can register only with that key;
    • the global --psk key works only for clients without their own key;
    • a manual client with its own key may omit --name: the key identifies it;
    • a manual client may only claim the local_cidr address of its own [client ID] block, and never the address of another client that has its own key.
  • The server accepts data packets only from an established session and only with the client's own tunnel address as the source; other source addresses are dropped.

License

MIT. See LICENSE for the full terms.

Support

Telegram

About

Minimal dependency-free UDP/TCP tunnel for embedded Linux targets.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages