mirror of
https://github.com/smittix/intercept.git
synced 2026-07-03 15:23:39 -07:00
Merge upstream/main and resolve acars, vdl2, dashboard conflicts
Resolved conflicts: - routes/acars.py: keep /messages and /clear endpoints for history reload - routes/vdl2.py: keep /messages and /clear endpoints for history reload - templates/adsb_dashboard.html: keep removal of hardcoded device-1 defaults for ACARS/VDL2 selectors (users pick their own device) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -300,6 +300,20 @@ SUBGHZ_PRESETS = {
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RADIOSONDE (Weather Balloon Tracking)
|
||||
# =============================================================================
|
||||
|
||||
# UDP port for radiosonde_auto_rx telemetry broadcast
|
||||
RADIOSONDE_UDP_PORT = 55673
|
||||
|
||||
# Radiosonde process termination timeout
|
||||
RADIOSONDE_TERMINATE_TIMEOUT = 5
|
||||
|
||||
# Maximum age for balloon data before cleanup (30 min — balloons move slowly)
|
||||
MAX_RADIOSONDE_AGE_SECONDS = 1800
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DEAUTH ATTACK DETECTION
|
||||
# =============================================================================
|
||||
|
||||
+59
-32
@@ -1,49 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Any
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger('intercept.dependencies')
|
||||
|
||||
# Additional paths to search for tools (e.g., /usr/sbin on Debian)
|
||||
EXTRA_TOOL_PATHS = ['/usr/sbin', '/sbin']
|
||||
|
||||
# Tools installed to non-standard locations (not on PATH)
|
||||
KNOWN_TOOL_PATHS: dict[str, list[str]] = {
|
||||
'auto_rx.py': [
|
||||
'/opt/radiosonde_auto_rx/auto_rx/auto_rx.py',
|
||||
'/opt/auto_rx/auto_rx.py',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def check_tool(name: str) -> bool:
|
||||
"""Check if a tool is installed."""
|
||||
return get_tool_path(name) is not None
|
||||
|
||||
|
||||
def get_tool_path(name: str) -> str | None:
|
||||
"""Get the full path to a tool, checking standard PATH and extra locations."""
|
||||
# Optional explicit override, e.g. INTERCEPT_RTL_FM_PATH=/opt/homebrew/bin/rtl_fm
|
||||
env_key = f"INTERCEPT_{name.upper().replace('-', '_')}_PATH"
|
||||
env_path = os.environ.get(env_key)
|
||||
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
|
||||
return env_path
|
||||
|
||||
# Prefer native Homebrew binaries on Apple Silicon to avoid mixing Rosetta
|
||||
# /usr/local tools with arm64 Python/runtime.
|
||||
if platform.system() == 'Darwin':
|
||||
machine = platform.machine().lower()
|
||||
preferred_paths: list[str] = []
|
||||
if machine in {'arm64', 'aarch64'}:
|
||||
preferred_paths.append('/opt/homebrew/bin')
|
||||
preferred_paths.append('/usr/local/bin')
|
||||
|
||||
for base in preferred_paths:
|
||||
full_path = os.path.join(base, name)
|
||||
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
|
||||
return full_path
|
||||
|
||||
# First check standard PATH
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
def get_tool_path(name: str) -> str | None:
|
||||
"""Get the full path to a tool, checking standard PATH and extra locations."""
|
||||
# Optional explicit override, e.g. INTERCEPT_RTL_FM_PATH=/opt/homebrew/bin/rtl_fm
|
||||
env_key = f"INTERCEPT_{name.upper().replace('-', '_')}_PATH"
|
||||
env_path = os.environ.get(env_key)
|
||||
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
|
||||
return env_path
|
||||
|
||||
# Prefer native Homebrew binaries on Apple Silicon to avoid mixing Rosetta
|
||||
# /usr/local tools with arm64 Python/runtime.
|
||||
if platform.system() == 'Darwin':
|
||||
machine = platform.machine().lower()
|
||||
preferred_paths: list[str] = []
|
||||
if machine in {'arm64', 'aarch64'}:
|
||||
preferred_paths.append('/opt/homebrew/bin')
|
||||
preferred_paths.append('/usr/local/bin')
|
||||
|
||||
for base in preferred_paths:
|
||||
full_path = os.path.join(base, name)
|
||||
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
|
||||
return full_path
|
||||
|
||||
# First check standard PATH
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
|
||||
# Check additional paths (e.g., /usr/sbin for aircrack-ng on Debian)
|
||||
for extra_path in EXTRA_TOOL_PATHS:
|
||||
@@ -51,6 +59,11 @@ def get_tool_path(name: str) -> str | None:
|
||||
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
|
||||
return full_path
|
||||
|
||||
# Check known non-standard install locations
|
||||
for known_path in KNOWN_TOOL_PATHS.get(name, []):
|
||||
if os.path.isfile(known_path):
|
||||
return known_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -447,6 +460,20 @@ TOOL_DEPENDENCIES = {
|
||||
}
|
||||
}
|
||||
},
|
||||
'radiosonde': {
|
||||
'name': 'Radiosonde Tracking',
|
||||
'tools': {
|
||||
'auto_rx.py': {
|
||||
'required': True,
|
||||
'description': 'Radiosonde weather balloon decoder',
|
||||
'install': {
|
||||
'apt': 'Run ./setup.sh (clones from GitHub)',
|
||||
'brew': 'Run ./setup.sh (clones from GitHub)',
|
||||
'manual': 'https://github.com/projecthorus/radiosonde_auto_rx'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'tscm': {
|
||||
'name': 'TSCM Counter-Surveillance',
|
||||
'tools': {
|
||||
|
||||
+129
-87
@@ -6,31 +6,31 @@ Detects RTL-SDR devices via rtl_test and other SDR hardware via SoapySDR.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .base import SDRCapabilities, SDRDevice, SDRType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Cache HackRF detection results so polling endpoints don't repeatedly run
|
||||
# hackrf_info while the device is actively streaming in SubGHz mode.
|
||||
_hackrf_cache: list[SDRDevice] = []
|
||||
_hackrf_cache_ts: float = 0.0
|
||||
_HACKRF_CACHE_TTL_SECONDS = 3.0
|
||||
|
||||
|
||||
def _hackrf_probe_blocked() -> bool:
|
||||
"""Return True when probing HackRF would interfere with an active stream."""
|
||||
try:
|
||||
from utils.subghz import get_subghz_manager
|
||||
return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'}
|
||||
except Exception:
|
||||
return False
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Cache HackRF detection results so polling endpoints don't repeatedly run
|
||||
# hackrf_info while the device is actively streaming in SubGHz mode.
|
||||
_hackrf_cache: list[SDRDevice] = []
|
||||
_hackrf_cache_ts: float = 0.0
|
||||
_HACKRF_CACHE_TTL_SECONDS = 3.0
|
||||
|
||||
|
||||
def _hackrf_probe_blocked() -> bool:
|
||||
"""Return True when probing HackRF would interfere with an active stream."""
|
||||
try:
|
||||
from utils.subghz import get_subghz_manager
|
||||
return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'}
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _check_tool(name: str) -> bool:
|
||||
@@ -112,21 +112,21 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
|
||||
lib_paths = ['/usr/local/lib', '/opt/homebrew/lib']
|
||||
current_ld = env.get('DYLD_LIBRARY_PATH', '')
|
||||
env['DYLD_LIBRARY_PATH'] = ':'.join(lib_paths + [current_ld] if current_ld else lib_paths)
|
||||
result = subprocess.run(
|
||||
['rtl_test', '-t'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
timeout=5,
|
||||
env=env
|
||||
)
|
||||
output = result.stderr + result.stdout
|
||||
|
||||
# Parse device info from rtl_test output
|
||||
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
|
||||
# Require a non-empty serial to avoid matching malformed lines like "SN:".
|
||||
device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$'
|
||||
result = subprocess.run(
|
||||
['rtl_test', '-t'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
timeout=5,
|
||||
env=env
|
||||
)
|
||||
output = result.stderr + result.stdout
|
||||
|
||||
# Parse device info from rtl_test output
|
||||
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
|
||||
# Require a non-empty serial to avoid matching malformed lines like "SN:".
|
||||
device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$'
|
||||
|
||||
from .rtlsdr import RTLSDRCommandBuilder
|
||||
|
||||
@@ -134,14 +134,14 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
|
||||
line = line.strip()
|
||||
match = re.match(device_pattern, line)
|
||||
if match:
|
||||
devices.append(SDRDevice(
|
||||
sdr_type=SDRType.RTL_SDR,
|
||||
index=int(match.group(1)),
|
||||
name=match.group(2).strip().rstrip(','),
|
||||
serial=match.group(3),
|
||||
driver='rtlsdr',
|
||||
capabilities=RTLSDRCommandBuilder.CAPABILITIES
|
||||
))
|
||||
devices.append(SDRDevice(
|
||||
sdr_type=SDRType.RTL_SDR,
|
||||
index=int(match.group(1)),
|
||||
name=match.group(2).strip().rstrip(','),
|
||||
serial=match.group(3),
|
||||
driver='rtlsdr',
|
||||
capabilities=RTLSDRCommandBuilder.CAPABILITIES
|
||||
))
|
||||
|
||||
# Fallback: if we found devices but couldn't parse details
|
||||
if not devices:
|
||||
@@ -314,29 +314,29 @@ def _add_soapy_device(
|
||||
))
|
||||
|
||||
|
||||
def detect_hackrf_devices() -> list[SDRDevice]:
|
||||
"""
|
||||
Detect HackRF devices using native hackrf_info tool.
|
||||
|
||||
Fallback for when SoapySDR is not available.
|
||||
"""
|
||||
global _hackrf_cache, _hackrf_cache_ts
|
||||
now = time.time()
|
||||
|
||||
# While HackRF is actively streaming in SubGHz mode, skip probe calls.
|
||||
# Re-running hackrf_info during active RX/TX can disrupt the USB stream.
|
||||
if _hackrf_probe_blocked():
|
||||
return list(_hackrf_cache)
|
||||
|
||||
if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS:
|
||||
return list(_hackrf_cache)
|
||||
|
||||
devices: list[SDRDevice] = []
|
||||
|
||||
if not _check_tool('hackrf_info'):
|
||||
_hackrf_cache = devices
|
||||
_hackrf_cache_ts = now
|
||||
return devices
|
||||
def detect_hackrf_devices() -> list[SDRDevice]:
|
||||
"""
|
||||
Detect HackRF devices using native hackrf_info tool.
|
||||
|
||||
Fallback for when SoapySDR is not available.
|
||||
"""
|
||||
global _hackrf_cache, _hackrf_cache_ts
|
||||
now = time.time()
|
||||
|
||||
# While HackRF is actively streaming in SubGHz mode, skip probe calls.
|
||||
# Re-running hackrf_info during active RX/TX can disrupt the USB stream.
|
||||
if _hackrf_probe_blocked():
|
||||
return list(_hackrf_cache)
|
||||
|
||||
if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS:
|
||||
return list(_hackrf_cache)
|
||||
|
||||
devices: list[SDRDevice] = []
|
||||
|
||||
if not _check_tool('hackrf_info'):
|
||||
_hackrf_cache = devices
|
||||
_hackrf_cache_ts = now
|
||||
return devices
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -374,12 +374,12 @@ def detect_hackrf_devices() -> list[SDRDevice]:
|
||||
capabilities=HackRFCommandBuilder.CAPABILITIES
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"HackRF detection error: {e}")
|
||||
|
||||
_hackrf_cache = list(devices)
|
||||
_hackrf_cache_ts = now
|
||||
return devices
|
||||
except Exception as e:
|
||||
logger.debug(f"HackRF detection error: {e}")
|
||||
|
||||
_hackrf_cache = list(devices)
|
||||
_hackrf_cache_ts = now
|
||||
return devices
|
||||
|
||||
|
||||
def probe_rtlsdr_device(device_index: int) -> str | None:
|
||||
@@ -413,31 +413,73 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
|
||||
lib_paths + [current_ld] if current_ld else lib_paths
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
# Use Popen with early termination instead of run() with full timeout.
|
||||
# rtl_test prints device info to stderr quickly, then keeps running
|
||||
# its test loop. We kill it as soon as we see success or failure.
|
||||
proc = subprocess.Popen(
|
||||
['rtl_test', '-d', str(device_index), '-t'],
|
||||
capture_output=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=3,
|
||||
env=env,
|
||||
)
|
||||
output = result.stderr + result.stdout
|
||||
|
||||
if 'usb_claim_interface' in output or 'Failed to open' in output:
|
||||
import select
|
||||
error_found = False
|
||||
device_found = False
|
||||
deadline = time.monotonic() + 3.0
|
||||
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
# Wait for stderr output with timeout
|
||||
ready, _, _ = select.select(
|
||||
[proc.stderr], [], [], min(remaining, 0.1)
|
||||
)
|
||||
if ready:
|
||||
line = proc.stderr.readline()
|
||||
if not line:
|
||||
break # EOF — process closed stderr
|
||||
# Check for no-device messages first (before success check,
|
||||
# since "No supported devices found" also contains "Found" + "device")
|
||||
if 'no supported devices' in line.lower() or 'no matching devices' in line.lower():
|
||||
error_found = True
|
||||
break
|
||||
if 'usb_claim_interface' in line or 'Failed to open' in line:
|
||||
error_found = True
|
||||
break
|
||||
if 'Found' in line and 'device' in line.lower():
|
||||
# Device opened successfully — no need to wait longer
|
||||
device_found = True
|
||||
break
|
||||
if proc.poll() is not None:
|
||||
break # Process exited
|
||||
if not device_found and not error_found and proc.poll() is not None and proc.returncode != 0:
|
||||
# rtl_test exited with error and we never saw a success message
|
||||
error_found = True
|
||||
finally:
|
||||
try:
|
||||
proc.kill()
|
||||
except OSError:
|
||||
pass
|
||||
proc.wait()
|
||||
if device_found:
|
||||
# Allow the kernel to fully release the USB interface
|
||||
# before the caller opens the device with dump1090/rtl_fm/etc.
|
||||
time.sleep(0.5)
|
||||
|
||||
if error_found:
|
||||
logger.warning(
|
||||
f"RTL-SDR device {device_index} USB probe failed: "
|
||||
f"device busy or unavailable"
|
||||
)
|
||||
return (
|
||||
f'SDR device {device_index} is busy at the USB level — '
|
||||
f'another process outside INTERCEPT may be using it. '
|
||||
f'Check for stale rtl_fm/rtl_433/dump1090 processes, '
|
||||
f'or try a different device.'
|
||||
f'SDR device {device_index} is not available — '
|
||||
f'check that the RTL-SDR is connected and not in use by another process.'
|
||||
)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
# rtl_test opened the device successfully and is running the
|
||||
# test — that means the device *is* available.
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"RTL-SDR probe error for device {device_index}: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user