Implement basic telnet shell for both orbic and tplink

This commit is contained in:
Markus Unterwaditzer
2025-11-16 18:39:13 +01:00
committed by Will Greenberg
parent 676cd3c862
commit 5fbc540fa0
6 changed files with 131 additions and 3 deletions
Generated
+14 -1
View File
@@ -1802,6 +1802,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -1883,6 +1884,7 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -2455,7 +2457,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.57.0",
"windows-core 0.61.2",
]
[[package]]
@@ -2711,6 +2713,7 @@ dependencies = [
"bytes",
"clap",
"env_logger 0.11.8",
"futures",
"hyper",
"hyper-util",
"md5 0.7.0",
@@ -2719,6 +2722,7 @@ dependencies = [
"reqwest",
"serde",
"sha2",
"termios",
"tokio",
"tokio-retry2",
"tokio-stream",
@@ -5972,6 +5976,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "termios"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
dependencies = [
"libc",
]
[[package]]
name = "thiserror"
version = "1.0.69"
+4
View File
@@ -31,6 +31,10 @@ sha2 = "0.10.8"
tokio = { version = "1.44.2", features = ["io-util", "macros", "rt"], default-features = false }
tokio-retry2 = "0.5.7"
tokio-stream = "0.1.17"
futures = "0.3"
[target.'cfg(unix)'.dependencies]
termios = "0.3"
[target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies.adb_client]
git = "https://github.com/EFForg/adb_client.git"
+8
View File
@@ -126,6 +126,8 @@ enum UtilSubCommand {
Uz801StartAdb(Uz801Args),
/// Root the tplink and launch telnetd.
TplinkStartTelnet(TplinkStartTelnet),
/// Root the TP-Link and open an interactive shell.
TplinkShell(TplinkStartTelnet),
/// Root the Wingtech and launch telnetd.
WingtechStartTelnet(WingtechArgs),
/// Root the Wingtech and launch adb.
@@ -138,6 +140,8 @@ enum UtilSubCommand {
PinephoneStopAdb,
/// Root the Orbic and launch telnetd.
OrbicStartTelnet(OrbicNetworkArgs),
/// Root the Orbic and open an interactive shell.
OrbicShell(OrbicNetworkArgs),
/// Send a file to the TP-Link device over telnet.
///
/// Before running this utility, you need to make telnet accessible with `installer util
@@ -261,6 +265,9 @@ async fn run(args: Args) -> Result<(), Error> {
UtilSubCommand::TplinkStartTelnet(options) => {
tplink::start_telnet(&options.admin_ip).await?;
}
UtilSubCommand::TplinkShell(options) => {
tplink::shell(&options.admin_ip).await.context("\nFailed to open shell on TP-Link device")?;
}
UtilSubCommand::TplinkSendFile(options) => {
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
}
@@ -274,6 +281,7 @@ async fn run(args: Args) -> Result<(), Error> {
#[cfg(not(target_os = "android"))]
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
UtilSubCommand::OrbicShell(args) => orbic_network::shell(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to open shell on Orbic RC400L")?,
}
}
}
+14 -1
View File
@@ -9,7 +9,7 @@ use tokio::time::sleep;
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
use crate::output::{eprintln, print, println};
use crate::util::{telnet_send_command, telnet_send_file};
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
#[derive(Deserialize, Debug)]
@@ -270,3 +270,16 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
Ok(())
}
/// Root the Orbic device and open an interactive shell
pub async fn shell(
admin_ip: &str,
admin_username: &str,
admin_password: Option<&str>,
) -> Result<()> {
start_telnet(admin_ip, admin_username, admin_password).await?;
eprintln!(
"This terminal is fairly limited. The shell prompt may not be visible, but it still accepts commands."
);
interactive_shell(admin_ip, 24, false).await
}
+7 -1
View File
@@ -19,7 +19,7 @@ use tokio::time::sleep;
use crate::InstallTpLink;
use crate::output::println;
use crate::util::{telnet_send_command, telnet_send_file};
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
@@ -383,6 +383,12 @@ fn get_rayhunter_daemon(sdcard_path: &str) -> String {
)
}
/// Root the TP-Link device and open an interactive shell
pub async fn shell(admin_ip: &str) -> Result<(), Error> {
start_telnet(admin_ip).await?;
interactive_shell(admin_ip, 23, true).await
}
#[test]
fn test_get_rayhunter_daemon() {
let s = get_rayhunter_daemon("/media/card");
+84
View File
@@ -11,6 +11,9 @@ 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,
@@ -225,3 +228,84 @@ pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
}
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 mut original_termios = termios::Termios::from_fd(fd)?;
let raw_termios = original_termios;
original_termios.c_lflag &= !(termios::ICANON | termios::ECHO | termios::ISIG);
original_termios.c_iflag &= !(termios::IXON | termios::ICRNL);
original_termios.c_oflag &= !termios::OPOST;
original_termios.c_cc[termios::VMIN] = 1;
original_termios.c_cc[termios::VTIME] = 0;
termios::tcsetattr(fd, termios::TCSANOW, &original_termios)?;
Ok(RawTerminal {
fd,
original_termios: raw_termios,
})
}
}
#[cfg(unix)]
impl Drop for RawTerminal {
fn drop(&mut self) {
let _ = termios::tcsetattr(self.fd, termios::TCSANOW, &self.original_termios);
}
}