mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 12:49:26 -07:00
* client mode added
* Prevent OTA daemons dmclient and upgrade from running and phoning home to Verizon
* Fix workflow
* WIFI changes to support moxee. May need to rebase as delivering refactoring under other PR.
* code changes for rust based wifi client mode docs next
* Doc changes & security fixes
* Added watchdog and recover if crash occurs for wifi.
* Remove changes which were from device UI work (seperate feature which snuck into this branch)
* Add missing wifi and firewall module declarations
* cleaning up the code a bit
* Gate wpa_suplicant in installer and workflow to avoid building binary every push
* fix to check diskspace
* Improved support for subnet colisions, and attempts to rejoin network.
* Add WiFi client support and S01iptables to T-Mobile and Wingtech installers
Both installers now deploy wpa_supplicant, wpa_cli, udhcpc-hook.sh, and
the S01iptables boot-time firewall script. Config generation uses the
shared install_config/install_wifi_creds helpers instead of manual string
replacement.
* Revert "Add WiFi client support and S01iptables to T-Mobile and Wingtech installers"
This reverts commit 944b369c4f.
* Fix build: ignore unused wifi_ssid/wifi_password fields in T-Mobile and Wingtech installers
* Moved to a wifi crate
* Add host route and arp_filter to prevent subnet collisions
* add wakelock so kernel doesn't shut down wifi on battery when wifi is enabled
* Move wifi to external wifi-station crate, remove wifi from installer, extract OTA blocking
* fixed outdated info, moved udhcpc hook to wifi-station crate.
* Update to new version of wifi-station
* Address PR review feedback: replace Docker wpa build, add iw, remove OTA, revert unrelated changes
- Replace Docker-based wpa_supplicant build with shell script (scripts/build-wpa-supplicant.sh)
- Add iw cross-compilation and deployment to Orbic installer
- Skip wifi tool install if binary already exists on device
- Remove OTA daemon blocker (extracted for separate PR)
- Revert unrelated UZ801 and T-Mobile installer changes
- Remove connection.rs test scaffolding
- Rewrite S01iptables init script to read config.toml directly
- Pin url crate to 2.5.4 to fix MSRV
* Fix build script: use bash for parameter substitution
The ${VAR//pattern/replacement} syntax is a bash extension that
doesn't work in dash (Ubuntu's /bin/sh).
* Fix iw build: export PKG_CONFIG_LIBDIR as env var
Passing PKG_CONFIG_LIBDIR as a make variable doesn't export it to
$(shell pkg-config ...) calls. Set it as an environment variable
so pkg-config finds the cross-compiled libnl.
* Point wifi-station to GitHub rev 97c579a
* add comment
* Update daemon/src/config.rs
Add decorators
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
* Update daemon/src/server.rs
add utopia doc support
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
* Update daemon/src/server.rs
add utopia doc support
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
* Update to wifi-station with utoipa doc strings
* add utoipa to wifi-station
* added WPA3 support
* fix firewall port detection, update wifi-station to c267d37
fix ntfy port_or_known_default, comment out ntfy_url in config
template, update wifi-station with resolv.conf bind mount
fallback, udhcpc_bin config, and module path fix for UZ801
* show wifi UI for tmobile and wingtech, add udhcpc_bin config
both devices have wifi hardware and backend support. wingtech
verified on hardware (QCA6174 via PCIe). uz801 excluded for now
due to driver scan limitations with hostapd active.
* install wifi tools from orbic-usb installer, fix DNS default to Quad9, bump wifi-station rev
* fix Modal scroll listener leak, correct file transfer timeout math, document firewall fail-open, clarify UZ801 wifi status
* build-dev.sh: build wifi tools so install-dev works for orbic-family devices
* update Cargo.lock for wifi-station e8ec5b4
* fix setup_timeout_server crypto provider install, apply rustfmt
* Update installer/src/connection.rs
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
* Update installer/src/orbic.rs
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
* apply rustfmt to AdbConnection::run_command
---------
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
315 lines
10 KiB
Rust
315 lines
10 KiB
Rust
use std::net::SocketAddr;
|
|
use std::str::FromStr;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use nusb::Device;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::TcpStream;
|
|
use tokio::time::{sleep, timeout};
|
|
|
|
use crate::output::{print, println};
|
|
|
|
#[cfg(unix)]
|
|
use std::os::fd::AsRawFd;
|
|
|
|
pub async fn telnet_send_command_with_output(
|
|
addr: SocketAddr,
|
|
command: &str,
|
|
wait_for_prompt: bool,
|
|
command_timeout: Duration,
|
|
) -> Result<String> {
|
|
if command.contains('\n') {
|
|
bail!("multi-line commands are not allowed");
|
|
}
|
|
|
|
let stream = TcpStream::connect(addr).await?;
|
|
let (mut reader, mut writer) = stream.into_split();
|
|
|
|
if wait_for_prompt {
|
|
// Wait for the shell prompt. This also consumes any telnet IAC negotiation
|
|
// the server sends at connection start, and ensures the shell is ready
|
|
// for input.
|
|
while reader.read_u8().await? != b'#' {}
|
|
}
|
|
|
|
// This contraption is there so we clearly know where the command output starts and ends,
|
|
// skipping telnet echoing the command back using START, and terminating the connection right
|
|
// after the command exits.
|
|
//
|
|
// 'TELNET' is quoted so that when the command gets echoed back, it does not match against
|
|
// RAYHUNTER_TELNET_COMMAND_DONE search string.
|
|
writer.write_all(format!("echo RAYHUNTER_'TELNET'_COMMAND_START; {command}; echo RAYHUNTER_'TELNET'_COMMAND_DONE\r\n").as_bytes()).await?;
|
|
|
|
let mut read_buf = Vec::new();
|
|
timeout(command_timeout, async {
|
|
while let Ok(byte) = reader.read_u8().await {
|
|
read_buf.push(byte);
|
|
|
|
// when we see this string we know the command is done and can terminate.
|
|
// even if we sent command; exit, certain "telnet-like" shells (like nc contraptions)
|
|
// may not terminate the connection appropriately on their own.
|
|
if byte == b'\n' {
|
|
let response = String::from_utf8_lossy(&read_buf);
|
|
if response.contains("RAYHUNTER_TELNET_COMMAND_DONE") {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.with_context(|| format!("command timed out after {}s", command_timeout.as_secs()))?;
|
|
let string = String::from_utf8_lossy(&read_buf);
|
|
let start = string.rfind("RAYHUNTER_TELNET_COMMAND_START");
|
|
let end = string.rfind("RAYHUNTER_TELNET_COMMAND_DONE");
|
|
let string = match (start, end) {
|
|
(Some(start), Some(end)) => {
|
|
// skip past the START marker and the trailing \r\n of the echoed command line
|
|
let start = start + "RAYHUNTER_TELNET_COMMAND_START".len();
|
|
string[start..end].trim_start_matches(['\r', '\n'])
|
|
}
|
|
_ => bail!("failed to parse command output from string: {string:?}"),
|
|
};
|
|
Ok(string.to_string())
|
|
}
|
|
|
|
pub async fn telnet_send_command(
|
|
addr: SocketAddr,
|
|
command: &str,
|
|
expected_output: &str,
|
|
wait_for_prompt: bool,
|
|
) -> Result<()> {
|
|
let command = format!("{command}; echo command done, exit code $?");
|
|
let output =
|
|
telnet_send_command_with_output(addr, &command, wait_for_prompt, Duration::from_secs(10))
|
|
.await?;
|
|
if !output.contains(expected_output) {
|
|
bail!("{expected_output:?} not found in: {output}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn telnet_send_file(
|
|
addr: SocketAddr,
|
|
filename: &str,
|
|
payload: &[u8],
|
|
wait_for_prompt: bool,
|
|
) -> Result<()> {
|
|
print!("Sending file {filename} ... ");
|
|
// Allow 30s base + 2s per MB for the nc command to complete (covers slow WiFi links)
|
|
let transfer_timeout =
|
|
Duration::from_secs(30 + (payload.len() as u64 / (1024 * 1024)).max(1) * 2);
|
|
let nc_output = {
|
|
let filename = filename.to_owned();
|
|
let handle = tokio::spawn(async move {
|
|
telnet_send_command_with_output(
|
|
addr,
|
|
&format!("nc -l -p 8081 2>&1 >{filename}.tmp"),
|
|
wait_for_prompt,
|
|
transfer_timeout,
|
|
)
|
|
.await
|
|
});
|
|
|
|
let mut addr = addr;
|
|
addr.set_port(8081);
|
|
|
|
let mut stream;
|
|
let mut attempts = 0;
|
|
|
|
loop {
|
|
// wait for nc to become available, with exponential backoff.
|
|
//
|
|
// if the installer fails with connection refused, this
|
|
// likely is not high enough.
|
|
sleep(Duration::from_millis(100 * (1 << attempts))).await;
|
|
|
|
stream = TcpStream::connect(addr).await;
|
|
attempts += 1;
|
|
if stream.is_ok() || attempts > 3 {
|
|
break;
|
|
}
|
|
|
|
print!("attempt {attempts}... ");
|
|
}
|
|
|
|
let send_result: Result<()> = async {
|
|
let mut stream = stream?;
|
|
stream.write_all(payload).await?;
|
|
|
|
// if the orbic is sluggish, we need for nc to write the data to disk before
|
|
// terminating the connection. if we terminate the connection while there is unflushed
|
|
// data, that data will just not be written from nc's buffer into OS disk buffer. the
|
|
// symptom is mismatched md5 hashes.
|
|
//
|
|
// this is NOT fixed by calling fsync or similar, we're talking about dropped
|
|
// application buffers here.
|
|
sleep(Duration::from_millis(1000)).await;
|
|
|
|
Ok(())
|
|
}
|
|
.await;
|
|
|
|
let nc_output = handle
|
|
.await
|
|
.context("background nc writer failed")?
|
|
.context("background nc writer failed")?;
|
|
|
|
if let Err(e) = send_result {
|
|
bail!(
|
|
"Failed to send data to nc: {e}. nc output: '{}'",
|
|
nc_output.trim()
|
|
);
|
|
}
|
|
|
|
nc_output
|
|
};
|
|
|
|
let checksum = md5::compute(payload);
|
|
telnet_send_command(
|
|
addr,
|
|
&format!("md5sum {filename}.tmp"),
|
|
&format!("{checksum:x} {filename}.tmp"),
|
|
wait_for_prompt,
|
|
)
|
|
.await
|
|
.with_context(|| {
|
|
format!(
|
|
"File transfer failed. nc command output: '{}'. Expected checksum: {:x}",
|
|
nc_output.trim(),
|
|
checksum
|
|
)
|
|
})?;
|
|
|
|
telnet_send_command(
|
|
addr,
|
|
&format!("mv {filename}.tmp {filename}"),
|
|
"exit code 0",
|
|
wait_for_prompt,
|
|
)
|
|
.await?;
|
|
|
|
println!("ok");
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn send_file(admin_ip: &str, local_path: &str, remote_path: &str) -> Result<()> {
|
|
let file_content = std::fs::read(local_path)
|
|
.with_context(|| format!("Failed to read local file: {local_path}"))?;
|
|
|
|
println!("Connecting to {admin_ip}");
|
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:23"))
|
|
.with_context(|| format!("Invalid IP address: {admin_ip}"))?;
|
|
|
|
telnet_send_file(addr, remote_path, &file_content, true)
|
|
.await
|
|
.with_context(|| format!("Failed to send file {local_path} to {remote_path}"))?;
|
|
|
|
println!("Successfully sent {local_path} to {remote_path}");
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn reboot_device(addr: SocketAddr, reboot_command: &str, admin_ip: &str) {
|
|
println!(
|
|
"Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080"
|
|
);
|
|
let _ = telnet_send_command(addr, reboot_command, "", true).await;
|
|
}
|
|
|
|
/// General function to open a USB device
|
|
#[cfg(not(target_os = "android"))]
|
|
pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
|
|
let devices = match nusb::list_devices() {
|
|
Ok(d) => d,
|
|
Err(_) => return Ok(None),
|
|
};
|
|
for device in devices {
|
|
if device.vendor_id() == vid && device.product_id() == pid {
|
|
match device.open() {
|
|
Ok(d) => return Ok(Some(d)),
|
|
Err(e) => bail!("device found but failed to open: {}", e),
|
|
}
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
/// Open an interactive shell to a device
|
|
///
|
|
/// Connects to a shell service on the device and forwards stdin/stdout bidirectionally.
|
|
pub async fn interactive_shell(admin_ip: &str, shell_port: u16, raw_mode: bool) -> Result<()> {
|
|
let shell_addr = SocketAddr::from_str(&format!("{admin_ip}:{shell_port}"))?;
|
|
let mut stream = TcpStream::connect(shell_addr)
|
|
.await
|
|
.context("Failed to connect to shell. Make sure the device is reachable.")?;
|
|
|
|
let stdin = tokio::io::stdin();
|
|
|
|
#[cfg(unix)]
|
|
let raw_terminal_guard = if raw_mode {
|
|
Some(RawTerminal::new(stdin.as_raw_fd())?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// suppress "unused variable" lint
|
|
#[cfg(not(unix))]
|
|
let _used = raw_mode;
|
|
|
|
let mut stdio = tokio::io::join(stdin, tokio::io::stdout());
|
|
let _ = tokio::io::copy_bidirectional(&mut stream, &mut stdio).await;
|
|
|
|
// hitting ctrl-d will not print a trailing newline on tplink at least, which messes up the
|
|
// next prompt
|
|
println!();
|
|
|
|
// The current_thread runtime in tokio will block forever until stdin receives a read error. To
|
|
// work around this cleanup issue we just exit directly from here.
|
|
//
|
|
// This is documented as a flaw in tokio::io::stdin()'s own docs, but the recommended
|
|
// workaround to spawn your own OS thread doesn't work.
|
|
//
|
|
// For some reason this only happens when the terminal is being put in raw mode (removing
|
|
// RawTerminal fixes it)
|
|
//
|
|
// We have to drop the RawTerminal guard before exiting, otherwise we will
|
|
// mess up the terminal.
|
|
#[cfg(unix)]
|
|
drop(raw_terminal_guard);
|
|
std::process::exit(0)
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
struct RawTerminal {
|
|
fd: std::os::fd::RawFd,
|
|
original_termios: termios::Termios,
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
impl RawTerminal {
|
|
fn new(fd: std::os::fd::RawFd) -> Result<Self> {
|
|
// put terminal in raw mode so that arrow keys, tab etc are correctly forwarded to the
|
|
// device's shell
|
|
let original_termios = termios::Termios::from_fd(fd)?;
|
|
let mut new_termios = original_termios;
|
|
|
|
// set flags on the struct
|
|
termios::cfmakeraw(&mut new_termios);
|
|
|
|
// apply changes
|
|
termios::tcsetattr(fd, termios::TCSANOW, &new_termios)?;
|
|
|
|
Ok(RawTerminal {
|
|
fd,
|
|
original_termios,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
impl Drop for RawTerminal {
|
|
fn drop(&mut self) {
|
|
let _ = termios::tcsetattr(self.fd, termios::TCSANOW, &self.original_termios);
|
|
}
|
|
}
|