Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de9e537
Add Wireshark Type-1 capture transport
GhostBassist Apr 1, 2026
04b826a
Extend Wireshark decode and add PCAP mirroring
GhostBassist Apr 1, 2026
35ba8da
Fix uplink Wireshark emission and expand Lua summaries
GhostBassist Apr 9, 2026
497efaf
Expand Wireshark uplink control summaries
GhostBassist Apr 9, 2026
f47fd8e
Fix supplementary MAC decoding in Wireshark
GhostBassist Apr 9, 2026
41c4c7d
Fix Wireshark LLC header decoding
GhostBassist Apr 9, 2026
1217895
Skip deep decode for CRC-failed control frames
GhostBassist Apr 9, 2026
be8252b
Fix uplink STCH block2 timing for Wireshark
GhostBassist Apr 9, 2026
59128e5
Improve uplink MAC-ACCESS and MM summaries
GhostBassist Apr 9, 2026
1731d8c
Clarify traffic BFI in Wireshark
GhostBassist Apr 9, 2026
78eef23
Reassemble SCH-HU uplink fragments in Lua
GhostBassist Apr 9, 2026
2ad0477
Keep full-slot uplink NTS2 traffic intact
GhostBassist Apr 9, 2026
692fca5
Fix uplink MM location update element ids
GhostBassist Apr 9, 2026
b4ea74f
Fix uplink NTS2 handling and Lua fragment reuse
GhostBassist Apr 9, 2026
43c4f42
Add Wireshark broadcast decode and suppression
GhostBassist Apr 9, 2026
c70c127
Document Wireshark display filters
GhostBassist Apr 9, 2026
6c19bae
Add Wireshark access-assign suppression
GhostBassist Apr 9, 2026
075468d
Fix Wireshark SCH-HU fragment cache and filter MAC-RESOURCE
GhostBassist Apr 9, 2026
e0d2cf3
Restore split NTS2 view for uplink traffic control
GhostBassist Apr 9, 2026
52bca8a
Stabilize Wireshark SCH-HU redissection state
GhostBassist Apr 9, 2026
a47b166
Queue SCH-HU fragments per timeslot in Wireshark
GhostBassist Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 90 additions & 4 deletions bins/bluestation-bs/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use clap::Parser;
use std::collections::HashMap;
use std::io::{IsTerminal, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
Expand All @@ -8,6 +9,9 @@ use tetra_entities::net_control::channel::build_all_control_links;
use tetra_entities::net_control::{
CONTROL_HEARTBEAT_INTERVAL, CONTROL_HEARTBEAT_TIMEOUT, CONTROL_PROTOCOL_VERSION, CommandDispatcher, ControlWorker,
};
use tetra_entities::net_wireshark::pcap::PcapWriter;
use tetra_entities::net_wireshark::worker::WiresharkWorker;
use tetra_entities::net_wireshark::{WiresharkSource, wireshark_channel};

use tetra_config::bluestation::{PhyBackend, SharedConfig, StackConfig, parsing};
use tetra_core::{TdmaTime, debug};
Expand All @@ -18,6 +22,8 @@ use tetra_entities::net_telemetry::worker::TelemetryWorker;
use tetra_entities::net_telemetry::{
TELEMETRY_HEARTBEAT_INTERVAL, TELEMETRY_HEARTBEAT_TIMEOUT, TELEMETRY_PROTOCOL_VERSION, TelemetrySource, telemetry_channel,
};
use tetra_entities::network::transports::NetworkAddress;
use tetra_entities::network::transports::udp::{UdpTransport, UdpTransportConfig};
use tetra_entities::network::transports::websocket::{WebSocketTransport, WebSocketTransportConfig};
use tetra_entities::{
cmce::cmce_bs::CmceBs,
Expand Down Expand Up @@ -107,8 +113,77 @@ fn start_control_worker(cfg: SharedConfig, command_dispatchers: HashMap<TetraEnt
})
}

fn start_wireshark_worker(cfg: SharedConfig, wireshark_source: WiresharkSource) -> thread::JoinHandle<()> {
let config = cfg.config();
let wcfg = config.wireshark.as_ref().unwrap();

let udp_config = UdpTransportConfig::new(NetworkAddress::Udp {
host: wcfg.host.clone(),
port: wcfg.port,
});
let pcap_file = wcfg.pcap_file.clone();
let pcap_host = wcfg.host.clone();
let pcap_port = wcfg.port;

thread::spawn(move || {
let transport = UdpTransport::new(udp_config);
let mut worker = WiresharkWorker::new(wireshark_source, transport);
if let Some(path) = pcap_file {
let pcap = PcapWriter::create(&path, &pcap_host, pcap_port).unwrap_or_else(|e| {
eprintln!("Failed to create Wireshark PCAP file '{}': {}", path, e);
std::process::exit(1);
});
worker = worker.with_pcap(pcap);
}
worker.run();
})
}

fn confirm_pcap_output(config: &StackConfig) {
let Some(wcfg) = config.wireshark.as_ref() else {
return;
};
let Some(pcap_file) = wcfg.pcap_file.as_ref() else {
return;
};

eprintln!("\nWARNING: Wireshark PCAP output is enabled.");
eprintln!(" PCAP file: {}", pcap_file);
eprintln!(" Synthetic UDP destination: {}:{}", wcfg.host, wcfg.port);
eprintln!(" This is intended for debugging and writes capture traffic to disk.");

if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
eprintln!("Refusing to continue without an interactive acknowledgement while PCAP output is enabled.");
std::process::exit(1);
}

print!("Type 'yes' to continue: ");
std::io::stdout().flush().unwrap_or_else(|e| {
eprintln!("Failed to flush stdout for PCAP acknowledgement prompt: {}", e);
std::process::exit(1);
});

let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap_or_else(|e| {
eprintln!("Failed to read PCAP acknowledgement response: {}", e);
std::process::exit(1);
});

if !input.trim().eq_ignore_ascii_case("yes") {
eprintln!("Aborting startup because PCAP output was not acknowledged.");
std::process::exit(1);
}
}

/// Start base station stack
fn build_bs_stack(cfg: &mut SharedConfig) -> (MessageRouter, Option<TelemetrySource>, HashMap<TetraEntity, CommandDispatcher>) {
fn build_bs_stack(
cfg: &mut SharedConfig,
) -> (
MessageRouter,
Option<TelemetrySource>,
Option<WiresharkSource>,
HashMap<TetraEntity, CommandDispatcher>,
) {
let mut router = MessageRouter::new(cfg.clone());

// Add suitable Phy component based on PhyIo type
Expand All @@ -131,6 +206,13 @@ fn build_bs_stack(cfg: &mut SharedConfig) -> (MessageRouter, Option<TelemetrySou
(None, None)
};

let (wsink, wsource) = if cfg.config().wireshark.is_some() {
let (a, b) = wireshark_channel();
(Some(a), Some(b))
} else {
(None, None)
};

// Build control links, if enabled
let (mut c_d, mut c_e) = if cfg.config().control.is_some() {
build_all_control_links()
Expand All @@ -139,7 +221,7 @@ fn build_bs_stack(cfg: &mut SharedConfig) -> (MessageRouter, Option<TelemetrySou
};

// Add remaining components
let lmac = LmacBs::new(cfg.clone());
let lmac = LmacBs::new(cfg.clone(), wsink.clone());
let umac = UmacBs::new(cfg.clone());
let llc = Llc::new(cfg.clone());
let mle = MleBs::new(cfg.clone());
Expand Down Expand Up @@ -171,7 +253,7 @@ fn build_bs_stack(cfg: &mut SharedConfig) -> (MessageRouter, Option<TelemetrySou
// Init network time
router.set_dl_time(TdmaTime::default());

(router, tsource, c_d)
(router, tsource, wsource, c_d)
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -201,15 +283,19 @@ fn main() {

// Build immutable, cheaply clonable SharedConfig and build the base station stack
let stack_cfg = load_config_from_toml(&args.config);
confirm_pcap_output(&stack_cfg);
let mut cfg = SharedConfig::from_parts(stack_cfg, None);

let _log_guard = debug::setup_logging_default(cfg.config().debug_log.clone());
let (mut router, tsource, cdispatchers) = build_bs_stack(&mut cfg);
let (mut router, tsource, wsource, cdispatchers) = build_bs_stack(&mut cfg);

// Start Telemetry and Control threads, if enabled
if let Some(telemetry_source) = tsource {
start_telemetry_worker(cfg.clone(), telemetry_source);
};
if let Some(wireshark_source) = wsource {
start_wireshark_worker(cfg.clone(), wireshark_source);
};
if cfg.config().control.is_some() {
start_control_worker(cfg.clone(), cdispatchers);
};
Expand Down
71 changes: 71 additions & 0 deletions contrib/wireshark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# BlueStation Wireshark Capture

`bluestation_type1.lua` decodes the custom UDP datagrams emitted by BlueStation's `[wireshark]` transport.

Example config:

```toml
[wireshark]
host = "127.0.0.1"
port = 42069
# pcap_file = "./bluestation-type1-debug.pcap"
# suppress_mac_resource = true
# suppress_access_assign = true
# suppress_d_mle_sync = true
# suppress_d_mle_sysinfo = true
```

Install in Wireshark by copying `bluestation_type1.lua` into your personal or global plugins directory, then restart Wireshark.

Useful display filters:

```wireshark
udp.port == 42069
```

```wireshark
bluestation.type1.direction == 1
```

```wireshark
bluestation.type1.direction == 0
```

```wireshark
bluestation.type1.direction == 1 && bluestation.type1.crc_pass == 0
```

```wireshark
bluestation.type1.direction == 1 && bluestation.type1.logical_channel == 7
```

```wireshark
bluestation.type1.direction == 1 && bluestation.type1.logical_channel == 5
```

```wireshark
bluestation.type1.logical_channel != 2 && bluestation.type1.logical_channel != 3
```

Field values:

- `bluestation.type1.direction`: `0 = Downlink`, `1 = Uplink`
- `bluestation.type1.crc_pass`: `0 = false`, `1 = true`
- `bluestation.type1.logical_channel`: `2 = BSCH`, `3 = BNCH`, `5 = SCH/F`, `7 = SCH/HU`, `8 = TCH/S`

Notes:

- The dissector registers heuristically on UDP by checking for the `TBSW` magic header, so it can decode any configured destination port.
- Port `42069` is also registered directly as a convenience for the example config.
- If `pcap_file` is configured, BlueStation also writes synthetic Ethernet/IPv4/UDP packets carrying the same `TBSW` datagrams into a PCAP file so the capture can be opened directly in Wireshark later.
- BlueStation requires an interactive `yes` acknowledgement at startup before it will write a PCAP file.
- `suppress_mac_resource` drops downlink `MAC-RESOURCE` exports at the source.
- `suppress_access_assign` drops AACH `ACCESS-ASSIGN` exports at the source.
- `suppress_d_mle_sync` drops BSCH `D-MLE-SYNC` exports at the source.
- `suppress_d_mle_sysinfo` drops BNCH `D-MLE-SYSINFO` exports at the source.
- The script decodes the BlueStation wrapper, AACH `ACCESS-ASSIGN`, BSCH `MAC-SYNC` plus `D-MLE-SYNC`, BNCH `MAC-SYSINFO` plus `D-MLE-SYSINFO`, and the main SCH/STCH MAC headers.
- The script also decodes downlink MLE `D-NWRK-BROADCAST` payloads carried through LLC/TL-SDU.
- SCH/STCH TM-SDUs are further decoded through LLC and the MLE protocol discriminator into downlink MM and CMCE control PDUs when the MAC payload is not air-interface encrypted.
- Downlink MM coverage now includes location update accept/proceeding/reject, attach/detach group identity, and MM status with structured decoding of the typed optional fields already modeled in the Rust parser.
- Downlink CMCE coverage now includes alert, call proceeding, setup, connect, connect acknowledge, disconnect, release, status, and SDS data, including nested basic service information, party addressing, transmission grant, disconnect cause, pre-coded status, and SDS payload metadata.
- Uplink control PDUs and traffic-channel voice/data payloads are still exported and labeled, but not yet fully dissected.
Loading