{"id":51595204,"url":"https://github.com/hyperized/beastmux","last_synced_at":"2026-07-11T18:01:45.859Z","repository":{"id":359381840,"uuid":"1245690581","full_name":"hyperized/beastmux","owner":"hyperized","description":"BEAST TCP Multiplexer","archived":false,"fork":false,"pushed_at":"2026-05-21T15:58:27.000Z","size":39,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T00:50:49.865Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hyperized.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-21T13:12:12.000Z","updated_at":"2026-05-21T15:59:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hyperized/beastmux","commit_stats":null,"previous_names":["hyperized/beastmux"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/hyperized/beastmux","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Fbeastmux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Fbeastmux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Fbeastmux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Fbeastmux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hyperized","download_url":"https://codeload.github.com/hyperized/beastmux/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Fbeastmux/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35370428,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-11T02:00:05.354Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-11T18:01:45.154Z","updated_at":"2026-07-11T18:01:45.852Z","avatar_url":"https://github.com/hyperized.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# beastmux\n\nMultiplex N upstream Mode S BEAST TCP streams into one consolidated, deduplicated stream.\n\n`beastmux` is the dual of [`beast-splitter`](https://github.com/openskynetwork/beast-splitter): splitter fans one BEAST source out to many consumers; beastmux fans many BEAST sources into one consumer. It does not demodulate — it operates entirely at the TCP tier on already-decoded BEAST frames.\n\n## Use case\n\nMultiple ADS-B receivers covering overlapping airspace (different antennas, different sites on the same network) each publish their decoded frames on `:30005` (the `dump1090` / `readsb` / `demod1090` convention). A consumer downstream — a feeder, a tar1090 instance, a logger — wants the combined recall of all receivers without paying for N× the frame volume from duplicates.\n\n```\n  RTL-SDR A → demod1090 (BEAST :30005) ─┐\n  RTL-SDR B → demod1090 (BEAST :30005) ─┼─→ beastmux ─→ BEAST :30005 → tar1090 / feeder / logger\n  RTL-SDR C → demod1090 (BEAST :30005) ─┘\n```\n\n## Scope\n\n- Pure Go, stdlib networking, no CGo. Single static binary.\n- TCP-in / TCP-out only. No demodulation, no decoding above frame-bytes level.\n- Payload-only dedup over a short sliding window (default 500 ms).\n- First-arrival wins — minimises end-to-end latency, avoids per-frame deadlines.\n\nOut of scope: any radio I/O, frame decoding (DF parsing, CPR, callsigns, etc. — that's [`hyperized/modes`](https://github.com/hyperized/modes)), TLS termination (use `stunnel`, WireGuard, or `socat`), and multi-process clustering.\n\n## Layout\n\n```\ndedup/          TTL'd 64-bit-key set + hash/maphash keying helper\nsourcemgr/      Per-upstream TCP dialer + beast.Reader + reconnect/backoff\ncmd/beastmux/   CLI entry point — wires sources → dedup → beastsrv\ncontrib/systemd Sample beastmux.service unit\n```\n\n`cmd/beastmux` depends on [`hyperized/demod1090`](https://github.com/hyperized/demod1090) for the streaming `beast.Reader` and the `beastsrv` fanout server.\n\n## Build \u0026 test\n\n```sh\nmake                 # fmt + vet + test (race + cover) — the default\nmake lint            # golangci-lint run ./...\nmake cover           # write coverage.html alongside coverage.out\nmake build           # build cmd/beastmux for the host\nmake build-aarch64   # cross-compile cmd/beastmux for linux/arm64\n```\n\nIntegration tests live behind the `integration` build tag and stay out of the default `make test`:\n\n```sh\nmake test-integration\n```\n\n## Usage\n\n```sh\nbeastmux \\\n    --source receiver-a:30005 \\\n    --source receiver-b:30005 \\\n    --source receiver-c:30005 \\\n    --listen :30005\n```\n\nThat binds one consolidated BEAST listener on `:30005` and pulls from three upstreams. `--source` is repeatable; at least one is required.\n\n### Flags\n\n| Flag | Default | Purpose |\n| --- | --- | --- |\n| `--source host:port` | — (required) | Upstream BEAST endpoint. Repeatable. |\n| `--listen host:port` | `:30005` | Consolidated BEAST listener. Empty disables the server (useful with `--stdout-hex`). |\n| `--dedup-window` | `500ms` | TTL applied to each payload key. |\n| `--reconnect-min` | `1s` | Initial backoff after a source disconnects / fails to dial. |\n| `--reconnect-max` | `30s` | Cap on reconnect backoff. |\n| `--stats-interval` | `10s` | Stats log cadence. `0` disables. |\n| `--stdout-hex` | `false` | Also emit each forwarded frame as a lowercase hex line on stdout. |\n| `--log-format` | `text` | `text` or `json`. |\n| `--version` | — | Print version and exit. |\n\n### Dedup keying\n\nThe key is `hash/maphash` over the raw Mode S payload only — the BEAST envelope's 12 MHz timestamp and RSSI byte are deliberately excluded so two receivers seeing the same on-air frame produce the same key. The seed is randomised per process start; keys are not stable across runs.\n\nA frame is a duplicate iff its key was seen within `--dedup-window`. The default of 500 ms covers ~50–200 ms of BEAST publish jitter plus a few ms of LAN propagation, while being short enough that a genuine retransmission seconds later counts as a fresh frame.\n\n### Backpressure\n\nThe shared source-to-dedup channel is buffered at 1024 frames. Each source uses a non-blocking send: if the channel is full, the frame is counted as a drop on that source rather than back-pressuring the upstream TCP read. The stats log line surfaces per-source drop counts; sustained drops indicate the downstream consumer can't keep up.\n\n## Deployment recipes\n\n### Single host: collocated with multiple dump1090 / demod1090 instances\n\nThe simplest case — one Pi runs several receivers (different antennas, different gain settings) plus beastmux to consolidate.\n\n```sh\n# Demod processes on local ports 30005, 30006, 30007 (one per dongle).\nbeastmux \\\n    --source 127.0.0.1:30005 \\\n    --source 127.0.0.1:30006 \\\n    --source 127.0.0.1:30007 \\\n    --listen :30100\n```\n\nPoint tar1090 / readsb at `:30100` as their BEAST input. Leave the original receiver ports for direct debugging.\n\n### Multi-host via SSH reverse tunnel\n\nTwo sites, no public IPs, one wants the other's frames. BEAST has no authentication or encryption — never expose it on a public network. SSH carries the bytes.\n\nOn the producer host:\n\n```sh\n# Reverse-tunnel the producer's :30005 onto the consumer's loopback :30006\nssh -N -R 30006:127.0.0.1:30005 consumer-host\n```\n\nOn the consumer host:\n\n```sh\nbeastmux \\\n    --source 127.0.0.1:30005 \\\n    --source 127.0.0.1:30006 \\\n    --listen :30100\n```\n\nFor permanent links use `autossh` or a systemd `ssh.service` with `ServerAliveInterval=15`/`ServerAliveCountMax=3`.\n\n### Sidecar to tar1090\n\ntar1090 expects a BEAST stream on `:30005`. If the box has both a local receiver and remote ones, run beastmux on `:30005` and move the local receiver to a different port.\n\n```sh\n# Local demod1090 publishes on :30006 (was :30005).\n# Remote site is reachable via SSH tunnel on :30007.\nbeastmux \\\n    --source 127.0.0.1:30006 \\\n    --source 127.0.0.1:30007 \\\n    --listen :30005 \\\n    --stats-interval 30s \\\n    --log-format json\n```\n\ntar1090 dials `:30005` as before. Stats land in journald as one structured log line every 30 s.\n\n## Operations\n\n### Stats log line\n\nDefault `--stats-interval 10s` emits one `slog.Info` record per interval with per-source frame / drop deltas, dedup ratio, and forwarded count. JSON mode (`--log-format json`) makes it grep-friendly:\n\n```json\n{\"time\":\"...\",\"level\":\"INFO\",\"msg\":\"beastmux: stats\",\"interval\":\"10s\",\n \"seen\":4123,\"dupes\":1872,\"forwarded\":2251,\"dedup_ratio\":0.453,\n \"receiver-a:30005\":{\"frames\":2110,\"drops\":0},\n \"receiver-b:30005\":{\"frames\":2013,\"drops\":0}}\n```\n\n`dedup_ratio` rising above ~0.5 with two well-aligned receivers is normal. Approaching 0 means your sources don't overlap; the dedup buys nothing and the second source is purely additive coverage.\n\n### systemd\n\n`contrib/systemd/beastmux.service` ships a hardened sample unit. Install:\n\n```sh\nsudo install -m 0644 contrib/systemd/beastmux.service /etc/systemd/system/\nsudo useradd --system --no-create-home --shell /usr/sbin/nologin beastmux\nsudo install -m 0755 -o beastmux -g beastmux beastmux /opt/beastmux/beastmux\nsudo systemctl daemon-reload\nsudo systemctl enable --now beastmux.service\n```\n\nThen edit the `ExecStart` line in the unit to point at your real upstream addresses. The unit defaults to `--log-format json` so the stats line flows through journald cleanly. Stdout is dropped to keep `--stdout-hex` (if you enable it for debugging) from flooding the journal.\n\n### Graceful shutdown\n\n`SIGINT` and `SIGTERM` cancel the daemon's root context. Sources exit, in-flight publishes drain, the listener closes, and the process exits with status 0. `systemctl stop` and Ctrl-C both go through this path.\n\n### Hot-reload\n\nThere is no reload — beastmux is restart-on-change. The systemd unit's `Restart=on-failure` only covers crashes; configuration changes need `systemctl restart beastmux`.\n\n### TLS / authentication\n\nBEAST has neither. For untrusted networks, terminate TLS externally:\n\n```sh\n# stunnel example on the producer host: accept TLS on :30443, forward to :30005.\nstunnel -fd 0 \u003c\u003c'EOF'\n[beast]\naccept = 30443\nconnect = 127.0.0.1:30005\ncert = /etc/stunnel/server.pem\nEOF\n```\n\nOr use WireGuard / Tailscale for transport-layer encryption + auth and let beastmux dial the WireGuard-local address.\n\n## License\n\n[Business Source License 1.1](LICENSE). Free for non-production use; production use requires a separate licence. Change Date: see `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyperized%2Fbeastmux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhyperized%2Fbeastmux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyperized%2Fbeastmux/lists"}