Skip to content

Commit

Permalink
support for forwarded subdomains
Browse files Browse the repository at this point in the history
  • Loading branch information
OlofBlomqvist committed Feb 25, 2024
1 parent 480fd4f commit 43a5dfd
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "odd-box"
description = "dead simple reverse proxy server"
version = "0.0.9"
version = "0.0.10"
edition = "2021"
authors = ["Olof Blomqvist <olof@twnet.se>"]
repository = "https://github.com/OlofBlomqvist/odd-box"
Expand Down
11 changes: 8 additions & 3 deletions odd-box-example-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ log_level = "info" # trace,info,debug,info,warn,error
alpn = false # optional - allows alpn negotiation for http/1.0 and h2 on tls connections
port_range_start = 4200 # port range for automatic port assignment (the env var PORT will be set if you did not specify one manually for a process)
default_log_format = "standard" # standard | dotnet
ip = "0.0.0.0" # ip for proxy to listen to , can be ipv4/6
http_port = 80
tls_port = 443
ip = "127.0.0.1" # ip for proxy to listen to , can be ipv4/6
http_port = 80 # optional, 8080 by default
tls_port = 443 # optional, 4343 by default
auto_start = false # optional, defaults to true - used as default value for configured sites. auto_start false means a site will not start automatically with odd-box,
# but it will still be automatically started on incoming requests to that site.
env_vars = [
Expand All @@ -19,6 +19,8 @@ env_vars = [
host_name = "lobsters.localtest.me" # incoming name for binding to (frontend)
target_hostname = "lobste.rs" # domain name or ip to proxy the request to (backend)
capture_subdomains = false # optional, false by default: allows capturing wildcard requests such as test.lobsters.local
forward_subdomains = false # optional, false by default: if the request is for subdomain.configureddomain.local with target example.com,
# this option would cause the proxied request go to subdomain.example.com instead of example.com.
disable_tcp_tunnel_mode = false # optional, false by default
https = true # optional, false by default: must be true if the target uses tls
port = 443 # optional - 80 by default if https is false, 443 by default if https is true
Expand All @@ -34,6 +36,9 @@ log_format = "standard" # standard | dotnet
auto_start = false # optional, uses global auto_start by default. set to false to prevent the process from starting when launching odd-box
port = 443 # optional, defaults to 443 for https configurations and 80 otherwise
https = true # must be set to https if the target expects tls connections
capture_subdomains = false # optional, false by default: allows capturing wildcard requests such as test.lobsters.local
forward_subdomains = false # optional, false by default: if the request is for subdomain.configureddomain.local with target example.com,
# this option would cause the proxied request go to subdomain.example.com instead of example.com.
env_vars = [
# environment variables specific to this process
{ key = "logserver", value = "http://www.example.com" },
Expand Down
1 change: 1 addition & 0 deletions src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl TryFrom<legacy::Config> for v1::OddBoxConfig {
port_range_start: old_config.port_range_start,
hosted_process: Some(old_config.processes.into_iter().map(|x|{
v1::InProcessSiteConfig {
forward_subdomains: None,
disable_tcp_tunnel_mode: x.disable_tcp_tunnel_mode,
args: x.args,
auto_start: x.auto_start,
Expand Down
17 changes: 15 additions & 2 deletions src/configuration/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ pub (crate) struct InProcessSiteConfig{
pub port: Option<u16>,
pub https : Option<bool>,
/// If you wish to use wildcard routing for any subdomain under the 'host_name'
pub capture_subdomains : Option<bool>
pub capture_subdomains : Option<bool>,
/// If you wish to use the subdomain from the request in forwarded requests:
/// test.example.com -> internal.site
/// vs
/// test.example.com -> test.internal.site
pub forward_subdomains : Option<bool>
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -53,7 +58,12 @@ pub (crate) struct RemoteSiteConfig{
/// If you wish to use wildcard routing for any subdomain under the 'host_name'
pub capture_subdomains : Option<bool>,
/// This is mostly useful in case the target uses SNI sniffing/routing
pub disable_tcp_tunnel_mode : Option<bool>
pub disable_tcp_tunnel_mode : Option<bool>,
/// If you wish to use the subdomain from the request in forwarded requests:
/// test.example.com -> internal.site
/// vs
/// test.example.com -> test.internal.site
pub forward_subdomains : Option<bool>
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -378,6 +388,7 @@ pub fn example_v1() -> OddBoxConfig {
port_range_start: 4200,
hosted_process: Some(vec![
InProcessSiteConfig {
forward_subdomains: None,
disable_tcp_tunnel_mode: Some(false),
args: vec!["--test".to_string()],
auto_start: Some(true),
Expand All @@ -398,6 +409,7 @@ pub fn example_v1() -> OddBoxConfig {
]),
remote_target: Some(vec![
RemoteSiteConfig {
forward_subdomains: None,
h2_hint: None,
host_name: "lobsters.local".into(),
target_hostname: "lobste.rs".into(),
Expand All @@ -407,6 +419,7 @@ pub fn example_v1() -> OddBoxConfig {
disable_tcp_tunnel_mode: Some(false)
},
RemoteSiteConfig {
forward_subdomains: Some(true),
h2_hint: None,
host_name: "google.local".into(),
target_hostname: "google.com".into(),
Expand Down
52 changes: 46 additions & 6 deletions src/http_proxy/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,33 @@ async fn handle(

let default_port = if enforce_https { 443 } else { 80 };

let resolved_host_name =

if target_cfg.forward_subdomains.unwrap_or_default() && let Some(subdomain) = get_subdomain(&req_host_name, &target_cfg.host_name) {
tracing::debug!("in-proc forward terminating proxy rewrote subdomain: {subdomain}!");
format!("{subdomain}.{}",&target_cfg.host_name)
} else {
target_cfg.host_name.clone()
};


// TODO - also support this mode for tcp tunnelling and remote sites ?
// need to be opt-in so that we either
// direct *.blah.com -> mysite.com
// or *.blah.com -> *.mysite.com (would obviously not work if target is ip?)

tracing::info!("USING THIS RESOLVED TARGET: {resolved_host_name}");
let target_url = format!("{scheme}://{}:{}{}",
target_cfg.host_name,
resolved_host_name,
target_cfg.port.unwrap_or(default_port),
original_path_and_query
);


let target = crate::http_proxy::Target::Proc(target_cfg.clone());

let result =
proxy(is_https,state.clone(),req,&target_url,target,client_ip).await;
proxy(&req_host_name,is_https,state.clone(),req,&target_url,target,client_ip).await;

map_result(&req_host_name,result).await
}
Expand All @@ -238,8 +254,8 @@ async fn handle(
req_host_name == p.host_name
|| p.capture_subdomains.unwrap_or_default() && req_host_name.ends_with(&format!(".{}",p.host_name))
}) {
return perform_remote_forwarding(is_https,state.clone(),client_ip,remote_target_cfg,req).await

return perform_remote_forwarding(req_host_name,is_https,state.clone(),client_ip,remote_target_cfg,req).await
}

tracing::warn!("Received request that does not match any known target: {:?}", req_host_name);
Expand All @@ -252,8 +268,20 @@ async fn handle(

}

fn get_subdomain(requested_hostname: &str, backend_hostname: &str) -> Option<String> {
if requested_hostname == backend_hostname { return None };
if requested_hostname.to_uppercase().ends_with(&backend_hostname.to_uppercase()) {
let part_to_remove_len = backend_hostname.len();
let start_index = requested_hostname.len() - part_to_remove_len;
if start_index == 0 || requested_hostname.as_bytes()[start_index - 1] == b'.' {
return Some(requested_hostname[..start_index].trim_end_matches('.').to_string());
}
}
None
}

async fn perform_remote_forwarding(
req_host_name:String,
is_https:bool,
state:Arc<tokio::sync::RwLock<crate::AppState>>,
client_ip:std::net::SocketAddr,
Expand All @@ -273,15 +301,27 @@ async fn perform_remote_forwarding(

let default_port = if enforce_https { 443 } else { 80 };


let resolved_host_name =

if remote_target_config.forward_subdomains.unwrap_or_default() && let Some(subdomain) = get_subdomain(&req_host_name, &remote_target_config.host_name) {
tracing::debug!("remote forward terminating proxy rewrote subdomain: {subdomain}!");
format!("{subdomain}.{}",&remote_target_config.target_hostname)
} else {
remote_target_config.target_hostname.clone()
};


let target_url = format!("{scheme}://{}:{}{}",
remote_target_config.target_hostname,
resolved_host_name,
remote_target_config.port.unwrap_or(default_port),
original_path_and_query
);

tracing::info!("Incoming request to '{}' for remote proxy target {target_url}",remote_target_config.host_name);
let result =
proxy(
&req_host_name,
is_https,
state.clone(),
req,
Expand Down
1 change: 1 addition & 0 deletions src/http_proxy/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub enum Target {
}

pub async fn proxy(
_req_host_name: &str,
is_https:bool,
state: std::sync::Arc<tokio::sync::RwLock<crate::AppState>>,
mut req: hyper::Request<hyper::body::Incoming>,
Expand Down
47 changes: 37 additions & 10 deletions src/proxy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use socket2::Socket;
use tokio::net::TcpSocket;
use tokio::net::TcpStream;
Expand All @@ -17,6 +18,7 @@ use crate::tcp_proxy::DataType;
use crate::tcp_proxy::PeekResult;
use crate::tcp_proxy::ReverseTcpProxyTarget;
use crate::types::app_state;
use crate::types::app_state::ProcState;

pub async fn listen(
cfg: ConfigWrapper,
Expand All @@ -39,17 +41,19 @@ pub async fn listen(
target_http_port = y.port;
}
tcp_targets.push(ReverseTcpProxyTarget {
target_hostname: y.host_name.clone(),
capture_subdomains: y.capture_subdomains.unwrap_or_default(),
forward_wildcard: y.forward_subdomains.unwrap_or_default(),
target_http_port,
target_tls_port,
target_hostname: y.host_name.to_owned(),
host_name: y.host_name.to_owned(),
is_hosted: true
is_hosted: true,
sub_domain: None
})
}
}

if let Some(x) = &cfg.0.remote_target {
// dont add non_tcp_enabled_targets_to_the_list
for y in x.iter().filter(|xx|xx.disable_tcp_tunnel_mode.unwrap_or_default() == false) {
let mut target_http_port = None;
let mut target_tls_port = None;
Expand All @@ -59,11 +63,14 @@ pub async fn listen(
target_http_port = y.port;
}
tcp_targets.push(ReverseTcpProxyTarget {
capture_subdomains: y.capture_subdomains.unwrap_or_default(),
forward_wildcard: y.forward_subdomains.unwrap_or_default(),
target_hostname: y.target_hostname.to_owned(),
target_http_port,
target_tls_port,
host_name: y.host_name.to_owned(),
is_hosted: false
is_hosted: false,
sub_domain: None
})
}
}
Expand Down Expand Up @@ -282,10 +289,6 @@ async fn handle_new_tcp_stream(

let targets = targets.clone();

// NOTE: toggling this to true will make all tls requests go thru the terminating proxy.
// it seems google does not like our tcp tunnel, gotta find out why.
let prevent_tcp = false;

// OK SO IT TURNS OUT GOOGLE USES SNI ROUTING, AND SINCE WE ARRIVE WITH THE GOOGLE.LOCAL SNI, IT WONT WORK.
// THIS MEANS WE MUST ALLOW CONFIGURING TCP_MODE=ALWAYS|NEVER|ALLOW

Expand All @@ -303,6 +306,7 @@ async fn handle_new_tcp_stream(


if target.is_hosted {

// we rely on terminating proxy do trigger this instead of doing it here
//_ = tx.send(ProcMessage::Start(target.host_name.clone()));

Expand All @@ -318,7 +322,30 @@ async fn handle_new_tcp_stream(
tracing::warn!("error 0001 has occurred")
},
Some(app_state::ProcState::Stopped) => {
tracing::debug!("target process is stopped, re-routing to terminating proxy so that user sees 'please wait' text")
_ = tx.send(ProcMessage::Start(target.host_name.clone()));
let thn = target.host_name.clone();
let mut has_started = false;
for _ in 0..5 {
tokio::time::sleep(Duration::from_secs(2)).await;
tracing::debug!("handling an incoming request to a stopped target, waiting for up to 10 seconds for {thn} to spin up - after this we will release the request to the terminating proxy and show a 'please wait' page instaead.");
{
let guard = state.read().await;
match guard.procs.get(&target.host_name) {
Some(&ProcState::Running) => {
has_started = true;
break
},
_ => { }
}
}
}
if has_started {
tracing::trace!("Using unencrypted tcp tunnel for remote target: {target:?}");
tcp_proxy::ReverseTcpProxy::tunnel(tcp_stream, target, false,state.clone(),source_addr).await;
return;
} else {
tracing::trace!("{thn} is still not running... handing this request over to the terminating proxy.")
}
}
,_=> {
tracing::trace!("Using unencrypted tcp tunnel for remote target: {target:?}");
Expand All @@ -344,7 +371,7 @@ async fn handle_new_tcp_stream(
typ: DataType::TLS,
http_version:_,
target_host: Some(target)
}) if expect_tls && !prevent_tcp => {
}) if expect_tls => {
if let Some(target) = tcp_proxy::ReverseTcpProxy::try_get_target_from_vec(targets, &target) {

if target.target_tls_port.is_some() {
Expand Down

0 comments on commit 43a5dfd

Please sign in to comment.