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
+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);
}
}