| pick your distro, get ZFS on root
kldload — your platform, your way, free
Source

Appliance Recipe: Dish Receiver & Signal Observatory

A kldload appliance that turns a satellite dish, SDR dongles, and a GPU edge node into a full signals observatory — capturing local broadcast, satellite downlinks, airborne transponders (ADS-B, ACARS, VDL2), weather imagery (NOAA APT, GOES HRIT), and wideband RF for SETI-style anomaly detection. Everything records to ZFS, transcodes on hardware, and every output carries a forensic watermark for chain-of-custody provenance.

What this replaces: Commercial signal monitoring equipment, satellite ground stations, and forensic recording systems. A dish, a few SDR dongles, a GPU, and kldload.

This is the most unusual recipe on the site — and possibly the most powerful. An RTL-SDR dongle costs less than lunch and receives everything from 24 MHz to 1.7 GHz: FM broadcast, aircraft transponders, airline datalink messages, weather satellite imagery, amateur radio, marine VHF, emergency beacons. Add a HackRF and you cover 1 MHz to 6 GHz. Add a satellite dish and you receive commercial TV, internet backbone, and military downlinks (receive-only — completely legal). kldload provides the storage backbone: ZFS datasets tuned per signal type (raw IQ uncompressed at 1M recordsize, text logs compressed with zstd-9 at 16K, imagery with zstd), hourly snapshots for continuous capture protection, and syncoid replication to archive everything offsite. The forensic watermarking gives you evidence-grade recordings with chain-of-custody provenance. This is a ground station, a SIGINT platform, and a broadcast monitor — running on commodity hardware.

Companion recipe: Live TV Streaming covers the video distribution side (SRT/HLS/IPTV). This recipe focuses on signal acquisition, wideband capture, automated analysis, and evidence-grade recording.


Architecture

                         kldload Signal Observatory

  SIGNAL SOURCES                    PROCESSING                  OUTPUT
  ──────────────                    ──────────────              ──────────

  Satellite Dish ──► DVB-S2 Tuner ──┐
    (Ku/C-band)      (TBS/DD)       │
                                     ├──► tvheadend ──► DVR recordings
  RTL-SDR v3 ──► USB ──────────────┤     (mux/EPG)    (ZFS snapshots)
    (24-1766 MHz)                    │
                                     ├──► GNU Radio ──► wideband IQ capture
  HackRF/Airspy ──► USB ──────────┤     (SDR engine)   (ZFS raw dataset)
    (1 MHz-6 GHz)                    │
                                     ├──► dump1090 ──► ADS-B aircraft map
  Dish + Feed Horn ──► LNA ──► SDR ─┤     (1090 MHz)
    (L/S-band)                       │
                                     ├──► acarsdec ──► ACARS messages
                                     │     (131 MHz)    (airline telemetry)
                                     │
                                     ├──► satdump ──► NOAA/GOES imagery
                                     │     (weather)    (APT/HRIT decode)
                                     │
                                     └──► anomaly ──► unknown signal log
                                           detector    (SETI-style scan)

  ALL OUTPUTS ──► ffmpeg (GPU transcode) ──► forensic watermark ──► ZFS

Hardware

ComponentExamplesCost
Edge serverAny x86_64 with PCIe + multiple USB 3.0 ports$300-800
DVB-S2 tuner cardTBS 6904 (4 tuner), Digital Devices Max S8 (8 tuner)$80-300
Satellite dish + LNB60-120cm dish, Ku-band LNB, DiSEqC switch for multi-sat$50-200
SDR dongle (VHF/UHF)RTL-SDR Blog v3 or v4 (24-1766 MHz, $30), Nooelec NESDR$25-40
SDR wideband (optional)Airspy R2 (24-1800 MHz, 10 MSPS), HackRF One (1 MHz-6 GHz)$100-350
L-band feed + LNAPatch antenna + SAWbird+ LNA for Inmarsat/Iridium (1.5 GHz)$50-100
GPU (hardware transcode)NVIDIA GTX 1650+ (NVENC) or Intel Arc A380 (QSV)$100-300
Storage2x NVMe for ZFS mirror + 4x HDD raidz2 for bulk capture$200-600
USB stickkldload installer$5

An RTL-SDR at $30 covers ADS-B (1090 MHz), ACARS (131 MHz), weather satellites (137 MHz), FM broadcast, and amateur radio. Add a HackRF and you cover everything from 1 MHz to 6 GHz. A satellite dish with a DVB-S2 tuner adds commercial satellite TV and free-to-air broadcasts. Total cost for a full-spectrum observatory: under $1,500.


Step 1: Install kldload

cat > /tmp/answers.env << 'EOF'
KLDLOAD_DISTRO=debian
KLDLOAD_DISK=/dev/sda
KLDLOAD_HOSTNAME=signal-obs
KLDLOAD_USERNAME=admin
KLDLOAD_PASSWORD=changeme
KLDLOAD_PROFILE=server
KLDLOAD_NVIDIA_DRIVERS=1
KLDLOAD_NET_METHOD=dhcp
EOF
kldload-install-target --config /tmp/answers.env

Step 2: Install the signal stack

# SDR tools and libraries
apt install -y rtl-sdr librtlsdr-dev soapysdr-tools gnuradio

# DVB satellite tools
apt install -y dvb-tools dtv-scan-tables w-scan2

# Tvheadend — satellite TV server (mux scanning, EPG, DVR)
apt install -y tvheadend

# Aircraft tracking (ADS-B on 1090 MHz)
apt install -y dump1090-mutability

# ACARS/VDL2 — airline datalink messages
apt install -y build-essential libxml2-dev cmake
git clone https://github.com/TLeconte/acarsdec.git /opt/acarsdec
cd /opt/acarsdec && mkdir build && cd build && cmake .. && make && make install

# Weather satellite imagery (NOAA APT, GOES HRIT, Meteor-M)
pip3 install satdump
# or build from source for full GPU decode:
# git clone https://github.com/SatDump/SatDump.git /opt/satdump

# ffmpeg for hardware transcoding + watermarking
apt install -y ffmpeg python3 python3-pip python3-opencv
pip3 install invisible-watermark numpy scipy

# Signal analysis
apt install -y inspectrum gqrx-sdr python3-matplotlib

Step 3: Verify hardware

# RTL-SDR — should show device info
rtl_test -t
# Found 1 device(s):  0: Realtek, RTL2838UHIDIR, SN:00000001

# HackRF — should show firmware version
hackrf_info
# Serial number: ...  Firmware Version: 2024.02.1

# DVB-S2 tuner — check adapters
ls /dev/dvb/
dvb-fe-tool -a 0

# GPU for transcoding
nvidia-smi          # NVIDIA
vainfo              # Intel QSV

# Quick RTL-SDR capture test (FM broadcast)
rtl_fm -f 101.1e6 -M wbfm -s 200000 -r 48000 - | \
  aplay -r 48000 -f S16_LE -t raw -c 1

Step 4: ZFS datasets for signal storage

# Raw IQ captures — massive wideband recordings, no compression
# (IQ data is effectively random noise, incompressible)
zfs create -o mountpoint=/srv/iq-capture \
           -o compression=off \
           -o recordsize=1M \
           -o atime=off \
           -o logbias=throughput \
           rpool/srv/iq-capture

# DVR recordings — satellite TV, compressed MPEG-TS
zfs create -o mountpoint=/srv/dvr \
           -o compression=zstd-3 \
           -o recordsize=1M \
           -o atime=off \
           rpool/srv/dvr

# Decoded imagery — NOAA/GOES/Meteor weather sat images
zfs create -o mountpoint=/srv/imagery \
           -o compression=zstd \
           -o atime=off \
           rpool/srv/imagery

# Aircraft and datalink logs — small, text-heavy, compresses well
zfs create -o mountpoint=/srv/adsb \
           -o compression=zstd-9 \
           -o recordsize=16K \
           rpool/srv/adsb

# Anomaly detection — flagged signals for review
zfs create -o mountpoint=/srv/anomalies \
           -o compression=zstd \
           -o recordsize=128K \
           rpool/srv/anomalies

# Watermarked output — forensically stamped exports
zfs create -o mountpoint=/srv/watermarked \
           -o compression=off \
           -o recordsize=1M \
           rpool/srv/watermarked

# Snapshot all signal datasets hourly
cat > /etc/cron.d/signal-snapshots << 'EOF'
0 * * * * root for ds in iq-capture dvr imagery adsb anomalies; do zfs snapshot rpool/srv/${ds}@auto-$(date +\%Y\%m\%d-\%H\%M); done
15 3 * * * root for ds in iq-capture dvr imagery adsb anomalies; do zfs list -t snapshot -o name -H rpool/srv/${ds} | grep @auto- | head -n -168 | xargs -r -n1 zfs destroy; done
EOF

Step 5: Satellite TV (dish + DVB-S2 tuner)

# Configure tvheadend for satellite reception
systemctl enable --now tvheadend
# Web UI: http://signal-obs:9981

# Configuration flow:
# 1. DVB Inputs > TV Adapters — select DVB-S2 adapter, set LNB type
# 2. DVB Inputs > Networks — add satellite (Astra, Hotbird, Galaxy, etc.)
# 3. DVB Inputs > Muxes — auto-scan transponders
# 4. DVB Inputs > Services — map to channels
# 5. Channel/EPG — enable OTA programme guide

# Record to ZFS DVR dataset
# Configuration > DVR > Recording path: /srv/dvr

# Transcode and redistribute to LAN (see Live TV recipe for full HLS setup)
ffmpeg \
  -i "http://localhost:9981/stream/channel/1?profile=pass" \
  -c:v hevc_nvenc -preset p4 -b:v 4M \
  -c:a aac -b:a 128k \
  -f hls -hls_time 4 -hls_list_size 20 -hls_flags delete_segments \
  /srv/dvr/live/channel1.m3u8

Step 6: Aircraft tracking (ADS-B + ACARS)

# ADS-B — track every aircraft within ~250 miles
# Uses RTL-SDR tuned to 1090 MHz
cat > /etc/systemd/system/adsb-tracker.service << 'UNIT'
[Unit]
Description=ADS-B aircraft tracker (1090 MHz)
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/dump1090-mutability \
  --device-index 0 \
  --net --net-http-port 8090 \
  --write-json /srv/adsb/live \
  --write-json-every 1 \
  --lat YOUR_LAT --lon YOUR_LON
Restart=always

[Install]
WantedBy=multi-user.target
UNIT
systemctl enable --now adsb-tracker

# Web map: http://signal-obs:8090
# Live aircraft positions, altitudes, speeds, callsigns

# ACARS — airline operational messages (131.550, 131.525, 131.725 MHz)
# Requires a second RTL-SDR dongle
cat > /etc/systemd/system/acars-decoder.service << 'UNIT'
[Unit]
Description=ACARS airline message decoder
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/acarsdec \
  -d 1 -v \
  -f 131.550 131.525 131.725 \
  -o 4 -j /srv/adsb/acars.json
Restart=always

[Install]
WantedBy=multi-user.target
UNIT
systemctl enable --now acars-decoder

# Log archival — rotate daily, compress old logs
cat > /etc/cron.d/adsb-archive << 'EOF'
0 0 * * * root cd /srv/adsb && tar czf archive/adsb-$(date +\%Y\%m\%d).tar.gz live/*.json && rm live/*.json
EOF

Step 7: Weather satellite imagery

# NOAA APT (137 MHz) — polar-orbiting, overhead passes ~4x/day
# Produces visible/infrared Earth imagery at ~4km resolution
# Requires RTL-SDR + V-dipole antenna

# Schedule captures based on satellite pass predictions
pip3 install orbit-predictor

cat > /usr/local/bin/noaa-capture.sh << 'CAPTURE'
#!/bin/bash
# Capture a NOAA satellite pass
# Usage: noaa-capture.sh <freq_mhz> <duration_sec> <output_name>
FREQ=$1; DUR=$2; NAME=$3
OUTDIR="/srv/imagery/noaa/$(date +%Y%m%d)"
mkdir -p "$OUTDIR"

# Record raw IQ
timeout "${DUR}" rtl_fm -f "${FREQ}e6" -M fm -s 60000 -r 11025 \
  -E deemp -E dc "${OUTDIR}/${NAME}.wav" 2>/dev/null

# Decode APT image
satdump live noaa_apt "${OUTDIR}/${NAME}.wav" "${OUTDIR}/${NAME}" \
  --source file 2>/dev/null

echo "[$(date)] NOAA capture: ${NAME} → ${OUTDIR}/" >> /var/log/signal-obs.log
CAPTURE
chmod +x /usr/local/bin/noaa-capture.sh

# GOES HRIT (1694.1 MHz) — geostationary, continuous full-disk imagery
# Requires dish pointed at GOES-16/17, LNA, and SDR
# SatDump handles the entire decode pipeline:
satdump live goes_hrit \
  --source rtlsdr --frequency 1694.1e6 --samplerate 2.4e6 \
  --output_folder /srv/imagery/goes/

# Meteor-M N2-3 (137.9 MHz) — Russian weather satellite, higher resolution
satdump live meteor_hrpt \
  --source rtlsdr --frequency 137.9e6 --samplerate 1e6 \
  --output_folder /srv/imagery/meteor/

Weather satellite reception is the gateway drug of SDR. NOAA satellites pass overhead 4+ times per day broadcasting imagery on 137 MHz — receivable with a $30 RTL-SDR and a wire antenna. SatDump decodes the APT signal into actual Earth images — your local weather, captured from space, decoded on your hardware. GOES geostationary satellites provide continuous full-disk imagery at 1694 MHz — higher resolution, but requires a small dish pointed at a fixed position in the sky. All of it records to ZFS datasets with automatic snapshots. Over time, you build an archive of weather imagery with precise timestamps — useful for climate research, agriculture, disaster analysis, or just the profound satisfaction of receiving images directly from spacecraft.

Step 8: Wideband IQ capture (your own SETI)

# The HackRF covers 1 MHz to 6 GHz — you can record entire spectrum bands
# and analyze them later for signals of interest.

# Record 20 MHz of L-band spectrum (1.5 GHz — Inmarsat, Iridium, GPS)
hackrf_transfer -r /srv/iq-capture/lband-$(date +%Y%m%d-%H%M).raw \
  -f 1545000000 -s 20000000 -n 400000000
# That's 20 seconds of 20 MHz bandwidth — ~800 MB of raw IQ

# Record VHF band (aircraft, marine, weather, emergency)
rtl_sdr -f 130e6 -s 2.4e6 -n 48000000 /srv/iq-capture/vhf-$(date +%s).iq
# 20 seconds of 2.4 MHz around 130 MHz

# GNU Radio companion — build flowgraphs for custom demodulation
# Launch: gnuradio-companion (GUI) or use Python API for headless

# Automated spectral scan — sweep a frequency range and log power levels
cat > /usr/local/bin/spectrum-scan.py << 'PYEOF'
#!/usr/bin/env python3
"""
Wideband spectrum scanner — sweeps a frequency range using rtl_power,
logs power levels, and flags anomalies (signals significantly above
the noise floor that don't match known frequencies).
"""
import subprocess, json, time, os
from datetime import datetime

SCAN_RANGE = "24M:1766M:1M"  # Full RTL-SDR range in 1 MHz steps
INTEGRATION = "10"             # 10 second integration per sweep
KNOWN_SIGNALS_FILE = "/etc/signal-obs/known-signals.json"
ANOMALY_DIR = "/srv/anomalies"
LOG = "/var/log/signal-obs.log"

def scan():
    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    outfile = f"/tmp/sweep-{timestamp}.csv"

    subprocess.run([
        "rtl_power", "-f", SCAN_RANGE, "-i", INTEGRATION,
        "-1", "-c", "20%", outfile
    ], timeout=300)

    # Parse results and find anomalies
    known = {}
    if os.path.exists(KNOWN_SIGNALS_FILE):
        known = json.load(open(KNOWN_SIGNALS_FILE))

    anomalies = []
    with open(outfile) as f:
        for line in f:
            parts = line.strip().split(",")
            if len(parts) < 7: continue
            freq_hz = int(parts[2])
            freq_mhz = freq_hz / 1e6
            powers = [float(x) for x in parts[6:] if x.strip()]
            peak = max(powers) if powers else -100
            # Flag if peak is >20 dB above noise and not a known signal
            if peak > -30 and str(round(freq_mhz, 1)) not in known:
                anomalies.append({
                    "freq_mhz": round(freq_mhz, 2),
                    "peak_db": round(peak, 1),
                    "time": timestamp
                })

    if anomalies:
        os.makedirs(ANOMALY_DIR, exist_ok=True)
        with open(f"{ANOMALY_DIR}/anomaly-{timestamp}.json", "w") as f:
            json.dump(anomalies, f, indent=2)
        with open(LOG, "a") as f:
            f.write(f"[{timestamp}] {len(anomalies)} anomalies detected\n")

    os.unlink(outfile)
    return anomalies

if __name__ == "__main__":
    while True:
        try:
            result = scan()
            if result:
                print(f"Found {len(result)} anomalies")
            else:
                print("Clean sweep")
        except Exception as e:
            print(f"Scan error: {e}")
        time.sleep(60)  # Scan every minute
PYEOF
chmod +x /usr/local/bin/spectrum-scan.py

# Known signals database — don't flag these as anomalies
mkdir -p /etc/signal-obs
cat > /etc/signal-obs/known-signals.json << 'KNOWN'
{
  "88.1": "FM broadcast",
  "101.1": "FM broadcast",
  "108.0": "VOR navigation",
  "121.5": "Aviation emergency",
  "131.5": "ACARS",
  "137.1": "NOAA-15",
  "137.9": "NOAA-18",
  "156.8": "Marine VHF ch16",
  "162.4": "NOAA weather radio",
  "406.0": "COSPAS-SARSAT",
  "462.5": "FRS/GMRS",
  "1090.0": "ADS-B",
  "1227.6": "GPS L2",
  "1575.4": "GPS L1",
  "1694.1": "GOES HRIT"
}
KNOWN

Step 9: Forensic watermarking

Forensic watermarking elevates this from "hobbyist SDR setup" to "evidence-grade signal observatory." The invisible watermark (burned into video at 1% opacity, embedded in images via DWT-DCT-SVD, and metadata-appended to IQ captures with SHA-256 checksums) creates a provenance chain: who captured it, when, where, on what hardware. Combined with ZFS checksumming (proving the recording hasn't been modified since capture), you have a complete chain of custody. This matters if your recordings are ever needed as evidence — interference investigations, spectrum enforcement, broadcast monitoring, or security research. The same techniques (Nagra watermarking, Irdeto forensic marks) are used by major broadcasters for anti-piracy.

Every signal recording, decoded image, and video export carries an invisible forensic watermark — a unique identifier baked into the content that proves who captured it, when, and on what equipment. The same technique used in surveillance evidence chains and broadcast anti-piracy (Nagra, Irdeto). If a recording is ever redistributed or leaked, the watermark traces it back to this station.

# Video watermarking (satellite TV, transcoded streams)
# Burn a station ID + timestamp into every frame at invisible opacity
ffmpeg \
  -i "http://localhost:9981/stream/channel/1?profile=pass" \
  -vf "drawtext=text='OBS_%{localtime\:%Y%m%d_%H%M%S}_STN01':
       fontsize=10:fontcolor=white@0.01:x=10:y=10" \
  -c:v hevc_nvenc -preset p4 -b:v 4M \
  -c:a copy \
  -f mpegts /srv/watermarked/stn01-ch1.ts

# Image watermarking (weather satellite imagery)
cat > /usr/local/bin/watermark-image.py << 'PYEOF'
#!/usr/bin/env python3
"""Embed forensic watermark into satellite imagery."""
import sys, time, struct
from imwatermark import WatermarkEncoder
import cv2, numpy as np

STATION_ID = 1
img = cv2.imread(sys.argv[1])
encoder = WatermarkEncoder()
payload = struct.pack(">HI", STATION_ID, int(time.time()))
encoder.set_watermark('bytes', payload)
marked = encoder.encode(img, 'dwtDctSvd')
out = sys.argv[1].replace('.png', '-wm.png').replace('.jpg', '-wm.jpg')
cv2.imwrite(out, marked)
print(f"Watermarked: {out}")
PYEOF
chmod +x /usr/local/bin/watermark-image.py

# IQ recording provenance — append station metadata to raw captures
# (IQ data can't be visually watermarked, but we embed provenance)
cat > /usr/local/bin/iq-provenance.sh << 'PROV'
#!/bin/bash
# Append provenance metadata to an IQ capture file
FILE="$1"
cat >> "${FILE}.meta" << META
station_id: STN01
capture_time: $(date -u +%Y-%m-%dT%H:%M:%SZ)
hostname: $(hostname)
sha256: $(sha256sum "$FILE" | cut -d' ' -f1)
gps_lat: YOUR_LAT
gps_lon: YOUR_LON
sdr_serial: $(hackrf_info 2>/dev/null | grep Serial | awk '{print $NF}')
META
echo "Provenance written: ${FILE}.meta"
PROV
chmod +x /usr/local/bin/iq-provenance.sh

Step 10: Systemd services

# Spectrum scanner (SETI-style anomaly detection)
cat > /etc/systemd/system/spectrum-scan.service << 'UNIT'
[Unit]
Description=Wideband spectrum anomaly scanner
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/spectrum-scan.py
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
UNIT

# Weather satellite auto-capture (scheduled by pass predictions)
cat > /etc/systemd/system/noaa-scheduler.service << 'UNIT'
[Unit]
Description=NOAA satellite pass scheduler

[Service]
Type=oneshot
ExecStart=/usr/local/bin/noaa-scheduler.py

[Install]
WantedBy=multi-user.target
UNIT
cat > /etc/systemd/system/noaa-scheduler.timer << 'TIMER'
[Unit]
Description=Check for upcoming NOAA passes every 30 minutes

[Timer]
OnBootSec=60
OnUnitActiveSec=30min

[Install]
WantedBy=timers.target
TIMER

systemctl daemon-reload
systemctl enable --now adsb-tracker acars-decoder spectrum-scan noaa-scheduler.timer

Monitoring

# SDR device status
rtl_test -t                    # RTL-SDR health check
hackrf_info                    # HackRF status

# DVB signal quality
dvb-fe-tool -a 0 -m           # SNR, BER, signal strength

# ADS-B stats
curl -s http://localhost:8090/data/stats.json | python3 -m json.tool

# Anomaly count (last 24 hours)
find /srv/anomalies -name '*.json' -mtime -1 | wc -l

# ZFS storage overview
zfs list -o name,used,compressratio,available -r rpool/srv

# GPU utilization
nvidia-smi --query-gpu=utilization.encoder,memory.used --format=csv

# Disk I/O (important for continuous IQ capture)
zpool iostat -v rpool 5

Why ZFS matters here

Checksums for evidence integrity

Every block on disk is checksummed. Combined with forensic watermarking, you have a complete chain of custody: the watermark proves who captured the signal, ZFS proves the recording hasn't been altered since capture. This matters if your recordings ever need to stand up as evidence.

Snapshots for continuous capture

Hourly ZFS snapshots protect days of signal recordings at near-zero cost. A power failure during a wideband capture? The last snapshot is consistent. Accidentally overwrite a rare satellite pass? Roll back in seconds.

Compression where it helps

Raw IQ is incompressible noise — store it uncompressed. But ADS-B JSON logs, ACARS messages, and weather imagery metadata compress 5-10x with zstd. ZFS lets you set the right policy per dataset.

Throughput for wideband

A 20 MHz wideband capture writes ~40 MB/s of raw IQ continuously. ZFS with recordsize=1M and logbias=throughput on a mirror or raidz2 handles this without dropping samples. The ARC cache absorbs metadata overhead.


  • Receive-only is legal almost everywhere. In most countries, you can listen to any signal. Transmitting, jamming, or decrypting signals you don't have authorization for is illegal.
  • Satellite TV descrambling requires a valid subscription card. Card sharing or circumventing access controls is illegal in most jurisdictions.
  • ADS-B data is unencrypted and intentionally broadcast for safety. Receiving and displaying it is legal. Many operators feed data to FlightAware, FlightRadar24, and ADSBexchange.
  • ACARS messages may contain operationally sensitive airline data. Receiving is legal; publishing specific message contents may have restrictions depending on jurisdiction.
  • Watermark provenance records should be stored in an encrypted ZFS dataset. The chain of custody is only as strong as its weakest link.
  • Amateur radio bands — if you have a ham license, you can transmit too. kldload with GNU Radio makes an excellent digital modes station (FT8, JS8Call, APRS).