How to set the source IP address for forwarded requests when using the https_connect_proxy setting #558
-
I’m having a bit of trouble with setting up I’m trying to figure out how to specify the secondary IP address on the client as well. I’ve looked through the documentation, but I can’t seem to find a way to do it. I’m feeling a bit lost and would really appreciate any help you can provide. Thanks a bunch! |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 4 replies
-
How are you currently binding the client? Could you share your code—either the actual implementation or an abstracted example that doesn't expose any sensitive information? Without more context, the most likely approach you'll want to take is to define your own TCP connector. The simplest way to do this is by setting the stream connector factory using Within the connector factory, you would typically create a raw Starting from Rama 0.3, this process will become a bit easier, as you'll be able to provide a simple config struct directly to the stream connector factory. For now, however, you'll need to write a few extra lines of boilerplate, though it's still straightforward and doesn't require reinventing the wheel. Let me know if that makes sense or if you have any further questions—happy to help! If this is part of a commercial project, you're also welcome to become a sponsor, which includes basic support. Alternatively, you can hire us for a one-time or long-term service contract, depending on your needs. That said, for small questions like this, I'm more than happy to assist for free. Feel free to ask if you need further clarification or guidance. If anything is unclear or if I misunderstood your question, a minimal reproducible example would help a lot. You can share a simplified version of your code, or even the actual code privately via DM on Discord or email, if preferred. |
Beta Was this translation helpful? Give feedback.
-
Thanks a bunch for your detailed response! 🙏 I’m excited to try out your suggestions and see how far I can get. Even if I can’t make it work exactly as i wish it to be, any hints or snippets would be super helpful. If you could share some snippets to pass the source ip in the Here’s what I did in the following order:
use rama::{
Context, Layer, Service,
graceful::Shutdown,
http::{
Body, Request, Response, StatusCode,
client::EasyHttpWebClient,
layer::{proxy_auth::ProxyAuthLayer, trace::TraceLayer, upgrade::UpgradeLayer},
matcher::MethodMatcher,
server::HttpServer,
service::web::response::IntoResponse,
},
layer::ConsumeErrLayer,
net::{
http::RequestContext,
proxy::ProxyTarget,
stream::layer::http::BodyLimitLayer,
tls::{SecureTransport, server::SelfSignedData},
user::Basic,
},
rt::Executor,
service::service_fn,
tcp::{client::service::Forwarder, server::TcpListener},
};
#[cfg(feature = "boring")]
use rama::{
net::tls::{
ApplicationProtocol,
server::{ServerAuth, ServerConfig},
},
tls::boring::server::TlsAcceptorLayer,
};
#[cfg(all(feature = "rustls", not(feature = "boring")))]
use rama::tls::rustls::server::{TlsAcceptorDataBuilder, TlsAcceptorLayer};
use argh::FromArgs;
use std::convert::Infallible;
use std::time::Duration;
use tracing::metadata::LevelFilter;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
#[derive(FromArgs)]
/// Proxy Config
struct ProxyConfig {
/// ip address to bind, Default to 0.0.0.0
#[argh(option, default = "String::from(\"0.0.0.0\")", short = 'i')]
ip: String,
/// port to bind, Default to 62016
#[argh(option, default = "String::from(\"62016\")", short = 'p')]
port: String,
/// username for authentication, Default to test
#[argh(option, default = "String::from(\"test\")", short = 'u')]
user: String,
/// secret for authentication, Default to test
#[argh(option, default = "String::from(\"test\")", short = 's')]
secret: String,
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();
let config = argh::from_env::<ProxyConfig>();
let shutdown = Shutdown::default();
#[cfg(feature = "boring")]
let tls_service_data = {
let tls_server_config = ServerConfig {
application_layer_protocol_negotiation: Some(vec![
ApplicationProtocol::HTTP_2,
ApplicationProtocol::HTTP_11,
]),
..ServerConfig::new(ServerAuth::SelfSigned(SelfSignedData {
organisation_name: Some("Example Server Acceptor".to_owned()),
..Default::default()
}))
};
tls_server_config
.try_into()
.expect("create tls server config")
};
#[cfg(all(feature = "rustls", not(feature = "boring")))]
let tls_service_data = {
TlsAcceptorDataBuilder::new_self_signed(SelfSignedData {
organisation_name: Some("Example Server Acceptor".to_owned()),
..Default::default()
})
.expect("self signed acceptor data")
.with_alpn_protocols_http_auto()
.with_env_key_logger()
.expect("with env key logger")
.build()
};
let bind_addr = format!("{}:{}", config.ip, config.port);
let bind_failed_msg = &*format!("bind tcp proxy to {}", &bind_addr).leak();
// create tls proxy
shutdown.spawn_task_fn(async |guard| {
let tcp_service = TcpListener::build()
.bind(bind_addr)
.await
.expect(bind_failed_msg);
let exec = Executor::graceful(guard.clone());
let http_service = HttpServer::auto(exec.clone()).service(
(
TraceLayer::new_for_http(),
// See [`ProxyAuthLayer::with_labels`] for more information,
// e.g. can also be used to extract upstream proxy filter
ProxyAuthLayer::new(Basic::new(config.user, config.secret)),
UpgradeLayer::new(
MethodMatcher::CONNECT,
service_fn(http_connect_accept),
ConsumeErrLayer::default().into_layer(Forwarder::ctx()),
),
)
.into_layer(service_fn(http_plain_proxy)),
);
tcp_service
.serve_graceful(
guard,
(
// protect the http proxy from too large bodies, both from request and response end
BodyLimitLayer::symmetric(2 * 1024 * 1024),
TlsAcceptorLayer::new(tls_service_data).with_store_client_hello(true),
)
.into_layer(http_service),
)
.await;
});
shutdown
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}
async fn http_connect_accept<S>(
mut ctx: Context<S>,
req: Request,
) -> Result<(Response, Context<S>, Request), Response>
where
S: Clone + Send + Sync + 'static,
{
match ctx
.get_or_try_insert_with_ctx::<RequestContext, _>(|ctx| (ctx, &req).try_into())
.map(|ctx| ctx.authority.clone())
{
Ok(authority) => {
tracing::info!(%authority, "accept CONNECT (lazy): insert proxy target into context");
ctx.insert(ProxyTarget(authority));
}
Err(err) => {
tracing::error!(err = %err, "error extracting authority");
return Err(StatusCode::BAD_REQUEST.into_response());
}
}
tracing::info!(
"proxy secure transport ingress: {:?}",
ctx.get::<SecureTransport>()
);
Ok((StatusCode::OK.into_response(), ctx, req))
}
async fn http_plain_proxy<S>(ctx: Context<S>, req: Request) -> Result<Response, Infallible>
where
S: Clone + Send + Sync + 'static,
{
let client = EasyHttpWebClient::default();
let uri = req.uri().clone();
tracing::debug!(uri = %req.uri(), "proxy connect plain text request");
match client.serve(ctx, req).await {
Ok(resp) => Ok(resp),
Err(err) => {
tracing::error!(error = %err, uri = %uri, "error in client request");
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty())
.unwrap())
}
}
}
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether bc:24:11:9c:9c:8b brd ff:ff:ff:ff:ff:ff
altname enp0s18
inet 161.XXX.XXX.129/24 brd 161.XXX.XXX.255 scope global ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.221/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.222/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.223/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.224/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.225/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.226/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.248.162.227/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.228/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.229/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever
inet 161.XXX.XXX.230/24 brd 161.XXX.XXX.255 scope global secondary ens18
valid_lft forever preferred_lft forever #!/usr/bin/env bash
ptproxy -i 161.XXX.XXX.129 -p 443 -u Pt_FSzJsDScmgk -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.221 -p 443 -u Pt_KNaPo34XV4a -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.222 -p 443 -u Pt_BxD0NFMY1U9 -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.223 -p 443 -u Pt_L56muvxImjx -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.224 -p 443 -u Pt_492KxzhzYRk -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.225 -p 443 -u Pt_IgZSJozDVsz -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.226 -p 443 -u Pt_9lyLB06T8Gh -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.227 -p 443 -u Pt_jKoEsehutD0 -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.228 -p 443 -u Pt_LZG36gkYKXu -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.229 -p 443 -u Pt_LiPBiATm9tY -s redacted > /dev/null 2>&1 &
sleep 1
ptproxy -i 161.XXX.XXX.230 -p 443 -u Pt_6DJxObcnhxj -s redacted > /dev/null 2>&1 &
#!/usr/bin/env bash
curl --proxy-insecure -v -x "https://161.XXX.XXX.129" --proxy-user 'Pt_FSzJsDScmgk:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.221" --proxy-user 'Pt_KNaPo34XV4a:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.222" --proxy-user 'Pt_BxD0NFMY1U9:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.223" --proxy-user 'Pt_L56muvxImjx:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.224" --proxy-user 'Pt_492KxzhzYRk:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.225" --proxy-user 'Pt_IgZSJozDVsz:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.226" --proxy-user 'Pt_9lyLB06T8Gh:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.227" --proxy-user 'Pt_jKoEsehutD0:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.228" --proxy-user 'Pt_LZG36gkYKXu:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.229" --proxy-user 'Pt_LiPBiATm9tY:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --proxy-insecure -v -x "https://161.XXX.XXX.230" --proxy-user 'Pt_6DJxObcnhxj:redacted' "https://api.ipify.org" -H "User-Agent: Chrome/138.0.0.0"
echo "\n===============================================\n"
curl --interface 161.XXX.XXX.221 -v "https://api.ipify.org/" import requests, socket
from time import sleep
from requests_toolbelt.adapters import source
source_ips = ["161.XXX.XXX.129"] + [ f"161.XXX.XXX.{i}" for i in range(221,231) ]
target_url = "https://api.ipify.org"
timeout_seconds = 5
for source_ip in source_ips:
print(f"\nAttempting request from source IP: {source_ip}")
source_adapter = source.SourceAddressAdapter(source_ip)
with requests.Session() as session:
session.mount('http://', source_adapter)
session.mount('https://', source_adapter)
try:
response = session.get(target_url, timeout=timeout_seconds, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"})
print(f"Status Code: {response.status_code}")
print(f"Response: {response.text}")
except requests.exceptions.ConnectTimeout:
print(f" ERROR: Connection timed out for IP {source_ip} to {target_url}.")
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout_seconds)
s.bind((source_ip, 0)) # Bind to the source IP, let OS pick ephemeral port
target_host = 'httpbin.org'
target_port = 80
try:
target_resolved_ip = socket.gethostbyname(target_host)
s.connect((target_resolved_ip, target_port))
print(f" Socket test: Successfully bound to {ip} and connected to {target_host}:{target_port}.")
s.close()
except socket.error as sock_err:
print(f" Socket test: Failed to connect to {target_host}:{target_port} from {source_ip}. Error: {sock_err}")
except Exception as ex:
print(f" Socket test: An unexpected error during connection test: {ex}")
except socket.error as bind_err:
print(f" Socket test: Failed to bind to source IP {source_ip}. Error: {bind_err}")
except Exception as ex:
print(f" Socket test: An unexpected error during socket binding test: {ex}")
except requests.exceptions.ReadTimeout:
print(f" ERROR: Read timed out for IP {source_ip} from {target_url}.")
print(" This means the connection was established, but no data was received within the timeout.")
print(" Possible causes: Server slow/stuck, large response, network congestion.")
except requests.exceptions.HTTPError as e:
print(f" ERROR: HTTP Error for IP {source_ip} - {e}")
print(f" Response content: {e.response.text}")
except requests.exceptions.RequestException as e:
print(f" ERROR: An unexpected requests error occurred for IP {source_ip}: {e}")
except Exception as e:
print(f" ERROR: An unhandled exception occurred for IP {source_ip}: {e}")
else:
sleep(0.5)
|
Beta Was this translation helpful? Give feedback.
-
Instead of binding many proxies to each IP you can just bind to an interface:
Or if you need more options you can use socket options:
Note that this only is available on linux, android and the like. I assume this is fine give you use the example of rama on main branch as well? or at least For your egress traffic you will not be able to use Your custom client service will be a lot simpler than the `EasyHttpWebClient implementation, |
Beta Was this translation helpful? Give feedback.
-
I also made the inner tcp connector part easier. You can now create it directly from |
Beta Was this translation helpful? Give feedback.
Instead of binding many proxies to each IP you can just bind to an interface:
Or if you need more options you can use socket options:
Note that this only is available on linux, android and the like.
An…