mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-04-30 09:29:58 -07:00
installer: add pinephone and pinephonepro support
This commit is contained in:
committed by
Cooper Quintin
parent
d3bd8d9dfc
commit
8583064e46
@@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
|
||||
use env_logger::Env;
|
||||
|
||||
mod orbic;
|
||||
mod pinephone;
|
||||
mod tmobile;
|
||||
mod tplink;
|
||||
mod util;
|
||||
@@ -18,12 +19,16 @@ struct Args {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
// A note on stylisation of device names: strip special characters and spell like This regardless
|
||||
// of the manufacturer's capitalisation.
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Install rayhunter on the Orbic Orbic RC400L.
|
||||
Orbic(InstallOrbic),
|
||||
/// Install rayhunter on the TMobile TMOHS1.
|
||||
Tmobile(TmobileArgs),
|
||||
/// Install rayhunter on a PinePhone's Quectel modem.
|
||||
Pinephone(InstallPinephone),
|
||||
/// Install rayhunter on the TP-Link M7350.
|
||||
Tplink(InstallTpLink),
|
||||
/// Install rayhunter on the Wingtech CT2MHS01.
|
||||
@@ -58,6 +63,9 @@ struct InstallTpLink {
|
||||
#[derive(Parser, Debug)]
|
||||
struct InstallOrbic {}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct InstallPinephone {}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Util {
|
||||
#[command(subcommand)]
|
||||
@@ -69,7 +77,7 @@ enum UtilSubCommand {
|
||||
/// Send a serial command to the Orbic.
|
||||
Serial(Serial),
|
||||
/// Start an ADB shell
|
||||
Shell(Shell),
|
||||
Shell(NoArgs),
|
||||
/// Root the Tmobile and launch adb.
|
||||
TmobileStartAdb(TmobileArgs),
|
||||
/// Root the Tmobile and launch telnetd.
|
||||
@@ -80,6 +88,10 @@ enum UtilSubCommand {
|
||||
WingtechStartTelnet(WingtechArgs),
|
||||
/// Root the Wingtech and launch adb.
|
||||
WingtechStartAdb(WingtechArgs),
|
||||
/// Unlock the Pinephone's modem and start adb.
|
||||
PinephoneStartAdb(NoArgs),
|
||||
/// Lock the Pinephone's modem and stop adb.
|
||||
PinephoneStopAdb(NoArgs),
|
||||
/// Send a file to the TP-Link device over telnet.
|
||||
///
|
||||
/// Before running this utility, you need to make telnet accessible with `installer util
|
||||
@@ -151,7 +163,7 @@ struct Serial {
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Shell {}
|
||||
struct NoArgs {}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
|
||||
@@ -160,6 +172,8 @@ async fn run() -> Result<(), Error> {
|
||||
match command {
|
||||
Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Pinephone(_) => pinephone::install().await
|
||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||
Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
||||
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
||||
Command::Util(subcommand) => match subcommand.command {
|
||||
@@ -195,6 +209,8 @@ async fn run() -> Result<(), Error> {
|
||||
}
|
||||
UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?,
|
||||
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?,
|
||||
UtilSubCommand::PinephoneStartAdb(_) => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
|
||||
UtilSubCommand::PinephoneStopAdb(_) => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ use std::time::Duration;
|
||||
|
||||
use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use nusb::Interface;
|
||||
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
||||
use nusb::{Device, Interface};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::util::echo;
|
||||
use crate::util::{echo, open_usb_device};
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
||||
@@ -138,7 +138,8 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
|
||||
get_adb().await
|
||||
}
|
||||
|
||||
async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
/// Test rayhunter on the device over adb without forwarding.
|
||||
pub async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
const MAX_FAILURES: u32 = 10;
|
||||
let mut failures = 0;
|
||||
while failures < MAX_FAILURES {
|
||||
@@ -474,22 +475,3 @@ pub fn open_orbic() -> Result<Option<Interface>> {
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// General function to open a USB device
|
||||
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)
|
||||
}
|
||||
|
||||
276
installer/src/pinephone.rs
Normal file
276
installer/src/pinephone.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use adb_client::{ADBDeviceExt, ADBUSBDevice};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use md5::compute as md5_compute;
|
||||
use md5crypt::md5crypt;
|
||||
use nusb::Interface;
|
||||
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::orbic::test_rayhunter;
|
||||
use crate::util::{echo, open_usb_device};
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
const USB_VENDOR_ID: u16 = 0x2C7C;
|
||||
const USB_PRODUCT_ID: u16 = 0x125;
|
||||
const USB_INTERFACE_NUMBER: u8 = 2;
|
||||
|
||||
pub async fn install() -> Result<()> {
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?;
|
||||
adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?;
|
||||
adb.install_file(
|
||||
"/data/rayhunter/config.toml",
|
||||
CONFIG_TOML
|
||||
.replace("#device = \"orbic\"", "device = \"pinephone\"")
|
||||
.as_bytes(),
|
||||
)?;
|
||||
adb.install_file(
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
RAYHUNTER_DAEMON_INIT.as_bytes(),
|
||||
)?;
|
||||
adb.install_file(
|
||||
"/etc/init.d/misc-daemon",
|
||||
include_bytes!("../../dist/scripts/misc-daemon"),
|
||||
)?;
|
||||
adb.run_command(
|
||||
&["chmod", "755", "/etc/init.d/rayhunter_daemon"],
|
||||
"exit code 0",
|
||||
)?;
|
||||
adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?;
|
||||
|
||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
echo!("Testing rayhunter ... ");
|
||||
test_rayhunter(&mut adb).await?;
|
||||
println!("ok");
|
||||
println!("rayhunter is running on the modem. Use adb to access the web interface.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Qusbcfg {
|
||||
vendor_id: u16,
|
||||
product_id: u16,
|
||||
diag: u8,
|
||||
nmea: u8,
|
||||
at: u8,
|
||||
modem: u8,
|
||||
net: u8,
|
||||
adb: u8,
|
||||
audio: u8,
|
||||
}
|
||||
|
||||
impl Default for Qusbcfg {
|
||||
fn default() -> Self {
|
||||
Qusbcfg {
|
||||
vendor_id: USB_VENDOR_ID,
|
||||
product_id: USB_PRODUCT_ID,
|
||||
diag: 1,
|
||||
nmea: 1,
|
||||
at: 1,
|
||||
modem: 1,
|
||||
net: 1,
|
||||
adb: 0,
|
||||
audio: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Qusbcfg {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(&format!(
|
||||
"AT+QCFG=\"usbcfg\",{:#X},{:#X},{},{},{},{},{},{},{}",
|
||||
self.vendor_id,
|
||||
self.product_id,
|
||||
self.diag,
|
||||
self.nmea,
|
||||
self.at,
|
||||
self.modem,
|
||||
self.net,
|
||||
self.adb,
|
||||
self.audio
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the adb daemon on the Quectel modem.
|
||||
/// A reimplementation of qadbkey-unlock.c by "igem, 2019 ;)"
|
||||
pub async fn start_adb() -> Result<()> {
|
||||
let tty = serial_interface()?.unwrap();
|
||||
|
||||
let get_qadbkey = tty
|
||||
.send_at_command("AT+QADBKEY?")
|
||||
.await
|
||||
.context("Failed to request QADBKEY")?;
|
||||
let resp = String::from_utf8_lossy(&get_qadbkey);
|
||||
if !resp.contains("\r\nOK\r\n") {
|
||||
bail!("Received unexpected response: {0}", resp);
|
||||
}
|
||||
let salt = match resp.find("+QADBKEY: ") {
|
||||
Some(i) => &resp[i + 10..i + 18],
|
||||
None => bail!("Received unexpected response: {0}", resp),
|
||||
};
|
||||
|
||||
let hashed = &md5crypt(b"SH_adb_quectel", salt.as_bytes())[12..28];
|
||||
let hashed = String::from_utf8_lossy(hashed);
|
||||
|
||||
let unlock = tty
|
||||
.send_at_command(&format!("AT+QADBKEY=\"{hashed}\""))
|
||||
.await
|
||||
.context("Failed to send AT+QADBKEY")?;
|
||||
let resp = String::from_utf8_lossy(&unlock);
|
||||
if !resp.contains("\r\nOK\r\n") {
|
||||
bail!("Received unexpected response: {0}", resp);
|
||||
}
|
||||
|
||||
let adb_enable = Qusbcfg {
|
||||
adb: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let start_adb = tty
|
||||
.send_at_command(&adb_enable.to_string())
|
||||
.await
|
||||
.context("Failed to send enable adb command.")?;
|
||||
let resp = String::from_utf8_lossy(&start_adb);
|
||||
if !resp.contains("\r\nOK\r\n") {
|
||||
bail!("Received unexpected response: {0}", resp);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the adb daemon on the Quectel modem.
|
||||
pub async fn stop_adb() -> Result<()> {
|
||||
let tty = serial_interface()?.unwrap();
|
||||
let adb_disable = Qusbcfg::default();
|
||||
let stop_adb = tty
|
||||
.send_at_command(&adb_disable.to_string())
|
||||
.await
|
||||
.context("Failed to disable adb.")?;
|
||||
let resp = String::from_utf8_lossy(&stop_adb);
|
||||
if !resp.contains("\r\nOK\r\n") {
|
||||
bail!("Received unexpected response: {0}", resp);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
trait Install {
|
||||
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()>;
|
||||
fn install_file(&mut self, dest: &str, payload: &[u8]) -> Result<()>;
|
||||
}
|
||||
|
||||
impl Install for ADBUSBDevice {
|
||||
/// Run an adb shell command, append '; echo exit code $?' to the command and verify its output.
|
||||
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
let mut cmd = Vec::<&str>::new();
|
||||
cmd.extend_from_slice(command);
|
||||
cmd.extend_from_slice(&[";", "echo", "exit code $?"]);
|
||||
self.shell_command(&cmd, &mut buf)?;
|
||||
let output = String::from_utf8_lossy(&buf);
|
||||
if !output.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {output}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfer a file to the modem's filesystem with adb push.
|
||||
/// Validates the file sends successfully to /tmp before overwriting the destination.
|
||||
fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
|
||||
echo!("Sending file {dest} ... ");
|
||||
let file_name = Path::new(dest)
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("{dest}'s file name is not UTF8"))?
|
||||
.to_owned();
|
||||
let push_tmp_path = format!("/tmp/{file_name}");
|
||||
let file_hash = md5_compute(payload);
|
||||
self.push(&mut payload, &push_tmp_path)?;
|
||||
self.run_command(&["md5sum", &push_tmp_path], &format!("{file_hash:x}"))?;
|
||||
self.run_command(&["mv", &push_tmp_path, dest], "exit code 0")?;
|
||||
println!("ok");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Claim the modem's USB interface for sending AT commands.
|
||||
fn serial_interface() -> Result<Option<Interface>> {
|
||||
if let Some(device) = open_usb_device(USB_VENDOR_ID, USB_PRODUCT_ID)? {
|
||||
let interface = device
|
||||
.detach_and_claim_interface(USB_INTERFACE_NUMBER)
|
||||
.context("detach_and_claim_interface({USB_INTERFACE_NUMBER}) failed")?;
|
||||
return Ok(Some(interface));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
trait AT {
|
||||
async fn send_at_command(&self, command: &str) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
impl AT for Interface {
|
||||
/// Send an AT command to the Quectel modem.
|
||||
async fn send_at_command(&self, command: &str) -> Result<Vec<u8>> {
|
||||
let mut data = String::new();
|
||||
data.push_str("\r\n");
|
||||
data.push_str(command);
|
||||
data.push_str("\r\n");
|
||||
|
||||
let timeout = Duration::from_secs(1);
|
||||
|
||||
let enable_serial_port = Control {
|
||||
control_type: ControlType::Class,
|
||||
recipient: Recipient::Interface,
|
||||
request: 0x22,
|
||||
value: 3,
|
||||
index: USB_INTERFACE_NUMBER as u16,
|
||||
};
|
||||
|
||||
self.control_out_blocking(enable_serial_port, &[], timeout)
|
||||
.context("Failed to send control request")?;
|
||||
|
||||
tokio::time::timeout(timeout, self.bulk_out(0x3, data.as_bytes().to_vec()))
|
||||
.await
|
||||
.context("Timed out writing command")?
|
||||
.into_result()
|
||||
.context("Failed to write command")?;
|
||||
|
||||
let response = tokio::time::timeout(timeout, self.bulk_in(0x84, RequestBuffer::new(256)))
|
||||
.await
|
||||
.context("Timed out reading response")?
|
||||
.into_result()
|
||||
.context("Failed to read response")?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qadbcfg_fmt() {
|
||||
assert_eq!(
|
||||
Qusbcfg::default().to_string(),
|
||||
"AT+QCFG=\"usbcfg\",0x2C7C,0x125,1,1,1,1,1,0,0"
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use nusb::Device;
|
||||
use reqwest::Client;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
@@ -132,3 +133,20 @@ pub async fn http_ok_every(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// General function to open a USB device
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user