Compare commits

..

3 Commits

Author SHA1 Message Date
sijie.sun
f272a1775e tmp 2025-09-21 22:35:56 +08:00
Sijie.Sun
a102a8bfc7 fix macos bind failed when addr is v6 (#1398) 2025-09-21 21:47:03 +08:00
Sijie.Sun
c9e8c35e77 fix log dir not work; fix stun config from file not work; (#1393)
Some checks failed
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Has been cancelled
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-loongarch64, ubuntu-24.04, loongarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-riscv64, ubuntu-22.04, riscv64gc-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (windows-arm64, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-i686, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / core-result (push) Has been cancelled
EasyTier Core / magisk_build (push) Has been cancelled
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (windows-arm64, aarch64-pc-windows-msvc, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-i686, i686-pc-windows-msvc, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / gui-result (push) Has been cancelled
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Has been cancelled
EasyTier Mobile / mobile-result (push) Has been cancelled
EasyTier OHOS / build-ohos (push) Has been cancelled
EasyTier Test / test (push) Has been cancelled
2025-09-20 00:20:08 +08:00
18 changed files with 1645 additions and 237 deletions

10
Cargo.lock generated
View File

@@ -2226,6 +2226,7 @@ dependencies = [
"windows-service",
"windows-sys 0.52.0",
"winreg 0.52.0",
"xmltree",
"zerocopy",
"zip",
"zstd",
@@ -11141,6 +11142,15 @@ version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
[[package]]
name = "xmltree"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6"
dependencies = [
"xml-rs",
]
[[package]]
name = "yansi"
version = "1.0.1"

View File

@@ -217,6 +217,9 @@ version-compare = "0.2.0"
hmac = "0.12.1"
sha2 = "0.10.8"
# upnp igd
xmltree = "0.11.0"
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
machine-uid = "0.5.3"

View File

@@ -109,8 +109,6 @@ enum SubCommand {
Stats(StatsArgs),
#[command(about = "manage logger configuration")]
Logger(LoggerArgs),
#[command(about = "manage network instance configuration")]
Config(ConfigArgs),
#[command(about = t!("core_clap.generate_completions").to_string())]
GenAutocomplete { shell: Shell },
}
@@ -295,23 +293,6 @@ enum LoggerSubCommand {
},
}
#[derive(Args, Debug)]
struct ConfigArgs {
#[command(subcommand)]
sub_command: Option<ConfigSubCommand>,
}
#[derive(Subcommand, Debug)]
enum ConfigSubCommand {
/// List network instances and their configurations
List,
/// Get configuration for a specific instance
Get {
#[arg(help = "Instance ID")]
inst_id: String,
},
}
#[derive(Args, Debug)]
struct ServiceArgs {
#[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")]
@@ -1305,68 +1286,6 @@ impl CommandHandler<'_> {
}
Ok(ports)
}
async fn handle_config_list(&self) -> Result<(), Error> {
let client = self.get_peer_manager_client().await?;
let node_info = client
.show_node_info(BaseController::default(), ShowNodeInfoRequest::default())
.await?
.node_info
.ok_or(anyhow::anyhow!("node info not found"))?;
if self.verbose || *self.output_format == OutputFormat::Json {
println!("{}", serde_json::to_string_pretty(&node_info)?);
return Ok(());
}
#[derive(tabled::Tabled, serde::Serialize)]
struct ConfigTableItem {
#[tabled(rename = "Instance ID")]
inst_id: String,
#[tabled(rename = "Virtual IP")]
ipv4: String,
#[tabled(rename = "Hostname")]
hostname: String,
#[tabled(rename = "Network Name")]
network_name: String,
}
let items = vec![ConfigTableItem {
inst_id: node_info.peer_id.to_string(),
ipv4: node_info.ipv4_addr,
hostname: node_info.hostname,
network_name: "".to_string(), // NodeInfo doesn't have network_name field
}];
print_output(&items, self.output_format)?;
Ok(())
}
async fn handle_config_get(&self, inst_id: &str) -> Result<(), Error> {
let client = self.get_peer_manager_client().await?;
let node_info = client
.show_node_info(BaseController::default(), ShowNodeInfoRequest::default())
.await?
.node_info
.ok_or(anyhow::anyhow!("node info not found"))?;
// Check if the requested instance ID matches the current node
if node_info.peer_id.to_string() != inst_id {
return Err(anyhow::anyhow!(
"Instance ID {} not found. Current instance ID is {}",
inst_id,
node_info.peer_id
));
}
if self.verbose || *self.output_format == OutputFormat::Json {
println!("{}", serde_json::to_string_pretty(&node_info)?);
return Ok(());
}
println!("{}", node_info.config);
Ok(())
}
}
#[derive(Debug)]
@@ -2178,14 +2097,6 @@ async fn main() -> Result<(), Error> {
handler.handle_logger_set(level).await?;
}
},
SubCommand::Config(config_args) => match &config_args.sub_command {
Some(ConfigSubCommand::List) | None => {
handler.handle_config_list().await?;
}
Some(ConfigSubCommand::Get { inst_id }) => {
handler.handle_config_get(inst_id).await?;
}
},
SubCommand::GenAutocomplete { shell } => {
let mut cmd = Cli::command();
easytier::print_completions(shell, &mut cmd, "easytier-cli");

View File

@@ -968,8 +968,17 @@ impl NetworkOptions {
old_udp_whitelist.extend(self.udp_whitelist.clone());
cfg.set_udp_whitelist(old_udp_whitelist);
cfg.set_stun_servers(self.stun_servers.clone());
cfg.set_stun_servers_v6(self.stun_servers_v6.clone());
if let Some(stun_servers) = &self.stun_servers {
let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default();
old_stun_servers.extend(stun_servers.iter().cloned());
cfg.set_stun_servers(Some(old_stun_servers));
}
if let Some(stun_servers_v6) = &self.stun_servers_v6 {
let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default();
old_stun_servers_v6.extend(stun_servers_v6.iter().cloned());
cfg.set_stun_servers_v6(Some(old_stun_servers_v6));
}
Ok(())
}
}

View File

@@ -8,3 +8,5 @@ pub mod listeners;
pub mod virtual_nic;
pub mod logger_rpc_service;
pub mod upnp_igd;

View File

@@ -0,0 +1,157 @@
use super::super::PortMappingProtocol;
use std::net::SocketAddr;
// Content of the request.
pub const SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r
Host:239.255.255.250:1900\r
ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r
Man:\"ssdp:discover\"\r
MX:3\r\n\r\n";
pub const GET_EXTERNAL_IP_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress""#;
pub const ADD_ANY_PORT_MAPPING_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddAnyPortMapping""#;
pub const ADD_PORT_MAPPING_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping""#;
pub const DELETE_PORT_MAPPING_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping""#;
pub const GET_GENERIC_PORT_MAPPING_ENTRY: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetGenericPortMappingEntry""#;
const MESSAGE_HEAD: &str = r#"<?xml version="1.0"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>"#;
const MESSAGE_TAIL: &str = r#"</s:Body>
</s:Envelope>"#;
fn format_message(body: String) -> String {
format!("{MESSAGE_HEAD}{body}{MESSAGE_TAIL}")
}
pub fn format_get_external_ip_message() -> String {
r#"<?xml version="1.0"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<m:GetExternalIPAddress xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
</m:GetExternalIPAddress>
</s:Body>
</s:Envelope>"#
.into()
}
pub fn format_add_any_port_mapping_message(
schema: &[String],
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> String {
let args = schema
.iter()
.filter_map(|argument| {
let value = match argument.as_str() {
"NewEnabled" => 1.to_string(),
"NewExternalPort" => external_port.to_string(),
"NewInternalClient" => local_addr.ip().to_string(),
"NewInternalPort" => local_addr.port().to_string(),
"NewLeaseDuration" => lease_duration.to_string(),
"NewPortMappingDescription" => description.to_string(),
"NewProtocol" => protocol.to_string(),
"NewRemoteHost" => "".to_string(),
unknown => {
tracing::warn!("Unknown argument: {}", unknown);
return None;
}
};
Some(format!("<{argument}>{value}</{argument}>"))
})
.collect::<Vec<_>>()
.join("\n");
format_message(format!(
r#"<u:AddAnyPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
{args}
</u:AddAnyPortMapping>"#,
))
}
pub fn format_add_port_mapping_message(
schema: &[String],
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> String {
let args = schema
.iter()
.filter_map(|argument| {
let value = match argument.as_str() {
"NewEnabled" => 1.to_string(),
"NewExternalPort" => external_port.to_string(),
"NewInternalClient" => local_addr.ip().to_string(),
"NewInternalPort" => local_addr.port().to_string(),
"NewLeaseDuration" => lease_duration.to_string(),
"NewPortMappingDescription" => description.to_string(),
"NewProtocol" => protocol.to_string(),
"NewRemoteHost" => "".to_string(),
unknown => {
tracing::warn!("Unknown argument: {}", unknown);
return None;
}
};
Some(format!("<{argument}>{value}</{argument}>",))
})
.collect::<Vec<_>>()
.join("\n");
format_message(format!(
r#"<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
{args}
</u:AddPortMapping>"#
))
}
pub fn format_delete_port_message(
schema: &[String],
protocol: PortMappingProtocol,
external_port: u16,
) -> String {
let args = schema
.iter()
.filter_map(|argument| {
let value = match argument.as_str() {
"NewExternalPort" => external_port.to_string(),
"NewProtocol" => protocol.to_string(),
"NewRemoteHost" => "".to_string(),
unknown => {
tracing::warn!("Unknown argument: {}", unknown);
return None;
}
};
Some(format!("<{argument}>{value}</{argument}>",))
})
.collect::<Vec<_>>()
.join("\n");
format_message(format!(
r#"<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
{args}
</u:DeletePortMapping>"#
))
}
pub fn formate_get_generic_port_mapping_entry_message(port_mapping_index: u32) -> String {
format_message(format!(
r#"<u:GetGenericPortMappingEntry xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewPortMappingIndex>{port_mapping_index}</NewPortMappingIndex>
</u:GetGenericPortMappingEntry>"#
))
}

View File

@@ -0,0 +1,11 @@
pub mod messages;
pub mod options;
pub mod parsing;
use rand::Rng;
pub use self::options::SearchOptions;
pub fn random_port() -> u16 {
rand::thread_rng().gen_range(32_768_u16..65_535_u16)
}

View File

@@ -0,0 +1,45 @@
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
/// Default timeout for a gateway search.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
/// Timeout for each broadcast response during a gateway search.
#[allow(dead_code)]
pub const RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
/// Gateway search configuration
///
/// SearchOptions::default() should suffice for most situations.
///
/// # Example
/// To customize only a few options you can use `Default::default()` or `SearchOptions::default()` and the
/// [struct update syntax](https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax).
/// ```
/// # use std::time::Duration;
/// # use igd_next::SearchOptions;
/// let opts = SearchOptions {
/// timeout: Some(Duration::from_secs(60)),
/// ..Default::default()
/// };
/// ```
pub struct SearchOptions {
/// Bind address for UDP socket (defaults to all `0.0.0.0`)
pub bind_addr: SocketAddr,
/// Broadcast address for discovery packets (defaults to `239.255.255.250:1900`)
pub broadcast_address: SocketAddr,
/// Timeout for a search iteration (defaults to 10s)
pub timeout: Option<Duration>,
/// Timeout for a single search response (defaults to 5s)
pub single_search_timeout: Option<Duration>,
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
bind_addr: (IpAddr::from([0, 0, 0, 0]), 0).into(),
broadcast_address: "239.255.255.250:1900".parse().unwrap(),
timeout: Some(DEFAULT_TIMEOUT),
single_search_timeout: Some(RESPONSE_TIMEOUT),
}
}
}

View File

@@ -0,0 +1,756 @@
use std::collections::HashMap;
use std::io;
use std::net::{IpAddr, SocketAddr};
use anyhow::Context;
use url::Url;
use xmltree::{self, Element};
use super::super::PortMappingProtocol;
// Parse the result.
pub fn parse_search_result(text: &str) -> anyhow::Result<(SocketAddr, String)> {
for line in text.lines() {
let line = line.trim();
if line.to_ascii_lowercase().starts_with("location:") {
if let Some(colon) = line.find(':') {
let url_text = &line[colon + 1..].trim();
let url = Url::parse(url_text).map_err(|_| anyhow::anyhow!("Invalid response"))?;
let addr: IpAddr = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
.and_then(|s| s.parse().map_err(|_| anyhow::anyhow!("Invalid response")))?;
let port: u16 = url
.port_or_known_default()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))?;
return Ok((SocketAddr::new(addr, port), url.path().to_string()));
}
}
}
Err(anyhow::anyhow!("Invalid response"))
}
pub fn parse_control_urls<R>(resp: R) -> anyhow::Result<(String, String)>
where
R: io::Read,
{
let root = Element::parse(resp)?;
let mut urls = root.children.iter().filter_map(|child| {
let child = child.as_element()?;
if child.name == "device" {
Some(parse_device(child)?)
} else {
None
}
});
urls.next()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
}
fn parse_device(device: &Element) -> Option<(String, String)> {
let services = device.get_child("serviceList").and_then(|service_list| {
service_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "service" {
parse_service(child)
} else {
None
}
})
.next()
});
let devices = device.get_child("deviceList").and_then(parse_device_list);
services.or(devices)
}
fn parse_device_list(device_list: &Element) -> Option<(String, String)> {
device_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "device" {
parse_device(child)
} else {
None
}
})
.next()
}
fn parse_service(service: &Element) -> Option<(String, String)> {
let service_type = service.get_child("serviceType")?;
let service_type = service_type
.get_text()
.map(|s| s.into_owned())
.unwrap_or_else(|| "".into());
if [
"urn:schemas-upnp-org:service:WANPPPConnection:1",
"urn:schemas-upnp-org:service:WANIPConnection:1",
"urn:schemas-upnp-org:service:WANIPConnection:2",
]
.contains(&service_type.as_str())
{
let scpd_url = service.get_child("SCPDURL");
let control_url = service.get_child("controlURL");
if let (Some(scpd_url), Some(control_url)) = (scpd_url, control_url) {
Some((
scpd_url
.get_text()
.map(|s| s.into_owned())
.unwrap_or_else(|| "".into()),
control_url
.get_text()
.map(|s| s.into_owned())
.unwrap_or_else(|| "".into()),
))
} else {
None
}
} else {
None
}
}
pub fn parse_schemas<R>(resp: R) -> anyhow::Result<HashMap<String, Vec<String>>>
where
R: io::Read,
{
let root = Element::parse(resp)?;
let mut schema = root.children.iter().filter_map(|child| {
let child = child.as_element()?;
if child.name == "actionList" {
parse_action_list(child)
} else {
None
}
});
schema
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
}
fn parse_action_list(action_list: &Element) -> Option<HashMap<String, Vec<String>>> {
Some(
action_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "action" {
parse_action(child)
} else {
None
}
})
.collect(),
)
}
fn parse_action(action: &Element) -> Option<(String, Vec<String>)> {
Some((
action.get_child("name")?.get_text()?.into_owned(),
parse_argument_list(action.get_child("argumentList")?)?,
))
}
fn parse_argument_list(argument_list: &Element) -> Option<Vec<String>> {
Some(
argument_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "argument" {
parse_argument(child)
} else {
None
}
})
.collect(),
)
}
fn parse_argument(action: &Element) -> Option<String> {
if action
.get_child("direction")?
.get_text()?
.into_owned()
.as_str()
== "in"
{
Some(action.get_child("name")?.get_text()?.into_owned())
} else {
None
}
}
pub struct RequestReponse {
text: String,
xml: xmltree::Element,
}
pub type RequestResult = anyhow::Result<RequestReponse>;
pub fn parse_response(text: String, ok: &str) -> RequestResult {
let mut xml = match xmltree::Element::parse(text.as_bytes()) {
Ok(xml) => xml,
Err(..) => return Err(anyhow::anyhow!("Invalid response: {}", text)),
};
let body = match xml.get_mut_child("Body") {
Some(body) => body,
None => return Err(anyhow::anyhow!("Invalid response: {}", text)),
};
if let Some(ok) = body.take_child(ok) {
return Ok(RequestReponse { text, xml: ok });
}
let upnp_error = match body
.get_child("Fault")
.and_then(|e| e.get_child("detail"))
.and_then(|e| e.get_child("UPnPError"))
{
Some(upnp_error) => upnp_error,
None => return Err(anyhow::anyhow!("Invalid response: {}", text)),
};
match (
upnp_error.get_child("errorCode"),
upnp_error.get_child("errorDescription"),
) {
(Some(e), Some(d)) => match (e.get_text().as_ref(), d.get_text().as_ref()) {
(Some(et), Some(dt)) => match et.parse::<u16>() {
Ok(en) => Err(anyhow::anyhow!("Error code {}: {}", en, dt)),
Err(..) => Err(anyhow::anyhow!("Invalid response: {}", text)),
},
_ => Err(anyhow::anyhow!("Invalid response: {}", text)),
},
_ => Err(anyhow::anyhow!("Invalid response: {}", text)),
}
}
pub fn parse_get_external_ip_response(result: RequestResult) -> anyhow::Result<Option<IpAddr>> {
if let Ok(resp) = &result {
let child = resp.xml.get_child("NewExternalIPAddress");
if let Some(child) = child {
let text = child.get_text();
println!("text {:?}", text);
}
let child_empty = resp.xml.get_child("NewExternalIPAddressFuck");
println!("child_empty {:?}", child_empty);
}
match result {
Ok(resp) => {
let child = resp.xml.get_child("NewExternalIPAddress");
if let Some(child) = child {
match child.get_text() {
Some(text) => {
Ok(Some(text.parse::<IpAddr>().with_context(|| {
format!("Invalid IP address: {}", text)
})?))
}
None => Ok(None),
}
} else {
anyhow::bail!("Invalid response: {}", resp.text);
}
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("Error code 606") {
Err(anyhow::anyhow!("Action not authorized"))
} else {
Err(e)
}
}
}
}
pub fn parse_add_any_port_mapping_response(result: RequestResult) -> anyhow::Result<u16> {
match result {
Ok(resp) => {
match resp
.xml
.get_child("NewReservedPort")
.and_then(|e| e.get_text())
.and_then(|t| t.parse::<u16>().ok())
{
Some(port) => Ok(port),
None => Err(anyhow::anyhow!("Invalid response: {}", resp.text)),
}
}
Err(err) => {
let error_msg = err.to_string();
if error_msg.contains("Error code 605") {
Err(anyhow::anyhow!("Description too long"))
} else if error_msg.contains("Error code 606") {
Err(anyhow::anyhow!("Action not authorized"))
} else if error_msg.contains("Error code 728") {
Err(anyhow::anyhow!("No ports available"))
} else {
Err(err)
}
}
}
}
pub fn convert_add_random_port_mapping_error(error: anyhow::Error) -> Option<anyhow::Error> {
let error_msg = error.to_string();
if error_msg.contains("Error code 724") {
None
} else if error_msg.contains("Error code 605") {
Some(anyhow::anyhow!("Description too long"))
} else if error_msg.contains("Error code 606") {
Some(anyhow::anyhow!("Action not authorized"))
} else if error_msg.contains("Error code 718") {
Some(anyhow::anyhow!("No ports available"))
} else if error_msg.contains("Error code 725") {
Some(anyhow::anyhow!("Only permanent leases supported"))
} else {
Some(error)
}
}
pub fn convert_add_same_port_mapping_error(error: anyhow::Error) -> anyhow::Error {
let error_msg = error.to_string();
if error_msg.contains("Error code 606") {
anyhow::anyhow!("Action not authorized")
} else if error_msg.contains("Error code 718") {
anyhow::anyhow!("External port in use")
} else if error_msg.contains("Error code 725") {
anyhow::anyhow!("Only permanent leases supported")
} else {
error
}
}
pub fn convert_add_port_error(err: anyhow::Error) -> anyhow::Error {
let error_msg = err.to_string();
if error_msg.contains("Error code 605") {
anyhow::anyhow!("Description too long")
} else if error_msg.contains("Error code 606") {
anyhow::anyhow!("Action not authorized")
} else if error_msg.contains("Error code 718") {
anyhow::anyhow!("Port in use")
} else if error_msg.contains("Error code 724") {
anyhow::anyhow!("Same port values required")
} else if error_msg.contains("Error code 725") {
anyhow::anyhow!("Only permanent leases supported")
} else {
err
}
}
pub fn parse_delete_port_mapping_response(result: RequestResult) -> anyhow::Result<()> {
match result {
Ok(_) => Ok(()),
Err(err) => {
let error_msg = err.to_string();
if error_msg.contains("Error code 606") {
Err(anyhow::anyhow!("Action not authorized"))
} else if error_msg.contains("Error code 714") {
Err(anyhow::anyhow!("No such port mapping"))
} else {
Err(err)
}
}
}
}
/// One port mapping entry as returned by GetGenericPortMappingEntry
pub struct PortMappingEntry {
/// The remote host for which the mapping is valid
/// Can be an IP address or a host name
pub remote_host: String,
/// The external port of the mapping
pub external_port: u16,
/// The protocol of the mapping
pub protocol: PortMappingProtocol,
/// The internal (local) port
pub internal_port: u16,
/// The internal client of the port mapping
/// Can be an IP address or a host name
pub internal_client: String,
/// A flag whether this port mapping is enabled
pub enabled: bool,
/// A description for this port mapping
pub port_mapping_description: String,
/// The lease duration of this port mapping in seconds
pub lease_duration: u32,
}
pub fn parse_get_generic_port_mapping_entry(
result: RequestResult,
) -> anyhow::Result<PortMappingEntry> {
let response = result?;
let xml = response.xml;
let make_err = |msg: String| move || anyhow::anyhow!("Invalid response: {}", msg);
let extract_field = |field: &str| {
xml.get_child(field)
.ok_or_else(make_err(format!("{field} is missing")))
};
let remote_host = extract_field("NewRemoteHost")?
.get_text()
.map(|c| c.into_owned())
.unwrap_or_else(|| "".into());
let external_port = extract_field("NewExternalPort")?
.get_text()
.and_then(|t| t.parse::<u16>().ok())
.ok_or_else(make_err("Field NewExternalPort is invalid".into()))?;
let protocol = match extract_field("NewProtocol")?.get_text() {
Some(std::borrow::Cow::Borrowed("UDP")) => PortMappingProtocol::Udp,
Some(std::borrow::Cow::Borrowed("TCP")) => PortMappingProtocol::Tcp,
_ => {
return Err(anyhow::anyhow!(
"Invalid response: Field NewProtocol is invalid"
))
}
};
let internal_port = extract_field("NewInternalPort")?
.get_text()
.and_then(|t| t.parse::<u16>().ok())
.ok_or_else(make_err("Field NewInternalPort is invalid".into()))?;
let internal_client = extract_field("NewInternalClient")?
.get_text()
.map(|c| c.into_owned())
.ok_or_else(make_err("Field NewInternalClient is empty".into()))?;
let enabled = match extract_field("NewEnabled")?
.get_text()
.and_then(|t| t.parse::<u16>().ok())
.ok_or_else(make_err("Field Enabled is invalid".into()))?
{
0 => false,
1 => true,
_ => {
return Err(anyhow::anyhow!(
"Invalid response: Field NewEnabled is invalid"
))
}
};
let port_mapping_description = extract_field("NewPortMappingDescription")?
.get_text()
.map(|c| c.into_owned())
.unwrap_or_else(|| "".into());
let lease_duration = extract_field("NewLeaseDuration")?
.get_text()
.and_then(|t| t.parse::<u32>().ok())
.ok_or_else(make_err("Field NewLeaseDuration is invalid".into()))?;
Ok(PortMappingEntry {
remote_host,
external_port,
protocol,
internal_port,
internal_client,
enabled,
port_mapping_description,
lease_duration,
})
}
#[test]
fn test_parse_search_result_case_insensitivity() {
assert!(parse_search_result("location:http://0.0.0.0:0/control_url").is_ok());
assert!(parse_search_result("LOCATION:http://0.0.0.0:0/control_url").is_ok());
}
#[test]
fn test_parse_search_result_ok() {
let result = parse_search_result("location:http://0.0.0.0:0/control_url").unwrap();
assert_eq!(
result.0.ip(),
IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))
);
assert_eq!(result.0.port(), 0);
assert_eq!(&result.1[..], "/control_url");
}
#[test]
fn test_parse_search_result_fail() {
assert!(parse_search_result("content-type:http://0.0.0.0:0/control_url").is_err());
}
#[test]
fn test_parse_device1() {
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber>1</modelNumber>
<serialNumber>00000000</serialNumber>
<UDN></UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId>
<controlURL>/ctl/L3F</controlURL>
<eventSubURL>/evt/L3F</eventSubURL>
<SCPDURL>/L3F.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice</friendlyName>
<manufacturer>MiniUPnP</manufacturer>
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
<modelDescription>WAN Device</modelDescription>
<modelName>WAN Device</modelName>
<modelNumber>20180615</modelNumber>
<modelURL>http://miniupnp.free.fr/</modelURL>
<serialNumber>00000000</serialNumber>
<UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
<UPC>MINIUPNPD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/ctl/CmnIfCfg</controlURL>
<eventSubURL>/evt/CmnIfCfg</eventSubURL>
<SCPDURL>/WANCfg.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice</friendlyName>
<manufacturer>MiniUPnP</manufacturer>
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
<modelDescription>MiniUPnP daemon</modelDescription>
<modelName>MiniUPnPd</modelName>
<modelNumber>20180615</modelNumber>
<modelURL>http://miniupnp.free.fr/</modelURL>
<serialNumber>00000000</serialNumber>
<UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
<UPC>MINIUPNPD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/ctl/IPConn</controlURL>
<eventSubURL>/evt/IPConn</eventSubURL>
<SCPDURL>/WANIPCn.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
<presentationURL>http://192.168.0.1/</presentationURL>
</device>
</root>"#;
let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
assert_eq!(control_url, "/ctl/IPConn");
assert_eq!(control_schema_url, "/WANIPCn.xml");
}
#[test]
fn test_parse_device2() {
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName>FRITZ!Box 7430</friendlyName>
<manufacturer>AVM Berlin</manufacturer>
<manufacturerURL>http://www.avm.de</manufacturerURL>
<modelDescription>FRITZ!Box 7430</modelDescription>
<modelName>FRITZ!Box 7430</modelName>
<modelNumber>avm</modelNumber>
<modelURL>http://www.avm.de</modelURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<iconList>
<icon>
<mimetype>image/gif</mimetype>
<width>118</width>
<height>119</height>
<depth>8</depth>
<url>/ligd.gif</url>
</icon>
</iconList>
<serviceList>
<service>
<serviceType>urn:schemas-any-com:service:Any:1</serviceType>
<serviceId>urn:any-com:serviceId:any1</serviceId>
<controlURL>/igdupnp/control/any</controlURL>
<eventSubURL>/igdupnp/control/any</eventSubURL>
<SCPDURL>/any.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice - FRITZ!Box 7430</friendlyName>
<manufacturer>AVM Berlin</manufacturer>
<manufacturerURL>www.avm.de</manufacturerURL>
<modelDescription>WANDevice - FRITZ!Box 7430</modelDescription>
<modelName>WANDevice - FRITZ!Box 7430</modelName>
<modelNumber>avm</modelNumber>
<modelURL>www.avm.de</modelURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>AVM IGD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/igdupnp/control/WANCommonIFC1</controlURL>
<eventSubURL>/igdupnp/control/WANCommonIFC1</eventSubURL>
<SCPDURL>/igdicfgSCPD.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice - FRITZ!Box 7430</friendlyName>
<manufacturer>AVM Berlin</manufacturer>
<manufacturerURL>www.avm.de</manufacturerURL>
<modelDescription>WANConnectionDevice - FRITZ!Box 7430</modelDescription>
<modelName>WANConnectionDevice - FRITZ!Box 7430</modelName>
<modelNumber>avm</modelNumber>
<modelURL>www.avm.de</modelURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>AVM IGD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANDSLLinkConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANDSLLinkC1</serviceId>
<controlURL>/igdupnp/control/WANDSLLinkC1</controlURL>
<eventSubURL>/igdupnp/control/WANDSLLinkC1</eventSubURL>
<SCPDURL>/igddslSCPD.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/igdupnp/control/WANIPConn1</controlURL>
<eventSubURL>/igdupnp/control/WANIPConn1</eventSubURL>
<SCPDURL>/igdconnSCPD.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPv6FirewallControl:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPv6Firewall1</serviceId>
<controlURL>/igd2upnp/control/WANIPv6Firewall1</controlURL>
<eventSubURL>/igd2upnp/control/WANIPv6Firewall1</eventSubURL>
<SCPDURL>/igd2ipv6fwcSCPD.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
<presentationURL>http://fritz.box</presentationURL>
</device>
</root>
"#;
let result = parse_control_urls(text.as_bytes());
assert!(result.is_ok());
let (control_schema_url, control_url) = result.unwrap();
assert_eq!(control_url, "/igdupnp/control/WANIPConn1");
assert_eq!(control_schema_url, "/igdconnSCPD.xml");
}
#[test]
fn test_parse_device3() {
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device xmlns="urn:schemas-upnp-org:device-1-0">
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber></modelNumber>
<serialNumber></serialNumber>
<presentationURL>http://192.168.1.1</presentationURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>999999999001</UPC>
<iconList>
<icon>
<mimetype>image/png</mimetype>
<width>16</width>
<height>16</height>
<depth>8</depth>
<url>/ligd.png</url>
</icon>
</iconList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber></modelNumber>
<modelURL></modelURL>
<serialNumber></serialNumber>
<presentationURL>http://192.168.1.254</presentationURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>999999999001</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/upnp/control/WANCommonIFC1</controlURL>
<eventSubURL>/upnp/control/WANCommonIFC1</eventSubURL>
<SCPDURL>/332b484d/wancomicfgSCPD.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber></modelNumber>
<modelURL></modelURL>
<serialNumber></serialNumber>
<presentationURL>http://192.168.1.254</presentationURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>999999999001</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/upnp/control/WANIPConn1</controlURL>
<eventSubURL>/upnp/control/WANIPConn1</eventSubURL>
<SCPDURL>/332b484d/wanipconnSCPD.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
</device>
</root>"#;
let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
assert_eq!(control_url, "/upnp/control/WANIPConn1");
assert_eq!(control_schema_url, "/332b484d/wanipconnSCPD.xml");
}

View File

@@ -0,0 +1,323 @@
use std::collections::HashMap;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::net::{IpAddr, SocketAddr};
use super::Provider;
use super::common::{self, messages, parsing, parsing::RequestReponse};
use super::PortMappingProtocol;
/// This structure represents a gateway found by the search functions.
#[derive(Clone, Debug)]
pub struct Gateway<P> {
/// Socket address of the gateway
pub addr: SocketAddr,
/// Root url of the device
pub root_url: String,
/// Control url of the device
pub control_url: String,
/// Url to get schema data from
pub control_schema_url: String,
/// Control schema for all actions
pub control_schema: HashMap<String, Vec<String>>,
/// Executor provider
pub provider: P,
}
impl<P: Provider> Gateway<P> {
async fn perform_request(
&self,
header: &str,
body: &str,
ok: &str,
) -> anyhow::Result<RequestReponse> {
let url = format!("{self}");
let text = P::send_async(&url, header, body).await?;
parsing::parse_response(text, ok)
}
/// Get the external IP address of the gateway in a tokio compatible way
pub async fn get_external_ip(&self) -> anyhow::Result<Option<IpAddr>> {
let result = self
.perform_request(
messages::GET_EXTERNAL_IP_HEADER,
&messages::format_get_external_ip_message(),
"GetExternalIPAddressResponse",
)
.await;
parsing::parse_get_external_ip_response(result)
}
/// Get an external socket address with our external ip and any port. This is a convenience
/// function that calls `get_external_ip` followed by `add_any_port`
///
/// The local_addr is the address where the traffic is sent to.
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
///
/// # Returns
///
/// The external address that was mapped on success. Otherwise an error.
pub async fn get_any_address(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<SocketAddr> {
let description = description.to_owned();
let ip = self
.get_external_ip()
.await?
.ok_or_else(|| anyhow::anyhow!("Router does not have an external IP address"))?;
let port = self
.add_any_port(protocol, local_addr, lease_duration, &description)
.await?;
Ok(SocketAddr::new(ip, port))
}
/// Add a port mapping.with any external port.
///
/// The local_addr is the address where the traffic is sent to.
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
///
/// # Returns
///
/// The external port that was mapped on success. Otherwise an error.
pub async fn add_any_port(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
// This function first attempts to call AddAnyPortMapping on the IGD with a random port
// number. If that fails due to the method being unknown it attempts to call AddPortMapping
// instead with a random port number. If that fails due to ConflictInMappingEntry it retrys
// with another port up to a maximum of 20 times. If it fails due to SamePortValuesRequired
// it retrys once with the same port values.
if local_addr.port() == 0 {
return Err(anyhow::anyhow!("Internal port zero is invalid"));
}
let schema = self.control_schema.get("AddAnyPortMapping");
if let Some(schema) = schema {
let external_port = common::random_port();
let description = description.to_owned();
let resp = self
.perform_request(
messages::ADD_ANY_PORT_MAPPING_HEADER,
&messages::format_add_any_port_mapping_message(
schema,
protocol,
external_port,
local_addr,
lease_duration,
&description,
),
"AddAnyPortMappingResponse",
)
.await;
parsing::parse_add_any_port_mapping_response(resp)
} else {
// The router does not have the AddAnyPortMapping method.
// Fall back to using AddPortMapping with a random port.
self.retry_add_random_port_mapping(protocol, local_addr, lease_duration, description)
.await
}
}
async fn retry_add_random_port_mapping(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
for _ in 0u8..20u8 {
match self
.add_random_port_mapping(protocol, local_addr, lease_duration, description)
.await
{
Ok(port) => return Ok(port),
Err(_) => continue,
}
}
Err(anyhow::anyhow!("No ports available"))
}
async fn add_random_port_mapping(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
let description = description.to_owned();
let external_port = common::random_port();
let res = self
.add_port_mapping(
protocol,
external_port,
local_addr,
lease_duration,
&description,
)
.await;
match res {
Ok(_) => Ok(external_port),
Err(_) => {
self.add_same_port_mapping(protocol, local_addr, lease_duration, &description)
.await
}
}
}
async fn add_same_port_mapping(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
let res = self
.add_port_mapping(
protocol,
local_addr.port(),
local_addr,
lease_duration,
description,
)
.await;
match res {
Ok(_) => Ok(local_addr.port()),
Err(err) => Err(anyhow::anyhow!("Add same port mapping failed: {}", err)),
}
}
async fn add_port_mapping(
&self,
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<()> {
self.perform_request(
messages::ADD_PORT_MAPPING_HEADER,
&messages::format_add_port_mapping_message(
self.control_schema
.get("AddPortMapping")
.ok_or_else(|| anyhow::anyhow!("Unsupported action: AddPortMapping"))?,
protocol,
external_port,
local_addr,
lease_duration,
description,
),
"AddPortMappingResponse",
)
.await?;
Ok(())
}
/// Add a port mapping.
///
/// The local_addr is the address where the traffic is sent to.
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
pub async fn add_port(
&self,
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<()> {
if external_port == 0 {
return Err(anyhow::anyhow!("External port zero is invalid"));
}
if local_addr.port() == 0 {
return Err(anyhow::anyhow!("Internal port zero is invalid"));
}
let res = self
.add_port_mapping(
protocol,
external_port,
local_addr,
lease_duration,
description,
)
.await;
if let Err(err) = res {
return Err(anyhow::anyhow!("Add port mapping failed: {}", err));
};
Ok(())
}
/// Remove a port mapping.
pub async fn remove_port(
&self,
protocol: PortMappingProtocol,
external_port: u16,
) -> anyhow::Result<()> {
let res = self
.perform_request(
messages::DELETE_PORT_MAPPING_HEADER,
&messages::format_delete_port_message(
self.control_schema
.get("DeletePortMapping")
.ok_or_else(|| anyhow::anyhow!("Unsupported action: DeletePortMapping"))?,
protocol,
external_port,
),
"DeletePortMappingResponse",
)
.await;
parsing::parse_delete_port_mapping_response(res)
}
/// Get one port mapping entry
///
/// Gets one port mapping entry by its index.
/// Not all existing port mappings might be visible to this client.
/// If the index is out of bound, GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid will be returned
pub async fn get_generic_port_mapping_entry(
&self,
index: u32,
) -> anyhow::Result<parsing::PortMappingEntry> {
let result = self
.perform_request(
messages::GET_GENERIC_PORT_MAPPING_ENTRY,
&messages::formate_get_generic_port_mapping_entry_message(index),
"GetGenericPortMappingEntryResponse",
)
.await;
parsing::parse_get_generic_port_mapping_entry(result)
}
}
impl<P> fmt::Display for Gateway<P> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "http://{}{}", self.addr, self.control_url)
}
}
impl<P> PartialEq for Gateway<P> {
fn eq(&self, other: &Gateway<P>) -> bool {
self.addr == other.addr && self.control_url == other.control_url
}
}
impl<P> Eq for Gateway<P> {}
impl<P> Hash for Gateway<P> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.addr.hash(state);
self.control_url.hash(state);
}
}

View File

@@ -0,0 +1,64 @@
mod common;
mod gateway;
mod search;
use std::fmt;
pub(crate) const MAX_RESPONSE_SIZE: usize = 1500;
pub(crate) const HEADER_NAME: &str = "SOAPAction";
/// Trait to allow abstracting over `tokio`.
#[async_trait::async_trait]
pub trait Provider {
/// Send an async request over the executor.
async fn send_async(url: &str, action: &str, body: &str) -> anyhow::Result<String>;
}
/// Represents the protocols available for port mapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortMappingProtocol {
/// TCP protocol
Tcp,
/// UDP protocol
Udp,
}
impl fmt::Display for PortMappingProtocol {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
PortMappingProtocol::Tcp => "TCP",
PortMappingProtocol::Udp => "UDP",
}
)
}
}
#[cfg(test)]
mod tests {
use crate::instance::upnp_igd::{
common::SearchOptions, search::search_gateway, PortMappingProtocol,
};
#[tokio::test]
async fn test_search_device() {
let ret = search_gateway(SearchOptions::default()).await.unwrap();
println!("{:?}", ret);
let external_ip = ret.get_external_ip().await.unwrap();
println!("{:?}", external_ip);
let add_port_ret = ret
.add_port(
PortMappingProtocol::Tcp,
51010,
"10.147.223.128:11010".parse().unwrap(),
1000,
"test",
)
.await;
println!("{:?}", add_port_ret);
}
}

View File

@@ -0,0 +1,250 @@
//! Tokio abstraction for the aio [`Gateway`].
use std::collections::HashMap;
use std::net::SocketAddr;
use http_req::response::Headers;
use tokio::{net::UdpSocket, time::timeout};
use super::common::options::{DEFAULT_TIMEOUT, RESPONSE_TIMEOUT};
use super::common::{messages, parsing, SearchOptions};
use super::gateway::Gateway;
use super::{Provider, HEADER_NAME, MAX_RESPONSE_SIZE};
use tracing::debug;
/// Tokio provider for the [`Gateway`].
#[derive(Debug, Clone)]
pub struct Tokio;
#[async_trait::async_trait]
impl Provider for Tokio {
async fn send_async(url: &str, action: &str, body: &str) -> anyhow::Result<String> {
use http_req::request;
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
let url_owned = url.to_string();
let body_clone = body.to_string();
let action_clone = action.to_string();
let (response, response_body) = tokio::task::spawn_blocking(move || {
let uri = http_req::uri::Uri::try_from(url_owned.as_str())
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
println!("body: {body_clone}, action: {action_clone}");
let mut response_body = Vec::new();
let response = request::Request::new(&uri)
.method(request::Method::POST)
.header(HEADER_NAME, &action_clone)
.header("Content-Type", "text/xml; charset=\"utf-8\"")
.body(body_clone.as_bytes())
.send(&mut response_body);
if response.is_err() {
if response_body.is_empty() {
anyhow::bail!("HTTP request error: {}", response.unwrap_err());
} else {
anyhow::bail!(
"HTTP request error: {} with response body: {}",
response.unwrap_err(),
String::from_utf8_lossy(&response_body)
);
}
}
let response = response.unwrap();
Ok::<_, anyhow::Error>((response, response_body))
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
if !response.status_code().is_success() {
if response_body.is_empty() {
return Err(anyhow::anyhow!(
"HTTP error with empty body: {}",
response.status_code()
));
}
}
let string = String::from_utf8(response_body)
.map_err(|e| anyhow::anyhow!("UTF-8 conversion error: {}", e))?;
Ok(string)
}
}
/// Search for a gateway with the provided options.
pub async fn search_gateway(options: SearchOptions) -> anyhow::Result<Gateway<Tokio>> {
let search_timeout = options.timeout.unwrap_or(DEFAULT_TIMEOUT);
match timeout(search_timeout, search_gateway_inner(options)).await {
Ok(Ok(gateway)) => Ok(gateway),
Ok(Err(err)) => Err(err),
Err(_err) => {
// Timeout
Err(anyhow::anyhow!("No response within timeout"))
}
}
}
async fn search_gateway_inner(options: SearchOptions) -> anyhow::Result<Gateway<Tokio>> {
// Create socket for future calls
let mut socket = UdpSocket::bind(&options.bind_addr)
.await
.map_err(|e| anyhow::anyhow!("Failed to bind socket: {}", e))?;
send_search_request(&mut socket, options.broadcast_address).await?;
let response_timeout = options.single_search_timeout.unwrap_or(RESPONSE_TIMEOUT);
loop {
let search_response = receive_search_response(&mut socket);
// Receive search response
let (response_body, from) = match timeout(response_timeout, search_response).await {
Ok(Ok(v)) => v,
Ok(Err(err)) => {
debug!("error while receiving broadcast response: {err}");
continue;
}
Err(_) => {
debug!("timeout while receiving broadcast response");
continue;
}
};
let (addr, root_url) = match handle_broadcast_resp(&from, &response_body) {
Ok(v) => v,
Err(e) => {
debug!("error handling broadcast response: {}", e);
continue;
}
};
let (control_schema_url, control_url) = match get_control_urls(&addr, &root_url).await {
Ok(v) => v,
Err(e) => {
debug!("error getting control URLs: {}", e);
continue;
}
};
let control_schema = match get_control_schemas(&addr, &control_schema_url).await {
Ok(v) => v,
Err(e) => {
debug!("error getting control schemas: {}", e);
continue;
}
};
return Ok(Gateway {
addr,
root_url,
control_url,
control_schema_url,
control_schema,
provider: Tokio,
});
}
}
// Create a new search.
async fn send_search_request(socket: &mut UdpSocket, addr: SocketAddr) -> anyhow::Result<()> {
debug!(
"sending broadcast request to: {} on interface: {:?}",
addr,
socket.local_addr()
);
socket
.send_to(messages::SEARCH_REQUEST.as_bytes(), &addr)
.await
.map(|_| ())
.map_err(|e| anyhow::anyhow!("Failed to send search request: {}", e))
}
async fn receive_search_response(socket: &mut UdpSocket) -> anyhow::Result<(Vec<u8>, SocketAddr)> {
let mut buff = [0u8; MAX_RESPONSE_SIZE];
let (n, from) = socket
.recv_from(&mut buff)
.await
.map_err(|e| anyhow::anyhow!("Failed to receive response: {}", e))?;
debug!("received broadcast response from: {}", from);
Ok((buff[..n].to_vec(), from))
}
// Handle a UDP response message.
fn handle_broadcast_resp(from: &SocketAddr, data: &[u8]) -> anyhow::Result<(SocketAddr, String)> {
debug!("handling broadcast response from: {}", from);
// Convert response to text.
let text =
std::str::from_utf8(data).map_err(|e| anyhow::anyhow!("UTF-8 conversion error: {}", e))?;
// Parse socket address and path.
let (addr, root_url) = parsing::parse_search_result(text)?;
Ok((addr, root_url))
}
async fn get_control_urls(addr: &SocketAddr, path: &str) -> anyhow::Result<(String, String)> {
use http_req::request;
let url = format!("http://{addr}{path}");
debug!("requesting control url from: {}", url);
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
let (response, response_body) = tokio::task::spawn_blocking(move || {
let uri = http_req::uri::Uri::try_from(url.as_str())
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
let mut response_body = Vec::new();
let response = request::Request::new(&uri)
.method(request::Method::GET)
.send(&mut response_body)
.map_err(|e| anyhow::anyhow!("HTTP GET error: {}", e))?;
Ok::<_, anyhow::Error>((response, response_body))
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
if !response.status_code().is_success() {
return Err(anyhow::anyhow!("HTTP error: {}", response.status_code()));
}
debug!("handling control response from: {addr}");
let c = std::io::Cursor::new(&response_body);
parsing::parse_control_urls(c)
}
async fn get_control_schemas(
addr: &SocketAddr,
control_schema_url: &str,
) -> anyhow::Result<HashMap<String, Vec<String>>> {
use http_req::request;
let url = format!("http://{addr}{control_schema_url}");
debug!("requesting control schema from: {}", url);
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
let (response, response_body) = tokio::task::spawn_blocking(move || {
let uri = http_req::uri::Uri::try_from(url.as_str())
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
let mut response_body = Vec::new();
let response = request::Request::new(&uri)
.method(request::Method::GET)
.send(&mut response_body)
.map_err(|e| anyhow::anyhow!("HTTP GET error: {}", e))?;
Ok::<_, anyhow::Error>((response, response_body))
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
if !response.status_code().is_success() {
return Err(anyhow::anyhow!("HTTP error: {}", response.status_code()));
}
debug!("handling schema response from: {addr}");
let c = std::io::Cursor::new(&response_body);
parsing::parse_schemas(c)
}

View File

@@ -140,47 +140,6 @@ impl NetworkInstanceManager {
.and_then(|instance| instance.value().get_running_info())
}
pub fn get_network_config(&self, instance_id: &uuid::Uuid) -> Option<TomlConfigLoader> {
self.instance_map
.get(instance_id)
.map(|instance| instance.value().get_config())
}
pub fn replace_network_config(
&self,
instance_id: &uuid::Uuid,
new_config: TomlConfigLoader,
) -> Result<(), anyhow::Error> {
let mut instance = self
.instance_map
.get_mut(instance_id)
.ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?;
// Stop the current instance if it's running
if instance.is_easytier_running() {
// Get the config source before stopping
let config_source = instance.get_config_source();
// Create a new instance with the new config
let mut new_instance = NetworkInstance::new(new_config, config_source);
// Start the new instance
new_instance.start()?;
// Replace the old instance with the new one
*instance = new_instance;
// Restart the instance task if needed
self.start_instance_task(*instance_id)?;
} else {
// If the instance is not running, just replace the config
let config_source = instance.get_config_source();
*instance = NetworkInstance::new(new_config, config_source);
}
Ok(())
}
pub fn list_network_instance_ids(&self) -> Vec<uuid::Uuid> {
self.instance_map.iter().map(|item| *item.key()).collect()
}

View File

@@ -460,10 +460,6 @@ impl NetworkInstance {
None
}
}
pub fn get_config(&self) -> TomlConfigLoader {
self.config.clone()
}
}
pub fn add_proxy_network_to_config(

View File

@@ -178,25 +178,6 @@ message DeleteNetworkInstanceResponse {
repeated common.UUID remain_inst_ids = 1;
}
message GetConfigRequest {
common.UUID inst_id = 1;
}
message GetConfigResponse {
NetworkConfig config = 1;
string toml_config = 2;
}
message ReplaceConfigRequest {
common.UUID inst_id = 1;
NetworkConfig config = 2;
}
message ReplaceConfigResponse {
bool success = 1;
optional string error_msg = 2;
}
service WebClientService {
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {}
rpc RunNetworkInstance(RunNetworkInstanceRequest) returns (RunNetworkInstanceResponse) {}
@@ -204,6 +185,4 @@ service WebClientService {
rpc CollectNetworkInfo(CollectNetworkInfoRequest) returns (CollectNetworkInfoResponse) {}
rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {}
rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {}
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {}
rpc ReplaceConfig(ReplaceConfigRequest) returns (ReplaceConfigResponse) {}
}

View File

@@ -384,7 +384,11 @@ pub(crate) fn setup_sokcet2_ext(
unsafe {
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
tracing::warn!(?dev_idx, ?dev_name, "bind device");
if bind_addr.is_ipv4() {
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
} else {
socket2_socket.bind_device_by_index_v6(std::num::NonZeroU32::new(dev_idx))?;
}
tracing::warn!(?dev_idx, ?dev_name, "bind device doen");
}
}

View File

@@ -81,9 +81,14 @@ pub fn init_logger(
});
}
let dir = file_config.dir.as_deref().unwrap_or(".");
let file = file_config.file.as_deref().unwrap_or("easytier.log");
let path = std::path::Path::new(dir).join(file);
let path_str = path.to_string_lossy().into_owned();
let builder = RollingFileAppenderBase::builder();
let file_appender = builder
.filename(file_config.file.unwrap_or("easytier.log".to_string()))
.filename(path_str)
.condition_daily()
.max_filecount(file_config.count.unwrap_or(10))
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)

View File

@@ -6,9 +6,8 @@ use crate::{
rpc_types::{self, controller::BaseController},
web::{
CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest,
DeleteNetworkInstanceResponse, GetConfigRequest, GetConfigResponse,
ListNetworkInstanceRequest, ListNetworkInstanceResponse, NetworkInstanceRunningInfoMap,
ReplaceConfigRequest, ReplaceConfigResponse, RetainNetworkInstanceRequest,
DeleteNetworkInstanceResponse, ListNetworkInstanceRequest, ListNetworkInstanceResponse,
NetworkInstanceRunningInfoMap, RetainNetworkInstanceRequest,
RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse,
ValidateConfigRequest, ValidateConfigResponse, WebClientService,
},
@@ -154,79 +153,4 @@ impl WebClientService for Controller {
remain_inst_ids: remain_inst_ids.into_iter().map(Into::into).collect(),
})
}
// rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {}
async fn get_config(
&self,
_: BaseController,
req: GetConfigRequest,
) -> Result<GetConfigResponse, rpc_types::error::Error> {
let inst_id = req.inst_id.ok_or_else(|| {
rpc_types::error::Error::ExecutionError(
anyhow::anyhow!("instance_id is required").into(),
)
})?;
let config = self
.manager
.get_network_config(&inst_id.into())
.ok_or_else(|| {
rpc_types::error::Error::ExecutionError(
anyhow::anyhow!("instance {} not found", inst_id).into(),
)
})?;
// Get the NetworkConfig from the instance
let network_config = crate::launcher::NetworkConfig::new_from_config(&config)?;
// Get the TOML config string
let toml_config = config.dump();
Ok(GetConfigResponse {
config: Some(network_config),
toml_config,
})
}
// rpc ReplaceConfig(ReplaceConfigRequest) returns (ReplaceConfigResponse) {}
async fn replace_config(
&self,
_: BaseController,
req: ReplaceConfigRequest,
) -> Result<ReplaceConfigResponse, rpc_types::error::Error> {
let inst_id = req.inst_id.ok_or_else(|| {
rpc_types::error::Error::ExecutionError(
anyhow::anyhow!("instance_id is required").into(),
)
})?;
let new_config = req.config.ok_or_else(|| {
rpc_types::error::Error::ExecutionError(anyhow::anyhow!("config is required").into())
})?;
// Generate the TomlConfigLoader from NetworkConfig
let new_toml_config = new_config.gen_config()?;
// Replace the configuration
match self
.manager
.replace_network_config(&inst_id.into(), new_toml_config)
{
Ok(()) => {
println!("instance {} config replaced successfully", inst_id);
Ok(ReplaceConfigResponse {
success: true,
error_msg: None,
})
}
Err(e) => {
let error_msg = format!("Failed to replace config for instance {}: {}", inst_id, e);
eprintln!("{}", error_msg);
Ok(ReplaceConfigResponse {
success: false,
error_msg: Some(error_msg),
})
}
}
}
}