Compare commits
9 Commits
main
...
openwrt-en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0651e8c615 | ||
|
|
7b69200244 | ||
|
|
2da8e2c698 | ||
| cebbd10127 | |||
| 5d05529d11 | |||
| 72e480e9f7 | |||
| 74d50719ac | |||
| eb74bc3fe4 | |||
| b13751e6d1 |
@ -244,6 +244,8 @@ impl RpcHandler {
|
|||||||
// OpenWrt / TollGate
|
// OpenWrt / TollGate
|
||||||
"openwrt.scan" => self.handle_openwrt_scan(params).await,
|
"openwrt.scan" => self.handle_openwrt_scan(params).await,
|
||||||
"openwrt.get-status" => self.handle_openwrt_get_status(params).await,
|
"openwrt.get-status" => self.handle_openwrt_get_status(params).await,
|
||||||
|
"openwrt.forget" => self.handle_openwrt_forget().await,
|
||||||
|
"openwrt.set-password" => self.handle_openwrt_set_password(params).await,
|
||||||
"openwrt.provision-tollgate" => self.handle_openwrt_provision_tollgate(params).await,
|
"openwrt.provision-tollgate" => self.handle_openwrt_provision_tollgate(params).await,
|
||||||
"openwrt.scan-wifi" => self.handle_openwrt_scan_wifi(params).await,
|
"openwrt.scan-wifi" => self.handle_openwrt_scan_wifi(params).await,
|
||||||
"openwrt.configure-wan" => self.handle_openwrt_configure_wan(params).await,
|
"openwrt.configure-wan" => self.handle_openwrt_configure_wan(params).await,
|
||||||
|
|||||||
@ -165,6 +165,66 @@ impl RpcHandler {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forget the saved router config — deletes `router_config.json` so the
|
||||||
|
/// app requires a fresh login next time. Takes no params.
|
||||||
|
pub(super) async fn handle_openwrt_forget(&self) -> Result<serde_json::Value> {
|
||||||
|
net_router::forget_router_config(&self.config.data_dir).await?;
|
||||||
|
Ok(serde_json::json!({ "ok": true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the router's SSH login password from the app — no manual SSH/
|
||||||
|
/// console session on the router required. Works for a fresh flash
|
||||||
|
/// (root has no password yet — connect with `current_password: ""`)
|
||||||
|
/// or to rotate an existing one. On success the new credentials are
|
||||||
|
/// persisted the same way a normal login does, so the app is fully
|
||||||
|
/// connected afterward with no separate "Connect" step needed.
|
||||||
|
///
|
||||||
|
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root",
|
||||||
|
/// "current_password": "", "new_password": "..." }`
|
||||||
|
pub(super) async fn handle_openwrt_set_password(
|
||||||
|
&self,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let p = params.unwrap_or_default();
|
||||||
|
let host = p
|
||||||
|
.get("host")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("host is required"))?
|
||||||
|
.to_string();
|
||||||
|
let ssh_user = p
|
||||||
|
.get("ssh_user")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("root")
|
||||||
|
.to_string();
|
||||||
|
let current_password = p
|
||||||
|
.get("current_password")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let new_password = p
|
||||||
|
.get("new_password")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("new_password is required"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let router = Router::connect_password(&host, 22, &ssh_user, ¤t_password)?;
|
||||||
|
router.verify_openwrt()?;
|
||||||
|
router.set_password(&ssh_user, &new_password)?;
|
||||||
|
|
||||||
|
net_router::configure_router(
|
||||||
|
&self.config.data_dir,
|
||||||
|
net_router::RouterType::OpenWrt,
|
||||||
|
&host,
|
||||||
|
None,
|
||||||
|
Some(&ssh_user),
|
||||||
|
Some(&new_password),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(serde_json::json!({ "ok": true, "host": host }))
|
||||||
|
}
|
||||||
|
|
||||||
/// Provision TollGate on an OpenWrt router and create the "archipelago" SSID.
|
/// Provision TollGate on an OpenWrt router and create the "archipelago" SSID.
|
||||||
///
|
///
|
||||||
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "",
|
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "",
|
||||||
|
|||||||
@ -366,6 +366,17 @@ pub async fn save_router_config(data_dir: &Path, config: &RouterConfig) -> Resul
|
|||||||
.context("Writing router config")
|
.context("Writing router config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove the saved router config (credentials + address) so the app forgets
|
||||||
|
/// this router entirely and the next call requires a fresh login.
|
||||||
|
pub async fn forget_router_config(data_dir: &Path) -> Result<()> {
|
||||||
|
let path = data_dir.join(ROUTER_CONFIG_FILE);
|
||||||
|
match fs::remove_file(&path).await {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||||
|
Err(e) => Err(e).context("Removing router config"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate that an IP string is a private/LAN address (not public, not localhost).
|
/// Validate that an IP string is a private/LAN address (not public, not localhost).
|
||||||
fn is_valid_private_ip(ip_str: &str) -> bool {
|
fn is_valid_private_ip(ip_str: &str) -> bool {
|
||||||
let ip: std::net::IpAddr = match ip_str.parse() {
|
let ip: std::net::IpAddr = match ip_str.parse() {
|
||||||
|
|||||||
@ -82,6 +82,22 @@ impl Router {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set `user`'s login password via BusyBox `passwd`, non-interactively.
|
||||||
|
///
|
||||||
|
/// BusyBox's passwd applet reads the new password twice from stdin even
|
||||||
|
/// without a tty (unlike shadow-utils' passwd, which requires one), so
|
||||||
|
/// piping two lines in works. Root can always set its own (or another
|
||||||
|
/// user's) password without supplying the old one first. This is what
|
||||||
|
/// lets a fresh-flash router (root has no password yet) be fully set up
|
||||||
|
/// from the app — no manual SSH/console session required.
|
||||||
|
pub fn set_password(&self, user: &str, new_password: &str) -> Result<()> {
|
||||||
|
let q = crate::uci::shell_quote(new_password);
|
||||||
|
let uq = crate::uci::shell_quote(user);
|
||||||
|
let cmd = format!("printf '%s\\n%s\\n' {q} {q} | passwd {uq} 2>&1");
|
||||||
|
self.run_ok(&cmd)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Verify the remote device is actually running OpenWrt.
|
/// Verify the remote device is actually running OpenWrt.
|
||||||
pub fn verify_openwrt(&self) -> Result<String> {
|
pub fn verify_openwrt(&self) -> Result<String> {
|
||||||
let release = self
|
let release = self
|
||||||
|
|||||||
@ -60,6 +60,6 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Wrap a value in single quotes, escaping any embedded single quotes.
|
/// Wrap a value in single quotes, escaping any embedded single quotes.
|
||||||
fn shell_quote(s: &str) -> String {
|
pub(crate) fn shell_quote(s: &str) -> String {
|
||||||
format!("'{}'", s.replace('\'', r"'\''"))
|
format!("'{}'", s.replace('\'', r"'\''"))
|
||||||
}
|
}
|
||||||
|
|||||||
25
neode-ui/package-lock.json
generated
25
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.115-alpha",
|
"version": "1.7.116-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.115-alpha",
|
"version": "1.7.116-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
@ -150,7 +150,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@ -1812,7 +1811,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@ -1836,7 +1834,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@ -3923,7 +3920,6 @@
|
|||||||
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
@ -3973,7 +3969,6 @@
|
|||||||
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
@ -4474,7 +4469,6 @@
|
|||||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@ -4960,7 +4954,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@ -5979,7 +5972,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@ -8172,7 +8164,6 @@
|
|||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@ -8222,7 +8213,6 @@
|
|||||||
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.1.0",
|
"cssstyle": "^4.1.0",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@ -8325,8 +8315,7 @@
|
|||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@ -9082,7 +9071,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@ -9744,7 +9732,6 @@
|
|||||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@ -11000,7 +10987,6 @@
|
|||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -11256,7 +11242,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -11498,7 +11483,6 @@
|
|||||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@ -11661,7 +11645,6 @@
|
|||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -11675,7 +11658,6 @@
|
|||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
@ -11768,7 +11750,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
||||||
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.30",
|
"@vue/compiler-dom": "3.5.30",
|
||||||
"@vue/compiler-sfc": "3.5.30",
|
"@vue/compiler-sfc": "3.5.30",
|
||||||
|
|||||||
@ -87,6 +87,15 @@ const detecting = ref(false)
|
|||||||
const detectError = ref('')
|
const detectError = ref('')
|
||||||
const detectedCandidates = ref<string[]>([])
|
const detectedCandidates = ref<string[]>([])
|
||||||
|
|
||||||
|
// Set/change router password — lets a fresh-flash router (root has no
|
||||||
|
// password yet) get fully connected without the user ever opening a
|
||||||
|
// terminal or LuCI themselves.
|
||||||
|
const showSetPassword = ref(false)
|
||||||
|
const newPassword = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const settingPassword = ref(false)
|
||||||
|
const setPasswordError = ref('')
|
||||||
|
|
||||||
const provisioning = ref(false)
|
const provisioning = ref(false)
|
||||||
const provisionError = ref('')
|
const provisionError = ref('')
|
||||||
const provisionSuccess = ref(false)
|
const provisionSuccess = ref(false)
|
||||||
@ -148,6 +157,39 @@ async function connect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setPasswordAndConnect() {
|
||||||
|
if (!host.value.trim() || !newPassword.value) return
|
||||||
|
setPasswordError.value = ''
|
||||||
|
if (newPassword.value !== confirmPassword.value) {
|
||||||
|
setPasswordError.value = 'Passwords do not match.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settingPassword.value = true
|
||||||
|
try {
|
||||||
|
await rpcClient.call({
|
||||||
|
method: 'openwrt.set-password',
|
||||||
|
params: {
|
||||||
|
host: host.value.trim(),
|
||||||
|
ssh_user: sshUser.value,
|
||||||
|
current_password: sshPassword.value,
|
||||||
|
new_password: newPassword.value,
|
||||||
|
},
|
||||||
|
timeout: 20000,
|
||||||
|
})
|
||||||
|
// Router accepted the new password — log in with it right away so the
|
||||||
|
// user never has to separately "Connect" after this.
|
||||||
|
sshPassword.value = newPassword.value
|
||||||
|
showSetPassword.value = false
|
||||||
|
newPassword.value = ''
|
||||||
|
confirmPassword.value = ''
|
||||||
|
await connect()
|
||||||
|
} catch (e) {
|
||||||
|
setPasswordError.value = e instanceof Error ? e.message : String(e)
|
||||||
|
} finally {
|
||||||
|
settingPassword.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface WiredInterface { name: string; type: string; state: string; ipv4: string[] }
|
interface WiredInterface { name: string; type: string; state: string; ipv4: string[] }
|
||||||
|
|
||||||
async function detectRouter() {
|
async function detectRouter() {
|
||||||
@ -203,6 +245,33 @@ function disconnectRouter() {
|
|||||||
showConnectForm.value = true
|
showConnectForm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const forgetting = ref(false)
|
||||||
|
const forgetError = ref('')
|
||||||
|
|
||||||
|
// Unlike disconnectRouter() (which only resets local state, leaving the saved
|
||||||
|
// router_config.json in place so onMounted reconnects on next visit), this
|
||||||
|
// deletes the saved credentials server-side so the router is truly forgotten.
|
||||||
|
async function forgetRouter() {
|
||||||
|
if (!confirm('Forget this router? You will need to log in again next time.')) return
|
||||||
|
forgetting.value = true
|
||||||
|
forgetError.value = ''
|
||||||
|
try {
|
||||||
|
await rpcClient.call({ method: 'openwrt.forget', timeout: 10000 })
|
||||||
|
status.value = null
|
||||||
|
connectedParams.value = null
|
||||||
|
host.value = ''
|
||||||
|
sshUser.value = 'root'
|
||||||
|
sshPassword.value = ''
|
||||||
|
detectError.value = ''
|
||||||
|
detectedCandidates.value = []
|
||||||
|
showConnectForm.value = true
|
||||||
|
} catch (e) {
|
||||||
|
forgetError.value = e instanceof Error ? e.message : String(e)
|
||||||
|
} finally {
|
||||||
|
forgetting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function provisionTollgate() {
|
async function provisionTollgate() {
|
||||||
provisioning.value = true
|
provisioning.value = true
|
||||||
provisionError.value = ''
|
provisionError.value = ''
|
||||||
@ -437,6 +506,45 @@ onMounted(() => load())
|
|||||||
>
|
>
|
||||||
{{ connecting ? 'Connecting…' : 'Connect' }}
|
{{ connecting ? 'Connecting…' : 'Connect' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full text-xs text-white/40 hover:text-white/70 transition-colors text-center"
|
||||||
|
@click="showSetPassword = !showSetPassword"
|
||||||
|
>
|
||||||
|
First login, or don't know the password? Set one →
|
||||||
|
</button>
|
||||||
|
<div v-if="showSetPassword" class="space-y-3 pt-2 border-t border-white/10">
|
||||||
|
<p class="text-xs text-white/40">
|
||||||
|
Sets the router's SSH password directly — no need to SSH in yourself first.
|
||||||
|
Leave "Password" above blank if this is a fresh flash (no password set yet).
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-white/40 mb-1">New password</label>
|
||||||
|
<input
|
||||||
|
v-model="newPassword"
|
||||||
|
type="password"
|
||||||
|
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-white/40 mb-1">Confirm password</label>
|
||||||
|
<input
|
||||||
|
v-model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
:disabled="settingPassword || !host.trim() || !newPassword"
|
||||||
|
class="glass-button w-full text-sm font-medium"
|
||||||
|
:class="settingPassword || !host.trim() || !newPassword ? 'opacity-40 cursor-not-allowed' : ''"
|
||||||
|
@click="setPasswordAndConnect"
|
||||||
|
>
|
||||||
|
{{ settingPassword ? 'Setting password…' : 'Set Password & Connect' }}
|
||||||
|
</button>
|
||||||
|
<p v-if="setPasswordError" class="text-xs text-red-400">{{ setPasswordError }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="mt-3 text-xs text-red-400">{{ error }}</p>
|
<p v-if="error" class="mt-3 text-xs text-red-400">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -499,7 +607,15 @@ onMounted(() => load())
|
|||||||
<button class="text-xs text-white/40 hover:text-white/70 transition-colors" @click="disconnectRouter">
|
<button class="text-xs text-white/40 hover:text-white/70 transition-colors" @click="disconnectRouter">
|
||||||
Switch router →
|
Switch router →
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="text-xs text-red-400/70 hover:text-red-400 transition-colors"
|
||||||
|
:disabled="forgetting"
|
||||||
|
@click="forgetRouter"
|
||||||
|
>
|
||||||
|
{{ forgetting ? 'Forgetting…' : 'Forget router' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="forgetError" class="mt-2 text-xs text-red-400">{{ forgetError }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- WAN / Uplink -->
|
<!-- WAN / Uplink -->
|
||||||
|
|||||||
278
scripts/flash-mt3000-openwrt.sh
Executable file
278
scripts/flash-mt3000-openwrt.sh
Executable file
@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Flash stock OpenWrt on a GL.iNet GL-MT3000 (Beryl AX)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./flash-mt3000-openwrt.sh <router-password> [router-ip] [--image /path/to/image.bin]
|
||||||
|
#
|
||||||
|
# Defaults to 192.168.8.1.
|
||||||
|
# Requires: curl, sshpass, ssh, sha256sum
|
||||||
|
# Optional: scp (falls back to pipe if unavailable on router)
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Verifies tools and connectivity
|
||||||
|
# 2. Downloads the stock OpenWrt sysupgrade image (or uses a pre-downloaded one)
|
||||||
|
# 3. Verifies SHA256 checksum
|
||||||
|
# 4. Uploads image to the router (SCP or pipe)
|
||||||
|
# 5. Flashes via sysupgrade (no settings preserved)
|
||||||
|
# 6. Waits for reboot and verifies new firmware
|
||||||
|
#
|
||||||
|
# After flash, stock OpenWrt is at 192.168.1.1 (root, no password).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[-]${NC} $*" >&2; }
|
||||||
|
|
||||||
|
# --- Parse args ---
|
||||||
|
PASSWORD=""
|
||||||
|
ROUTER="192.168.8.1"
|
||||||
|
LOCAL_IMAGE=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--image)
|
||||||
|
LOCAL_IMAGE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--router)
|
||||||
|
ROUTER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "$PASSWORD" ]; then
|
||||||
|
PASSWORD="$1"
|
||||||
|
else
|
||||||
|
ROUTER="$1"
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$PASSWORD" ]; then
|
||||||
|
echo "Usage: $0 <router-password> [router-ip] [--image /path/to/sysupgrade.bin]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --image PATH Use a pre-downloaded sysupgrade image instead of downloading"
|
||||||
|
echo " --router IP Router IP (default: 192.168.8.1)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_USER="root"
|
||||||
|
OPENWRT_VERSION="24.10.2"
|
||||||
|
IMAGE_NAME="openwrt-${OPENWRT_VERSION}-mediatek-filogic-glinet_gl-mt3000-squashfs-sysupgrade.bin"
|
||||||
|
IMAGE_URL="https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/mediatek/filogic/${IMAGE_NAME}"
|
||||||
|
CHECKSUM_URL="https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/mediatek/filogic/sha256sums"
|
||||||
|
DOWNLOAD_DIR="/tmp/openwrt-flash"
|
||||||
|
DEFAULT_IMAGE="${DOWNLOAD_DIR}/${IMAGE_NAME}"
|
||||||
|
NEW_IP="192.168.1.1"
|
||||||
|
|
||||||
|
SSH_CMD="sshpass -p ${PASSWORD} ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${SSH_USER}@${ROUTER}"
|
||||||
|
|
||||||
|
# --- Step 1: Check tools ---
|
||||||
|
log "Checking required tools..."
|
||||||
|
MISSING=()
|
||||||
|
for cmd in curl sshpass ssh sha256sum; do
|
||||||
|
command -v "$cmd" >/dev/null 2>&1 || MISSING+=("$cmd")
|
||||||
|
done
|
||||||
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||||
|
err "Missing tools: ${MISSING[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "All tools found."
|
||||||
|
|
||||||
|
# --- Step 2: Check router connectivity ---
|
||||||
|
log "Checking router connectivity at ${ROUTER}..."
|
||||||
|
if ! $SSH_CMD "echo ok" >/dev/null 2>&1; then
|
||||||
|
err "Cannot SSH into ${ROUTER}. Is the router reachable and is the password correct?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Router reachable."
|
||||||
|
|
||||||
|
# Verify it's a GL-MT3000
|
||||||
|
BOARD=$($SSH_CMD "cat /tmp/sysinfo/board_name 2>/dev/null" || true)
|
||||||
|
if [[ "$BOARD" != *"mt3000"* ]]; then
|
||||||
|
err "Router board is '${BOARD}', expected GL-MT3000. Aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Confirmed GL-MT3000 (board: ${BOARD})."
|
||||||
|
|
||||||
|
# --- Step 3: Get image ---
|
||||||
|
if [ -n "$LOCAL_IMAGE" ]; then
|
||||||
|
# User provided a local image
|
||||||
|
if [ ! -f "$LOCAL_IMAGE" ]; then
|
||||||
|
err "Image file not found: ${LOCAL_IMAGE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Using provided image: ${LOCAL_IMAGE}"
|
||||||
|
else
|
||||||
|
# Download the image
|
||||||
|
log "Downloading OpenWrt ${OPENWRT_VERSION} sysupgrade image..."
|
||||||
|
mkdir -p "$DOWNLOAD_DIR"
|
||||||
|
|
||||||
|
if [ -f "$DEFAULT_IMAGE" ]; then
|
||||||
|
warn "Image already exists locally, skipping download."
|
||||||
|
else
|
||||||
|
# Test if HTTPS is being intercepted (common with GL.iNet routers)
|
||||||
|
CERT_ISSUE=false
|
||||||
|
CERT_INFO=$(curl -v --connect-timeout 5 "https://downloads.openwrt.org" 2>&1 || true)
|
||||||
|
if echo "$CERT_INFO" | grep -qi "GLiNet\|gl-inet\|self-signed"; then
|
||||||
|
CERT_ISSUE=true
|
||||||
|
warn "HTTPS interception detected — the router is MITM-ing TLS connections."
|
||||||
|
warn "This is the same issue that breaks Tailscale."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CERT_ISSUE" = true ]; then
|
||||||
|
warn "Attempting download via HTTP..."
|
||||||
|
HTTP_URL="${IMAGE_URL/https:/http:}"
|
||||||
|
if ! curl -fSL --progress-bar --connect-timeout 10 -o "$DEFAULT_IMAGE" "$HTTP_URL" 2>/dev/null; then
|
||||||
|
err ""
|
||||||
|
err "Download failed. The router is intercepting HTTPS and HTTP is also blocked."
|
||||||
|
err ""
|
||||||
|
err "Workaround: download the image on a machine NOT behind this router,"
|
||||||
|
err "then run this script with --image /path/to/${IMAGE_NAME}"
|
||||||
|
err ""
|
||||||
|
err "Direct download URL:"
|
||||||
|
err " ${IMAGE_URL}"
|
||||||
|
err ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if ! curl -fSL --progress-bar -o "$DEFAULT_IMAGE" "$IMAGE_URL"; then
|
||||||
|
err "Download failed from ${IMAGE_URL}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
LOCAL_IMAGE="$DEFAULT_IMAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Image ready: $(ls -lh "$LOCAL_IMAGE" | awk '{print $5}')"
|
||||||
|
|
||||||
|
# --- Step 4: Verify checksum ---
|
||||||
|
log "Verifying SHA256 checksum..."
|
||||||
|
EXPECTED_HASH=""
|
||||||
|
CHECKSUM_ATTEMPTS=(
|
||||||
|
"https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/mediatek/filogic/sha256sums"
|
||||||
|
"http://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/mediatek/filogic/sha256sums"
|
||||||
|
)
|
||||||
|
|
||||||
|
for url in "${CHECKSUM_ATTEMPTS[@]}"; do
|
||||||
|
EXPECTED_HASH=$(curl -fsSL --connect-timeout 5 "$url" 2>/dev/null | grep "${IMAGE_NAME}" | awk '{print $1}' || true)
|
||||||
|
if [ -n "$EXPECTED_HASH" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$EXPECTED_HASH" ]; then
|
||||||
|
ACTUAL_HASH=$(sha256sum "$LOCAL_IMAGE" | awk '{print $1}')
|
||||||
|
if [ "$EXPECTED_HASH" != "$ACTUAL_HASH" ]; then
|
||||||
|
err "Checksum mismatch!"
|
||||||
|
err " Expected: ${EXPECTED_HASH}"
|
||||||
|
err " Actual: ${ACTUAL_HASH}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Checksum OK: ${ACTUAL_HASH:0:16}..."
|
||||||
|
else
|
||||||
|
warn "Could not fetch expected checksum (network issue?), skipping verification."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Step 5: Pre-flash checks ---
|
||||||
|
log "Running pre-flash checks on router..."
|
||||||
|
|
||||||
|
# Check free space on /tmp
|
||||||
|
FREE_KB=$($SSH_CMD "df /tmp | tail -1 | awk '{print \$4}'")
|
||||||
|
IMAGE_SIZE_KB=$(( $(stat -c%s "$LOCAL_IMAGE") / 1024 ))
|
||||||
|
if [ "$FREE_KB" -lt "$((IMAGE_SIZE_KB + 10240))" ]; then
|
||||||
|
err "Not enough space on /tmp. Need ~$((IMAGE_SIZE_KB/1024))MB, have $((FREE_KB/1024))MB."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Free space on /tmp: $((FREE_KB/1024))MB, image: $((IMAGE_SIZE_KB/1024))MB — OK."
|
||||||
|
|
||||||
|
# Verify current firmware
|
||||||
|
CURRENT_FW=$($SSH_CMD "cat /etc/openwrt_release 2>/dev/null | grep DISTRIB_DESCRIPTION" || true)
|
||||||
|
log "Current firmware: ${CURRENT_FW}"
|
||||||
|
|
||||||
|
# --- Step 6: Upload image ---
|
||||||
|
log "Uploading image to router..."
|
||||||
|
|
||||||
|
# Try SCP first, fall back to pipe (GL.iNet firmware lacks sftp-server)
|
||||||
|
if command -v scp >/dev/null 2>&1 && \
|
||||||
|
sshpass -p "$PASSWORD" scp -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes "$LOCAL_IMAGE" "${SSH_USER}@${ROUTER}:/tmp/openwrt-sysupgrade.bin" 2>/dev/null; then
|
||||||
|
log "Upload complete (via SCP)."
|
||||||
|
else
|
||||||
|
log "SCP unavailable on router, using pipe transfer..."
|
||||||
|
cat "$LOCAL_IMAGE" | $SSH_CMD "cat > /tmp/openwrt-sysupgrade.bin"
|
||||||
|
log "Upload complete (via pipe)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify uploaded file
|
||||||
|
REMOTE_HASH=$($SSH_CMD "sha256sum /tmp/openwrt-sysupgrade.bin | awk '{print \$1}'")
|
||||||
|
LOCAL_HASH=$(sha256sum "$LOCAL_IMAGE" | awk '{print $1}')
|
||||||
|
if [ "$REMOTE_HASH" != "$LOCAL_HASH" ]; then
|
||||||
|
err "Remote file hash mismatch after upload!"
|
||||||
|
$SSH_CMD "rm -f /tmp/openwrt-sysupgrade.bin"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Remote file verified."
|
||||||
|
|
||||||
|
# --- Step 7: Flash ---
|
||||||
|
echo ""
|
||||||
|
log "============================================"
|
||||||
|
log "FLASHING STOCK OPENWRT ${OPENWRT_VERSION}"
|
||||||
|
log "The router will reboot. This takes ~3-5 min."
|
||||||
|
log "After flash, OpenWrt will be at ${NEW_IP}"
|
||||||
|
log "============================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
$SSH_CMD "sysupgrade -n /tmp/openwrt-sysupgrade.bin" &
|
||||||
|
SCP_PID=$!
|
||||||
|
|
||||||
|
# sysupgrade disconnects SSH, wait for it
|
||||||
|
wait $SCP_PID 2>/dev/null || true
|
||||||
|
|
||||||
|
log "Flash initiated. Waiting for router to reboot..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# --- Step 8: Wait for new router ---
|
||||||
|
log "Waiting for stock OpenWrt at ${NEW_IP}..."
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
if ping -c1 -W2 "$NEW_IP" >/dev/null 2>&1; then
|
||||||
|
log "Router is up at ${NEW_IP}!"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -eq 90 ]; then
|
||||||
|
warn "Router not responding at ${NEW_IP} after 3 minutes."
|
||||||
|
warn "It may still be booting. Try: ssh root@${NEW_IP}"
|
||||||
|
warn "If unreachable, hold reset for U-Boot recovery at 192.168.1.1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf "\r Waiting... (%d/90)" $i
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# --- Step 9: Verify new firmware ---
|
||||||
|
log "Verifying new firmware..."
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new root@${NEW_IP} "cat /etc/openwrt_release" 2>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
log "============================================"
|
||||||
|
log "FLASH COMPLETE"
|
||||||
|
log "Stock OpenWrt is running at ${NEW_IP}"
|
||||||
|
log "Login: root (no password)"
|
||||||
|
log "Run 'passwd' to set a root password."
|
||||||
|
log "============================================"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
warn "Could not verify firmware. Router is reachable but SSH may still be starting."
|
||||||
|
warn "Try: ssh root@${NEW_IP}"
|
||||||
85
scripts/gl-inet-enable-ssh.sh
Executable file
85
scripts/gl-inet-enable-ssh.sh
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ./gl-inet-enable-ssh.sh <password> [router-ip]
|
||||||
|
#
|
||||||
|
# Defaults to 192.168.8.1. It does:
|
||||||
|
#
|
||||||
|
# 1. Completes OOBE/init (sets password + enables SSH)
|
||||||
|
# 2. Challenge-response login
|
||||||
|
# 3. Explicitly enables SSH via API
|
||||||
|
# 4. Verifies SSH is working
|
||||||
|
#
|
||||||
|
# Needs curl, python3, openssl, sshpass, and md5sum on the machine running it.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROUTER="${GL_ROUTER:-192.168.8.1}"
|
||||||
|
PASSWORD="${1:?Usage: $0 <password> [router-ip]}"
|
||||||
|
HOST="${2:-$ROUTER}"
|
||||||
|
|
||||||
|
challenge() {
|
||||||
|
curl -sk "http://$HOST/rpc" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"jsonrpc\":\"2.0\",\"method\":\"challenge\",\"params\":{\"username\":\"root\"},\"id\":1}"
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
local hash="$1"
|
||||||
|
curl -sk "http://$HOST/rpc" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"jsonrpc\":\"2.0\",\"method\":\"login\",\"params\":{\"username\":\"root\",\"hash\":\"$hash\"},\"id\":2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc_call() {
|
||||||
|
local sid="$1" module="$2" method="$3" params="$4"
|
||||||
|
curl -sk "http://$HOST/rpc" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":[\"$sid\",\"$module\",\"$method\",$params],\"id\":3}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: Complete OOBE / init (enables SSH)
|
||||||
|
echo "Initializing router..."
|
||||||
|
INIT=$(curl -sk "http://$HOST/rpc" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"call\",\"params\":[\"\",\"ui\",\"init\",{\"lang\":\"en\",\"username\":\"root\",\"password\":\"$PASSWORD\",\"security_rule\":0}]}")
|
||||||
|
echo "$INIT"
|
||||||
|
|
||||||
|
if echo "$INIT" | grep -q '"error"'; then
|
||||||
|
echo "Init returned error (may already be initialized), continuing..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Step 2: Challenge-response login
|
||||||
|
echo "Logging in..."
|
||||||
|
CHALLENGE=$(challenge)
|
||||||
|
SALT=$(echo "$CHALLENGE" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['salt'])")
|
||||||
|
NONCE=$(echo "$CHALLENGE" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['nonce'])")
|
||||||
|
ALG=$(echo "$CHALLENGE" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['alg'])")
|
||||||
|
|
||||||
|
CIPHER=$(openssl passwd "-$ALG" -salt "$SALT" "$PASSWORD" 2>/dev/null)
|
||||||
|
HASH=$(printf "root:%s:%s" "$CIPHER" "$NONCE" | md5sum | cut -d' ' -f1)
|
||||||
|
|
||||||
|
LOGIN=$(login "$HASH")
|
||||||
|
SID=$(echo "$LOGIN" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['sid'])" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$SID" ]; then
|
||||||
|
echo "Login failed: $LOGIN"
|
||||||
|
echo "SSH should still be enabled from the init call. Try: ssh root@$HOST"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Logged in. SID: $SID"
|
||||||
|
|
||||||
|
# Step 3: Enable SSH explicitly
|
||||||
|
echo "Enabling SSH..."
|
||||||
|
SSH_RESULT=$(rpc_call "$SID" "system" "set_settings" '{"key":"ssh","value":{"enable":true}}')
|
||||||
|
echo "$SSH_RESULT"
|
||||||
|
|
||||||
|
# Step 4: Verify SSH
|
||||||
|
echo "Verifying SSH on $HOST:22..."
|
||||||
|
if sshpass -p "$PASSWORD" ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no root@$HOST "echo SSH OK" 2>/dev/null; then
|
||||||
|
echo "Done. SSH is enabled on root@$HOST"
|
||||||
|
else
|
||||||
|
echo "SSH port not responding yet. May need a moment or SSH may already be enabled."
|
||||||
|
echo "Try: ssh root@$HOST"
|
||||||
|
fi
|
||||||
Loading…
x
Reference in New Issue
Block a user