mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-13 17:35:37 +00:00
Fix IPv6 TCP tunnel display formatting (#1980)
Normalize composite tunnel display values before rendering peer and debug output so IPv6 tunnel types no longer append `6` to the port. - Preserve prefixes like `txt-` while converting tunnel schemes to their IPv6 form. - Recover malformed values such as `txt-tcp://...:110106` into `txt-tcp6://...:11010`. - Reuse the normalized remote address display in CLI debug output.
This commit is contained in:
@@ -149,23 +149,8 @@ pub mod instance {
|
||||
ret
|
||||
}
|
||||
|
||||
fn is_tunnel_ipv6(tunnel_info: &super::super::common::TunnelInfo) -> bool {
|
||||
let Some(local_addr) = &tunnel_info.local_addr else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let u: url::Url = local_addr.clone().into();
|
||||
u.host()
|
||||
.map(|h| matches!(h, url::Host::Ipv6(_)))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_tunnel_proto_str(tunnel_info: &super::super::common::TunnelInfo) -> String {
|
||||
if Self::is_tunnel_ipv6(tunnel_info) {
|
||||
format!("{}6", tunnel_info.tunnel_type)
|
||||
} else {
|
||||
tunnel_info.tunnel_type.clone()
|
||||
}
|
||||
tunnel_info.display_tunnel_type()
|
||||
}
|
||||
|
||||
pub fn get_conn_protos(&self) -> Option<Vec<String>> {
|
||||
|
||||
@@ -201,6 +201,7 @@ message TunnelInfo {
|
||||
string tunnel_type = 1;
|
||||
common.Url local_addr = 2;
|
||||
common.Url remote_addr = 3;
|
||||
common.Url resolved_remote_addr = 4;
|
||||
}
|
||||
|
||||
message StunInfo {
|
||||
|
||||
@@ -5,8 +5,9 @@ use std::{
|
||||
|
||||
use anyhow::Context;
|
||||
use base64::{prelude::BASE64_STANDARD, Engine as _};
|
||||
use strum::VariantArray;
|
||||
|
||||
use crate::tunnel::packet_def::CompressorAlgo;
|
||||
use crate::tunnel::{packet_def::CompressorAlgo, IpScheme};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/common.rs"));
|
||||
|
||||
@@ -284,6 +285,105 @@ impl fmt::Display for Url {
|
||||
}
|
||||
}
|
||||
|
||||
fn split_tunnel_scheme(raw_scheme: &str) -> Option<(&str, &'static str, bool)> {
|
||||
for scheme in IpScheme::VARIANTS {
|
||||
let scheme: &'static str = scheme.into();
|
||||
if let Some(base) = raw_scheme.strip_suffix('6') {
|
||||
if let Some(prefix) = base.strip_suffix(scheme) {
|
||||
if prefix.is_empty() || prefix.ends_with('-') {
|
||||
return Some((prefix, scheme, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prefix) = raw_scheme.strip_suffix(scheme) {
|
||||
if prefix.is_empty() || prefix.ends_with('-') {
|
||||
return Some((prefix, scheme, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn normalize_tunnel_scheme(raw_scheme: &str, is_ipv6: bool) -> Option<String> {
|
||||
let (prefix, scheme, had_ipv6_suffix) = split_tunnel_scheme(raw_scheme)?;
|
||||
let suffix = if is_ipv6 || had_ipv6_suffix { "6" } else { "" };
|
||||
Some(format!("{prefix}{scheme}{suffix}"))
|
||||
}
|
||||
|
||||
fn infer_tunnel_ipv6(raw: &str) -> Option<bool> {
|
||||
let (_, rest) = raw.split_once("://")?;
|
||||
if rest.starts_with('[') {
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
match url::Url::parse(raw).ok()?.host() {
|
||||
Some(url::Host::Ipv4(_)) => Some(false),
|
||||
Some(url::Host::Ipv6(_)) => Some(true),
|
||||
Some(url::Host::Domain(_)) | None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_tunnel_port(raw_port: &str, is_ipv6: bool) -> Option<u16> {
|
||||
if let Ok(port) = raw_port.parse::<u16>() {
|
||||
return Some(port);
|
||||
}
|
||||
|
||||
if is_ipv6 && raw_port.ends_with('6') {
|
||||
return raw_port[..raw_port.len() - 1].parse::<u16>().ok();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn normalize_tunnel_url(raw: &str, fallback_ipv6: Option<bool>) -> Option<String> {
|
||||
let (raw_scheme, rest) = raw.split_once("://")?;
|
||||
|
||||
if let Some(rest) = rest.strip_prefix('[') {
|
||||
let (host, remainder) = rest.split_once(']')?;
|
||||
let scheme = normalize_tunnel_scheme(raw_scheme, true)?;
|
||||
|
||||
if remainder.is_empty() {
|
||||
return Some(format!("{scheme}://[{host}]"));
|
||||
}
|
||||
|
||||
let raw_port = remainder.strip_prefix(':')?;
|
||||
let port = normalize_tunnel_port(raw_port, true)?;
|
||||
return Some(format!("{scheme}://[{host}]:{port}"));
|
||||
}
|
||||
|
||||
let is_ipv6 = infer_tunnel_ipv6(raw).or(fallback_ipv6).unwrap_or(false);
|
||||
let scheme = normalize_tunnel_scheme(raw_scheme, is_ipv6)?;
|
||||
|
||||
if let Ok(url) = url::Url::parse(raw) {
|
||||
let host = match url.host()? {
|
||||
url::Host::Ipv4(host) => host.to_string(),
|
||||
url::Host::Ipv6(host) => format!("[{host}]"),
|
||||
url::Host::Domain(host) => host.to_string(),
|
||||
};
|
||||
|
||||
return Some(match url.port_or_known_default() {
|
||||
Some(port) => format!("{scheme}://{host}:{port}"),
|
||||
None => format!("{scheme}://{host}"),
|
||||
});
|
||||
}
|
||||
|
||||
let (host, raw_port) = rest.rsplit_once(':')?;
|
||||
let port = normalize_tunnel_port(raw_port, is_ipv6)?;
|
||||
Some(format!("{scheme}://{host}:{port}"))
|
||||
}
|
||||
|
||||
impl Url {
|
||||
pub fn is_ipv6_tunnel_endpoint(&self) -> bool {
|
||||
infer_tunnel_ipv6(&self.url).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn normalized_tunnel_display(&self) -> String {
|
||||
normalize_tunnel_url(&self.url, None).unwrap_or_else(|| self.url.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::net::SocketAddr> for SocketAddr {
|
||||
fn from(value: std::net::SocketAddr) -> Self {
|
||||
match value {
|
||||
@@ -325,6 +425,38 @@ impl Display for SocketAddr {
|
||||
}
|
||||
}
|
||||
|
||||
impl TunnelInfo {
|
||||
pub fn effective_remote_addr(&self) -> Option<&Url> {
|
||||
self.resolved_remote_addr
|
||||
.as_ref()
|
||||
.or(self.remote_addr.as_ref())
|
||||
}
|
||||
|
||||
pub fn display_tunnel_type(&self) -> String {
|
||||
let is_ipv6 = infer_tunnel_ipv6(&self.tunnel_type).or_else(|| {
|
||||
self.resolved_remote_addr
|
||||
.as_ref()
|
||||
.or(self.local_addr.as_ref())
|
||||
.or(self.remote_addr.as_ref())
|
||||
.map(Url::is_ipv6_tunnel_endpoint)
|
||||
});
|
||||
|
||||
if self.tunnel_type.contains("://") {
|
||||
normalize_tunnel_url(&self.tunnel_type, is_ipv6)
|
||||
.unwrap_or_else(|| self.tunnel_type.clone())
|
||||
} else {
|
||||
is_ipv6
|
||||
.and_then(|is_ipv6| normalize_tunnel_scheme(&self.tunnel_type, is_ipv6))
|
||||
.unwrap_or_else(|| self.tunnel_type.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_remote_addr(&self) -> Option<String> {
|
||||
self.effective_remote_addr()
|
||||
.map(Url::normalized_tunnel_display)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CompressionAlgoPb> for CompressorAlgo {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
@@ -397,3 +529,149 @@ impl SecureModeConfig {
|
||||
Ok(x25519_dalek::PublicKey::from(k))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{normalize_tunnel_url, TunnelInfo, Url};
|
||||
|
||||
fn assert_ipv6_tunnel_normalization(scheme: &str, port: u16) {
|
||||
let expected = format!("{scheme}6://[2001:db8::1]:{port}");
|
||||
assert_eq!(
|
||||
normalize_tunnel_url(&format!("{scheme}://[2001:db8::1]:{port}"), None).as_deref(),
|
||||
Some(expected.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_plain_ipv6_tunnel_url() {
|
||||
let url = Url {
|
||||
url: "tcp://[2001:db8::1]:11010".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
url.normalized_tunnel_display(),
|
||||
"tcp6://[2001:db8::1]:11010"
|
||||
);
|
||||
assert!(url.is_ipv6_tunnel_endpoint());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_all_enabled_ipv6_tunnel_urls() {
|
||||
assert_ipv6_tunnel_normalization("tcp", 11010);
|
||||
assert_ipv6_tunnel_normalization("udp", 11010);
|
||||
|
||||
#[cfg(feature = "wireguard")]
|
||||
assert_ipv6_tunnel_normalization("wg", 11011);
|
||||
|
||||
#[cfg(feature = "quic")]
|
||||
assert_ipv6_tunnel_normalization("quic", 11012);
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
assert_ipv6_tunnel_normalization("ws", 80);
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
assert_ipv6_tunnel_normalization("wss", 443);
|
||||
|
||||
#[cfg(feature = "faketcp")]
|
||||
assert_ipv6_tunnel_normalization("faketcp", 11013);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_composite_ipv6_tunnel_url() {
|
||||
assert_eq!(
|
||||
normalize_tunnel_url("txt-tcp://[2001:db8::1]:11010", None).as_deref(),
|
||||
Some("txt-tcp6://[2001:db8::1]:11010")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recover_malformed_composite_ipv6_tunnel_url() {
|
||||
assert_eq!(
|
||||
normalize_tunnel_url("txt-tcp://[2001:db8::1]:110106", None).as_deref(),
|
||||
Some("txt-tcp6://[2001:db8::1]:11010")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keep_normalized_ipv6_tunnel_url_stable() {
|
||||
assert_eq!(
|
||||
normalize_tunnel_url("tcp6://[2001:db8::1]:11010", None).as_deref(),
|
||||
Some("tcp6://[2001:db8::1]:11010")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_ipv6_tunnel_url_without_explicit_port() {
|
||||
assert_eq!(
|
||||
normalize_tunnel_url("tcp://[2001:db8::1]", None).as_deref(),
|
||||
Some("tcp6://[2001:db8::1]")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keep_domain_host_unbracketed_when_ipv6_falls_back() {
|
||||
assert_eq!(
|
||||
normalize_tunnel_url("tcp://localhost:11010", Some(true)).as_deref(),
|
||||
Some("tcp6://localhost:11010")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_info_display_tunnel_type_preserves_composite_prefix() {
|
||||
let tunnel = TunnelInfo {
|
||||
tunnel_type: "txt-tcp://[2001:db8::2]:110106".to_string(),
|
||||
local_addr: None,
|
||||
remote_addr: Some(Url {
|
||||
url: "txt://et.example.com".to_string(),
|
||||
}),
|
||||
resolved_remote_addr: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
tunnel.display_tunnel_type(),
|
||||
"txt-tcp6://[2001:db8::2]:11010"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_info_display_tunnel_type_uses_remote_addr_fallback() {
|
||||
let tunnel = TunnelInfo {
|
||||
tunnel_type: "tcp".to_string(),
|
||||
local_addr: None,
|
||||
remote_addr: Some(Url {
|
||||
url: "tcp://[2001:db8::2]:11010".to_string(),
|
||||
}),
|
||||
resolved_remote_addr: None,
|
||||
};
|
||||
|
||||
assert_eq!(tunnel.display_tunnel_type(), "tcp6");
|
||||
assert_eq!(
|
||||
tunnel.display_remote_addr().as_deref(),
|
||||
Some("tcp6://[2001:db8::2]:11010")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_info_prefers_resolved_remote_addr() {
|
||||
let tunnel = TunnelInfo {
|
||||
tunnel_type: "txt-tcp".to_string(),
|
||||
local_addr: None,
|
||||
remote_addr: Some(Url {
|
||||
url: "txt://et.example.com".to_string(),
|
||||
}),
|
||||
resolved_remote_addr: Some(Url {
|
||||
url: "tcp://[2001:db8::3]:11010".to_string(),
|
||||
}),
|
||||
};
|
||||
|
||||
assert_eq!(tunnel.display_tunnel_type(), "txt-tcp6");
|
||||
assert_eq!(
|
||||
tunnel.display_remote_addr().as_deref(),
|
||||
Some("tcp6://[2001:db8::3]:11010")
|
||||
);
|
||||
assert_eq!(
|
||||
tunnel.effective_remote_addr().map(|url| url.url.as_str()),
|
||||
Some("tcp://[2001:db8::3]:11010")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user