Flexible ingress
Receive plain UDP over IPv4 or IPv6, or authenticated encrypted UDPSEC station traffic.
AIS stream processing and routing platform
Normalize · Deduplicate · Tag · Route · Forward
Combine AIS feeds from fixed, shore, harbour, and mobile receivers into one controlled logical stream, and deliver it to the UDP egress targets that need it.
The source code is publicly available under CC BY-NC 4.0; commercial use is not permitted by that license.
AIS stream processing
AISMixer keeps transport, NMEA assembly, metadata, deduplication, and delivery in one near-real-time processing path. Its runtime components are the aismixer mixer, router, and UDPSEC server; aismixerctl for local routing control and runtime statistics; and nmea_sproxy, the station-side proxy from one local UDP or serial/USB source to one UDPSEC or explicitly configured plain-UDP destination.
Receive plain UDP over IPv4 or IPv6, or authenticated encrypted UDPSEC station traffic.
Extract both !AIVDM and !AIVDO from realistic receiver and application output.
Assemble fully out-of-order fragments into complete logical messages and emit them in ordinal order.
Make one group-atomic decision for each complete multipart message, globally in broadcast mode or separately for each routing target.
Control NMEA TAG s, c, and g across the multipart lifecycle while keeping metadata separate from routing identity.
Forward accepted sentences to every configured UDP output in broadcast mode or to the named targets selected by logical routing.
Project explainer
A short visual walkthrough from multiple AIS sources through assembly, deduplication, TAG handling, and logical routing to selected outputs, including nmea_sproxy and UDPSEC. The explainer is in English.
Native-ready data-plane foundation
Immutable bytes-based ingress moves through bounded, explicit single-process stages. After shared processing capacity becomes available, each admitted frame is bound to one ProcessingSnapshot and handled by the instance-owned PythonDataPlaneProcessor—the sole current production and reference processor. It returns one ordered immutable OutputBatch whose ProcessorOutput values carry exact bytes and numeric target IDs through the bounded egress handoff.
Built-in UDP and UDPSEC producers create immutable bytes-based IngressFrame objects. Bytes-native scanning and parse-once ParsedSentence metadata carry fragment and TAG information into processing without repeated extraction.
Processing capacity is obtained before one immutable ProcessingSnapshot is bound to the frame. Admitted work keeps that snapshot; frames still waiting for capacity may observe a later process-local routing replacement.
One long-lived PythonDataPlaneProcessor owns mutable assembler, deduplication, source, multipart-metadata, and processor-metric state. No native processor or binding exists today.
Each emitted NMEA sentence is encoded as UTF-8 once and becomes one exact immutable payload in an ordered OutputBatch. Every ProcessorOutput carries explicit numeric target IDs; multipart fragments remain separate payloads.
A full private-ingress, shared-processing, or egress boundary waits instead of dropping its queued item. A non-empty batch prevents later-frame processing until sequential local dispatch completes. This is neither durability nor remote delivery acknowledgement, and UDP remains lossy.
Essential asyncio stages share one supervised lifecycle within the current process. Failure or unexpected completion cancels and awaits sibling tasks; there is no automatic restart, retry, rollback, delivery acknowledgement, or replay.
Need the exact edge-case semantics? Read the behavioural contract.
Explicit lifecycle ownership
The reference implementation makes retained state, lifetime progress, capacity admission, cleanup, and observation explicit instead of relying on incidental container behaviour.
Identity is the exact sentence or ordered multipart tuple, in a global or per-target scope. TTL is deterministic, duplicates do not refresh retention, and expired entries are removed before the oldest live entry is evicted.
The Python object accepts optional max_entries; current service wiring leaves it at None, so this dimension is unbounded in the current service.
Fragments occupy ordinal slots. Unique progress refreshes group lifetime; an exact duplicate does not. Conflict, expiry, capacity removal, completion, and reset remain distinct lifecycle outcomes and clean related TAG context.
The Python object accepts optional max_fragments_per_group and max_pending_groups; current service wiring leaves both at None.
Handshake replay records, pending sessions, active sessions, and the DATA-nonce replay ledger of each key epoch are hard-bounded; during an in-session key-epoch refresh a session holds at most three live epochs. Handshake and session lifetimes use monotonic TTLs. Every admitted DATA nonce is instead retained for its epoch's full usable lifetime, has no independent TTL, and is not evicted. Exhausting the current epoch's ledger fails closed and ends the session, and recovery uses a fresh authenticated handshake; exhausting a pending or retiring epoch discards only that epoch. Path-migration state is limited to one candidate and one retired path per session.
A pending candidate replaces only an older candidate for the same physical-listener/peer relation and preserves any active session until authenticated encrypted confirmation succeeds. Sessions from the same peer on separate physical UDPSEC listeners are isolated from one another; aggregate capacity budgets remain process-wide. All secure state is in-memory, process-local, and lost at restart.
Deduplication, assembly, and secure owners expose immutable internal lifecycle snapshots. The runtime separately offers fresh pull-based views of queues, processor and egress activity, and input and output traffic. Reading either kind of snapshot does not mutate processing state.
These views are process-local and non-durable; they are not distributed metrics or a Prometheus/time-series export system.
Local by design. This state is non-durable and is not shared across processes. The current runtime supervises essential asyncio tasks within one process; coordinator/worker processes, IPC, and cross-process state synchronization do not exist yet.
Worker readiness — implemented
The current Python runtime has bounded, observable boundaries around its processing stages. Mutable processing state has an explicit instance owner, work is admitted only when capacity is available, and fresh pull-based statistics make stage activity visible without changing it.
Every configured UDP or UDPSEC input has its own bounded queue, keeping one input’s queued backlog from consuming another input’s private capacity.
Shared processing admission is bounded. A frame receives its immutable ProcessingSnapshot only after capacity is obtained, and a full stage waits with backpressure.
One processor instance owns mutable assembly, deduplication, source, multipart-metadata, and processor-metric state behind an explicit lifecycle boundary.
A bounded handoff carries each non-empty OutputBatch to sequential local dispatch. The completion barrier preserves processing order but is not remote receipt.
Fresh immutable snapshots cover queue and backpressure state, processor and egress activity, raw and admitted input traffic, and per-target output messages and bytes.
Worker Readiness is implemented; workers are not. The current service still has one process with supervised asyncio stages. There is no coordinator, separate ingress or egress worker process, multiprocessing, IPC, cross-process routing or metrics aggregation, or automatic worker restart and recovery. No native processor or bindings exist today.
Routing and logical zones
Static logical routing connects named ingress sources to named UDP egress targets through reusable source sets and ordered routes. Names remain the configuration and control interface, while production matching uses a precompiled numeric target-only plan.
Define zones with include, union, intersection, and difference.
Zones are sets of internal source identities—not map areas, MMSI filters, vessel filters, or payload rules.
The compiled plan preserves declared target order. Routing mode scopes deduplication to each logical target, so one dispatch path does not suppress another.
Names stay operator-facing. Process-local numeric target IDs are not configuration values. source_id remains separate from the NMEA TAG s value emitted downstream.
Local control and runtime observability
The optional local POSIX Unix-domain control plane exposes routing operations and read-only aggregate, per-input, and per-output runtime statistics through the globally installed aismixerctl command.
Inspect the active generation, zones, routes, and targets; replace a validated snapshot or disable routing to return to legacy broadcast mode. Optional generation checks reject stale updates.
runtime.statistics reports bounded queue and backpressure state, processor activity, and egress activity in one fresh process-local pull.
runtime.statistics.inputs separates transport traffic from accepted frames. runtime.statistics.outputs reports per-target local dispatches, messages, and bytes.
Use aismixerctl for local routing changes and read-only runtime inspection, with human-readable output for operators and JSON output for one-shot automation.
Read-only statistics boundary. Statistics are in-memory, process-local, non-durable, and reset at restart. They are not persistent history, distributed metrics, or a Prometheus/time-series system. Successful local UDP dispatch is not remote delivery acknowledgement.
Deliberately local control. Runtime routes are not persisted after restart, configuration files are not rewritten, and adapters are not created dynamically. Unix socket permissions are the current authorization boundary; there is no application-level control token.
Network endpoint controls
Small application-level controls make IPv4 and IPv6 endpoints easier to place inside an operator’s wider firewall and routing policy.
Restrict UDP and UDPSEC listeners to literal IP addresses or CIDR networks, with explicit deny-all behavior available.
Bind an egress socket to a literal IPv4 or IPv6 source address and constrain destination resolution to the same family.
These policies complement operating-system firewall and routing rules; they do not replace them.
source_ip is source-address binding—not interface selection, routing-table selection, socket marking, or SDN.
UDPSECv2 secure transport
UDPSEC is AISMixer's project-specific authenticated and encrypted UDP transport between nmea_sproxy stations and the aismixer service. The current source implements UDPSECv2: configured long-term P-256 ECDSA identities, signed ephemeral P-256 ECDHE establishment with encrypted sequence-zero proof of key possession, AES-256-GCM protected DATA, replay protection, authenticated liveness, in-session key-epoch refresh, and authenticated path migration. The session-continuity parts are not yet in a tagged release or in the published OpenWrt packages.
For every session handshake, each endpoint creates a fresh ephemeral P-256 ECDHE keypair. Long-term P-256 identity keys only authenticate canonical, role-separated SHA-256 transcript digests with ECDSA; they are not ECDHE key-agreement inputs.
HKDF-SHA256 derives independent C2S and S2C keys from the shared secret and authenticated transcript. AES-256-GCM protects encrypted data and control traffic.
The client sends an encrypted sequence-zero ping for the pending session. The server promotes only after authenticating it and returns an encrypted sequence-zero pong under the promoted S2C key; the client treats the session as established only after authenticating that pong.
Unrelated, malformed, stale-key, wrong-sequence, and wrong-source datagrams neither confirm the candidate nor extend its fixed confirmation deadline.
A session is bound to the authenticated station and identified by a mixer-issued session locator, never by the source IP address and port. After confirmation, NMEA DATA, keepalive ping/pong, in-session control, and best-effort close are authenticated and encrypted, and every packet is bound to its session and key epoch. Receivers keep every admitted nonce for its epoch's lifetime and reject replays.
With session_refresh_interval above zero, the station renews the traffic keys inside the same session through a fresh signed ECDHE exchange; this is neither a new session nor a path change. UDPSEC has no plaintext reset, NOSESSION recovery, downgrade control, or automatic fallback to plain UDP, and it does not buffer or replay NMEA payloads. Secure session and replay state is in-memory, process-local, non-durable, and lost at restart.
Update both endpoints together: the current UDPSECv2 wire format does not interoperate with earlier builds, including the published 0.2.1 OpenWrt packages.
Forward-secrecy boundary: Later compromise of a long-term identity key does not by itself reconstruct keys for completed sessions when old ephemeral private keys and raw shared secrets are gone and neither endpoint was compromised while they were in memory. An identity key compromised in the present enables future impersonation. This implemented property is supported by engineering validation, not formal cryptographic verification.
Transport boundary: UDPSEC authenticates and encrypts data and control packets within its scope, but does not prove vessel identity, position, physical origin, or the absence of AIS spoofing; guarantee UDP delivery or ordering; protect compromised endpoints; or provide complete denial-of-service resistance. It does not hide peer IP addresses, packet timing or lengths, or the cleartext ClientHello station identifier.
Read the security policy, behavioural contract, nmea_sproxy guide, and Wiki for the detailed boundaries.
Mobile continuity
UDPSEC works through NAT and CGNAT. Mobile stations lose packets, and their public IP address or UDP port can change. Field runs showed that cellular handovers often keep the same public address and port, so UDPSECv2 treats a temporary loss differently from a real address change.
Liveness recovery. The station resends its one outstanding keepalive ping at a bounded rate, with the same sequence number and a fresh encryption each time, until an authenticated answer arrives. One lost exchange does not end the session; the only terminal bound is peer_timeout after the last authenticated evidence.
Authenticated path migration. A new address is not trusted automatically: its packets must authenticate under the session's current keys. The aismixer service then challenges that address and moves its replies there only after the station's authenticated response arrives from it. The challenge is sent at most four times in total, at least 2 seconds apart, within a fixed 10-second window; retries improve delivery but grant no authority.
Fresh authenticated establishment. Once the liveness bound is reached, the station performs a new signed handshake, and the aismixer service expires the idle old session on its own.
Bounded, not lossless. Forwarding continues while liveness is in doubt, but UDPSEC never buffers or resends NMEA: sentences sent into an outage can be lost even when the session survives, and no session survives every outage. Same-path liveness recovery has been observed in a real road test; path migration is validated by end-to-end tests. Both are in the current source, not yet in a tagged release.
nmea_sproxy and physical AIS receivers
nmea_sproxy is the station-side proxy that connects one local UDP or physical serial/USB AIS input to one network output: UDPSEC or explicitly configured plain UDP.
One local input maps to one configured output. The proxy forwards the bare supported AIS sentence match, stripping ingress TAG blocks, prefixes, surrounding bytes, and input terminators. nmea_sproxy does not mix, fan out, route, deduplicate, or assemble multipart AIS, and path-migration decisions belong to the aismixer service.
A networked receiver can feed local UDP into nmea_sproxy or send UDP directly to aismixer, depending on the deployment. The main aismixer process does not read serial ports directly. Trusted plain UDP does not provide UDPSEC encryption, authentication, replay protection, or liveness.
OpenWrt edge deployment
OpenWrt is a supported edge-deployment target for both the aismixer service and nmea_sproxy. The signed AISMixer APK repository for OpenWrt 25.12 currently publishes the 0.2.1-r4 Python/procd package snapshot, built from the v0.2.1 release, for x86_64 and mips_24kc.
Install aismixer or nmea_sproxy. The shared aismixer-common dependency is resolved automatically when installing either package.
Published repository indexes:
Both architectures use the same repository public key; add it under /etc/apk/keys/ before using either repository index.
Key SHA-256: 170d30219e0e05d59898cd8ccd5ec9804e915df7882ab56b8e869ef6e99c8f9c
The aismixer service can run on an OpenWrt edge/router host. nmea_sproxy can run independently near a receiver with UDP or physical serial input, including USB virtual serial devices exposed by the OS. Its output can be plain UDP, or UDPSEC can protect the network hop to the aismixer service.
CDC ACM devices commonly appear as /dev/ttyACM*; USB-UART adapters commonly appear as /dev/ttyUSB*. The required OpenWrt kernel driver may already be available in the image or may need the appropriate kmod package, such as kmod-usb-acm for CDC ACM. Native UART and UDP input do not require a USB serial driver.
The packages ship no private keys. The OpenWrt aismixer init flow prepares its local identity and, when a valid private key exists, can repair the matching public key. nmea_sproxy may create the canonical station key pair only when both key files are absent; a partial, invalid, or mismatched proxy pair fails closed without automatic replacement. UDPSEC peer trust is always provisioned manually: install the aismixer public key at the station and authorize the station public key on the aismixer side.
Package architecture. PKGARCH:=all describes the architecture-independent Python and shell payload; it does not make the package available in every target feed. The currently built, published, and validated feeds are x86_64 and mips_24kc, without intentionally excluding other suitable targets. Select the feed architecture configured for the device; do not derive it from apk --print-arch.
Writable space. The Python runtime and dependencies require materially more writable space than a minimal OpenWrt installation. Verify available overlay space before installation; extroot is an OpenWrt deployment option where internal writable storage is limited.
First-start safety. Apply firewall or network isolation and verify writable space before installation. The package hooks enable and start aismixer during installation, and the seeded configuration includes broadly listening plain-UDP listeners. Stop the service immediately afterward, configure listeners and UDPSEC authorization, and only then start and inspect it.
# Currently published feeds: x86_64 or mips_24kc
AISMIXER_ARCH='x86_64'
wget -O /etc/apk/keys/aismixer-openwrt.pem \
https://aismixer.net/openwrt/keys/aismixer-openwrt.pem
REPO_FILE=/etc/apk/repositories.d/customfeeds.list
REPO_URL="https://aismixer.net/openwrt/25.12/${AISMIXER_ARCH}/packages.adb"
grep -qxF "$REPO_URL" "$REPO_FILE" 2>/dev/null || {
printf '\n# AISMixer OpenWrt 25.12 %s repository\n%s\n' \
"$AISMIXER_ARCH" "$REPO_URL" >> "$REPO_FILE"
}
apk update
apk add aismixer
/etc/init.d/aismixer stop
vi /etc/aismixer/config.yaml
vi /etc/aismixer/authorized_keys.yaml
/etc/init.d/aismixer start
/etc/init.d/aismixer status
logread -e aismixer
Package snapshot. The published 0.2.1-r4 packages are built from the pinned v0.2.1 release. They predate the UDPSECv2 session-continuity work in the current source, and their UDPSEC wire format does not interoperate with current source-tree builds: keep both ends of a UDPSEC relation on the same release line. Packages with this work need a later release, at which the recipe is repinned. Check the package revision, Changelog, and Releases.
End-to-end validation. The mips_24kc deployment path of the published package line has been validated end to end with physical serial AIS ingress, nmea_sproxy, authenticated UDPSEC, processing by the aismixer service, procd service operation, and local runtime control and observability.
Receiver-side endpoint only. Use apk add nmea_sproxy instead of apk add aismixer to install the station proxy independently. See the README, nmea_sproxy guide, and OpenWrt Deployment Wiki for complete deployment and trust-provisioning steps.
Debian-family deployment and operations
For Debian-family systemd hosts, including Debian and Raspberry Pi OS, the repository-managed deployment keeps runtime files, operator state, local routing control, and read-only runtime statistics aligned. OpenWrt uses the APK/procd path described above.
The aismixer service provisions /run/aismixer while running and supports the optional local control socket.
Privilege-aware lifecycle scripts handle root or sudo operation and maintain the deployed runtime and service units. The aismixer updater restarts its service and can start an inactive service; the nmea_sproxy updater reloads systemd but does not start or restart proxy instances. Update UDPSEC stations and the mixer together. See the README and nmea_sproxy guide.
Normal install, update, and uninstall flows preserve operator configuration and cryptographic keys.
The installed workflow places aismixerctl in /usr/local/bin for local runtime routing and read-only statistics.
Documentation and project status
The repository README is the current project and operator overview, the behavioural contract defines exact semantics, and the Wiki holds deeper architecture and deployment guides. The Roadmap separates the completed baseline from future work without promising dates.
Release status. The latest tagged release is v0.2.1. The current main source also contains unreleased work, including the UDPSECv2 session continuity described above. The published OpenWrt packages (0.2.1-r4) are built from v0.2.1. See the Changelog.
Project community
Choose the public channel that fits the conversation. Discussions, Ideas, and Q&A help other users too; reproducible defects belong in Issues. Private contact remains available in the maintainer profile below.
Explore public project conversations, announcements, deployment experience, field testing, and other community topics.
Browse GitHub DiscussionsDiscussions is suitable for architecture proposals, integration ideas, deployment experience, AIS field testing, and academic or research collaboration that can be discussed publicly.
Open IdeasUse GitHub Issues for reproducible defects and concrete repository work, not general private correspondence.
Open GitHub IssuesUse Q&A for focused questions about installation, configuration, usage, architecture, and operation.
Open Q&AFounder and maintainer
Founder, originator, and maintainer of AISMixer
Backend and network systems developer and researcher with interests in AIS/NMEA processing, maritime and regional security, transport, and tourism.
AI-assisted development: ChatGPT has supported architectural reasoning, planning, documentation, and review. OpenAI Codex has supported repository analysis, implementation, testing, and validation. Anthropic Claude Code has supported repository review, implementation handoff, cross-checking, testing, and validation. These tools are used under the founder’s direction; project decisions and final responsibility remain human.