diff --git a/DEMO_README.md b/DEMO_README.md new file mode 100644 index 0000000..8183236 --- /dev/null +++ b/DEMO_README.md @@ -0,0 +1,181 @@ +# MILCOM Demo — Running the Jammer Attacks + +Step-by-step guide for running the software-defined USRP jammer during the +MILCOM demo. Covers the two attack modes used in the demo (**barrage** and +**random**) and the **clean** baseline (jammer off). + +> **Safety / legal:** This jammer transmits real RF. Only run it over the air +> inside a shielded lab or on frequencies you are authorized to use. Keep the +> jammer co-channel with the victim gNB and nothing else. + +--- + +## 1. What each attack does + +| Mode | `attack_type` | RF behavior | Config file | +|------|---------------|-------------|-------------| +| **Clean** | *(jammer off)* | No interference — baseline traffic | *(none — do not start the jammer)* | +| **Barrage** | `barrage` | Wideband noise, transmitted **continuously** across the band | `configs/barrage.yaml` | +| **Random** | `random` | Same wideband noise, gated **50 ms ON / 50 ms OFF** (bursty) | `configs/random.yaml` | + +The demo classifier is trained on **clean / barrage / random**. + +--- + +## 2. Requirements + +- Linux host with a **USRP B210** connected (attack radio) +- **UHD** drivers installed and the device visible (`uhd_find_devices`) +- Build tools: `cmake`, `make`, a C++ compiler + +```bash +sudo apt update +sudo apt install build-essential cmake libuhd-dev uhd-host +``` + +--- + +## 3. Build the jammer (one time) + +From the `jammer/` directory of the repo: + +```bash +mkdir -p build && cd build +cmake .. +make -j +``` + +This produces the `jammer` binary in `jammer/build/`. + +> Rebuild (`make -j`) only if you change C++ source. Editing a YAML config does +> **not** require a rebuild. + +--- + +## 4. Confirm the USRP is detected + +```bash +uhd_find_devices +``` + +You should see your B210 listed. If not, replug the USRP and re-run before +starting the demo. + +--- + +## 5. Run each attack + +All commands are run from `jammer/build/`. Stop any attack with **Ctrl+C**. + +### 5.1 Clean (baseline) + +Do **not** start the jammer. Keep the gNB + UE link running with traffic so the +detector sees a clean KPI baseline. + +### 5.2 Barrage jamming + +```bash +./jammer --config ../configs/barrage.yaml +``` + +You will see repeating `Transmitting (barrage, fresh buffer)....` lines while it +runs. This transmits continuous wideband noise. + +### 5.3 Random (bursty) jamming + +```bash +./jammer --config ../configs/random.yaml +``` + +Output alternates between `Transmitting (random) ON ...` and +`Transmitting (random) IDLE ...` every 50 ms, producing the bursty signature. + +--- + +## 6. Key config parameters + +Edit these in the YAML files under `configs/` before the demo to match your +lab. Defaults below are from the checked-in configs. + +| Field | Meaning | Barrage / Random default | +|-------|---------|--------------------------| +| `attack_type` | `barrage` or `random` | per file | +| `center_frequency` | Victim carrier center, Hz | `1842050000` (Band 3) | +| `bandwidth` | Jam bandwidth, Hz | `40e6` | +| `sampling_freq` | USRP TX sample rate | `23.04e6` | +| `tx_gain` | USRP TX gain, dB — **controls jamming strength** | `50` | +| `amplitude` | Waveform amplitude (0–1) | `0.7` | +| `num_samples` | Samples per TX buffer | `20000` | +| `burst_duration_ms` | (random only) ON time | `50` | +| `idle_duration_ms` | (random only) OFF time | `50` | +| `device_args` | UHD device selector | `type=b200` | + +> **Match `center_frequency` and `sampling_freq` to your gNB.** The jammer must +> sit on the same carrier as the victim gNB for the attack to register in the +> KPIs. + +--- + +## 7. Command-line overrides (no rebuild needed) + +Any config field can be overridden on the command line, which is handy for +tuning gain live during the demo without editing files: + +```bash +# Raise barrage power on the fly +./jammer --config ../configs/barrage.yaml --tx_gain 55 + +# Retune to a different carrier +./jammer --config ../configs/barrage.yaml --center_frequency 3619200000 + +# Switch mode without editing YAML +./jammer --config ../configs/barrage.yaml --attack_type random + +# Change the random duty cycle +./jammer --config ../configs/random.yaml --burst_duration_ms 100 --idle_duration_ms 100 +``` + +Supported override flags: `--attack_type`, `--tx_gain`, `--center_frequency`, +`--bandwidth`, `--sampling_freq`, `--amplitude`, `--amplitude_width`, +`--num_samples`, `--initial_phase`, `--burst_duration_ms`, `--idle_duration_ms`, +`--device_args`, `--write_iq`, `--write_csv`. + +If KPIs still look clean when jamming, raise `--tx_gain` in ~2 dB steps. + +--- + +## 8. Suggested demo sequence + +Run the detector alongside so the audience sees the label change as you toggle +the jammer: + +```text +1. Clean — jammer OFF → detector shows "clean" +2. Barrage — ./jammer --config ../configs/barrage.yaml + (Ctrl+C to stop) → detector shows "barrage" +3. Clean — jammer OFF again → detector returns to "clean" +4. Random — ./jammer --config ../configs/random.yaml + (Ctrl+C to stop) → detector shows "random" +``` + +The detector needs ~30 s of KPI history (its sliding window) before the label +settles after each toggle. + +**Live detector (separate terminal, from the `ml/` directory of the repo):** + +```bash +source .venv/bin/activate +python detect_live_gnb.py --config config_gnb.yaml --run-id 20260702_190948 +``` + +--- + +## 9. Troubleshooting + +| Symptom | Fix | +|---------|-----| +| `Error: center_frequency, sampling_freq, and tx_gain must be set` | Fill those fields in the YAML (or pass via CLI). | +| No USRP found / UHD error | Run `uhd_find_devices`; replug the B210; check `device_args`. | +| KPIs stay "clean" under jamming | Increase `tx_gain`; confirm `center_frequency` matches the gNB. | +| `Unknown attack_type` | Use exactly `barrage` or `random`. | +| Old behavior after editing `.cc` | Rebuild with `make -j` in `build/`. | diff --git a/configs/barrage.yaml b/configs/barrage.yaml new file mode 100644 index 0000000..9ec1778 --- /dev/null +++ b/configs/barrage.yaml @@ -0,0 +1,14 @@ +attack_type: barrage +amplitude: 0.7 +amplitude_width: 0.05 +center_frequency: 1842050000 +bandwidth: 40e6 +initial_phase: 0 +sampling_freq: 23.04e6 +num_samples: 20000 +output_iq_file: "output.fc32" +output_csv_file: "output.csv" +write_iq: false +write_csv: true +device_args: "type=b200" +tx_gain: 50 diff --git a/configs/basic_jammer.yaml b/configs/basic_jammer.yaml index 4c30cd1..63150e3 100644 --- a/configs/basic_jammer.yaml +++ b/configs/basic_jammer.yaml @@ -1,3 +1,4 @@ +attack_type: barrage amplitude: 0.7 amplitude_width: 0.05 center_frequency: 3.619200e9 diff --git a/configs/constant.yaml b/configs/constant.yaml new file mode 100644 index 0000000..02aa025 --- /dev/null +++ b/configs/constant.yaml @@ -0,0 +1,42 @@ +attack_type: constant + +# Narrowband Gaussian-noise jammer (v3). +# +# Tuned per published 5G NR jamming experiments at 23.04 MS/s: +# STORM (CEUR 2025) used 1.92 MHz of white noise at ±1.92 MHz offset +# from the SSB center with tx_gain in the 60–80 dB range on a B210. +# Antosik (IEEE 2024) measured ~25 dB power-efficiency gain for +# narrowband over barrage jamming when the waveform is true noise. + +amplitude: 0.6 +amplitude_width: 0.05 + +# Baseband offset from LO. 0 = auto-shift to +1.92 MHz to avoid the NR DC +# subcarrier and the B-series DC-offset cancellation null. Try ±1.92 MHz, +# ±3.84 MHz, or any value within ±10 MHz to target different PRB regions. +tone_offset_hz: 0 + +# Width of the noise band in Hz. 1.92 MHz matches the STORM SSB-attack +# bandwidth; up to ~5 MHz is reasonable. Narrower = more power per +# subcarrier and more "narrowband gain". +jam_bandwidth_hz: 1.92e6 + +# Legacy fields used by older constant generators; ignored by v3 but kept +# for YAML compatibility. +num_tones: 64 + +center_frequency: 1842050000 +bandwidth: 40e6 +initial_phase: 0 +sampling_freq: 23.04e6 +num_samples: 23040 # 1 ms of waveform at 23.04 MS/s; buffer is re-streamed +output_iq_file: "output.fc32" +output_csv_file: "output.csv" +write_iq: false +write_csv: true +device_args: "type=b200" + +# Recommended starting point. Barrage works well around 50; narrowband +# noise with the correct waveform should produce moderate degradation +# at 45–55. If KPIs still look clean, raise in steps of 2 dB. +tx_gain: 50 diff --git a/configs/random.yaml b/configs/random.yaml new file mode 100644 index 0000000..3f62e16 --- /dev/null +++ b/configs/random.yaml @@ -0,0 +1,16 @@ +attack_type: random +burst_duration_ms: 50 +idle_duration_ms: 50 +amplitude: 0.7 +amplitude_width: 0.05 +center_frequency: 1842050000 +bandwidth: 40e6 +initial_phase: 0 +sampling_freq: 23.04e6 +num_samples: 20000 +output_iq_file: "output.fc32" +output_csv_file: "output.csv" +write_iq: false +write_csv: true +device_args: "type=b200" +tx_gain: 50 diff --git a/configs/test_constant.yaml b/configs/test_constant.yaml new file mode 100644 index 0000000..23a98a2 --- /dev/null +++ b/configs/test_constant.yaml @@ -0,0 +1,38 @@ +attack_type: constant + +# Narrowband Gaussian-noise constant jammer (v3). +# See configs/constant.yaml and jammer/src/constant.cc for the literature +# references that motivated this design. + +amplitude: 0.6 +amplitude_width: 0.05 + +# 0 = auto-shift to +1.92 MHz off LO (avoids the NR DC subcarrier + the +# B-series DC-offset null). Try ±1.92 MHz to target the alternate SSB +# halves, or larger offsets like ±5 MHz to land in the data PRBs. +tone_offset_hz: 0 + +# 1.92 MHz matches the STORM SSB-attack recipe. Wider (e.g. 3.84e6) hits +# more subcarriers but loses some narrowband gain. Narrower (e.g. 1.0e6) +# concentrates power further. +jam_bandwidth_hz: 1.92e6 + +num_tones: 64 + +center_frequency: 1842050000 +bandwidth: 40e6 +initial_phase: 0 +sampling_freq: 23.04e6 +num_samples: 23040 +output_iq_file: "output.fc32" +output_csv_file: "output.csv" +write_iq: false +write_csv: true +device_args: "type=b200" + +# Tuning ladder if results are too weak / too strong: +# gain 45 — first test +# gain 48 — usual sweet spot for moderate degradation +# gain 50 — strong but should not release RRC with this waveform +# gain 52+ — risk of disconnect; back off and try wider jam_bandwidth_hz +tx_gain: 48 diff --git a/hdr/args.h b/hdr/args.h index 5ac41f2..f09bc81 100644 --- a/hdr/args.h +++ b/hdr/args.h @@ -23,6 +23,11 @@ typedef struct db_args_s { std::string data_id = ""; } db_args_t; +enum class attack_type_t { BARRAGE, CONSTANT, RANDOM }; + +attack_type_t parseAttackType(const std::string &type); +const char *attackTypeToString(attack_type_t type); + typedef struct all_args_s { float amplitude; float amplitude_width; @@ -37,6 +42,15 @@ typedef struct all_args_s { bool write_csv; rf_args_t rf; + attack_type_t attack_type = attack_type_t::BARRAGE; + float tone_offset_hz = 0.0f; + float burst_duration_ms = 50.0f; + float idle_duration_ms = 50.0f; + + // Constant (partial-band noise) jamming controls + float jam_bandwidth_hz = 5.0e6f; // width of narrowband noise around tone_offset_hz + int num_tones = 64; // tones used to synthesize the noise comb + //autoconfig control bool enable_autoconfigure = false; db_args_t db; @@ -46,4 +60,6 @@ all_args_t parseConfig(const std::string &filename); void overrideConfig(all_args_t &args, int argc, char *argv[]); +void validateAttackArgs(const all_args_t &args); + #endif // !ARGS_H diff --git a/hdr/attack.h b/hdr/attack.h new file mode 100644 index 0000000..34cb320 --- /dev/null +++ b/hdr/attack.h @@ -0,0 +1,14 @@ +#ifndef ATTACK_H +#define ATTACK_H + +#include "args.h" + +#include +#include +#include + +std::vector> generate_samples(const all_args_t &args); + +void transmission(uhd::usrp::multi_usrp::sptr usrp, const all_args_t &args); + +#endif diff --git a/hdr/barrage.h b/hdr/barrage.h new file mode 100644 index 0000000..c2114fb --- /dev/null +++ b/hdr/barrage.h @@ -0,0 +1,13 @@ +#ifndef BARRAGE_H +#define BARRAGE_H + +#include "args.h" + +#include +#include + +// Wideband barrage jamming: random frequency and amplitude per sample +// across [center_frequency ± bandwidth/2]. +std::vector> generate_barrage(const all_args_t &args); + +#endif diff --git a/hdr/constant.h b/hdr/constant.h new file mode 100644 index 0000000..4fe90c2 --- /dev/null +++ b/hdr/constant.h @@ -0,0 +1,17 @@ +#ifndef CONSTANT_H +#define CONSTANT_H + +#include "args.h" + +#include +#include + +// Constant (narrowband Gaussian noise) jamming: true complex Gaussian +// noise band-limited to jam_bandwidth_hz and centered at tone_offset_hz +// off LO. Method matches published 5G NR jamming experiments on USRP +// B-series at 23.04 MS/s (STORM CEUR 2025; Antosik IEEE 2024) which +// found Gaussian noise raises the UE noise floor instead of being +// rejected by AGC the way discrete CW tones are. +std::vector> generate_constant(const all_args_t &args); + +#endif diff --git a/hdr/noise.h b/hdr/noise.h deleted file mode 100644 index 0369f11..0000000 --- a/hdr/noise.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef NOISE_H -#define NOISE_H -#include "args.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -const float PI = std::acos(-1.0); - -std::vector> generateComplexSineWave(const all_args_t args); - -void transmission(uhd::usrp::multi_usrp::sptr usrp, const all_args_t args); - -#endif diff --git a/src/args.cc b/src/args.cc index 33d7936..bd1ca67 100644 --- a/src/args.cc +++ b/src/args.cc @@ -1,4 +1,5 @@ #include "args.h" +#include #include #define REQUIRE_FIELD(node, key) \ @@ -6,6 +7,32 @@ throw std::runtime_error("Missing required field in config: '" #key "'"); \ } +attack_type_t parseAttackType(const std::string &type) { + if (type == "barrage") { + return attack_type_t::BARRAGE; + } + if (type == "constant") { + return attack_type_t::CONSTANT; + } + if (type == "random") { + return attack_type_t::RANDOM; + } + throw std::runtime_error("Unknown attack_type: '" + type + + "' (expected barrage, constant, or random)"); +} + +const char *attackTypeToString(attack_type_t type) { + switch (type) { + case attack_type_t::BARRAGE: + return "barrage"; + case attack_type_t::CONSTANT: + return "constant"; + case attack_type_t::RANDOM: + return "random"; + } + return "unknown"; +} + all_args_t parseConfig( const std::string &filename) { // change the filename to the real filename @@ -46,7 +73,36 @@ all_args_t parseConfig( args.center_frequency = config["center_frequency"] ? config["center_frequency"].as() : 0.0f; args.sampling_freq = config["sampling_freq"] ? config["sampling_freq"].as() : 0.0f; args.rf.tx_gain = config["tx_gain"] ? config["tx_gain"].as() : 0.0f; -// + + args.attack_type = attack_type_t::BARRAGE; + if (config["attack_type"]) { + args.attack_type = parseAttackType(config["attack_type"].as()); + } + + args.tone_offset_hz = 0.0f; + if (config["tone_offset_hz"]) { + args.tone_offset_hz = config["tone_offset_hz"].as(); + } + + args.burst_duration_ms = 50.0f; + if (config["burst_duration_ms"]) { + args.burst_duration_ms = config["burst_duration_ms"].as(); + } + + args.idle_duration_ms = 50.0f; + if (config["idle_duration_ms"]) { + args.idle_duration_ms = config["idle_duration_ms"].as(); + } + + args.jam_bandwidth_hz = 5.0e6f; + if (config["jam_bandwidth_hz"]) { + args.jam_bandwidth_hz = config["jam_bandwidth_hz"].as(); + } + + args.num_tones = 64; + if (config["num_tones"]) { + args.num_tones = config["num_tones"].as(); + } // autoconfig field args.enable_autoconfigure = false; @@ -107,8 +163,34 @@ void overrideConfig(all_args_t &args, int argc, char *argv[]) { } else if (std::strcmp(argv[i], "--enable_autoconfigure") == 0 && i + 1 < argc) { args.enable_autoconfigure = (std::string(argv[++i]) == "true"); + } else if (std::strcmp(argv[i], "--attack_type") == 0 && i + 1 < argc) { + args.attack_type = parseAttackType(argv[++i]); + } else if (std::strcmp(argv[i], "--tone_offset_hz") == 0 && i + 1 < argc) { + args.tone_offset_hz = std::atof(argv[++i]); + } else if (std::strcmp(argv[i], "--burst_duration_ms") == 0 && i + 1 < argc) { + args.burst_duration_ms = std::atof(argv[++i]); + } else if (std::strcmp(argv[i], "--idle_duration_ms") == 0 && i + 1 < argc) { + args.idle_duration_ms = std::atof(argv[++i]); + } else if (std::strcmp(argv[i], "--jam_bandwidth_hz") == 0 && i + 1 < argc) { + args.jam_bandwidth_hz = std::atof(argv[++i]); + } else if (std::strcmp(argv[i], "--num_tones") == 0 && i + 1 < argc) { + args.num_tones = std::atoi(argv[++i]); } else { std::cerr << "Unknown or incomplete option: " << argv[i] << std::endl; } } } + +void validateAttackArgs(const all_args_t &args) { + if (args.attack_type != attack_type_t::RANDOM) { + return; + } + + if (args.burst_duration_ms <= 0.0f) { + throw std::runtime_error( + "burst_duration_ms must be > 0 for random jamming"); + } + if (args.idle_duration_ms <= 0.0f) { + throw std::runtime_error("idle_duration_ms must be > 0 for random jamming"); + } +} diff --git a/src/attack.cc b/src/attack.cc new file mode 100644 index 0000000..618d61f --- /dev/null +++ b/src/attack.cc @@ -0,0 +1,106 @@ +#include "attack.h" + +#include "barrage.h" +#include "constant.h" + +#include +#include +#include +#include +#include + +namespace { + +size_t packets_for_duration_ms(float duration_ms, size_t num_samples, + float sampling_freq) { + const float packet_duration_ms = + (static_cast(num_samples) / sampling_freq) * 1000.0f; + if (packet_duration_ms <= 0.0f) { + return 1; + } + + const size_t packets = + static_cast(std::ceil(duration_ms / packet_duration_ms)); + return std::max(packets, size_t{1}); +} + +void transmission_continuous(uhd::usrp::multi_usrp::sptr usrp, + const all_args_t &args) { + uhd::stream_args_t stream_args("fc32", "sc16"); + uhd::tx_streamer::sptr tx_stream = usrp->get_tx_stream(stream_args); + + uhd::tx_metadata_t metadata; + metadata.start_of_burst = true; + metadata.end_of_burst = false; + metadata.has_time_spec = false; + + const std::vector> samples = generate_samples(args); + + while (true) { + tx_stream->send(samples.data(), samples.size(), metadata); + std::cout << "Transmitting (" << attackTypeToString(args.attack_type) + << ")...." << std::endl; + metadata.start_of_burst = false; + } +} + +void transmission_random(uhd::usrp::multi_usrp::sptr usrp, + const all_args_t &args) { + uhd::stream_args_t stream_args("fc32", "sc16"); + uhd::tx_streamer::sptr tx_stream = usrp->get_tx_stream(stream_args); + + uhd::tx_metadata_t metadata; + metadata.start_of_burst = true; + metadata.end_of_burst = false; + metadata.has_time_spec = false; + + const std::vector> burst_samples = generate_barrage(args); + std::vector> silence_samples(args.num_samples, {0.0f, 0.0f}); + + const size_t burst_packets = packets_for_duration_ms( + args.burst_duration_ms, args.num_samples, args.sampling_freq); + const size_t idle_packets = packets_for_duration_ms( + args.idle_duration_ms, args.num_samples, args.sampling_freq); + + while (true) { + for (size_t i = 0; i < burst_packets; ++i) { + tx_stream->send(burst_samples.data(), burst_samples.size(), metadata); + metadata.start_of_burst = false; + } + std::cout << "Transmitting (random) ON for " << args.burst_duration_ms + << " ms...." << std::endl; + + for (size_t i = 0; i < idle_packets; ++i) { + tx_stream->send(silence_samples.data(), silence_samples.size(), metadata); + } + std::cout << "Transmitting (random) IDLE for " << args.idle_duration_ms + << " ms...." << std::endl; + } +} + +} // namespace + +std::vector> generate_samples(const all_args_t &args) { + switch (args.attack_type) { + case attack_type_t::BARRAGE: + return generate_barrage(args); + case attack_type_t::CONSTANT: + return generate_constant(args); + case attack_type_t::RANDOM: + return generate_barrage(args); + } + + return generate_barrage(args); +} + +void transmission(uhd::usrp::multi_usrp::sptr usrp, const all_args_t &args) { + switch (args.attack_type) { + case attack_type_t::BARRAGE: + case attack_type_t::CONSTANT: + transmission_continuous(usrp, args); + return; + case attack_type_t::RANDOM: + transmission_random(usrp, args); + return; + } +} diff --git a/src/barrage.cc b/src/barrage.cc new file mode 100644 index 0000000..fc8b730 --- /dev/null +++ b/src/barrage.cc @@ -0,0 +1,41 @@ +#include "barrage.h" + +#include +#include + +namespace { +constexpr float PI = std::acos(-1.0f); +} // namespace + +std::vector> generate_barrage(const all_args_t &args) { + std::vector> samples; + samples.reserve(args.num_samples); + + const float delta_t = 1.0f / args.sampling_freq; + const float half_bandwidth = args.bandwidth / 2.0f; + float phase = args.initial_phase; + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution freq_dist( + args.center_frequency - half_bandwidth, + args.center_frequency + half_bandwidth); + std::uniform_real_distribution ampl( + args.amplitude - args.amplitude_width / 2.0f, + args.amplitude + args.amplitude_width / 2.0f); + + for (size_t i = 0; i < args.num_samples; ++i) { + const float current_freq = freq_dist(gen); + const float current_ampl = ampl(gen); + + samples.push_back(std::polar(current_ampl, phase)); + + phase += 2.0f * PI * current_freq * delta_t; + phase = std::fmod(phase, 2.0f * PI); + if (phase < 0.0f) { + phase += 2.0f * PI; + } + } + + return samples; +} diff --git a/src/constant.cc b/src/constant.cc new file mode 100644 index 0000000..fcc0e82 --- /dev/null +++ b/src/constant.cc @@ -0,0 +1,102 @@ +#include "constant.h" + +#include +#include +#include +#include + +namespace { +constexpr float PI = std::acos(-1.0f); +} // namespace + +// Constant jammer — v3 (narrowband Gaussian noise jamming). +// +// Method validated by published 5G NR jamming experiments on USRP B210 at +// 23.04 MS/s (e.g. "STORM: Stealth Jamming Attacks on 5G Synchronization", +// CEUR 2025; Antosik et al., "Narrowband jamming in 5G private networks", +// IEEE 2024). Key findings these papers establish: +// +// * Effective narrowband 5G jamming uses **true complex Gaussian noise**, +// not a sum of equal-amplitude tones. Equal-amplitude tones look like +// discrete CW tones to the UE RX AGC, which suppresses each +// individually; Gaussian noise raises the receiver's noise floor. +// * Bandwidth around **1.92 MHz** (one SSB-worth, 128 subcarriers at +// 15 kHz SCS) concentrates power on a meaningful sub-band. Wider +// bandwidth loses the ~25 dB "narrowband jamming gain" advantage +// (Antosik 2024). +// * The jammer must be **offset from DC**, because NR's DC subcarrier +// is empty and the USRP B-series DC-offset cancellation suppresses +// any tone near LO. +// +// Pipeline: +// 1. Generate complex white Gaussian noise samples at rate jam_bandwidth_hz +// (using zero-order hold from a slow noise source to the USRP sample rate). +// This naturally band-limits the energy to roughly ±jam_bw/2 around DC. +// 2. Frequency-shift by tone_offset_hz so the band lands where we want. +// 3. If the caller picks a near-zero offset, automatically shift to a safe +// non-DC offset (default 1.92 MHz, matching the STORM recipe). +std::vector> generate_constant(const all_args_t &args) { + std::vector> samples; + samples.reserve(args.num_samples); + + const float dt = 1.0f / args.sampling_freq; + const float jam_bw = std::max(args.jam_bandwidth_hz, 100.0e3f); // floor 100 kHz + + // Auto-shift off DC if the user requested ~0 offset. NR leaves DC empty + // and the B-series DC-offset correction nulls anything close to LO. + float center_offset = args.tone_offset_hz; + if (std::fabs(center_offset) < 50.0e3f) { + center_offset = 1.92e6f; // STORM-style: one SSB-width above LO + } + + // Zero-order hold upsampling factor: how many output samples each random + // sample is held. For 23.04 MS/s sampling and 1.92 MHz jam bandwidth this + // is 12, producing a sinc-shaped band-limited noise of width ≈ jam_bw. + const int hold = + std::max(1, static_cast(std::round(args.sampling_freq / jam_bw))); + + // Complex Gaussian noise with amplitude calibrated so the typical sample + // magnitude is around args.amplitude. Sigma = amplitude * 0.5 gives an + // RMS magnitude near amplitude/sqrt(2) and peak excursions around + // 3 * sigma ~ 1.5 * amplitude, well within DAC headroom for amplitude ≤ 0.6. + const float sigma = args.amplitude * 0.5f; + + std::random_device rd; + std::mt19937 gen(rd()); + std::normal_distribution normal(0.0f, sigma); + + float phase = args.initial_phase; + const float dphase = 2.0f * PI * center_offset * dt; + + float re_held = 0.0f; + float im_held = 0.0f; + int hold_cnt = 0; + + for (size_t i = 0; i < args.num_samples; ++i) { + // Draw a fresh complex Gaussian sample every `hold` output samples. + if (hold_cnt == 0) { + re_held = normal(gen); + im_held = normal(gen); + } + if (++hold_cnt >= hold) { + hold_cnt = 0; + } + + // Mix the held noise sample up to the desired baseband offset: + // y[n] = noise[n] * exp(j * 2*pi * center_offset * n * dt) + const float c = std::cos(phase); + const float s = std::sin(phase); + const float re_out = re_held * c - im_held * s; + const float im_out = re_held * s + im_held * c; + samples.emplace_back(re_out, im_out); + + phase += dphase; + if (phase > 2.0f * PI) { + phase -= 2.0f * PI; + } else if (phase < 0.0f) { + phase += 2.0f * PI; + } + } + + return samples; +} diff --git a/src/main.cc b/src/main.cc index a9aba1a..e6c2990 100644 --- a/src/main.cc +++ b/src/main.cc @@ -3,7 +3,7 @@ #include "autoconfig.h" #include "args.h" -#include "noise.h" +#include "attack.h" #include "rf.h" #include #include @@ -90,6 +90,12 @@ int main(int argc, char *argv[]) { // Override config with any command-line arguments provided overrideConfig(args, argc, argv); + try { + validateAttackArgs(args); + } catch (const std::exception &ex) { + fprintf(stderr, "Config error: %s\n", ex.what()); + return EXIT_FAILURE; + } // Validate that RF fields were provided (either from YAML or autoconfig) if (args.center_frequency == 0.0f || args.sampling_freq == 0.0f || args.rf.tx_gain == 0.0f) { @@ -98,8 +104,10 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - // Generate the complex sine wave - auto samples = generateComplexSineWave(args); + // Generate attack waveform samples + std::cout << "Attack type: " << attackTypeToString(args.attack_type) + << std::endl; + auto samples = generate_samples(args); // Write IQ binary file if enabled if (args.write_iq) { diff --git a/src/noise.cc b/src/noise.cc deleted file mode 100644 index 82d93ef..0000000 --- a/src/noise.cc +++ /dev/null @@ -1,87 +0,0 @@ -#include "noise.h" - -std::vector> -generateComplexSineWave(const all_args_t args) { - - std::vector> samples; - samples.reserve(args.num_samples); - - const float delta_t = - 1.0f / args.sampling_freq; // Calculates the time between each sample; - // Sample interval - float halfBandwidth = args.bandwidth / (2.0f); - float phase = args.initial_phase; - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution freq_dist( - args.center_frequency - halfBandwidth, - args.center_frequency + halfBandwidth); - std::uniform_real_distribution ampl( - args.amplitude - args.amplitude_width / 2.0f, - args.amplitude + args.amplitude_width / 2.0f); - // generates random frequencies in the range [center-halfband, - // center+halfband] - - for (size_t i = 0; i < args.num_samples; - i++) { // infinite no of samples generated - - float current_freq = freq_dist(gen); - float current_ampl = ampl(gen); - - // Generate complex sample using polar coordinates - /*Computes z = args.amplitude * cos(phase) {real part} + args.amplitude * - (i) sin(phase) {imaginary part Do note that imaginary part is the coff. of i - so sin(phase)} If the case of converting to cartesian plane arises the - angle (phase) can be found by - arcsin(imaginary/args.amplitude) or arccos(real/amp.)*/ - samples.push_back(std::polar(current_ampl, phase)); - - // Update phase for next sample (correct frequency ramp integration) - phase += 2 * PI * current_freq * delta_t; - - // 2pif = angular velocity, 2pif(deltat) = change in angle - // per sec * delta_t = - // change in angle in delta_t time. - - // Keep phase wrapped to [0, 2π) to prevent precision loss - - phase = fmod( - phase, 2 * PI); // remander angle after subtracting the multiples of 2pi - // doesn't change the value of cos or sin and also returns it to the - // principle value from 0 to 2pi. - if (phase < 0.0) { - phase += 2 * PI; - } - } - - return samples; -} - -void transmission(uhd::usrp::multi_usrp::sptr usrp, const all_args_t args) { - - // Configure the USRP transmission stream - uhd::stream_args_t stream_args("fc32", - "sc16"); // Complex float to short conversion - uhd::tx_streamer::sptr tx_stream = usrp->get_tx_stream(stream_args); - - uhd::tx_metadata_t metadata; - metadata.start_of_burst = - true; // First packet should have start_of_burst = true - metadata.end_of_burst = false; - metadata.has_time_spec = false; - - std::vector> samples = generateComplexSineWave(args); - - while (true) { - - // Transmit samples - tx_stream->send(samples.data(), samples.size(), metadata); - std::cout << "Transmitting...." << std::endl; - - // After the first packet, set `start_of_burst = false` - metadata.start_of_burst = false; - } - - // We will never reach this point unless we manually break the loop -} diff --git a/worker_thread.py b/worker_thread.py index 249eb66..5b40ce4 100644 --- a/worker_thread.py +++ b/worker_thread.py @@ -1,4 +1,6 @@ from worker_thread import WorkerThread +import os + class jammer(WorkerThread): def start(self): @@ -6,7 +8,17 @@ def start(self): self.cleanup_old_containers() self.setup_env() self.setup_networks() - self.config.container_volumes[self.config.config_file] = {"bind": "/jammer.yaml", "mode": "ro"} - self.setup_volumes() + self.config.container_volumes[self.config.config_file] = { + "bind": "/jammer.yaml", "mode": "ro" + } + + config_dir = os.path.dirname(os.path.abspath(self.config.config_file)) + self.config.container_volumes[config_dir] = { + "bind": "/jammer", "mode": "rw" + } + + self.config.host_network = True + + self.setup_volumes() self.start_container()