mirror of https://github.com/ospab/ostp.git
feat(server): multi-address egress — global source IP plus per-rule send_from
A server with two IPs (one burned by Google, one fresh) needs to control which address traffic leaves from, per destination — direct from the clean IP to a picky site, via the proxy from the other for everything else. - Receive: already selectable via the listen addresses. - Send (global): server config `bind_ip` binds every outbound socket to a chosen source address (threaded through run_server → Router → the direct/proxy/UDP connect paths, which already supported a bind_ip). - Send (per rule): outbound rules gain `send_from` — a source IP that overrides the global bind_ip when that rule matches. select_outbound_action now returns the matched rule's source alongside its action, and both the direct and proxy connect paths (TCP and the SOCKS5 UDP path) bind to rule.send_from, falling back to the global bind_ip. So "direct from 1.2.3.4 to youtube, proxy from 5.6.7.8 otherwise" is expressible. New fields default (send_from: None), so existing configs are unaffected and the migrator backfills them into canonical form. Fixed two pre-existing test call sites that predated the bind_ip parameter.
This commit is contained in:
parent
4da4f9c1a5
commit
245e79215c
|
|
@ -437,6 +437,7 @@ impl UserConfig {
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
pub listen: ListenConfig,
|
pub listen: ListenConfig,
|
||||||
|
pub bind_ip: Option<String>,
|
||||||
pub access_keys: Vec<UserConfig>,
|
pub access_keys: Vec<UserConfig>,
|
||||||
pub debug: Option<bool>,
|
pub debug: Option<bool>,
|
||||||
pub outbound: Option<OutboundConfig>,
|
pub outbound: Option<OutboundConfig>,
|
||||||
|
|
@ -570,6 +571,10 @@ pub struct OutboundRule {
|
||||||
pub ip_cidr: Option<Vec<String>>,
|
pub ip_cidr: Option<Vec<String>>,
|
||||||
pub protocol: Option<String>,
|
pub protocol: Option<String>,
|
||||||
pub action: Option<String>,
|
pub action: Option<String>,
|
||||||
|
/// Local source IP to egress from when this rule matches (overrides the
|
||||||
|
/// server's global `bind_ip` for this rule). Lets one destination leave via
|
||||||
|
/// one address and another via a different one.
|
||||||
|
pub send_from: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
|
|
||||||
|
|
@ -878,7 +878,7 @@ mod tests {
|
||||||
config_path: None,
|
config_path: None,
|
||||||
dns_server: crate::dns::DnsServer::new(Default::default()),
|
dns_server: crate::dns::DnsServer::new(Default::default()),
|
||||||
audit_logs: Arc::new(RwLock::new(Vec::new())),
|
audit_logs: Arc::new(RwLock::new(Vec::new())),
|
||||||
router: Arc::new(crate::router::Router::new(None, crate::dns::DnsServer::new(Default::default()), false)),
|
router: Arc::new(crate::router::Router::new(None, None, crate::dns::DnsServer::new(Default::default()), false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ pub(crate) struct RemoteState {
|
||||||
pub async fn run_server(
|
pub async fn run_server(
|
||||||
bind_addrs: Vec<String>,
|
bind_addrs: Vec<String>,
|
||||||
server_public_ip: Option<String>,
|
server_public_ip: Option<String>,
|
||||||
|
bind_ip: Option<String>,
|
||||||
access_keys: Vec<(String, crate::api::UserMeta)>,
|
access_keys: Vec<(String, crate::api::UserMeta)>,
|
||||||
outbound: Option<OutboundConfig>,
|
outbound: Option<OutboundConfig>,
|
||||||
api_config: Option<ApiConfig>,
|
api_config: Option<ApiConfig>,
|
||||||
|
|
@ -255,6 +256,7 @@ pub async fn run_server(
|
||||||
// Initialize Router
|
// Initialize Router
|
||||||
let router = std::sync::Arc::new(router::Router::new(
|
let router = std::sync::Arc::new(router::Router::new(
|
||||||
outbound.clone(),
|
outbound.clone(),
|
||||||
|
bind_ip,
|
||||||
dns_server.clone(),
|
dns_server.clone(),
|
||||||
debug,
|
debug,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@ pub struct OutboundRule {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub protocol: Option<String>,
|
pub protocol: Option<String>,
|
||||||
pub action: OutboundAction,
|
pub action: OutboundAction,
|
||||||
|
/// Local source IP to egress from when this rule matches. Overrides the
|
||||||
|
/// server's global `bind_ip` for this rule only, so different destinations
|
||||||
|
/// can leave the machine from different addresses — e.g. one clean IP
|
||||||
|
/// direct to a picky site, another via the proxy for everything else. None
|
||||||
|
/// falls back to the global `bind_ip`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub send_from: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -41,12 +48,15 @@ pub struct OutboundConfig {
|
||||||
pub async fn connect_target(
|
pub async fn connect_target(
|
||||||
target: &str,
|
target: &str,
|
||||||
outbound: Option<&OutboundConfig>,
|
outbound: Option<&OutboundConfig>,
|
||||||
|
bind_ip: Option<&str>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<TcpStream> {
|
) -> Result<TcpStream> {
|
||||||
let connect_timeout = Duration::from_secs(10);
|
let connect_timeout = Duration::from_secs(10);
|
||||||
if let Some(outbound) = outbound {
|
if let Some(outbound) = outbound {
|
||||||
if outbound.enabled {
|
if outbound.enabled {
|
||||||
let action = select_outbound_action(target, "tcp", outbound, debug).await;
|
let (action, rule_src) = select_outbound_action(target, "tcp", outbound, debug).await;
|
||||||
|
// Per-rule source wins; otherwise the server's global bind_ip.
|
||||||
|
let eff_bind = rule_src.as_deref().or(bind_ip);
|
||||||
if action == OutboundAction::Block {
|
if action == OutboundAction::Block {
|
||||||
return Err(anyhow::anyhow!("blocked by outbound rule: {}", target));
|
return Err(anyhow::anyhow!("blocked by outbound rule: {}", target));
|
||||||
}
|
}
|
||||||
|
|
@ -55,8 +65,8 @@ pub async fn connect_target(
|
||||||
// Case-insensitive: a config saying "SOCKS5" means the same thing
|
// Case-insensitive: a config saying "SOCKS5" means the same thing
|
||||||
// as "socks5", and silently treating it as unknown is a trap.
|
// as "socks5", and silently treating it as unknown is a trap.
|
||||||
return match outbound.protocol.to_ascii_lowercase().as_str() {
|
return match outbound.protocol.to_ascii_lowercase().as_str() {
|
||||||
"socks5" => connect_via_socks5(&proxy_addr, target, &outbound.username, &outbound.password).await,
|
"socks5" => connect_via_socks5(&proxy_addr, target, eff_bind, &outbound.username, &outbound.password).await,
|
||||||
"http" => connect_via_http(&proxy_addr, target).await,
|
"http" => connect_via_http(&proxy_addr, target, eff_bind).await,
|
||||||
// FAIL CLOSED. This used to fall through to a direct
|
// FAIL CLOSED. This used to fall through to a direct
|
||||||
// connection, so any unrecognised protocol string — a typo,
|
// connection, so any unrecognised protocol string — a typo,
|
||||||
// a case difference, an empty value — silently sent ALL TCP
|
// a case difference, an empty value — silently sent ALL TCP
|
||||||
|
|
@ -71,10 +81,14 @@ pub async fn connect_target(
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// action == Direct: egress directly, but still honour this rule's
|
||||||
|
// send_from (falling back to the global bind_ip) — that is the whole
|
||||||
|
// point of "direct from THIS ip to that destination".
|
||||||
|
return connect_direct(target, connect_timeout, eff_bind).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connect_direct(target, connect_timeout).await
|
connect_direct(target, connect_timeout, bind_ip).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-candidate-address connect attempt, tried in turn (see `connect_direct`
|
/// Per-candidate-address connect attempt, tried in turn (see `connect_direct`
|
||||||
|
|
@ -96,7 +110,23 @@ const PER_ADDR_CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
|
||||||
/// attempted: every dual-stack destination (i.e. most popular sites) never
|
/// attempted: every dual-stack destination (i.e. most popular sites) never
|
||||||
/// loads, while IPv4-only destinations work fine - exactly the "traffic
|
/// loads, while IPv4-only destinations work fine - exactly the "traffic
|
||||||
/// counter moves but sites don't open" symptom this fixes.
|
/// counter moves but sites don't open" symptom this fixes.
|
||||||
async fn connect_direct(target: &str, connect_timeout: Duration) -> Result<TcpStream> {
|
async fn connect_tcp_with_bind(addr: std::net::SocketAddr, bind_ip: Option<&str>) -> Result<TcpStream> {
|
||||||
|
if let Some(ip_str) = bind_ip {
|
||||||
|
let ip: std::net::IpAddr = ip_str.parse().map_err(|e| anyhow::anyhow!("invalid bind_ip: {}", e))?;
|
||||||
|
let socket = match addr {
|
||||||
|
std::net::SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?,
|
||||||
|
std::net::SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?,
|
||||||
|
};
|
||||||
|
if (addr.is_ipv4() && ip.is_ipv4()) || (addr.is_ipv6() && ip.is_ipv6()) {
|
||||||
|
socket.bind(std::net::SocketAddr::new(ip, 0))?;
|
||||||
|
}
|
||||||
|
Ok(socket.connect(addr).await?)
|
||||||
|
} else {
|
||||||
|
Ok(TcpStream::connect(addr).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_direct(target: &str, connect_timeout: Duration, bind_ip: Option<&str>) -> Result<TcpStream> {
|
||||||
tokio::time::timeout(connect_timeout, async {
|
tokio::time::timeout(connect_timeout, async {
|
||||||
let mut addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host(target)
|
let mut addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host(target)
|
||||||
.await
|
.await
|
||||||
|
|
@ -109,7 +139,7 @@ async fn connect_direct(target: &str, connect_timeout: Duration) -> Result<TcpSt
|
||||||
|
|
||||||
let mut last_err = None;
|
let mut last_err = None;
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
match tokio::time::timeout(PER_ADDR_CONNECT_TIMEOUT, TcpStream::connect(addr)).await {
|
match tokio::time::timeout(PER_ADDR_CONNECT_TIMEOUT, connect_tcp_with_bind(addr, bind_ip)).await {
|
||||||
Ok(Ok(stream)) => return Ok(stream),
|
Ok(Ok(stream)) => return Ok(stream),
|
||||||
Ok(Err(e)) => last_err = Some(anyhow::anyhow!("{}: {}", addr, e)),
|
Ok(Err(e)) => last_err = Some(anyhow::anyhow!("{}: {}", addr, e)),
|
||||||
Err(_) => last_err = Some(anyhow::anyhow!("{}: connect timeout ({}s)", addr, PER_ADDR_CONNECT_TIMEOUT.as_secs())),
|
Err(_) => last_err = Some(anyhow::anyhow!("{}: connect timeout ({}s)", addr, PER_ADDR_CONNECT_TIMEOUT.as_secs())),
|
||||||
|
|
@ -134,39 +164,35 @@ pub async fn select_outbound_action(
|
||||||
protocol: &str,
|
protocol: &str,
|
||||||
outbound: &OutboundConfig,
|
outbound: &OutboundConfig,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> OutboundAction {
|
) -> (OutboundAction, Option<String>) {
|
||||||
let (host, port) = match split_host_port(target) {
|
let (host, port) = match split_host_port(target) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return outbound.default_action,
|
None => return (outbound.default_action, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut matched = None;
|
// Capture the matched rule's action AND its per-rule source IP together, so
|
||||||
|
// the caller egresses from the address that rule asked for.
|
||||||
|
let mut matched: Option<(OutboundAction, Option<String>)> = None;
|
||||||
for rule in &outbound.rules {
|
for rule in &outbound.rules {
|
||||||
if let Some(ref rule_proto) = rule.protocol {
|
if let Some(ref rule_proto) = rule.protocol {
|
||||||
if !rule_proto.is_empty() && rule_proto.to_lowercase() != protocol {
|
if !rule_proto.is_empty() && rule_proto.to_lowercase() != protocol {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rule.domain_suffix.is_empty() && rule.ip_cidr.is_empty() {
|
let hit = (rule.domain_suffix.is_empty() && rule.ip_cidr.is_empty())
|
||||||
// Protocol-only rule match
|
|| match_domain_rule(&host, &rule.domain_suffix)
|
||||||
matched = Some(rule.action);
|
|| match_ip_rule(&host, port, &rule.ip_cidr).await;
|
||||||
break;
|
if hit {
|
||||||
}
|
matched = Some((rule.action, rule.send_from.clone()));
|
||||||
if match_domain_rule(&host, &rule.domain_suffix) {
|
|
||||||
matched = Some(rule.action);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if match_ip_rule(&host, port, &rule.ip_cidr).await {
|
|
||||||
matched = Some(rule.action);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = matched.unwrap_or(outbound.default_action);
|
let (action, send_from) = matched.unwrap_or((outbound.default_action, None));
|
||||||
if debug {
|
if debug {
|
||||||
tracing::debug!("Outbound routing: target={target} action={action:?}");
|
tracing::debug!("Outbound routing: target={target} action={action:?} send_from={send_from:?}");
|
||||||
}
|
}
|
||||||
action
|
(action, send_from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_domain_rule(host: &str, suffixes: &[String]) -> bool {
|
fn match_domain_rule(host: &str, suffixes: &[String]) -> bool {
|
||||||
|
|
@ -250,12 +276,18 @@ where
|
||||||
async fn connect_via_socks5(
|
async fn connect_via_socks5(
|
||||||
proxy_addr: &str,
|
proxy_addr: &str,
|
||||||
target: &str,
|
target: &str,
|
||||||
|
bind_ip: Option<&str>,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<TcpStream> {
|
) -> Result<TcpStream> {
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
let mut stream = TcpStream::connect(proxy_addr).await?;
|
let addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host(proxy_addr).await?.collect();
|
||||||
|
let mut stream = if let Some(addr) = addrs.into_iter().next() {
|
||||||
|
connect_tcp_with_bind(addr, bind_ip).await?
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("could not resolve proxy address");
|
||||||
|
};
|
||||||
socks5_negotiate_auth(&mut stream, username, password).await?;
|
socks5_negotiate_auth(&mut stream, username, password).await?;
|
||||||
|
|
||||||
let (host, port) = split_host_port(target).ok_or_else(|| anyhow::anyhow!("invalid target"))?;
|
let (host, port) = split_host_port(target).ok_or_else(|| anyhow::anyhow!("invalid target"))?;
|
||||||
|
|
@ -304,10 +336,15 @@ async fn connect_via_socks5(
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect_via_http(proxy_addr: &str, target: &str) -> Result<TcpStream> {
|
async fn connect_via_http(proxy_addr: &str, target: &str, bind_ip: Option<&str>) -> Result<TcpStream> {
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
let mut stream = TcpStream::connect(proxy_addr).await?;
|
let addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host(proxy_addr).await?.collect();
|
||||||
|
let mut stream = if let Some(addr) = addrs.into_iter().next() {
|
||||||
|
connect_tcp_with_bind(addr, bind_ip).await?
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("could not resolve proxy address");
|
||||||
|
};
|
||||||
let request = format!("CONNECT {target} HTTP/1.1\r\nHost: {target}\r\n\r\n");
|
let request = format!("CONNECT {target} HTTP/1.1\r\nHost: {target}\r\n\r\n");
|
||||||
stream.write_all(request.as_bytes()).await?;
|
stream.write_all(request.as_bytes()).await?;
|
||||||
|
|
||||||
|
|
@ -426,19 +463,21 @@ impl UdpProxySocket {
|
||||||
pub async fn connect_udp_target(
|
pub async fn connect_udp_target(
|
||||||
target: &str,
|
target: &str,
|
||||||
outbound: Option<&OutboundConfig>,
|
outbound: Option<&OutboundConfig>,
|
||||||
|
bind_ip: Option<&str>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
server_udp: std::sync::Arc<tokio::net::UdpSocket>,
|
server_udp: std::sync::Arc<tokio::net::UdpSocket>,
|
||||||
) -> Result<UdpProxySocket> {
|
) -> Result<UdpProxySocket> {
|
||||||
if let Some(outbound) = outbound {
|
if let Some(outbound) = outbound {
|
||||||
if outbound.enabled {
|
if outbound.enabled {
|
||||||
let action = select_outbound_action(target, "udp", outbound, debug).await;
|
let (action, rule_src) = select_outbound_action(target, "udp", outbound, debug).await;
|
||||||
|
let eff_bind = rule_src.as_deref().or(bind_ip);
|
||||||
if action == OutboundAction::Block {
|
if action == OutboundAction::Block {
|
||||||
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
||||||
}
|
}
|
||||||
if action == OutboundAction::Proxy {
|
if action == OutboundAction::Proxy {
|
||||||
let proxy_addr = format!("{}:{}", outbound.address, outbound.port);
|
let proxy_addr = format!("{}:{}", outbound.address, outbound.port);
|
||||||
if outbound.protocol.eq_ignore_ascii_case("socks5") {
|
if outbound.protocol.eq_ignore_ascii_case("socks5") {
|
||||||
return connect_udp_via_socks5(&proxy_addr, server_udp, &outbound.username, &outbound.password).await;
|
return connect_udp_via_socks5(&proxy_addr, server_udp, eff_bind, &outbound.username, &outbound.password).await;
|
||||||
}
|
}
|
||||||
// FAIL CLOSED. HTTP CONNECT genuinely cannot carry UDP — but the
|
// FAIL CLOSED. HTTP CONNECT genuinely cannot carry UDP — but the
|
||||||
// answer to that is not to send the datagrams in the clear. The
|
// answer to that is not to send the datagrams in the clear. The
|
||||||
|
|
@ -462,12 +501,18 @@ pub async fn connect_udp_target(
|
||||||
pub async fn connect_udp_via_socks5(
|
pub async fn connect_udp_via_socks5(
|
||||||
proxy_addr: &str,
|
proxy_addr: &str,
|
||||||
server_udp: std::sync::Arc<tokio::net::UdpSocket>,
|
server_udp: std::sync::Arc<tokio::net::UdpSocket>,
|
||||||
|
bind_ip: Option<&str>,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<UdpProxySocket> {
|
) -> Result<UdpProxySocket> {
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
let mut stream = TcpStream::connect(proxy_addr).await?;
|
let addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host(proxy_addr).await?.collect();
|
||||||
|
let mut stream = if let Some(addr) = addrs.into_iter().next() {
|
||||||
|
connect_tcp_with_bind(addr, bind_ip).await?
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("could not resolve proxy address");
|
||||||
|
};
|
||||||
socks5_negotiate_auth(&mut stream, username, password).await?;
|
socks5_negotiate_auth(&mut stream, username, password).await?;
|
||||||
|
|
||||||
// Send UDP Associate request
|
// Send UDP Associate request
|
||||||
|
|
@ -688,7 +733,7 @@ mod tests {
|
||||||
let _ = listener.accept().await;
|
let _ = listener.accept().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = connect_direct(&addr.to_string(), Duration::from_secs(2)).await;
|
let result = connect_direct(&addr.to_string(), Duration::from_secs(2), None).await;
|
||||||
assert!(result.is_ok(), "expected connect_direct to reach a live local listener: {:?}", result.err());
|
assert!(result.is_ok(), "expected connect_direct to reach a live local listener: {:?}", result.err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -701,7 +746,7 @@ mod tests {
|
||||||
drop(listener);
|
drop(listener);
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let result = connect_direct(&addr.to_string(), Duration::from_secs(5)).await;
|
let result = connect_direct(&addr.to_string(), Duration::from_secs(5), None).await;
|
||||||
assert!(result.is_err(), "connecting to a closed port should fail");
|
assert!(result.is_err(), "connecting to a closed port should fail");
|
||||||
assert!(start.elapsed() < Duration::from_secs(4), "a refused connection must not wait out the full timeout");
|
assert!(start.elapsed() < Duration::from_secs(4), "a refused connection must not wait out the full timeout");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,10 +155,16 @@ pub async fn handle_relay_message(
|
||||||
if router.debug {
|
if router.debug {
|
||||||
let _ = ui_event_tx.send(UiEvent::Log(format!("Relay UDP ASSOCIATE stream_id={stream_id}")));
|
let _ = ui_event_tx.send(UiEvent::Log(format!("Relay UDP ASSOCIATE stream_id={stream_id}")));
|
||||||
}
|
}
|
||||||
let udp_bind_result = match UdpSocket::bind("[::]:0").await {
|
|
||||||
Ok(s) => Ok(s),
|
let udp_bind_result = if let Some(ref bind_ip) = router.bind_ip {
|
||||||
Err(_) => UdpSocket::bind("0.0.0.0:0").await,
|
tokio::net::UdpSocket::bind(format!("{}:0", bind_ip)).await
|
||||||
|
} else {
|
||||||
|
match tokio::net::UdpSocket::bind("[::]:0").await {
|
||||||
|
Ok(s) => Ok(s),
|
||||||
|
Err(_) => tokio::net::UdpSocket::bind("0.0.0.0:0").await,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let server_udp = match udp_bind_result {
|
let server_udp = match udp_bind_result {
|
||||||
Ok(s) => std::sync::Arc::new(s),
|
Ok(s) => std::sync::Arc::new(s),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,16 @@ use crate::dns::DnsServer;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Router {
|
pub struct Router {
|
||||||
pub outbound_cfg: Arc<RwLock<Option<OutboundConfig>>>,
|
pub outbound_cfg: Arc<RwLock<Option<OutboundConfig>>>,
|
||||||
|
pub bind_ip: Option<String>,
|
||||||
pub dns_server: Arc<DnsServer>,
|
pub dns_server: Arc<DnsServer>,
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Router {
|
impl Router {
|
||||||
pub fn new(outbound_cfg: Option<OutboundConfig>, dns_server: Arc<DnsServer>, debug: bool) -> Self {
|
pub fn new(outbound_cfg: Option<OutboundConfig>, bind_ip: Option<String>, dns_server: Arc<DnsServer>, debug: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
outbound_cfg: Arc::new(RwLock::new(outbound_cfg)),
|
outbound_cfg: Arc::new(RwLock::new(outbound_cfg)),
|
||||||
|
bind_ip,
|
||||||
dns_server,
|
dns_server,
|
||||||
debug,
|
debug,
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +28,7 @@ impl Router {
|
||||||
let lock = self.outbound_cfg.read().unwrap();
|
let lock = self.outbound_cfg.read().unwrap();
|
||||||
lock.clone()
|
lock.clone()
|
||||||
};
|
};
|
||||||
connect_target(target, cfg.as_ref(), self.debug).await
|
connect_target(target, cfg.as_ref(), self.bind_ip.as_deref(), self.debug).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UDP Target Routing
|
/// UDP Target Routing
|
||||||
|
|
@ -35,7 +37,7 @@ impl Router {
|
||||||
let lock = self.outbound_cfg.read().unwrap();
|
let lock = self.outbound_cfg.read().unwrap();
|
||||||
lock.clone()
|
lock.clone()
|
||||||
};
|
};
|
||||||
crate::outbound::connect_udp_target(target, cfg.as_ref(), self.debug, server_udp).await
|
crate::outbound::connect_udp_target(target, cfg.as_ref(), self.bind_ip.as_deref(), self.debug, server_udp).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establish a UDP session router that can dynamically route packets
|
/// Establish a UDP session router that can dynamically route packets
|
||||||
|
|
@ -50,7 +52,7 @@ impl Router {
|
||||||
if c.enabled {
|
if c.enabled {
|
||||||
if c.protocol == "socks5" {
|
if c.protocol == "socks5" {
|
||||||
let proxy_addr = format!("{}:{}", c.address, c.port);
|
let proxy_addr = format!("{}:{}", c.address, c.port);
|
||||||
match crate::outbound::connect_udp_via_socks5(&proxy_addr, server_udp.clone(), &c.username, &c.password).await {
|
match crate::outbound::connect_udp_via_socks5(&proxy_addr, server_udp.clone(), self.bind_ip.as_deref(), &c.username, &c.password).await {
|
||||||
Ok(p) => proxy = Some(Arc::new(p)),
|
Ok(p) => proxy = Some(Arc::new(p)),
|
||||||
// Warn unconditionally, not only under `debug`. Every UDP
|
// Warn unconditionally, not only under `debug`. Every UDP
|
||||||
// flow the rules want proxied is now dropped instead of
|
// flow the rules want proxied is now dropped instead of
|
||||||
|
|
@ -99,7 +101,7 @@ impl UdpSessionRouter {
|
||||||
pub async fn send_to(&self, data: &[u8], target: &str) -> Result<usize> {
|
pub async fn send_to(&self, data: &[u8], target: &str) -> Result<usize> {
|
||||||
if let Some(cfg) = &self.cfg {
|
if let Some(cfg) = &self.cfg {
|
||||||
if cfg.enabled {
|
if cfg.enabled {
|
||||||
let action = crate::outbound::select_outbound_action(target, "udp", cfg, self.debug).await;
|
let (action, _rule_src) = crate::outbound::select_outbound_action(target, "udp", cfg, self.debug).await;
|
||||||
if action == crate::outbound::OutboundAction::Block {
|
if action == crate::outbound::OutboundAction::Block {
|
||||||
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1372,6 +1372,7 @@ async fn run_app() -> Result<()> {
|
||||||
ip_cidr: r.ip_cidr.unwrap_or_default(),
|
ip_cidr: r.ip_cidr.unwrap_or_default(),
|
||||||
protocol: r.protocol,
|
protocol: r.protocol,
|
||||||
action: parse_outbound_action(r.action),
|
action: parse_outbound_action(r.action),
|
||||||
|
send_from: r.send_from,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
default_action: parse_outbound_action(o.default_action),
|
default_action: parse_outbound_action(o.default_action),
|
||||||
|
|
@ -1407,8 +1408,9 @@ async fn run_app() -> Result<()> {
|
||||||
.map(serde_json::from_value)
|
.map(serde_json::from_value)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|e| anyhow!("Invalid 'dns' section in server config: {e}"))?;
|
.map_err(|e| anyhow!("Invalid 'dns' section in server config: {e}"))?;
|
||||||
|
let bind_ip = server_cfg.bind_ip;
|
||||||
// Pass all listen addresses for multi-listener support
|
// Pass all listen addresses for multi-listener support
|
||||||
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config)).await?;
|
ostp_server::run_server(listen_addrs, Some(host), bind_ip, access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config)).await?;
|
||||||
}
|
}
|
||||||
AppMode::Client(client_cfg) => {
|
AppMode::Client(client_cfg) => {
|
||||||
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
|
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue