mirror of
https://github.com/TrustTunnel/TrustTunnel.git
synced 2026-04-14 23:25:25 +00:00
Explicitly set IPV6_V6ONLY=false for dual-stack listen sockets
Change addresses type from Vec<SocketAddr> to Vec<String> Accept domain names in -a flag for client config export Warn when -a domain does not match any hostname in hosts.toml Update -a flag documentation to reflect domain name support Add unit tests for parse_endpoint_address Code quality improvements Unmap IPv6-mapped IPv4 addresses (::ffff:a.b.c.d) before rules evaluation Add more tests Code cleanup
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# CHANGELOG
|
||||
|
||||
- [Feature] The `-a` flag now accepts `domain` and `domain:port` in addition to `ip` and `ip:port`.
|
||||
The exported client configuration will contain the domain name, which the client resolves via DNS at connect time.
|
||||
- [Feature] When listening on `[::]`, the endpoint now explicitly sets `IPV6_V6ONLY=false` to accept
|
||||
both IPv4 and IPv6 connections on a single socket (dual-stack).
|
||||
|
||||
## 0.9.127
|
||||
|
||||
- [Feature] Added GPG signing of the endpoint binaries.
|
||||
|
||||
@@ -52,7 +52,7 @@ The endpoint binary accepts the following command line arguments:
|
||||
| `<settings>` | - | **Required.** Path to main settings file | - |
|
||||
| `<tls_hosts_settings>` | - | **Required.** Path to TLS hosts settings file | - |
|
||||
| `--client_config` | `-c` | Print endpoint config for specified client and exit | - |
|
||||
| `--address` | `-a` | Endpoint address to add to client config (requires `-c`) | - |
|
||||
| `--address` | `-a` | Endpoint address to add to client config (requires `-c`). Accepts `ip`, `ip:port`, `domain`, or `domain:port`. | - |
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -66,11 +66,17 @@ The endpoint binary accepts the following command line arguments:
|
||||
# Start with file logging
|
||||
./trusttunnel_endpoint vpn.toml hosts.toml --logfile /var/log/trusttunnel.log
|
||||
|
||||
# Export client configuration
|
||||
# Export client configuration with IP address
|
||||
./trusttunnel_endpoint vpn.toml hosts.toml -c username -a 203.0.113.1
|
||||
|
||||
# Export client configuration with explicit port
|
||||
./trusttunnel_endpoint vpn.toml hosts.toml -c username -a 203.0.113.1:443
|
||||
|
||||
# Export client configuration with domain name
|
||||
./trusttunnel_endpoint vpn.toml hosts.toml -c username -a vpn.example.com
|
||||
|
||||
# Export client configuration with domain name and explicit port
|
||||
./trusttunnel_endpoint vpn.toml hosts.toml -c username -a vpn.example.com:443
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -256,10 +256,10 @@ To generate the configuration, run the following command:
|
||||
|
||||
```shell
|
||||
# <client_name> - name of the client those credentials will be included in the configuration
|
||||
# <public_ip> - `ip` or `ip:port` that the user will use to connect to the endpoint
|
||||
# If only `ip` is specified, the port from the `listen_address` field will be used
|
||||
# <address> - `ip`, `ip:port`, `domain`, or `domain:port` that the client will use to connect
|
||||
# If only `ip` or `domain` is specified, the port from the `listen_address` field will be used
|
||||
cd /opt/trusttunnel/
|
||||
./trusttunnel_endpoint vpn.toml hosts.toml -c <client_name> -a <public_ip>
|
||||
./trusttunnel_endpoint vpn.toml hosts.toml -c <client_name> -a <address>
|
||||
```
|
||||
|
||||
This will print the configuration with the credentials for the client named
|
||||
|
||||
@@ -112,7 +112,7 @@ fn main() {
|
||||
.requires(CLIENT_CONFIG_PARAM_NAME)
|
||||
.short('a')
|
||||
.long("address")
|
||||
.help("Endpoint address to be added to client's config.")
|
||||
.help("Endpoint address to be added to client's config. Accepts ip, ip:port, domain, or domain:port.")
|
||||
])
|
||||
.disable_version_flag(true)
|
||||
.get_matches();
|
||||
@@ -184,20 +184,25 @@ fn main() {
|
||||
|
||||
if args.contains_id(CLIENT_CONFIG_PARAM_NAME) {
|
||||
let username = args.get_one::<String>(CLIENT_CONFIG_PARAM_NAME).unwrap();
|
||||
let addresses: Vec<SocketAddr> = args
|
||||
let listen_port = settings.get_listen_address().port();
|
||||
let addresses: Vec<String> = args
|
||||
.get_many::<String>(ADDRESS_PARAM_NAME)
|
||||
.expect("At least one address should be specified")
|
||||
.map(|x| {
|
||||
SocketAddr::from_str(x)
|
||||
.or_else(|_| {
|
||||
SocketAddr::from_str(&format!("{}:{}", x, settings.get_listen_address().port()))
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("Failed to parse address. Expected `ip` or `ip:port` format, found: `{}`", x);
|
||||
})
|
||||
})
|
||||
.map(|x| parse_endpoint_address(x, listen_port))
|
||||
.collect();
|
||||
|
||||
for addr in &addresses {
|
||||
if let Some(domain) = extract_domain_for_warning(addr) {
|
||||
if !domain_matches_tls_hosts(domain, &tls_hosts_settings) {
|
||||
warn!(
|
||||
"Domain '{}' does not match any hostname in TLS hosts settings. \
|
||||
Please verify this is correct (it may be a typo).",
|
||||
domain
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client_config = client_config::build(
|
||||
username,
|
||||
addresses,
|
||||
@@ -295,3 +300,222 @@ fn main() {
|
||||
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
/// Returns the domain part of an address string if it is a domain (not an IP).
|
||||
/// Returns `None` for IP addresses (both IPv4 and IPv6).
|
||||
fn extract_domain_for_warning(addr: &str) -> Option<&str> {
|
||||
// If the whole string parses as a SocketAddr (covers IPv4, [IPv6]:port), it's an IP.
|
||||
if SocketAddr::from_str(addr).is_ok() {
|
||||
return None;
|
||||
}
|
||||
// If the whole string parses as an IpAddr (bare IPv4 or bare IPv6 like ::1), it's an IP.
|
||||
if addr.parse::<std::net::IpAddr>().is_ok() {
|
||||
return None;
|
||||
}
|
||||
let domain = addr.rsplit_once(':').map(|(d, _)| d).unwrap_or(addr);
|
||||
// After splitting on ':', check the domain part is not an IP.
|
||||
if domain.parse::<std::net::IpAddr>().is_ok() {
|
||||
return None;
|
||||
}
|
||||
Some(domain)
|
||||
}
|
||||
|
||||
/// Returns `true` if `domain` matches any hostname or allowed SNI in the TLS hosts settings.
|
||||
fn domain_matches_tls_hosts(domain: &str, tls_hosts_settings: &settings::TlsHostsSettings) -> bool {
|
||||
tls_hosts_settings
|
||||
.get_main_hosts()
|
||||
.iter()
|
||||
.any(|h| h.hostname == domain || h.allowed_sni.iter().any(|s| s == domain))
|
||||
}
|
||||
|
||||
/// Parse an endpoint address string into a normalized `host:port` format.
|
||||
///
|
||||
/// Accepts the following formats:
|
||||
/// - `IP:port` (e.g. `1.2.3.4:443`, `[::1]:443`)
|
||||
/// - `IP` without port (e.g. `1.2.3.4`, `::1`) — `default_port` is appended
|
||||
/// - `domain:port` (e.g. `vpn.example.com:443`)
|
||||
/// - `domain` without port (e.g. `vpn.example.com`) — `default_port` is appended
|
||||
///
|
||||
/// **Note:** IPv6 addresses with a port **must** use bracket notation: `[::1]:443`.
|
||||
/// A bare IPv6 address without brackets (e.g. `::1`) is accepted only when no port
|
||||
/// is specified — `default_port` is then appended. The ambiguous form `2001:db8::1:443`
|
||||
/// (IPv6 with port, no brackets) is not supported and will be misinterpreted.
|
||||
fn parse_endpoint_address(input: &str, default_port: u16) -> String {
|
||||
if let Ok(addr) = SocketAddr::from_str(input) {
|
||||
return addr.to_string();
|
||||
}
|
||||
if let Ok(addr) = SocketAddr::from_str(&format!("{input}:{default_port}")) {
|
||||
return addr.to_string();
|
||||
}
|
||||
// Bare IPv6 without brackets, e.g. "::1"
|
||||
if let Ok(ip) = input.parse::<std::net::IpAddr>() {
|
||||
return SocketAddr::new(ip, default_port).to_string();
|
||||
}
|
||||
if let Some((domain, port_str)) = input.rsplit_once(':') {
|
||||
let port: u16 = port_str.parse().unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to parse port in address '{}'. \
|
||||
Expected `ip`, `ip:port`, `domain`, or `domain:port` format.",
|
||||
input
|
||||
);
|
||||
});
|
||||
format!("{domain}:{port}")
|
||||
} else {
|
||||
format!("{input}:{default_port}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_tls_hosts(hostnames: &[&str]) -> settings::TlsHostsSettings {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let tmp = std::env::temp_dir().join(format!("trusttunnel_test_cert_{id}.pem"));
|
||||
std::fs::write(&tmp, b"").unwrap();
|
||||
let path = tmp.to_str().unwrap();
|
||||
let entries: String = hostnames
|
||||
.iter()
|
||||
.map(|&h| {
|
||||
format!(
|
||||
"[[main_hosts]]\nhostname = \"{}\"\ncert_chain_path = \"{}\"\nprivate_key_path = \"{}\"\n",
|
||||
h, path, path
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
toml::from_str(&entries).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_domain_ipv4_returns_none() {
|
||||
assert_eq!(extract_domain_for_warning("1.2.3.4:443"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_domain_ipv6_returns_none() {
|
||||
assert_eq!(extract_domain_for_warning("[::1]:443"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_domain_bare_ipv6_returns_none() {
|
||||
assert_eq!(extract_domain_for_warning("::1"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_domain_with_port_returns_domain() {
|
||||
assert_eq!(
|
||||
extract_domain_for_warning("vpn.example.com:443"),
|
||||
Some("vpn.example.com")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_domain_without_port_returns_domain() {
|
||||
assert_eq!(
|
||||
extract_domain_for_warning("vpn.example.com"),
|
||||
Some("vpn.example.com")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_matches_tls_hosts_exact() {
|
||||
let hosts = make_tls_hosts(&["vpn.example.com"]);
|
||||
assert!(domain_matches_tls_hosts("vpn.example.com", &hosts));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_matches_tls_hosts_no_match() {
|
||||
let hosts = make_tls_hosts(&["vpn.example.com"]);
|
||||
assert!(!domain_matches_tls_hosts("other.example.com", &hosts));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_matches_tls_hosts_allowed_sni() {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(1000);
|
||||
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let tmp = std::env::temp_dir().join(format!("trusttunnel_test_cert_{id}.pem"));
|
||||
std::fs::write(&tmp, b"").unwrap();
|
||||
let path = tmp.to_str().unwrap();
|
||||
let toml = format!(
|
||||
"[[main_hosts]]\nhostname = \"vpn.example.com\"\ncert_chain_path = \"{}\"\nprivate_key_path = \"{}\"\nallowed_sni = [\"alias.example.com\"]\n",
|
||||
path, path
|
||||
);
|
||||
let hosts: settings::TlsHostsSettings = toml::from_str(&toml).unwrap();
|
||||
assert!(domain_matches_tls_hosts("alias.example.com", &hosts));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_matches_tls_hosts_different_host() {
|
||||
let hosts = make_tls_hosts(&["other.example.com"]);
|
||||
assert!(!domain_matches_tls_hosts("vpn.example.com", &hosts));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ipv4_with_port() {
|
||||
assert_eq!(parse_endpoint_address("1.2.3.4:443", 8443), "1.2.3.4:443");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ipv4_without_port() {
|
||||
assert_eq!(parse_endpoint_address("1.2.3.4", 443), "1.2.3.4:443");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ipv6_with_port() {
|
||||
assert_eq!(parse_endpoint_address("[::1]:443", 8443), "[::1]:443");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ipv6_without_port() {
|
||||
assert_eq!(parse_endpoint_address("::1", 443), "[::1]:443");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_domain_with_port() {
|
||||
assert_eq!(
|
||||
parse_endpoint_address("vpn.example.com:8443", 443),
|
||||
"vpn.example.com:8443"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_domain_without_port() {
|
||||
assert_eq!(
|
||||
parse_endpoint_address("vpn.example.com", 443),
|
||||
"vpn.example.com:443"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_domain_default_port_applied() {
|
||||
assert_eq!(
|
||||
parse_endpoint_address("my-vpn.example.org", 8443),
|
||||
"my-vpn.example.org:8443"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Failed to parse port")]
|
||||
fn test_parse_domain_invalid_port() {
|
||||
parse_endpoint_address("vpn.example.com:notaport", 443);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ipv6_with_port_bracket_notation() {
|
||||
assert_eq!(
|
||||
parse_endpoint_address("[2001:db8::1]:443", 8443),
|
||||
"[2001:db8::1]:443"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_bare_ipv6_without_port_gets_default() {
|
||||
assert_eq!(
|
||||
parse_endpoint_address("2001:db8::1", 443),
|
||||
"[2001:db8::1]:443"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ use crate::{authentication::registry_based, settings::TlsHostsSettings, utils::T
|
||||
#[cfg(feature = "rt_doc")]
|
||||
use macros::{Getter, RuntimeDoc};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::net::SocketAddr;
|
||||
use toml_edit::{value, Document};
|
||||
|
||||
pub fn build(
|
||||
client: &String,
|
||||
addresses: Vec<SocketAddr>,
|
||||
addresses: Vec<String>,
|
||||
username: &[registry_based::Client],
|
||||
hostsettings: &TlsHostsSettings,
|
||||
) -> ClientConfig {
|
||||
@@ -39,8 +38,8 @@ pub fn build(
|
||||
pub struct ClientConfig {
|
||||
/// Endpoint host name, used for TLS session establishment
|
||||
hostname: String,
|
||||
/// Endpoint addresses.
|
||||
addresses: Vec<SocketAddr>,
|
||||
/// Endpoint addresses in `IP:port` or `hostname:port` format
|
||||
addresses: Vec<String>,
|
||||
/// Whether IPv6 traffic can be routed through the endpoint
|
||||
has_ipv6: bool,
|
||||
/// Username for authorization
|
||||
@@ -63,7 +62,7 @@ impl ClientConfig {
|
||||
pub fn compose_toml(&self) -> String {
|
||||
let mut doc: Document = TEMPLATE.parse().unwrap();
|
||||
doc["hostname"] = value(&self.hostname);
|
||||
let vec = toml_edit::Array::from_iter(self.addresses.iter().map(|x| x.to_string()));
|
||||
let vec = toml_edit::Array::from_iter(self.addresses.iter().map(|x| x.as_str()));
|
||||
doc["addresses"] = value(vec);
|
||||
doc["has_ipv6"] = value(self.has_ipv6);
|
||||
doc["username"] = value(&self.username);
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
authentication, http_ping_handler, http_speedtest_handler, log_id, log_utils, metrics,
|
||||
net_utils, reverse_proxy, rules, settings, tls_demultiplexer, tunnel,
|
||||
};
|
||||
use socket2::SockRef;
|
||||
use socket2::{Domain, Protocol as SockProtocol, SockRef, Socket, Type};
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -221,7 +221,23 @@ impl Core {
|
||||
let has_tcp_based_codec =
|
||||
settings.listen_protocols.http1.is_some() || settings.listen_protocols.http2.is_some();
|
||||
|
||||
let tcp_listener = TcpListener::bind(settings.listen_address).await?;
|
||||
let tcp_listener = {
|
||||
let addr = settings.listen_address;
|
||||
let domain = if addr.is_ipv6() {
|
||||
Domain::IPV6
|
||||
} else {
|
||||
Domain::IPV4
|
||||
};
|
||||
let socket = Socket::new(domain, Type::STREAM, Some(SockProtocol::TCP))?;
|
||||
if domain == Domain::IPV6 {
|
||||
socket.set_only_v6(false)?;
|
||||
}
|
||||
socket.set_reuse_address(true)?;
|
||||
socket.set_nonblocking(true)?;
|
||||
socket.bind(&addr.into())?;
|
||||
socket.listen(1024)?;
|
||||
TcpListener::from_std(socket.into())?
|
||||
};
|
||||
info!("Listening to TCP {}", settings.listen_address);
|
||||
|
||||
let tls_listener = Arc::new(TlsListener::new());
|
||||
@@ -272,7 +288,7 @@ impl Core {
|
||||
if let Err((client_id, message)) = Core::on_new_tls_connection(
|
||||
context.clone(),
|
||||
acceptor,
|
||||
client_addr.ip(),
|
||||
net_utils::unmap_ipv6(client_addr.ip()),
|
||||
client_id,
|
||||
)
|
||||
.await
|
||||
@@ -293,7 +309,21 @@ impl Core {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let socket = UdpSocket::bind(settings.listen_address).await?;
|
||||
let socket = {
|
||||
let addr = settings.listen_address;
|
||||
let domain = if addr.is_ipv6() {
|
||||
Domain::IPV6
|
||||
} else {
|
||||
Domain::IPV4
|
||||
};
|
||||
let socket = Socket::new(domain, Type::DGRAM, Some(SockProtocol::UDP))?;
|
||||
if domain == Domain::IPV6 {
|
||||
socket.set_only_v6(false)?;
|
||||
}
|
||||
socket.set_nonblocking(true)?;
|
||||
socket.bind(&addr.into())?;
|
||||
UdpSocket::from_std(socket.into())?
|
||||
};
|
||||
info!("Listening to UDP {}", settings.listen_address);
|
||||
|
||||
let mut quic_listener = QuicMultiplexer::new(
|
||||
@@ -524,7 +554,7 @@ impl Core {
|
||||
client_id: log_utils::IdChain<u64>,
|
||||
) {
|
||||
// Apply connection filtering rules
|
||||
let client_ip = socket.peer_addr().ok().map(|addr| addr.ip());
|
||||
let client_ip = socket.peer_addr().ok().map(|addr| net_utils::unmap_ipv6(addr.ip()));
|
||||
let client_random = Some(socket.client_random());
|
||||
|
||||
if let Err(deny_reason) = Self::evaluate_connection_rules(
|
||||
|
||||
@@ -451,6 +451,21 @@ pub(crate) const fn is_global_ipv6(ip: &Ipv6Addr) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an IPv6-mapped IPv4 address (`::ffff:a.b.c.d`) to a plain `IpAddr::V4`.
|
||||
///
|
||||
/// When the endpoint listens on `[::]` with `IPV6_V6ONLY=false`, the OS presents
|
||||
/// incoming IPv4 connections as IPv6-mapped addresses. This function unmaps them so
|
||||
/// that IP-based filtering (rules engine, `allow_private_network_connections`) works
|
||||
/// correctly with IPv4 CIDR ranges.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub(crate) fn unmap_ipv6(ip: IpAddr) -> IpAddr {
|
||||
match ip {
|
||||
IpAddr::V6(v6) => v6.to_ipv4_mapped().map(IpAddr::V4).unwrap_or(ip),
|
||||
v4 => v4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`true`] if the address appears to be globally routable.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
@@ -665,4 +680,25 @@ mod tests {
|
||||
.count()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unmap_ipv6_mapped_ipv4() {
|
||||
use std::net::IpAddr;
|
||||
let mapped: IpAddr = "::ffff:1.2.3.4".parse().unwrap();
|
||||
assert_eq!(super::unmap_ipv6(mapped), IpAddr::from([1, 2, 3, 4]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unmap_ipv6_pure_ipv6_unchanged() {
|
||||
use std::net::IpAddr;
|
||||
let v6: IpAddr = "::1".parse().unwrap();
|
||||
assert_eq!(super::unmap_ipv6(v6), v6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unmap_ipv6_plain_ipv4_unchanged() {
|
||||
use std::net::IpAddr;
|
||||
let v4: IpAddr = "1.2.3.4".parse().unwrap();
|
||||
assert_eq!(super::unmap_ipv6(v4), v4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +571,10 @@ impl TlsHostsSettings {
|
||||
TlsSettingsBuilder::new()
|
||||
}
|
||||
|
||||
pub fn get_main_hosts(&self) -> &[TlsHostInfo] {
|
||||
&self.main_hosts
|
||||
}
|
||||
|
||||
pub(crate) fn is_built(&self) -> bool {
|
||||
self.built
|
||||
}
|
||||
|
||||
@@ -317,11 +317,12 @@ fn print_setup_complete_summary(
|
||||
lib_settings_path, hosts_settings_path
|
||||
);
|
||||
println!();
|
||||
println!("2. Export client configuration (replace <username> and <public_ip>):");
|
||||
println!("2. Export client configuration (replace <username> and <address>):");
|
||||
println!(
|
||||
" ./trusttunnel_endpoint {} {} -c <username> -a <public_ip>:443",
|
||||
" ./trusttunnel_endpoint {} {} -c <username> -a <address>",
|
||||
lib_settings_path, hosts_settings_path
|
||||
);
|
||||
println!(" where <address> is ip, ip:port, domain, or domain:port");
|
||||
println!();
|
||||
println!("3. Use the exported config with:");
|
||||
println!(" • TrustTunnel CLI Client - Pass to setup_wizard --endpoint_config");
|
||||
|
||||
Reference in New Issue
Block a user