Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion tunnelto_server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::auth::SigKey;
use std::net::IpAddr;
use std::str::FromStr;
use uuid::Uuid;
use regex::Regex;

/// Global service configuration
pub struct Config {
Expand Down Expand Up @@ -63,6 +64,7 @@ impl Config {
.map(|app_name| format!("global.{}.internal", app_name))
.ok();

// SECURITY: Load API key but don't expose in logs/debug. Mark as sensitive.
let honeycomb_api_key = std::env::var("HONEYCOMB_API_KEY").ok();
let instance_id = std::env::var("FLY_ALLOC_ID").unwrap_or(Uuid::new_v4().to_string());
let blocked_ips = std::env::var("BLOCKED_IPS")
Expand All @@ -74,7 +76,12 @@ impl Config {
})
.unwrap_or(vec![]);

let tunnel_host = std::env::var("TUNNEL_HOST").unwrap_or("tunnelto.dev".to_string());
let tunnel_host_raw = std::env::var("TUNNEL_HOST").unwrap_or("tunnelto.dev".to_string());
let tunnel_host = validate_hostname(&tunnel_host_raw)
.unwrap_or_else(|e| {
tracing::warn!("Invalid TUNNEL_HOST value, using default: {}", e);
"tunnelto.dev".to_string()
});

Config {
allowed_hosts,
Expand All @@ -101,3 +108,20 @@ fn get_port(var: &'static str, default: u16) -> u16 {
default
}
}

/// Validate hostname to prevent injection attacks
fn validate_hostname(hostname: &str) -> Result<String, String> {
// Only allow alphanumeric, dots, and hyphens
let hostname_regex = Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$")
.expect("Failed to compile hostname regex");

if hostname.is_empty() || hostname.len() > 253 {
return Err("Hostname length must be between 1 and 253 characters".to_string());
}

if !hostname_regex.is_match(hostname) {
return Err("Hostname contains invalid characters".to_string());
}

Ok(hostname.to_string())
}
5 changes: 5 additions & 0 deletions tunnelto_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ async fn main() {
let listen_addr = format!("[::]:{}", CONFIG.remote_port);
info!("listening on: {}", &listen_addr);

// SECURITY: This tunnel server is designed to be deployed behind a reverse proxy
// (e.g., Caddy, Nginx, HAProxy) that handles TLS/HTTPS termination.
// Tunnel traffic MUST be encrypted end-to-end through TLS.
// Direct exposure of this port without a TLS-terminating reverse proxy is insecure.

// create our accept any server
let listener = TcpListener::bind(listen_addr)
.await
Expand Down
3 changes: 3 additions & 0 deletions tunnelto_server/src/network/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub struct HostQueryResponse {

fn handle_query(query: HostQuery) -> HostQueryResponse {
tracing::debug!(host=%query.host, "got query");
// SECURITY: Only return client_id for authenticated requests from authorized instances
// The host lookup is intentionally limited to internal gossip protocol only
// to prevent unauthorized users from discovering tunnel ownership
HostQueryResponse {
client_id: Connections::client_for_host(&query.host),
}
Expand Down