Add proxy subscription runtime support
Add daemon RPCs, Apple and GTK import flows, packet proxy runtime support, diagnostics, and BEPs for proxy subscription handling. Redact subscription URL secrets from fetch errors before they reach logs or UI surfaces.
This commit is contained in:
parent
97c569fb35
commit
d1638726ca
46 changed files with 15079 additions and 456 deletions
4559
burrow-gtk/Cargo.lock
generated
4559
burrow-gtk/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
use super::*;
|
||||
use crate::account_store::{self, AccountKind, AccountRecord};
|
||||
use std::time::Duration;
|
||||
use std::{cell::RefCell, rc::Rc, time::Duration};
|
||||
|
||||
pub struct HomeScreen {
|
||||
daemon_banner: adw::Banner,
|
||||
|
|
@ -23,6 +23,7 @@ pub enum HomeScreenMsg {
|
|||
OpenWireGuard,
|
||||
OpenTor,
|
||||
OpenTailnet,
|
||||
OpenProxySubscription,
|
||||
AddWireGuard {
|
||||
title: String,
|
||||
account: String,
|
||||
|
|
@ -52,6 +53,14 @@ pub enum HomeScreenMsg {
|
|||
hostname: Option<String>,
|
||||
tailnet: Option<String>,
|
||||
},
|
||||
PreviewProxySubscription {
|
||||
url: String,
|
||||
},
|
||||
AddProxySubscription {
|
||||
url: String,
|
||||
name: String,
|
||||
selected_ordinal: Option<i32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[relm4::component(pub, async)]
|
||||
|
|
@ -234,7 +243,7 @@ impl AsyncComponent for HomeScreen {
|
|||
configure_add_popover(&widgets.add_button, &sender);
|
||||
|
||||
let refresh_sender = sender.input_sender().clone();
|
||||
relm4::spawn(async move {
|
||||
gtk::glib::MainContext::default().spawn_local(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
refresh_sender.emit(HomeScreenMsg::Refresh);
|
||||
|
|
@ -272,6 +281,7 @@ impl AsyncComponent for HomeScreen {
|
|||
HomeScreenMsg::OpenWireGuard => open_wireguard_window(root, &sender),
|
||||
HomeScreenMsg::OpenTor => open_tor_window(root, &sender),
|
||||
HomeScreenMsg::OpenTailnet => open_tailnet_window(root, &sender),
|
||||
HomeScreenMsg::OpenProxySubscription => open_proxy_subscription_window(root, &sender),
|
||||
HomeScreenMsg::AddWireGuard {
|
||||
title,
|
||||
account,
|
||||
|
|
@ -304,6 +314,13 @@ impl AsyncComponent for HomeScreen {
|
|||
self.add_tailnet(authority, account, identity, hostname, tailnet)
|
||||
.await;
|
||||
}
|
||||
HomeScreenMsg::PreviewProxySubscription { url } => {
|
||||
self.preview_proxy_subscription(url).await;
|
||||
}
|
||||
HomeScreenMsg::AddProxySubscription { url, name, selected_ordinal } => {
|
||||
self.add_proxy_subscription(url, name, selected_ordinal)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -667,6 +684,85 @@ impl HomeScreen {
|
|||
}
|
||||
}
|
||||
|
||||
async fn preview_proxy_subscription(&mut self, url: String) {
|
||||
let Ok(url) = daemon_api::require_value(&url, "Subscription URL") else {
|
||||
self.network_status
|
||||
.set_label("Enter a subscription URL before previewing.");
|
||||
return;
|
||||
};
|
||||
|
||||
self.network_status
|
||||
.set_label("Previewing proxy subscription...");
|
||||
match daemon_api::preview_proxy_subscription(url).await {
|
||||
Ok(preview) => {
|
||||
let first_nodes = preview
|
||||
.nodes
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|node| {
|
||||
let warning = if node.warnings.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" - {}", node.warnings.join(", "))
|
||||
};
|
||||
format!(
|
||||
"{}: {} {}:{}{}",
|
||||
node.protocol, node.name, node.server, node.port, warning
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let warnings = if preview.warnings.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n{}", preview.warnings.join("\n"))
|
||||
};
|
||||
self.network_status.set_label(&format!(
|
||||
"Found {} compatible nodes and {} rejected entries in {}. Suggested name: {}.{}{}",
|
||||
preview.compatible_count,
|
||||
preview.rejected_count,
|
||||
preview.detected_format,
|
||||
preview.suggested_name,
|
||||
warnings,
|
||||
if first_nodes.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n{first_nodes}")
|
||||
}
|
||||
));
|
||||
}
|
||||
Err(error) => self
|
||||
.network_status
|
||||
.set_label(&format!("Proxy subscription preview failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_proxy_subscription(
|
||||
&mut self,
|
||||
url: String,
|
||||
name: String,
|
||||
selected_ordinal: Option<i32>,
|
||||
) {
|
||||
let Ok(url) = daemon_api::require_value(&url, "Subscription URL") else {
|
||||
self.network_status
|
||||
.set_label("Enter a subscription URL before importing.");
|
||||
return;
|
||||
};
|
||||
|
||||
self.network_status
|
||||
.set_label("Importing proxy subscription...");
|
||||
match daemon_api::add_proxy_subscription(url, name, selected_ordinal).await {
|
||||
Ok(id) => {
|
||||
self.network_status
|
||||
.set_label(&format!("Imported proxy subscription network #{id}."));
|
||||
self.refresh().await;
|
||||
}
|
||||
Err(error) => self
|
||||
.network_status
|
||||
.set_label(&format!("Unable to import proxy subscription: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_login_status(&mut self, status: &daemon_api::TailnetLoginStatus) {
|
||||
self.tailnet_session_id = Some(status.session_id.clone());
|
||||
self.tailnet_running = status.running;
|
||||
|
|
@ -735,6 +831,10 @@ fn configure_add_popover(button: >k::MenuButton, sender: &AsyncComponentSender
|
|||
|
||||
for (label, msg) in [
|
||||
("Add WireGuard Network", HomeScreenMsg::OpenWireGuard),
|
||||
(
|
||||
"Import Proxy Subscription",
|
||||
HomeScreenMsg::OpenProxySubscription,
|
||||
),
|
||||
("Save Tor Account", HomeScreenMsg::OpenTor),
|
||||
("Add Tailnet Account", HomeScreenMsg::OpenTailnet),
|
||||
] {
|
||||
|
|
@ -755,6 +855,7 @@ fn msg_from_template(msg: &HomeScreenMsg) -> HomeScreenMsg {
|
|||
HomeScreenMsg::OpenWireGuard => HomeScreenMsg::OpenWireGuard,
|
||||
HomeScreenMsg::OpenTor => HomeScreenMsg::OpenTor,
|
||||
HomeScreenMsg::OpenTailnet => HomeScreenMsg::OpenTailnet,
|
||||
HomeScreenMsg::OpenProxySubscription => HomeScreenMsg::OpenProxySubscription,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -1091,6 +1192,169 @@ fn open_tailnet_window(root: >k::ScrolledWindow, sender: &AsyncComponentSender
|
|||
window.present();
|
||||
}
|
||||
|
||||
fn open_proxy_subscription_window(
|
||||
root: >k::ScrolledWindow,
|
||||
sender: &AsyncComponentSender<HomeScreen>,
|
||||
) {
|
||||
let window = sheet_window(root, "Proxy Subscription", 560, 520);
|
||||
let content = sheet_content(
|
||||
&window,
|
||||
"Import Proxy Subscription",
|
||||
"Import Trojan and Shadowsocks nodes through the Burrow daemon.",
|
||||
);
|
||||
|
||||
let url = gtk::Entry::new();
|
||||
url.set_placeholder_text(Some("Subscription URL"));
|
||||
let name = gtk::Entry::new();
|
||||
name.set_placeholder_text(Some("Name"));
|
||||
let node_list = gtk::ListBox::new();
|
||||
node_list.set_selection_mode(gtk::SelectionMode::Single);
|
||||
node_list.set_sensitive(false);
|
||||
node_list.add_css_class("boxed-list");
|
||||
let node_scroll = gtk::ScrolledWindow::builder()
|
||||
.min_content_height(220)
|
||||
.vexpand(true)
|
||||
.child(&node_list)
|
||||
.build();
|
||||
let preview_status = gtk::Label::new(Some("Preview the subscription to choose a node."));
|
||||
preview_status.add_css_class("dim-label");
|
||||
preview_status.set_xalign(0.0);
|
||||
preview_status.set_wrap(true);
|
||||
let selected_ordinal = Rc::new(RefCell::new(None::<i32>));
|
||||
let node_ordinals = Rc::new(RefCell::new(Vec::<i32>::new()));
|
||||
|
||||
content.append(§ion_label("Subscription"));
|
||||
content.append(&url);
|
||||
content.append(&name);
|
||||
content.append(§ion_label("Node"));
|
||||
content.append(&node_scroll);
|
||||
content.append(&preview_status);
|
||||
|
||||
let actions = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let preview = gtk::Button::with_label("Preview");
|
||||
let import = gtk::Button::with_label("Import");
|
||||
import.add_css_class("suggested-action");
|
||||
actions.append(&preview);
|
||||
actions.append(&import);
|
||||
content.append(&actions);
|
||||
|
||||
let url_for_preview = url.clone();
|
||||
let name_for_preview = name.clone();
|
||||
let list_for_preview = node_list.clone();
|
||||
let status_for_preview = preview_status.clone();
|
||||
let selected_for_preview = selected_ordinal.clone();
|
||||
let ordinals_for_preview = node_ordinals.clone();
|
||||
preview.connect_clicked(move |_| {
|
||||
let url = url_for_preview.text().to_string();
|
||||
let name = name_for_preview.clone();
|
||||
let list = list_for_preview.clone();
|
||||
let status = status_for_preview.clone();
|
||||
let selected = selected_for_preview.clone();
|
||||
let ordinals = ordinals_for_preview.clone();
|
||||
status.set_label("Previewing proxy subscription...");
|
||||
while let Some(child) = list.first_child() {
|
||||
list.remove(&child);
|
||||
}
|
||||
list.set_sensitive(false);
|
||||
*selected.borrow_mut() = None;
|
||||
ordinals.borrow_mut().clear();
|
||||
gtk::glib::MainContext::default().spawn_local(async move {
|
||||
match daemon_api::preview_proxy_subscription(url).await {
|
||||
Ok(preview) => {
|
||||
if name.text().trim().is_empty() {
|
||||
name.set_text(&preview.suggested_name);
|
||||
}
|
||||
for node in preview.nodes.iter().filter(|node| node.runtime_supported) {
|
||||
ordinals.borrow_mut().push(node.ordinal);
|
||||
let row = gtk::ListBoxRow::new();
|
||||
row.set_selectable(true);
|
||||
row.set_activatable(true);
|
||||
|
||||
let row_content = gtk::Box::new(gtk::Orientation::Vertical, 2);
|
||||
row_content.set_margin_top(8);
|
||||
row_content.set_margin_bottom(8);
|
||||
row_content.set_margin_start(10);
|
||||
row_content.set_margin_end(10);
|
||||
|
||||
let title = gtk::Label::new(Some(&format!(
|
||||
"{} {}",
|
||||
node.protocol.to_uppercase(),
|
||||
node.name
|
||||
)));
|
||||
title.set_xalign(0.0);
|
||||
title.set_ellipsize(gtk::pango::EllipsizeMode::End);
|
||||
let endpoint = gtk::Label::new(Some(&format!("{}:{}", node.server, node.port)));
|
||||
endpoint.add_css_class("dim-label");
|
||||
endpoint.set_xalign(0.0);
|
||||
endpoint.set_ellipsize(gtk::pango::EllipsizeMode::Middle);
|
||||
|
||||
row_content.append(&title);
|
||||
row_content.append(&endpoint);
|
||||
row.set_child(Some(&row_content));
|
||||
list.append(&row);
|
||||
}
|
||||
if let Some(first) = preview.nodes.iter().find(|node| node.runtime_supported) {
|
||||
if let Some(row) = list.row_at_index(0) {
|
||||
list.select_row(Some(&row));
|
||||
}
|
||||
list.set_sensitive(true);
|
||||
*selected.borrow_mut() = Some(first.ordinal);
|
||||
}
|
||||
let unsupported = preview
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| !node.runtime_supported)
|
||||
.count();
|
||||
let supported = preview.nodes.len().saturating_sub(unsupported);
|
||||
let warnings = if preview.warnings.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", preview.warnings.join(" "))
|
||||
};
|
||||
status.set_label(&format!(
|
||||
"Found {} runtime-supported nodes, {} unsupported nodes, and {} rejected entries in {}.{}",
|
||||
supported,
|
||||
unsupported,
|
||||
preview.rejected_count,
|
||||
preview.detected_format,
|
||||
warnings
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
status.set_label(&format!("Proxy subscription preview failed: {error}"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let selected_for_list = selected_ordinal.clone();
|
||||
let ordinals_for_list = node_ordinals.clone();
|
||||
node_list.connect_row_selected(move |_, row| {
|
||||
*selected_for_list.borrow_mut() = row.and_then(|row| {
|
||||
let index = row.index();
|
||||
if index < 0 {
|
||||
return None;
|
||||
}
|
||||
ordinals_for_list.borrow().get(index as usize).copied()
|
||||
});
|
||||
});
|
||||
|
||||
let input = sender.input_sender().clone();
|
||||
let window_for_click = window.clone();
|
||||
let selected_for_import = selected_ordinal.clone();
|
||||
import.connect_clicked(move |_| {
|
||||
input.emit(HomeScreenMsg::AddProxySubscription {
|
||||
url: url.text().to_string(),
|
||||
name: name.text().to_string(),
|
||||
selected_ordinal: *selected_for_import.borrow(),
|
||||
});
|
||||
window_for_click.close();
|
||||
});
|
||||
|
||||
window.set_child(Some(&content));
|
||||
window.present();
|
||||
}
|
||||
|
||||
fn sheet_window(root: >k::ScrolledWindow, title: &str, width: i32, height: i32) -> gtk::Window {
|
||||
let window = gtk::Window::builder()
|
||||
.title(title)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ use burrow::{
|
|||
grpc_defs::{
|
||||
Empty, Network, NetworkType, State, TailnetDiscoverRequest, TailnetLoginCancelRequest,
|
||||
TailnetLoginStartRequest, TailnetLoginStatusRequest, TailnetProbeRequest,
|
||||
ProxySubscriptionApplyRequest, ProxySubscriptionImportRequest,
|
||||
},
|
||||
proxy_subscription::ProxySubscriptionPayload,
|
||||
BurrowClient,
|
||||
};
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
|
@ -54,6 +56,27 @@ pub struct TailnetLoginStatus {
|
|||
pub health: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxySubscriptionPreview {
|
||||
pub suggested_name: String,
|
||||
pub detected_format: String,
|
||||
pub compatible_count: i32,
|
||||
pub rejected_count: i32,
|
||||
pub nodes: Vec<ProxyNodePreview>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyNodePreview {
|
||||
pub ordinal: i32,
|
||||
pub name: String,
|
||||
pub protocol: String,
|
||||
pub server: String,
|
||||
pub port: i32,
|
||||
pub warnings: Vec<String>,
|
||||
pub runtime_supported: bool,
|
||||
}
|
||||
|
||||
pub fn default_tailnet_authority() -> &'static str {
|
||||
MANAGED_TAILSCALE_AUTHORITY
|
||||
}
|
||||
|
|
@ -216,6 +239,63 @@ pub async fn add_tailnet(
|
|||
add_network(NetworkType::Tailnet, payload).await
|
||||
}
|
||||
|
||||
pub async fn preview_proxy_subscription(url: String) -> Result<ProxySubscriptionPreview> {
|
||||
let mut client = BurrowClient::from_uds().await?;
|
||||
let response = timeout(
|
||||
Duration::from_secs(20),
|
||||
client
|
||||
.proxy_subscription_client
|
||||
.preview_import(ProxySubscriptionImportRequest { url }),
|
||||
)
|
||||
.await
|
||||
.context("timed out previewing proxy subscription")??
|
||||
.into_inner();
|
||||
|
||||
Ok(ProxySubscriptionPreview {
|
||||
suggested_name: response.suggested_name,
|
||||
detected_format: response.detected_format,
|
||||
compatible_count: response.compatible_count,
|
||||
rejected_count: response.rejected_count,
|
||||
nodes: response
|
||||
.node
|
||||
.into_iter()
|
||||
.map(|node| ProxyNodePreview {
|
||||
ordinal: node.ordinal,
|
||||
name: node.name,
|
||||
protocol: node.protocol,
|
||||
server: node.server,
|
||||
port: node.port,
|
||||
warnings: node.warnings,
|
||||
runtime_supported: node.runtime_supported,
|
||||
})
|
||||
.collect(),
|
||||
warnings: response.warnings,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn add_proxy_subscription(
|
||||
url: String,
|
||||
name: String,
|
||||
selected_ordinal: Option<i32>,
|
||||
) -> Result<i32> {
|
||||
let id = next_network_id().await?;
|
||||
let mut client = BurrowClient::from_uds().await?;
|
||||
timeout(
|
||||
Duration::from_secs(25),
|
||||
client
|
||||
.proxy_subscription_client
|
||||
.apply_import(ProxySubscriptionApplyRequest {
|
||||
id,
|
||||
url,
|
||||
name,
|
||||
selected_ordinal: selected_ordinal.unwrap_or(-1),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("timed out importing proxy subscription")??;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn discover_tailnet(email: String) -> Result<TailnetDiscovery> {
|
||||
let mut client = BurrowClient::from_uds().await?;
|
||||
let response = timeout(
|
||||
|
|
@ -328,6 +408,12 @@ fn summarize_network(network: &Network) -> NetworkSummary {
|
|||
match network.r#type() {
|
||||
NetworkType::WireGuard => summarize_wireguard(network),
|
||||
NetworkType::Tailnet => summarize_tailnet(network),
|
||||
NetworkType::Trojan => NetworkSummary {
|
||||
id: network.id,
|
||||
title: "Legacy Trojan Proxy".to_owned(),
|
||||
detail: "Unsupported SOCKS-era profile. Import a proxy subscription instead.".to_owned(),
|
||||
},
|
||||
NetworkType::ProxySubscription => summarize_proxy_subscription(network),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,6 +458,21 @@ fn summarize_tailnet(network: &Network) -> NetworkSummary {
|
|||
}
|
||||
}
|
||||
|
||||
fn summarize_proxy_subscription(network: &Network) -> NetworkSummary {
|
||||
match serde_json::from_slice::<ProxySubscriptionPayload>(&network.payload) {
|
||||
Ok(payload) => NetworkSummary {
|
||||
id: network.id,
|
||||
title: payload.name.clone(),
|
||||
detail: burrow::proxy_subscription::summarize_payload(&payload),
|
||||
},
|
||||
Err(error) => NetworkSummary {
|
||||
id: network.id,
|
||||
title: "Proxy Subscription".to_owned(),
|
||||
detail: format!("Unable to read proxy subscription payload: {error}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_tailnet_status(
|
||||
response: burrow::grpc_defs::TailnetLoginStatusResponse,
|
||||
) -> TailnetLoginStatus {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue