Open source • Python • AIS NMEA 0183

AISMixer

AIS stream processing and routing platform

Normalize · Deduplicate · Tag · Route · Forward

Receive AIS feeds, turn receiver traffic into one controlled logical stream, and deliver it to the UDP egress targets that need it.

AIS stream processing

From receiver traffic to a clean, source-aware feed

AISMixer keeps transport, NMEA assembly, metadata, deduplication, and delivery in one near-real-time processing path.

Flexible ingress

Receive plain UDP over IPv4 or IPv6, or authenticated encrypted UDPSEC station traffic.

AIS sentence extraction

Extract both !AIVDM and !AIVDO from realistic receiver and application output.

Multipart assembly

Assemble fully out-of-order fragments into complete logical messages and emit them in ordinal order.

Near-real-time deduplication

Make one group-atomic decision for each complete multipart message, globally in broadcast mode or separately for each routing target.

Controlled TAG metadata

Control NMEA TAG s, c, and g across the multipart lifecycle while keeping metadata separate from routing identity.

Selected forwarding

Forward accepted sentences to every configured UDP output in broadcast mode or to the named targets selected by logical routing.

Project explainer

How AISMixer works

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.

Campaign C–F native-ready data-plane foundation

Explicit processing, bounded stages, and unified egress

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.

UDP / UDPSEC producers Private bounded IngressFrame queue per input
Bounded processing admission Backpressure, then bind one ProcessingSnapshot
Instance-owned PythonDataPlaneProcessor Bytes scan, assembly, TAG policy, and deduplication
Bounded ordered OutputBatch handoff Exact bytes and explicit numeric targets
Unified UDP egress Sequential local dispatch through send_to_ids()

Immutable ingress boundary

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.

Capacity-safe snapshot admission

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.

Instance-owned Python processor

One long-lived PythonDataPlaneProcessor owns mutable assembler, deduplication, source, multipart-metadata, and processor-metric state. No native processor or binding exists today.

Exact bytes in ordered results

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.

Backpressure and ordered completion

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.

Process-local fail-fast lifecycle

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

Deterministic state under load

The reference implementation makes retained state, lifetime progress, capacity admission, cleanup, and observation explicit instead of relying on incidental container behaviour.

Deduplication state

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.

Multipart assembly state

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.

Secure replay and sessions

Handshake replay records, pending sessions, active sessions, and the private nonce state owned by each pending or active session are hard-bounded. Monotonic local time governs TTL, and expired state is removed before deterministic live-state eviction.

Installing a same-address pending candidate preserves the current active session and replaces only an older pending candidate. The active session changes only after authenticated encrypted confirmation and pending-to-active promotion; a failed confirmation, or pending expiry, replacement, or capacity eviction, leaves it intact. All secure state is in-memory, process-local, and lost at restart.

Observable component and runtime state

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.

Campaign F — implemented

Worker-ready boundaries in one process

Campaign F completed bounded, observable boundaries around the current Python runtime. 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.

Private bounded ingress

Every configured UDP or UDPSEC input has its own bounded queue, keeping one input’s queued backlog from consuming another input’s private capacity.

Capacity-safe admission

Shared processing admission is bounded. A frame receives its immutable ProcessingSnapshot only after capacity is obtained, and a full stage waits with backpressure.

Processor-instance ownership

One processor instance owns mutable assembly, deduplication, source, multipart-metadata, and processor-metric state behind an explicit lifecycle boundary.

Bounded ordered egress

A bounded handoff carries each non-empty OutputBatch to sequential local dispatch. The completion barrier preserves processing order but is not remote receipt.

Pull-based runtime observability

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

Express stream policy with source identities

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.

Named ingressUDP and UDPSEC sources
Logical zonesSets of source identities
Ordered routesDeclarative target selection
Named egressSelected UDP targets

Composable source sets

Define zones with include, union, intersection, and difference.

Logical, not geographic

Zones are sets of internal source identities—not map areas, MMSI filters, vessel filters, or payload rules.

Preserved order and per-target quality

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

Control routing and inspect runtime activity

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. Running it without a command opens the interactive operator shell.

Routing status and atomic changes

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.

Aggregate runtime statistics

runtime.statistics reports bounded queue and backpressure state, processor activity, and egress activity in one fresh process-local pull.

Input and output traffic

runtime.statistics.inputs separates transport traffic from accepted frames. runtime.statistics.outputs reports per-target local dispatches, messages, and bytes.

Interactive or one-shot

The no-command shell provides help, tables, history where available, and the same routing and statistics operations retained for one-shot JSON use.

aismixerctl> status
aismixerctl> show statistics
aismixerctl> show statistics inputs
aismixerctl> show statistics outputs

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

Constrain peers and outbound source addresses

Small application-level controls make IPv4 and IPv6 endpoints easier to place inside an operator’s wider firewall and routing policy.

Ingress allow_from

Restrict UDP and UDPSEC listeners to literal IP addresses or CIDR networks, with explicit deny-all behavior available.

Outbound source_ip

Bind an egress socket to a literal IPv4 or IPv6 source address and constrain destination resolution to the same family.

Part of a layered boundary

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.

UDPSEC lifecycle

Authenticated transport with bounded local state

The current nmea_sproxy and AISMixer UDPSEC path uses authenticated ephemeral ECDHE and independent directional keys, with encrypted sequence-zero confirmation before the client treats a new session as established.

Authenticated ClientHello Identity-signed, fresh P-256 ephemeral key
Ephemeral P-256 ECDHE Fresh shared secret for this handshake
HKDF-SHA256 Independent C2S and S2C keys
Encrypted sequence-zero confirmation C2S ping, then S2C pong
Active session Authenticated ping promotes; authenticated pong completes

Authenticated ephemeral key agreement

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.

Encrypted mutual confirmation

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.

Coordinated update and local state

Both endpoints must be updated together; there is no legacy fallback, mixed mode, negotiation switch, certificate system, or algorithm negotiation.

Allowed traffic drives expiry cleanup rather than a background timer. Replay, pending-session, active-session, and nonce state is in-memory, process-local, non-durable, and lost when the service restarts.

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 AIS truthfulness or detect 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; NOSESSION hints are unauthenticated.

Read the security policy, behavioural contract, nmea_sproxy guide, and Wiki for the detailed boundaries.

nmea_sproxy and physical AIS receivers

Connect networked or physical AIS receivers through nmea_sproxy

nmea_sproxy is the station-side proxy that connects one local AIS source to one configured AISMixer destination.

Physical AIS receiver
serial port or USB virtual COM
nmea_sproxy
UDPSEC or explicitly trusted plain UDP
AISMixer

Implemented local inputs

  • UDP from a local networked receiver or AIS application.
  • Direct physical serial-port input.
  • USB virtual COM input from a physical AIS receiver.

Implemented outputs

  • Authenticated encrypted UDPSEC using the bounded session lifecycle described above.
  • Explicit plain UDP for a trusted LAN, VPN, or equivalent boundary.

Clear relation boundary

One local input maps to one configured output. nmea_sproxy does not mix, fan out, route, deduplicate, assemble multipart AIS, or rewrite TAG metadata.

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.

Deployment and operations

A Linux and systemd lifecycle built for real installations

The repository-managed deployment keeps runtime files, operator state, systemd integration, local routing control, and read-only runtime statistics aligned.

Repository-managed systemd

The service provisions /run/aismixer while AISMixer is running and supports the optional local control socket.

Install, update, and uninstall

Privilege-aware lifecycle scripts handle root or sudo operation and maintain the deployed runtime and service units.

Operator state preserved

Normal install, update, and uninstall flows preserve operator configuration and cryptographic keys.

Global operator CLI

The installed workflow places aismixerctl in /usr/local/bin for local runtime routing and read-only statistics, including the no-command interactive shell.

Documentation and project status

Implemented foundations, clearly separated from what comes next

The Wiki is the detailed guide to architecture, configuration, routing, security, and operations. The Roadmap separates the completed baseline from future work without promising dates.

Implemented now

  • Immutable bytes-based IngressFrame values, bytes-native scanning, parse-once metadata, and PythonDataPlaneProcessor as the sole current production and reference processor.
  • Private bounded ingress queues, shared bounded processing admission, bounded egress handoff, backpressure, processor-instance state ownership, and admission-time ProcessingSnapshot binding.
  • Compiled numeric target-only routing, exact immutable bytes encoded once per emitted sentence, ordered OutputBatch results, and an ordered local egress completion barrier.
  • Logical zones and target-scoped deduplication, atomic process-local routing updates, interactive aismixerctl, and read-only aggregate, per-input, and per-output statistics.
  • Deterministic multipart assembly, TAG s/c/g handling, and global or target-scoped deduplication.
  • Authenticated ephemeral P-256 ECDHE UDPSEC, long-term P-256 ECDSA identities, independent directional AES-GCM keys, and hard-bounded local secure state.
  • One UDP or serial input to one UDPSEC or explicit trusted plain-UDP output per nmea_sproxy process or systemd instance, plus IPv4/IPv6 endpoint controls.
  • systemd RuntimeDirectory, privilege-aware lifecycle scripts, preserved operator state, and the global operator CLI.

Planned work

  • A coordinator process and actual separate ingress and egress worker processes.
  • IPC, cross-process routing-snapshot distribution and supervision, worker restart and recovery policy, and metrics aggregation where required.
  • A future native processor and bindings behind the established contracts, with differential conformance; none exists today, and no performance result is claimed.
  • A broader ingress and egress adapter architecture beyond the current UDP pathways.

Project community

Community and contact

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.

Browse discussions

Explore public project conversations, announcements, deployment experience, field testing, and other community topics.

Browse GitHub Discussions

Share an idea

Discussions is suitable for architecture proposals, integration ideas, deployment experience, AIS field testing, and academic or research collaboration that can be discussed publicly.

Open Ideas

Report a problem

Use GitHub Issues for reproducible defects and concrete repository work, not general private correspondence.

Open GitHub Issues

Ask in Q&A

Use Q&A for focused questions about installation, configuration, usage, architecture, and operation.

Open Q&A

Founder and maintainer

Iliyan Iliev, PhD

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. Both tools are used under the founder’s direction; project decisions and final responsibility remain human.