mirror of
https://github.com/orbien-org/orbien.git
synced 2026-09-22 00:01:31 +00:00
refactor: Refactor and optimize server code, redesign configuration file
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ name = "orbien-server"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "orbien server — TCP proxy over TCP/QUIC"
|
||||
description = "orbien server"
|
||||
|
||||
[[bin]]
|
||||
name = "orbien-server"
|
||||
|
||||
+14
-179
@@ -1,199 +1,34 @@
|
||||
use anyhow::{bail, Result};
|
||||
use orbien_core::net::{try_consume_proxy_protocol, PpConsume, PROXY_PROTOCOL_MAX_HEADER};
|
||||
use anyhow::Result;
|
||||
use orbien_core::tls::PrefixedStream;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessPolicy {
|
||||
pub proxy_protocol: bool,
|
||||
pub trusted_proxy_cidrs: Vec<Cidr>,
|
||||
pub deny_src_cidrs: Vec<Cidr>,
|
||||
pub pp_header_timeout: Duration,
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AccessPolicy;
|
||||
|
||||
impl AccessPolicy {
|
||||
pub fn from_server_config(cfg: &orbien_core::config::ServerConfig) -> Result<Self> {
|
||||
let trusted = cfg
|
||||
.proxy_protocol_trusted_cidrs
|
||||
.iter()
|
||||
.map(|s| Cidr::parse(s))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let deny = cfg
|
||||
.deny_src_cidrs
|
||||
.iter()
|
||||
.map(|s| Cidr::parse(s))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if cfg.proxy_protocol && trusted.is_empty() {
|
||||
tracing::warn!(
|
||||
"proxyProtocol=true but proxyProtocolTrustedCidrs empty — \
|
||||
PP accepted from any peer (spoof risk); set trusted CIDRs in production"
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
proxy_protocol: cfg.proxy_protocol,
|
||||
trusted_proxy_cidrs: trusted,
|
||||
deny_src_cidrs: deny,
|
||||
pp_header_timeout: Duration::from_secs(cfg.proxy_protocol_timeout_secs.max(1)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_trusted_proxy(&self, ip: IpAddr) -> bool {
|
||||
if self.trusted_proxy_cidrs.is_empty() {
|
||||
return true;
|
||||
}
|
||||
self.trusted_proxy_cidrs.iter().any(|c| c.contains(ip))
|
||||
}
|
||||
|
||||
pub fn is_denied(&self, ip: IpAddr) -> bool {
|
||||
!self.deny_src_cidrs.is_empty() && self.deny_src_cidrs.iter().any(|c| c.contains(ip))
|
||||
pub fn from_server_config(_cfg: &orbien_core::config::ServerConfig) -> Result<Self> {
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VisitorConn {
|
||||
pub struct IngressConn {
|
||||
pub stream: PrefixedStream<TcpStream>,
|
||||
pub peer: SocketAddr,
|
||||
pub visitor: SocketAddr,
|
||||
pub source: SocketAddr,
|
||||
pub local: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
pub async fn prepare_visitor(
|
||||
pub async fn prepare_ingress(
|
||||
stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
policy: &AccessPolicy,
|
||||
) -> Result<VisitorConn> {
|
||||
_policy: &AccessPolicy,
|
||||
) -> Result<IngressConn> {
|
||||
let local = stream.local_addr().ok();
|
||||
let (stream, mut visitor) = if policy.proxy_protocol && policy.is_trusted_proxy(peer.ip()) {
|
||||
read_optional_pp(stream, peer, policy.pp_header_timeout).await?
|
||||
} else {
|
||||
(PrefixedStream::new(Vec::new(), stream), peer)
|
||||
};
|
||||
|
||||
if visitor.ip().is_unspecified() {
|
||||
visitor = peer;
|
||||
}
|
||||
|
||||
if policy.is_denied(visitor.ip()) {
|
||||
tracing::info!(visitor = %visitor.ip(), peer = %peer, "denied by denySrcCidrs");
|
||||
bail!("visitor {} denied by denySrcCidrs", visitor.ip());
|
||||
}
|
||||
|
||||
Ok(VisitorConn {
|
||||
stream,
|
||||
Ok(IngressConn {
|
||||
stream: PrefixedStream::new(Vec::new(), stream),
|
||||
peer,
|
||||
visitor,
|
||||
source: peer,
|
||||
local,
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_optional_pp(
|
||||
mut stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
hdr_timeout: Duration,
|
||||
) -> Result<(PrefixedStream<TcpStream>, SocketAddr)> {
|
||||
let mut buf = vec![0u8; PROXY_PROTOCOL_MAX_HEADER];
|
||||
let mut filled = 0usize;
|
||||
|
||||
loop {
|
||||
match try_consume_proxy_protocol(&buf[..filled])? {
|
||||
PpConsume::Done(parsed) => {
|
||||
let leftover = buf[parsed.header_len..filled].to_vec();
|
||||
tracing::debug!(
|
||||
peer = %peer,
|
||||
visitor = %parsed.src,
|
||||
header_len = parsed.header_len,
|
||||
"PROXY protocol accepted from trusted peer"
|
||||
);
|
||||
return Ok((PrefixedStream::new(leftover, stream), parsed.src));
|
||||
}
|
||||
PpConsume::NotProxy => {
|
||||
let leftover = buf[..filled].to_vec();
|
||||
return Ok((PrefixedStream::new(leftover, stream), peer));
|
||||
}
|
||||
PpConsume::Incomplete => {
|
||||
if filled >= buf.len() {
|
||||
bail!("PROXY protocol header incomplete / too large");
|
||||
}
|
||||
let n = timeout(hdr_timeout, stream.read(&mut buf[filled..]))
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("timeout waiting for PROXY protocol header"))??;
|
||||
if n == 0 {
|
||||
bail!("connection closed while reading PROXY protocol");
|
||||
}
|
||||
filled += n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cidr {
|
||||
addr: IpAddr,
|
||||
prefix: u8,
|
||||
}
|
||||
|
||||
impl Cidr {
|
||||
pub fn parse(s: &str) -> Result<Self> {
|
||||
let s = s.trim();
|
||||
if let Some((ip, pref)) = s.split_once('/') {
|
||||
let addr: IpAddr = ip.parse()?;
|
||||
let prefix: u8 = pref.parse()?;
|
||||
let max = match addr {
|
||||
IpAddr::V4(_) => 32,
|
||||
IpAddr::V6(_) => 128,
|
||||
};
|
||||
if prefix > max {
|
||||
bail!("CIDR prefix {prefix} too large for {addr}");
|
||||
}
|
||||
Ok(Self { addr, prefix })
|
||||
} else {
|
||||
let addr: IpAddr = s.parse()?;
|
||||
let prefix = match addr {
|
||||
IpAddr::V4(_) => 32,
|
||||
IpAddr::V6(_) => 128,
|
||||
};
|
||||
Ok(Self { addr, prefix })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, ip: IpAddr) -> bool {
|
||||
match (self.addr, ip) {
|
||||
(IpAddr::V4(net), IpAddr::V4(ip)) => ipv4_in_cidr(net, self.prefix, ip),
|
||||
(IpAddr::V6(net), IpAddr::V6(ip)) => ipv6_in_cidr(net, self.prefix, ip),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ipv4_in_cidr(net: Ipv4Addr, prefix: u8, ip: Ipv4Addr) -> bool {
|
||||
if prefix == 0 {
|
||||
return true;
|
||||
}
|
||||
let mask = if prefix >= 32 {
|
||||
u32::MAX
|
||||
} else {
|
||||
!((1u32 << (32 - prefix)) - 1)
|
||||
};
|
||||
(u32::from(net) & mask) == (u32::from(ip) & mask)
|
||||
}
|
||||
|
||||
fn ipv6_in_cidr(net: Ipv6Addr, prefix: u8, ip: Ipv6Addr) -> bool {
|
||||
if prefix == 0 {
|
||||
return true;
|
||||
}
|
||||
let net_o = net.octets();
|
||||
let ip_o = ip.octets();
|
||||
let full = (prefix / 8) as usize;
|
||||
let rem = prefix % 8;
|
||||
if net_o[..full] != ip_o[..full] {
|
||||
return false;
|
||||
}
|
||||
if rem == 0 {
|
||||
return true;
|
||||
}
|
||||
let mask = 0xffu8 << (8 - rem);
|
||||
(net_o[full] & mask) == (ip_o[full] & mask)
|
||||
}
|
||||
|
||||
@@ -1,512 +0,0 @@
|
||||
use crate::access::AccessPolicy;
|
||||
use crate::metrics::{MemMetrics, ServerMetrics};
|
||||
use crate::proxy::{
|
||||
format_local_addr, HttpProxy, HttpVhost, HttpsProxy, HttpsVhost, ProxyManager, RegisteredProxy,
|
||||
TcpProxy, UdpProxy,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::config::ServerConfig;
|
||||
use orbien_core::msg::{
|
||||
self, CloseProxy, KickOut, Message, NewProxy, NewProxyResp, Ping, Pong, ReqWorkConn,
|
||||
StartWorkConn,
|
||||
};
|
||||
use orbien_core::transport::DynStream;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::{ReadHalf, WriteHalf};
|
||||
use tokio::sync::{mpsc, Mutex, Notify};
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::sleep;
|
||||
|
||||
type CtrlRead = ReadHalf<DynStream>;
|
||||
type CtrlWrite = WriteHalf<DynStream>;
|
||||
|
||||
pub struct Control {
|
||||
pub run_id: String,
|
||||
pub user: String,
|
||||
pub hostname: String,
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub version: String,
|
||||
pub client_ip: String,
|
||||
pub connected_at: Instant,
|
||||
cfg: ServerConfig,
|
||||
reader: Mutex<CtrlRead>,
|
||||
writer: Mutex<CtrlWrite>,
|
||||
work_tx: mpsc::Sender<DynStream>,
|
||||
work_rx: Mutex<mpsc::Receiver<DynStream>>,
|
||||
work_notify: Notify,
|
||||
shutdown_notify: Notify,
|
||||
proxies: Mutex<ProxyManager>,
|
||||
bg_tasks: Mutex<JoinSet<()>>,
|
||||
closed: AtomicBool,
|
||||
pool_count: usize,
|
||||
http_vhost: Option<Arc<HttpVhost>>,
|
||||
https_vhost: Option<Arc<HttpsVhost>>,
|
||||
access: Arc<AccessPolicy>,
|
||||
pub metrics: Arc<MemMetrics>,
|
||||
}
|
||||
|
||||
impl Control {
|
||||
pub fn new(
|
||||
run_id: String,
|
||||
stream: DynStream,
|
||||
cfg: ServerConfig,
|
||||
pool_count: usize,
|
||||
http_vhost: Option<Arc<HttpVhost>>,
|
||||
https_vhost: Option<Arc<HttpsVhost>>,
|
||||
access: Arc<AccessPolicy>,
|
||||
user: String,
|
||||
hostname: String,
|
||||
os: String,
|
||||
arch: String,
|
||||
version: String,
|
||||
client_ip: String,
|
||||
metrics: Arc<MemMetrics>,
|
||||
) -> Self {
|
||||
let (reader, writer) = tokio::io::split(stream);
|
||||
let (work_tx, work_rx) = mpsc::channel(64);
|
||||
Self {
|
||||
run_id,
|
||||
user,
|
||||
hostname,
|
||||
os,
|
||||
arch,
|
||||
version,
|
||||
client_ip,
|
||||
connected_at: Instant::now(),
|
||||
cfg,
|
||||
reader: Mutex::new(reader),
|
||||
writer: Mutex::new(writer),
|
||||
work_tx,
|
||||
work_rx: Mutex::new(work_rx),
|
||||
work_notify: Notify::new(),
|
||||
shutdown_notify: Notify::new(),
|
||||
proxies: Mutex::new(ProxyManager::new()),
|
||||
bg_tasks: Mutex::new(JoinSet::new()),
|
||||
closed: AtomicBool::new(false),
|
||||
pool_count: pool_count.max(1),
|
||||
http_vhost,
|
||||
https_vhost,
|
||||
access,
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn proxy_summaries(&self) -> Vec<crate::proxy::ProxySummary> {
|
||||
self.proxies.lock().await.summaries()
|
||||
}
|
||||
|
||||
pub async fn proxy_count(&self) -> usize {
|
||||
self.proxies.lock().await.len()
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>) -> Result<()> {
|
||||
for _ in 0..self.pool_count {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
self.request_work_conn().await?;
|
||||
}
|
||||
|
||||
loop {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let msg = tokio::select! {
|
||||
_ = self.shutdown_notify.notified() => {
|
||||
break;
|
||||
}
|
||||
msg = async {
|
||||
let mut reader = self.reader.lock().await;
|
||||
msg::read_msg(&mut *reader).await
|
||||
} => {
|
||||
match msg {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
if !self.closed.load(Ordering::SeqCst) {
|
||||
tracing::debug!(error = %e, "control read ended");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match msg {
|
||||
Message::NewProxy(np) => self.handle_new_proxy(np).await?,
|
||||
Message::CloseProxy(cp) => self.handle_close_proxy(cp).await?,
|
||||
Message::Ping(p) => self.handle_ping(p).await?,
|
||||
other => {
|
||||
tracing::warn!(ty = other.type_byte(), "ignored control message");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) {
|
||||
if self.closed.swap(true, Ordering::SeqCst) {
|
||||
self.shutdown_notify.notify_waiters();
|
||||
self.work_notify.notify_waiters();
|
||||
return;
|
||||
}
|
||||
self.shutdown_notify.notify_waiters();
|
||||
self.work_notify.notify_waiters();
|
||||
{
|
||||
let mut pm = self.proxies.lock().await;
|
||||
for (name, ty) in pm.close_all().await {
|
||||
self.metrics.close_proxy(&name, ty);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = writer.shutdown().await;
|
||||
}
|
||||
let mut bg = self.bg_tasks.lock().await;
|
||||
bg.abort_all();
|
||||
while bg.join_next().await.is_some() {}
|
||||
}
|
||||
|
||||
pub async fn kick(&self, reason: impl Into<String>) {
|
||||
let reason = reason.into();
|
||||
{
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = msg::write_msg(
|
||||
&mut *writer,
|
||||
&Message::KickOut(KickOut {
|
||||
reason: reason.clone(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
tracing::info!(run_id = %self.run_id, %reason, "kicking client");
|
||||
self.shutdown().await;
|
||||
}
|
||||
|
||||
fn note_proxy_registered(&self, name: &str, proxy_type: &str) {
|
||||
self.metrics
|
||||
.new_proxy(name, proxy_type, &self.user, &self.run_id);
|
||||
}
|
||||
|
||||
pub async fn push_work_conn(&self, stream: DynStream) {
|
||||
let _ = self.work_tx.send(stream).await;
|
||||
self.work_notify.notify_waiters();
|
||||
}
|
||||
|
||||
async fn try_pop_work(&self) -> Option<DynStream> {
|
||||
let mut rx = self.work_rx.lock().await;
|
||||
rx.try_recv().ok()
|
||||
}
|
||||
|
||||
async fn spawn_refill(self: &Arc<Self>) {
|
||||
let ctl = Arc::clone(self);
|
||||
self.bg_tasks.lock().await.spawn(async move {
|
||||
if ctl.closed.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
let _ = ctl.request_work_conn().await;
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_work_conn(self: &Arc<Self>) -> Result<DynStream> {
|
||||
if let Some(conn) = self.try_pop_work().await {
|
||||
self.spawn_refill().await;
|
||||
return Ok(conn);
|
||||
}
|
||||
|
||||
self.request_work_conn().await?;
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
return Err(anyhow!("control closed while waiting for work conn"));
|
||||
}
|
||||
if let Some(conn) = self.try_pop_work().await {
|
||||
self.spawn_refill().await;
|
||||
return Ok(conn);
|
||||
}
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
return Err(anyhow!("timeout waiting for work conn"));
|
||||
}
|
||||
tokio::select! {
|
||||
_ = self.work_notify.notified() => {}
|
||||
_ = sleep(remaining.min(Duration::from_millis(100))) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_work_conn(&self) -> Result<()> {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
return Err(anyhow!("control closed"));
|
||||
}
|
||||
let mut writer = self.writer.lock().await;
|
||||
msg::write_msg(&mut *writer, &Message::ReqWorkConn(ReqWorkConn {})).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_new_proxy(self: &Arc<Self>, np: NewProxy) -> Result<()> {
|
||||
let resp = match self.register_proxy(&np).await {
|
||||
Ok(remote_addr) => NewProxyResp {
|
||||
proxy_name: np.proxy_name.clone(),
|
||||
remote_addr,
|
||||
error: String::new(),
|
||||
},
|
||||
Err(e) => NewProxyResp {
|
||||
proxy_name: np.proxy_name.clone(),
|
||||
remote_addr: String::new(),
|
||||
error: e.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let mut writer = self.writer.lock().await;
|
||||
msg::write_msg(&mut *writer, &Message::NewProxyResp(resp)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_proxy(self: &Arc<Self>, np: &NewProxy) -> Result<String> {
|
||||
match np.proxy_type.as_str() {
|
||||
"tcp" => self.register_tcp_proxy(np).await,
|
||||
"http" => self.register_http_proxy(np).await,
|
||||
"https" => self.register_https_proxy(np).await,
|
||||
"udp" => self.register_udp_proxy(np).await,
|
||||
other => Err(anyhow!("unsupported proxy type: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_tcp_proxy(self: &Arc<Self>, np: &NewProxy) -> Result<String> {
|
||||
if np.remote_port <= 0 || np.remote_port > 65535 {
|
||||
return Err(anyhow!("invalid remote_port"));
|
||||
}
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_mode(
|
||||
&np.bandwidth_limit,
|
||||
&np.bandwidth_limit_mode,
|
||||
orbien_core::limit::BandwidthLimitMode::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
proxy = %np.proxy_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let bind_addr = self.cfg.proxy_bind_addr.clone();
|
||||
let remote_port = np.remote_port as u16;
|
||||
let name = np.proxy_name.clone();
|
||||
let control = Arc::clone(self);
|
||||
|
||||
let proxy = TcpProxy::start(
|
||||
name.clone(),
|
||||
bind_addr,
|
||||
remote_port,
|
||||
control,
|
||||
limiter,
|
||||
Arc::clone(&self.access),
|
||||
)
|
||||
.await?;
|
||||
let remote_addr = format!(":{}", remote_port);
|
||||
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut pm = self.proxies.lock().await;
|
||||
if let Some(old_ty) = pm
|
||||
.insert(name.clone(), RegisteredProxy::Tcp(proxy), local_addr)
|
||||
.await
|
||||
{
|
||||
self.metrics.close_proxy(&name, old_ty);
|
||||
}
|
||||
self.note_proxy_registered(&name, "tcp");
|
||||
tracing::info!(proxy = %np.proxy_name, port = remote_port, "tcp proxy registered");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
async fn register_http_proxy(self: &Arc<Self>, np: &NewProxy) -> Result<String> {
|
||||
let vhost = self
|
||||
.http_vhost
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("http proxy requires server vhostHTTPPort > 0"))?;
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_mode(
|
||||
&np.bandwidth_limit,
|
||||
&np.bandwidth_limit_mode,
|
||||
orbien_core::limit::BandwidthLimitMode::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
proxy = %np.proxy_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let proxy = HttpProxy::register(
|
||||
np,
|
||||
Arc::clone(self),
|
||||
Arc::clone(&vhost),
|
||||
&self.cfg.sub_domain_host,
|
||||
limiter,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let remote_addr = proxy
|
||||
.domains
|
||||
.iter()
|
||||
.map(|d| format!("{d}:{}", vhost.listen_port))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
let name = np.proxy_name.clone();
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut pm = self.proxies.lock().await;
|
||||
if let Some(old_ty) = pm
|
||||
.insert(name.clone(), RegisteredProxy::Http(proxy), local_addr)
|
||||
.await
|
||||
{
|
||||
self.metrics.close_proxy(&name, old_ty);
|
||||
}
|
||||
self.note_proxy_registered(&name, "http");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
async fn register_https_proxy(self: &Arc<Self>, np: &NewProxy) -> Result<String> {
|
||||
let vhost = self
|
||||
.https_vhost
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("https proxy requires server vhostHTTPSPort > 0"))?;
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_mode(
|
||||
&np.bandwidth_limit,
|
||||
&np.bandwidth_limit_mode,
|
||||
orbien_core::limit::BandwidthLimitMode::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
proxy = %np.proxy_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let proxy = HttpsProxy::register(
|
||||
np,
|
||||
Arc::clone(self),
|
||||
Arc::clone(&vhost),
|
||||
&self.cfg.sub_domain_host,
|
||||
limiter,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let remote_addr = proxy
|
||||
.domains
|
||||
.iter()
|
||||
.map(|d| format!("{d}:{}", vhost.listen_port))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
let name = np.proxy_name.clone();
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut pm = self.proxies.lock().await;
|
||||
if let Some(old_ty) = pm
|
||||
.insert(name.clone(), RegisteredProxy::Https(proxy), local_addr)
|
||||
.await
|
||||
{
|
||||
self.metrics.close_proxy(&name, old_ty);
|
||||
}
|
||||
self.note_proxy_registered(&name, "https");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
async fn register_udp_proxy(self: &Arc<Self>, np: &NewProxy) -> Result<String> {
|
||||
if np.remote_port <= 0 || np.remote_port > 65535 {
|
||||
return Err(anyhow!("invalid remote_port"));
|
||||
}
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_mode(
|
||||
&np.bandwidth_limit,
|
||||
&np.bandwidth_limit_mode,
|
||||
orbien_core::limit::BandwidthLimitMode::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
proxy = %np.proxy_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let bind_addr = self.cfg.proxy_bind_addr.clone();
|
||||
let remote_port = np.remote_port as u16;
|
||||
let name = np.proxy_name.clone();
|
||||
let control = Arc::clone(self);
|
||||
let packet_size = self.cfg.udp_packet_size.max(512);
|
||||
|
||||
let proxy = UdpProxy::start(
|
||||
name.clone(),
|
||||
bind_addr,
|
||||
remote_port,
|
||||
control,
|
||||
limiter,
|
||||
packet_size,
|
||||
)
|
||||
.await?;
|
||||
let remote_addr = format!(":{}", remote_port);
|
||||
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut pm = self.proxies.lock().await;
|
||||
if let Some(old_ty) = pm
|
||||
.insert(name.clone(), RegisteredProxy::Udp(proxy), local_addr)
|
||||
.await
|
||||
{
|
||||
self.metrics.close_proxy(&name, old_ty);
|
||||
}
|
||||
self.note_proxy_registered(&name, "udp");
|
||||
tracing::info!(proxy = %np.proxy_name, port = remote_port, "udp proxy registered");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
async fn handle_close_proxy(&self, cp: CloseProxy) -> Result<()> {
|
||||
let mut pm = self.proxies.lock().await;
|
||||
if let Some(ty) = pm.remove(&cp.proxy_name).await {
|
||||
self.metrics.close_proxy(&cp.proxy_name, ty);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_ping(&self, _p: Ping) -> Result<()> {
|
||||
let mut writer = self.writer.lock().await;
|
||||
msg::write_msg(&mut *writer, &Message::Pong(Pong::default())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_work_conn(
|
||||
&self,
|
||||
mut work: DynStream,
|
||||
proxy_name: &str,
|
||||
src_addr: String,
|
||||
src_port: u16,
|
||||
dst_addr: String,
|
||||
dst_port: u16,
|
||||
) -> Result<DynStream> {
|
||||
msg::write_msg(
|
||||
&mut work,
|
||||
&Message::StartWorkConn(StartWorkConn {
|
||||
proxy_name: proxy_name.to_string(),
|
||||
src_addr,
|
||||
src_port,
|
||||
dst_addr,
|
||||
dst_port,
|
||||
error: String::new(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(work)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use super::Control;
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::msg::{self, Message, ReqDataConn, StartDataConn};
|
||||
use orbien_core::transport::DynStream;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
impl Control {
|
||||
pub async fn push_data_conn(&self, stream: DynStream) {
|
||||
let _ = self.data_tx.send(stream).await;
|
||||
self.data_notify.notify_waiters();
|
||||
}
|
||||
|
||||
async fn try_pop_data(&self) -> Option<DynStream> {
|
||||
let mut rx = self.data_rx.lock().await;
|
||||
rx.try_recv().ok()
|
||||
}
|
||||
|
||||
async fn spawn_refill(self: &Arc<Self>) {
|
||||
let ctl = Arc::clone(self);
|
||||
self.bg_tasks.lock().await.spawn(async move {
|
||||
if ctl.closed.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
let _ = ctl.request_data_conn().await;
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_data_conn(self: &Arc<Self>) -> Result<DynStream> {
|
||||
if let Some(conn) = self.try_pop_data().await {
|
||||
self.spawn_refill().await;
|
||||
return Ok(conn);
|
||||
}
|
||||
|
||||
self.request_data_conn().await?;
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
return Err(anyhow!("control closed while waiting for data conn"));
|
||||
}
|
||||
if let Some(conn) = self.try_pop_data().await {
|
||||
self.spawn_refill().await;
|
||||
return Ok(conn);
|
||||
}
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
return Err(anyhow!("timeout waiting for data conn"));
|
||||
}
|
||||
tokio::select! {
|
||||
_ = self.data_notify.notified() => {}
|
||||
_ = sleep(remaining.min(Duration::from_millis(100))) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn request_data_conn(&self) -> Result<()> {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
return Err(anyhow!("control closed"));
|
||||
}
|
||||
let mut writer = self.writer.lock().await;
|
||||
msg::write_msg(&mut *writer, &Message::ReqDataConn(ReqDataConn {})).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_data_conn(
|
||||
&self,
|
||||
mut data: DynStream,
|
||||
tunnel_name: &str,
|
||||
src_addr: String,
|
||||
src_port: u16,
|
||||
dst_addr: String,
|
||||
dst_port: u16,
|
||||
) -> Result<DynStream> {
|
||||
msg::write_msg(
|
||||
&mut data,
|
||||
&Message::StartDataConn(StartDataConn {
|
||||
tunnel_name: tunnel_name.to_string(),
|
||||
src_addr,
|
||||
src_port,
|
||||
dst_addr,
|
||||
dst_port,
|
||||
error: String::new(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
mod data_pool;
|
||||
mod register;
|
||||
|
||||
use crate::access::AccessPolicy;
|
||||
use crate::metrics::{MemMetrics, ServerMetrics};
|
||||
use crate::tunnel::{HttpGw, HttpsGw, TunnelManager};
|
||||
use anyhow::Result;
|
||||
use orbien_core::config::ServerConfig;
|
||||
use orbien_core::msg::{self, KickOut, Message, Ping, Pong};
|
||||
use orbien_core::transport::DynStream;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::{ReadHalf, WriteHalf};
|
||||
use tokio::sync::{mpsc, Mutex, Notify};
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::sleep;
|
||||
|
||||
type CtrlRead = ReadHalf<DynStream>;
|
||||
type CtrlWrite = WriteHalf<DynStream>;
|
||||
|
||||
pub struct Control {
|
||||
pub session_id: String,
|
||||
pub user: String,
|
||||
pub hostname: String,
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub version: String,
|
||||
pub client_ip: String,
|
||||
pub connected_at: Instant,
|
||||
cfg: ServerConfig,
|
||||
reader: Mutex<CtrlRead>,
|
||||
writer: Mutex<CtrlWrite>,
|
||||
data_tx: mpsc::Sender<DynStream>,
|
||||
data_rx: Mutex<mpsc::Receiver<DynStream>>,
|
||||
data_notify: Notify,
|
||||
shutdown_notify: Notify,
|
||||
tunnels: Mutex<TunnelManager>,
|
||||
bg_tasks: Mutex<JoinSet<()>>,
|
||||
closed: AtomicBool,
|
||||
pool_count: usize,
|
||||
http_gw: Option<Arc<HttpGw>>,
|
||||
https_gw: Option<Arc<HttpsGw>>,
|
||||
access: Arc<AccessPolicy>,
|
||||
pub metrics: Arc<MemMetrics>,
|
||||
last_ping_unix: AtomicI64,
|
||||
}
|
||||
|
||||
impl Control {
|
||||
pub fn new(
|
||||
session_id: String,
|
||||
stream: DynStream,
|
||||
cfg: ServerConfig,
|
||||
pool_count: usize,
|
||||
http_gw: Option<Arc<HttpGw>>,
|
||||
https_gw: Option<Arc<HttpsGw>>,
|
||||
access: Arc<AccessPolicy>,
|
||||
user: String,
|
||||
hostname: String,
|
||||
os: String,
|
||||
arch: String,
|
||||
version: String,
|
||||
client_ip: String,
|
||||
metrics: Arc<MemMetrics>,
|
||||
) -> Self {
|
||||
let (reader, writer) = tokio::io::split(stream);
|
||||
let (data_tx, data_rx) = mpsc::channel(64);
|
||||
Self {
|
||||
session_id,
|
||||
user,
|
||||
hostname,
|
||||
os,
|
||||
arch,
|
||||
version,
|
||||
client_ip,
|
||||
connected_at: Instant::now(),
|
||||
cfg,
|
||||
reader: Mutex::new(reader),
|
||||
writer: Mutex::new(writer),
|
||||
data_tx,
|
||||
data_rx: Mutex::new(data_rx),
|
||||
data_notify: Notify::new(),
|
||||
shutdown_notify: Notify::new(),
|
||||
tunnels: Mutex::new(TunnelManager::new()),
|
||||
bg_tasks: Mutex::new(JoinSet::new()),
|
||||
closed: AtomicBool::new(false),
|
||||
pool_count: pool_count.max(1),
|
||||
http_gw,
|
||||
https_gw,
|
||||
access,
|
||||
metrics,
|
||||
last_ping_unix: AtomicI64::new(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tunnel_summaries(&self) -> Vec<crate::tunnel::TunnelSummary> {
|
||||
self.tunnels.lock().await.summaries()
|
||||
}
|
||||
|
||||
pub async fn tunnel_count(&self) -> usize {
|
||||
self.tunnels.lock().await.len()
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>) -> Result<()> {
|
||||
for _ in 0..self.pool_count {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
self.request_data_conn().await?;
|
||||
}
|
||||
|
||||
{
|
||||
let timeout = self.effective_ping_timeout();
|
||||
if timeout > 0 {
|
||||
let this = Arc::clone(&self);
|
||||
self.bg_tasks.lock().await.spawn(async move {
|
||||
loop {
|
||||
if this.closed.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let last = this.last_ping_unix.load(Ordering::Relaxed);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0);
|
||||
if last > 0 && now.saturating_sub(last) > timeout {
|
||||
tracing::warn!(
|
||||
session_id = %this.session_id,
|
||||
timeout_secs = timeout,
|
||||
"heartbeat timeout"
|
||||
);
|
||||
this.shutdown().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let msg = tokio::select! {
|
||||
_ = self.shutdown_notify.notified() => {
|
||||
break;
|
||||
}
|
||||
msg = async {
|
||||
let mut reader = self.reader.lock().await;
|
||||
msg::read_msg(&mut *reader).await
|
||||
} => {
|
||||
match msg {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
if !self.closed.load(Ordering::SeqCst) {
|
||||
tracing::debug!(error = %e, "control read ended");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match msg {
|
||||
Message::NewTunnel(np) => self.handle_new_tunnel(np).await?,
|
||||
Message::CloseTunnel(cp) => self.handle_close_tunnel(cp).await?,
|
||||
Message::Ping(p) => self.handle_ping(p).await?,
|
||||
other => {
|
||||
tracing::warn!(ty = other.type_byte(), "ignored control message");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) {
|
||||
if self.closed.swap(true, Ordering::SeqCst) {
|
||||
self.shutdown_notify.notify_waiters();
|
||||
self.data_notify.notify_waiters();
|
||||
return;
|
||||
}
|
||||
self.shutdown_notify.notify_waiters();
|
||||
self.data_notify.notify_waiters();
|
||||
{
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
for (name, ty) in tm.close_all().await {
|
||||
self.metrics.close_tunnel(&name, ty);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = writer.shutdown().await;
|
||||
}
|
||||
let mut bg = self.bg_tasks.lock().await;
|
||||
bg.abort_all();
|
||||
while bg.join_next().await.is_some() {}
|
||||
}
|
||||
|
||||
pub async fn kick(&self, reason: impl Into<String>) {
|
||||
let reason = reason.into();
|
||||
{
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = msg::write_msg(
|
||||
&mut *writer,
|
||||
&Message::KickOut(KickOut {
|
||||
reason: reason.clone(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
tracing::info!(session_id = %self.session_id, %reason, "kicking client");
|
||||
self.shutdown().await;
|
||||
}
|
||||
|
||||
fn effective_ping_timeout(&self) -> i64 {
|
||||
let hb_to = self.cfg.transport.heartbeat_timeout;
|
||||
if hb_to > 0 {
|
||||
return hb_to;
|
||||
}
|
||||
if self.cfg.transport.tcp_mux {
|
||||
let mux_ka = self.cfg.transport.mux_keepalive_secs;
|
||||
if mux_ka > 0 {
|
||||
return mux_ka.saturating_mul(3);
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
async fn handle_ping(&self, _p: Ping) -> Result<()> {
|
||||
self.last_ping_unix.store(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
let mut writer = self.writer.lock().await;
|
||||
msg::write_msg(&mut *writer, &Message::Pong(Pong::default())).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
use super::Control;
|
||||
use crate::metrics::ServerMetrics;
|
||||
use crate::tunnel::{
|
||||
format_local_addr, HttpTunnel, HttpsTunnel, RegisteredTunnel, TcpTunnel, UdpTunnel,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::msg::{self, CloseTunnel, Message, NewTunnel, NewTunnelResp};
|
||||
use std::sync::Arc;
|
||||
|
||||
impl Control {
|
||||
fn note_tunnel_registered(&self, name: &str, tunnel_type: &str) {
|
||||
self.metrics
|
||||
.new_tunnel(name, tunnel_type, &self.user, &self.session_id);
|
||||
}
|
||||
|
||||
pub(super) async fn handle_new_tunnel(self: &Arc<Self>, np: NewTunnel) -> Result<()> {
|
||||
let resp = match self.register_tunnel(&np).await {
|
||||
Ok(remote_addr) => NewTunnelResp {
|
||||
tunnel_name: np.tunnel_name.clone(),
|
||||
remote_addr,
|
||||
error: String::new(),
|
||||
},
|
||||
Err(e) => NewTunnelResp {
|
||||
tunnel_name: np.tunnel_name.clone(),
|
||||
remote_addr: String::new(),
|
||||
error: e.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let mut writer = self.writer.lock().await;
|
||||
msg::write_msg(&mut *writer, &Message::NewTunnelResp(resp)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_tunnel(self: &Arc<Self>, np: &NewTunnel) -> Result<String> {
|
||||
match np.protocol.as_str() {
|
||||
"tcp" => self.register_tcp_tunnel(np).await,
|
||||
"http" => self.register_http_tunnel(np).await,
|
||||
"https" => self.register_https_tunnel(np).await,
|
||||
"udp" => self.register_udp_tunnel(np).await,
|
||||
other => Err(anyhow!("unsupported tunnel protocol: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_tcp_tunnel(self: &Arc<Self>, np: &NewTunnel) -> Result<String> {
|
||||
if np.remote_port <= 0 || np.remote_port > 65535 {
|
||||
return Err(anyhow!("invalid remote_port"));
|
||||
}
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_side(
|
||||
np.bandwidth,
|
||||
&np.bandwidth_limit_side,
|
||||
orbien_core::limit::BandwidthLimitSide::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
tunnel = %np.tunnel_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let bind_addr = self.cfg.proxy_addr.clone();
|
||||
let remote_port = np.remote_port as u16;
|
||||
let name = np.tunnel_name.clone();
|
||||
let control = Arc::clone(self);
|
||||
|
||||
{
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
if let Some(old_ty) = tm.remove(&name).await {
|
||||
self.metrics.close_tunnel(&name, old_ty);
|
||||
}
|
||||
}
|
||||
|
||||
let tunnel = TcpTunnel::start(
|
||||
name.clone(),
|
||||
bind_addr,
|
||||
remote_port,
|
||||
control,
|
||||
limiter,
|
||||
Arc::clone(&self.access),
|
||||
)
|
||||
.await?;
|
||||
let remote_addr = format!(":{}", remote_port);
|
||||
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
let _ = tm
|
||||
.insert(name.clone(), RegisteredTunnel::Tcp(tunnel), local_addr)
|
||||
.await;
|
||||
self.note_tunnel_registered(&name, "tcp");
|
||||
tracing::info!(tunnel = %np.tunnel_name, port = remote_port, "tcp tunnel registered");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
async fn register_http_tunnel(self: &Arc<Self>, np: &NewTunnel) -> Result<String> {
|
||||
let gw = self
|
||||
.http_gw
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("http tunnel requires server httpGwPort > 0"))?;
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_side(
|
||||
np.bandwidth,
|
||||
&np.bandwidth_limit_side,
|
||||
orbien_core::limit::BandwidthLimitSide::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
tunnel = %np.tunnel_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let name = np.tunnel_name.clone();
|
||||
{
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
if let Some(old_ty) = tm.remove(&name).await {
|
||||
self.metrics.close_tunnel(&name, old_ty);
|
||||
}
|
||||
}
|
||||
|
||||
let tunnel = HttpTunnel::register(
|
||||
np,
|
||||
Arc::clone(self),
|
||||
Arc::clone(&gw),
|
||||
&self.cfg.root_domain,
|
||||
limiter,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let remote_addr = tunnel
|
||||
.domains
|
||||
.iter()
|
||||
.map(|d| format!("{d}:{}", gw.listen_port))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
let _ = tm
|
||||
.insert(name.clone(), RegisteredTunnel::Http(tunnel), local_addr)
|
||||
.await;
|
||||
self.note_tunnel_registered(&name, "http");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
async fn register_https_tunnel(self: &Arc<Self>, np: &NewTunnel) -> Result<String> {
|
||||
let gw = self
|
||||
.https_gw
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("https tunnel requires server httpsGwPort > 0"))?;
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_side(
|
||||
np.bandwidth,
|
||||
&np.bandwidth_limit_side,
|
||||
orbien_core::limit::BandwidthLimitSide::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
tunnel = %np.tunnel_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let name = np.tunnel_name.clone();
|
||||
{
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
if let Some(old_ty) = tm.remove(&name).await {
|
||||
self.metrics.close_tunnel(&name, old_ty);
|
||||
}
|
||||
}
|
||||
|
||||
let tunnel = HttpsTunnel::register(
|
||||
np,
|
||||
Arc::clone(self),
|
||||
Arc::clone(&gw),
|
||||
&self.cfg.root_domain,
|
||||
limiter,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let remote_addr = tunnel
|
||||
.domains
|
||||
.iter()
|
||||
.map(|d| format!("{d}:{}", gw.listen_port))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
let _ = tm
|
||||
.insert(name.clone(), RegisteredTunnel::Https(tunnel), local_addr)
|
||||
.await;
|
||||
self.note_tunnel_registered(&name, "https");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
async fn register_udp_tunnel(self: &Arc<Self>, np: &NewTunnel) -> Result<String> {
|
||||
if np.remote_port <= 0 || np.remote_port > 65535 {
|
||||
return Err(anyhow!("invalid remote_port"));
|
||||
}
|
||||
|
||||
let limiter = orbien_core::limit::limiter_if_side(
|
||||
np.bandwidth,
|
||||
&np.bandwidth_limit_side,
|
||||
orbien_core::limit::BandwidthLimitSide::Server,
|
||||
)?;
|
||||
if let Some(ref l) = limiter {
|
||||
tracing::info!(
|
||||
tunnel = %np.tunnel_name,
|
||||
bytes_per_sec = l.bytes_per_sec(),
|
||||
mode = "server",
|
||||
"bandwidth limit enabled"
|
||||
);
|
||||
}
|
||||
|
||||
let bind_addr = self.cfg.proxy_addr.clone();
|
||||
let remote_port = np.remote_port as u16;
|
||||
let name = np.tunnel_name.clone();
|
||||
let control = Arc::clone(self);
|
||||
let packet_size = self.cfg.udp_packet_size.max(512);
|
||||
|
||||
{
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
if let Some(old_ty) = tm.remove(&name).await {
|
||||
self.metrics.close_tunnel(&name, old_ty);
|
||||
}
|
||||
}
|
||||
|
||||
let tunnel = UdpTunnel::start(
|
||||
name.clone(),
|
||||
bind_addr,
|
||||
remote_port,
|
||||
control,
|
||||
limiter,
|
||||
packet_size,
|
||||
)
|
||||
.await?;
|
||||
let remote_addr = format!(":{}", remote_port);
|
||||
|
||||
let local_addr = format_local_addr(&np.local_ip, np.local_port);
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
let _ = tm
|
||||
.insert(name.clone(), RegisteredTunnel::Udp(tunnel), local_addr)
|
||||
.await;
|
||||
self.note_tunnel_registered(&name, "udp");
|
||||
tracing::info!(tunnel = %np.tunnel_name, port = remote_port, "udp tunnel registered");
|
||||
Ok(remote_addr)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_close_tunnel(&self, cp: CloseTunnel) -> Result<()> {
|
||||
let mut tm = self.tunnels.lock().await;
|
||||
if let Some(ty) = tm.remove(&cp.tunnel_name).await {
|
||||
self.metrics.close_tunnel(&cp.tunnel_name, ty);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ mod routes;
|
||||
use crate::service::Service;
|
||||
use anyhow::Result;
|
||||
use axum::middleware;
|
||||
use orbien_core::config::WebServerConfig;
|
||||
use orbien_core::config::DashboardConfig;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
pub async fn run(svc: Arc<Service>, cfg: WebServerConfig) -> Result<()> {
|
||||
pub async fn run(svc: Arc<Service>, cfg: DashboardConfig) -> Result<()> {
|
||||
let addr = format!("{}:{}", cfg.addr, cfg.port);
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!(%addr, user = %cfg.user, "webServer dashboard listening");
|
||||
tracing::info!(%addr, user = %cfg.user, "dashboard listening");
|
||||
|
||||
let state = Arc::new(DashState { svc, cfg });
|
||||
let app = routes::router(state.clone())
|
||||
@@ -25,5 +25,5 @@ pub async fn run(svc: Arc<Service>, cfg: WebServerConfig) -> Result<()> {
|
||||
#[derive(Clone)]
|
||||
pub struct DashState {
|
||||
pub svc: Arc<Service>,
|
||||
pub cfg: WebServerConfig,
|
||||
pub cfg: DashboardConfig,
|
||||
}
|
||||
|
||||
@@ -35,26 +35,23 @@ pub struct SystemInfo {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SystemConfig {
|
||||
#[serde(rename = "bindAddr")]
|
||||
pub bind_addr: String,
|
||||
#[serde(rename = "bindPort")]
|
||||
pub bind_port: u16,
|
||||
#[serde(rename = "quicBindPort")]
|
||||
pub quic_bind_port: u16,
|
||||
#[serde(rename = "kcpBindPort")]
|
||||
pub kcp_bind_port: u16,
|
||||
#[serde(rename = "vhostHTTPPort")]
|
||||
pub vhost_http_port: u16,
|
||||
#[serde(rename = "vhostHTTPSPort")]
|
||||
pub vhost_https_port: u16,
|
||||
#[serde(rename = "subDomainHost")]
|
||||
pub sub_domain_host: String,
|
||||
pub listen: String,
|
||||
#[serde(rename = "quicPort")]
|
||||
pub quic_port: u16,
|
||||
#[serde(rename = "kcpPort")]
|
||||
pub kcp_port: u16,
|
||||
#[serde(rename = "httpGwPort")]
|
||||
pub http_gw_port: u16,
|
||||
#[serde(rename = "httpsGwPort")]
|
||||
pub https_gw_port: u16,
|
||||
#[serde(rename = "rootDomain")]
|
||||
pub root_domain: String,
|
||||
#[serde(rename = "tcpMux")]
|
||||
pub tcp_mux: bool,
|
||||
#[serde(rename = "tlsForce")]
|
||||
pub tls_force: bool,
|
||||
#[serde(rename = "maxPoolCount")]
|
||||
pub max_pool_count: i64,
|
||||
#[serde(rename = "maxConnPool")]
|
||||
pub max_conn_pool: i64,
|
||||
#[serde(rename = "heartbeatTimeout")]
|
||||
pub heartbeat_timeout: i64,
|
||||
}
|
||||
@@ -65,10 +62,10 @@ pub struct SystemStatus {
|
||||
pub client_counts: usize,
|
||||
#[serde(rename = "totalClientCounts")]
|
||||
pub total_client_counts: usize,
|
||||
#[serde(rename = "proxyTypeCount")]
|
||||
pub proxy_type_count: std::collections::BTreeMap<String, usize>,
|
||||
#[serde(rename = "curConns")]
|
||||
pub cur_conns: usize,
|
||||
#[serde(rename = "tunnelTypeCount")]
|
||||
pub tunnel_type_count: std::collections::BTreeMap<String, usize>,
|
||||
#[serde(rename = "activeConns")]
|
||||
pub active_conns: usize,
|
||||
#[serde(rename = "totalTrafficIn")]
|
||||
pub total_traffic_in: u64,
|
||||
#[serde(rename = "totalTrafficOut")]
|
||||
@@ -77,8 +74,8 @@ pub struct SystemStatus {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ClientInfo {
|
||||
#[serde(rename = "runId")]
|
||||
pub run_id: String,
|
||||
#[serde(rename = "sessionId")]
|
||||
pub session_id: String,
|
||||
pub user: String,
|
||||
pub hostname: String,
|
||||
pub os: String,
|
||||
@@ -86,39 +83,39 @@ pub struct ClientInfo {
|
||||
#[serde(rename = "clientIP")]
|
||||
pub client_ip: String,
|
||||
pub version: String,
|
||||
#[serde(rename = "proxyCount")]
|
||||
pub proxy_count: usize,
|
||||
#[serde(rename = "curConns")]
|
||||
pub cur_conns: usize,
|
||||
#[serde(rename = "tunnelCount")]
|
||||
pub tunnel_count: usize,
|
||||
#[serde(rename = "activeConns")]
|
||||
pub active_conns: usize,
|
||||
#[serde(rename = "connectedSecs")]
|
||||
pub connected_secs: u64,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProxyInfo {
|
||||
pub struct TunnelInfo {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub proxy_type: String,
|
||||
pub tunnel_type: String,
|
||||
#[serde(rename = "remoteAddr")]
|
||||
pub remote_addr: String,
|
||||
#[serde(rename = "localAddr")]
|
||||
pub local_addr: String,
|
||||
#[serde(rename = "clientId")]
|
||||
pub client_id: String,
|
||||
#[serde(rename = "sessionId")]
|
||||
pub session_id: String,
|
||||
pub status: String,
|
||||
#[serde(rename = "todayTrafficIn")]
|
||||
pub today_traffic_in: u64,
|
||||
#[serde(rename = "todayTrafficOut")]
|
||||
pub today_traffic_out: u64,
|
||||
#[serde(rename = "curConns")]
|
||||
pub cur_conns: usize,
|
||||
#[serde(rename = "activeConns")]
|
||||
pub active_conns: usize,
|
||||
#[serde(rename = "lastStartTime", skip_serializing_if = "Option::is_none")]
|
||||
pub last_start_time: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProxyTrafficPoint {
|
||||
pub struct TunnelTrafficPoint {
|
||||
pub date: String,
|
||||
#[serde(rename = "trafficIn")]
|
||||
pub traffic_in: u64,
|
||||
@@ -127,9 +124,9 @@ pub struct ProxyTrafficPoint {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProxyTrafficResp {
|
||||
pub struct TunnelTrafficResp {
|
||||
pub name: String,
|
||||
pub unit: &'static str,
|
||||
pub granularity: &'static str,
|
||||
pub history: Vec<ProxyTrafficPoint>,
|
||||
pub history: Vec<TunnelTrafficPoint>,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use super::model::{
|
||||
ApiResponse, ClientInfo, Page, ProxyInfo, ProxyTrafficPoint, ProxyTrafficResp, SystemConfig,
|
||||
ApiResponse, ClientInfo, Page, TunnelInfo, TunnelTrafficPoint, TunnelTrafficResp, SystemConfig,
|
||||
SystemInfo, SystemStatus,
|
||||
};
|
||||
use super::DashState;
|
||||
use crate::metrics::{ProxyTrafficHistory, TrafficWindow};
|
||||
use crate::metrics::{TunnelTrafficHistory, TrafficWindow};
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, Request, StatusCode};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use base64::Engine;
|
||||
@@ -27,29 +27,17 @@ pub fn router(state: Arc<DashState>) -> Router {
|
||||
.route("/healthz", get(|| async { "ok" }))
|
||||
.route("/", get(index_html))
|
||||
.route("/favicon.ico", get(favicon))
|
||||
.route("/static", get(|| async { Redirect::permanent("/") }))
|
||||
.route("/static/", get(|| async { Redirect::permanent("/") }))
|
||||
.route("/static/{*path}", get(redirect_legacy_static))
|
||||
.route("/api/v1/system/info", get(system_info))
|
||||
.route("/api/v1/system/traffic", get(system_traffic))
|
||||
.route("/api/v1/clients", get(list_clients))
|
||||
.route("/api/v1/clients/{run_id}", get(get_client))
|
||||
.route("/api/v1/clients/{run_id}/kick", post(kick_client))
|
||||
.route("/api/v1/proxies", get(list_proxies))
|
||||
.route("/api/v1/proxies/{name}/traffic", get(proxy_traffic))
|
||||
.route("/api/v1/clients/{session_id}", get(get_client))
|
||||
.route("/api/v1/clients/{session_id}/kick", post(kick_client))
|
||||
.route("/api/v1/tunnels", get(list_tunnels))
|
||||
.route("/api/v1/tunnels/{name}/traffic", get(tunnel_traffic))
|
||||
.route("/{*path}", get(static_file))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn redirect_legacy_static(Path(path): Path<String>) -> Redirect {
|
||||
let rel = path.trim_start_matches('/');
|
||||
if rel.is_empty() {
|
||||
Redirect::permanent("/")
|
||||
} else {
|
||||
Redirect::permanent(&format!("/{rel}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn basic_auth(
|
||||
State(state): State<Arc<DashState>>,
|
||||
req: Request<Body>,
|
||||
@@ -99,14 +87,14 @@ fn authorized(state: &DashState, headers: &HeaderMap) -> bool {
|
||||
}
|
||||
|
||||
async fn index_html(State(state): State<Arc<DashState>>) -> Response {
|
||||
if let Some(bytes) = load_override(&state.cfg.assets_dir, "index.html") {
|
||||
if let Some(bytes) = load_override(&state.cfg.static_dir, "index.html") {
|
||||
return bytes_response("text/html; charset=utf-8", bytes);
|
||||
}
|
||||
serve_asset("index.html")
|
||||
}
|
||||
|
||||
async fn favicon(State(state): State<Arc<DashState>>) -> Response {
|
||||
if let Some(bytes) = load_override(&state.cfg.assets_dir, "favicon.ico") {
|
||||
if let Some(bytes) = load_override(&state.cfg.static_dir, "favicon.ico") {
|
||||
return bytes_response("image/x-icon", bytes);
|
||||
}
|
||||
if let Some(res) = try_embedded("favicon.ico") {
|
||||
@@ -117,7 +105,7 @@ async fn favicon(State(state): State<Arc<DashState>>) -> Response {
|
||||
|
||||
async fn static_file(State(state): State<Arc<DashState>>, Path(path): Path<String>) -> Response {
|
||||
let rel = path.trim_start_matches('/');
|
||||
if let Some(bytes) = load_override(&state.cfg.assets_dir, rel) {
|
||||
if let Some(bytes) = load_override(&state.cfg.static_dir, rel) {
|
||||
return bytes_response(content_type(rel), bytes);
|
||||
}
|
||||
|
||||
@@ -169,15 +157,15 @@ fn traffic_window(q: &TrafficQuery) -> TrafficWindow {
|
||||
TrafficWindow::parse(&q.range)
|
||||
}
|
||||
|
||||
fn traffic_resp(hist: ProxyTrafficHistory) -> ProxyTrafficResp {
|
||||
ProxyTrafficResp {
|
||||
fn traffic_resp(hist: TunnelTrafficHistory) -> TunnelTrafficResp {
|
||||
TunnelTrafficResp {
|
||||
name: hist.name,
|
||||
unit: hist.unit,
|
||||
granularity: hist.granularity,
|
||||
history: hist
|
||||
.history
|
||||
.into_iter()
|
||||
.map(|p| ProxyTrafficPoint {
|
||||
.map(|p| TunnelTrafficPoint {
|
||||
date: p.date,
|
||||
traffic_in: p.traffic_in,
|
||||
traffic_out: p.traffic_out,
|
||||
@@ -191,16 +179,15 @@ async fn system_info(State(state): State<Arc<DashState>>) -> Json<ApiResponse<Sy
|
||||
Json(ApiResponse::ok(SystemInfo {
|
||||
version: VERSION.to_string(),
|
||||
config: SystemConfig {
|
||||
bind_addr: state.svc.cfg().bind_addr.clone(),
|
||||
bind_port: state.svc.cfg().bind_port,
|
||||
quic_bind_port: state.svc.cfg().quic_bind_port,
|
||||
kcp_bind_port: state.svc.cfg().kcp_bind_port,
|
||||
vhost_http_port: state.svc.cfg().vhost_http_port,
|
||||
vhost_https_port: state.svc.cfg().vhost_https_port,
|
||||
sub_domain_host: state.svc.cfg().sub_domain_host.clone(),
|
||||
listen: state.svc.cfg().listen.clone(),
|
||||
quic_port: state.svc.cfg().quic_port,
|
||||
kcp_port: state.svc.cfg().kcp_port,
|
||||
http_gw_port: state.svc.cfg().http_gw_port,
|
||||
https_gw_port: state.svc.cfg().https_gw_port,
|
||||
root_domain: state.svc.cfg().root_domain.clone(),
|
||||
tcp_mux: state.svc.cfg().transport.tcp_mux,
|
||||
tls_force: state.svc.cfg().transport.tls.force,
|
||||
max_pool_count: state.svc.cfg().transport.max_pool_count,
|
||||
max_conn_pool: state.svc.cfg().transport.max_conn_pool,
|
||||
heartbeat_timeout: state.svc.cfg().transport.heartbeat_timeout,
|
||||
},
|
||||
status: SystemStatus {
|
||||
@@ -210,8 +197,8 @@ async fn system_info(State(state): State<Arc<DashState>>) -> Json<ApiResponse<Sy
|
||||
.filter(|c| c.status.is_empty() || c.status == "online")
|
||||
.count(),
|
||||
total_client_counts: snap.total_client_counts,
|
||||
proxy_type_count: snap.proxy_type_count,
|
||||
cur_conns: snap.cur_conns,
|
||||
tunnel_type_count: snap.tunnel_type_count,
|
||||
active_conns: snap.active_conns,
|
||||
total_traffic_in: snap.total_traffic_in,
|
||||
total_traffic_out: snap.total_traffic_out,
|
||||
},
|
||||
@@ -221,7 +208,7 @@ async fn system_info(State(state): State<Arc<DashState>>) -> Json<ApiResponse<Sy
|
||||
async fn system_traffic(
|
||||
State(state): State<Arc<DashState>>,
|
||||
Query(q): Query<TrafficQuery>,
|
||||
) -> Json<ApiResponse<ProxyTrafficResp>> {
|
||||
) -> Json<ApiResponse<TunnelTrafficResp>> {
|
||||
let hist = state.svc.metrics().server_traffic(traffic_window(&q));
|
||||
Json(ApiResponse::ok(traffic_resp(hist)))
|
||||
}
|
||||
@@ -250,11 +237,11 @@ async fn list_clients(
|
||||
|
||||
async fn get_client(
|
||||
State(state): State<Arc<DashState>>,
|
||||
Path(run_id): Path<String>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<ApiResponse<ClientInfo>>, StatusCode> {
|
||||
let run_id = urlencoding_decode(&run_id);
|
||||
let session_id = urlencoding_decode(&session_id);
|
||||
let snap = state.svc.dashboard_snapshot().await;
|
||||
match snap.clients.into_iter().find(|c| c.run_id == run_id) {
|
||||
match snap.clients.into_iter().find(|c| c.session_id == session_id) {
|
||||
Some(c) => Ok(Json(ApiResponse::ok(c))),
|
||||
None => Err(StatusCode::NOT_FOUND),
|
||||
}
|
||||
@@ -262,10 +249,10 @@ async fn get_client(
|
||||
|
||||
async fn kick_client(
|
||||
State(state): State<Arc<DashState>>,
|
||||
Path(run_id): Path<String>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Json<ApiResponse<()>> {
|
||||
let run_id = urlencoding_decode(&run_id);
|
||||
match state.svc.kick_client(&run_id).await {
|
||||
let session_id = urlencoding_decode(&session_id);
|
||||
match state.svc.kick_client(&session_id).await {
|
||||
Ok(()) => Json(ApiResponse::ok(())),
|
||||
Err(e) => Json(ApiResponse {
|
||||
code: 404,
|
||||
@@ -276,30 +263,30 @@ async fn kick_client(
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ProxyListQuery {
|
||||
struct TunnelListQuery {
|
||||
#[serde(default = "default_page")]
|
||||
page: usize,
|
||||
#[serde(default = "default_page_size", rename = "pageSize")]
|
||||
page_size: usize,
|
||||
#[serde(default, rename = "clientId")]
|
||||
client_id: String,
|
||||
#[serde(default, rename = "sessionId")]
|
||||
session_id: String,
|
||||
#[serde(default)]
|
||||
q: String,
|
||||
}
|
||||
|
||||
async fn list_proxies(
|
||||
async fn list_tunnels(
|
||||
State(state): State<Arc<DashState>>,
|
||||
Query(q): Query<ProxyListQuery>,
|
||||
) -> Json<ApiResponse<Page<ProxyInfo>>> {
|
||||
Query(q): Query<TunnelListQuery>,
|
||||
) -> Json<ApiResponse<Page<TunnelInfo>>> {
|
||||
let page = q.page.max(1);
|
||||
let page_size = q.page_size.clamp(1, 200);
|
||||
let snap = state.svc.dashboard_snapshot().await;
|
||||
let client_id = q.client_id.trim();
|
||||
let session_id = q.session_id.trim();
|
||||
let needle = q.q.trim().to_ascii_lowercase();
|
||||
let filtered: Vec<ProxyInfo> = snap
|
||||
.proxies
|
||||
let filtered: Vec<TunnelInfo> = snap
|
||||
.tunnels
|
||||
.into_iter()
|
||||
.filter(|p| client_id.is_empty() || p.client_id == client_id)
|
||||
.filter(|p| session_id.is_empty() || p.session_id == session_id)
|
||||
.filter(|p| needle.is_empty() || p.name.to_ascii_lowercase().contains(&needle))
|
||||
.collect();
|
||||
let total = filtered.len();
|
||||
@@ -312,13 +299,13 @@ async fn list_proxies(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn proxy_traffic(
|
||||
async fn tunnel_traffic(
|
||||
State(state): State<Arc<DashState>>,
|
||||
Path(name): Path<String>,
|
||||
Query(q): Query<TrafficQuery>,
|
||||
) -> Result<Json<ApiResponse<ProxyTrafficResp>>, StatusCode> {
|
||||
) -> Result<Json<ApiResponse<TunnelTrafficResp>>, StatusCode> {
|
||||
let name = urlencoding_decode(&name);
|
||||
match state.svc.metrics().proxy_traffic(&name, traffic_window(&q)) {
|
||||
match state.svc.metrics().tunnel_traffic(&name, traffic_window(&q)) {
|
||||
Some(hist) => Ok(Json(ApiResponse::ok(traffic_resp(hist)))),
|
||||
None => Err(StatusCode::NOT_FOUND),
|
||||
}
|
||||
@@ -362,11 +349,11 @@ fn from_hex(b: u8) -> Option<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_override(assets_dir: &str, rel: &str) -> Option<Vec<u8>> {
|
||||
if assets_dir.trim().is_empty() {
|
||||
fn load_override(static_dir: &str, rel: &str) -> Option<Vec<u8>> {
|
||||
if static_dir.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let path = safe_join(FsPath::new(assets_dir), rel)?;
|
||||
let path = safe_join(FsPath::new(static_dir), rel)?;
|
||||
std::fs::read(path).ok()
|
||||
}
|
||||
|
||||
|
||||
+9
-64
@@ -2,7 +2,7 @@ mod access;
|
||||
mod control;
|
||||
mod dashboard;
|
||||
mod metrics;
|
||||
mod proxy;
|
||||
mod tunnel;
|
||||
mod service;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -14,53 +14,11 @@ use tracing_subscriber::EnvFilter;
|
||||
#[command(
|
||||
name = "orbien-server",
|
||||
about = "orbien server — TCP tunnel",
|
||||
after_help = "Without -c/--config, orbien-server uses built-in defaults:\n bind 0.0.0.0:9527, QUIC/KCP/vhost/dashboard disabled unless set via flags."
|
||||
after_help = "Example:\n orbien-server -c conf/orbien-server.toml"
|
||||
)]
|
||||
struct Args {
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
config: Option<String>,
|
||||
|
||||
#[arg(long = "bind_addr", default_value = "0.0.0.0")]
|
||||
bind_addr: String,
|
||||
|
||||
#[arg(short = 'p', long = "bind_port", default_value_t = 9527)]
|
||||
bind_port: u16,
|
||||
|
||||
#[arg(long = "kcp_bind_port", default_value_t = 0)]
|
||||
kcp_bind_port: u16,
|
||||
|
||||
#[arg(long = "quic_bind_port", default_value_t = 0)]
|
||||
quic_bind_port: u16,
|
||||
|
||||
#[arg(long = "proxy_bind_addr", default_value = "0.0.0.0")]
|
||||
proxy_bind_addr: String,
|
||||
|
||||
#[arg(long = "vhost_http_port", default_value_t = 0)]
|
||||
vhost_http_port: u16,
|
||||
|
||||
#[arg(long = "vhost_https_port", default_value_t = 0)]
|
||||
vhost_https_port: u16,
|
||||
|
||||
#[arg(long = "dashboard_addr", default_value = "0.0.0.0")]
|
||||
dashboard_addr: String,
|
||||
|
||||
#[arg(long = "dashboard_port", default_value_t = 0)]
|
||||
dashboard_port: u16,
|
||||
|
||||
#[arg(long = "dashboard_user", default_value = "admin")]
|
||||
dashboard_user: String,
|
||||
|
||||
#[arg(long = "dashboard_pwd", default_value = "admin")]
|
||||
dashboard_pwd: String,
|
||||
|
||||
#[arg(short = 't', long = "token", default_value = "")]
|
||||
token: String,
|
||||
|
||||
#[arg(long = "subdomain_host", default_value = "")]
|
||||
subdomain_host: String,
|
||||
|
||||
#[arg(long = "tls_only", default_value_t = false)]
|
||||
tls_only: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -73,11 +31,11 @@ async fn main() -> Result<()> {
|
||||
let cfg = load_server_config(&args)?;
|
||||
|
||||
tracing::info!(
|
||||
bind = %format!("{}:{}", cfg.bind_addr, cfg.bind_port),
|
||||
quic_bind = cfg.quic_bind_port,
|
||||
kcp_bind = cfg.kcp_bind_port,
|
||||
vhost_http = cfg.vhost_http_port,
|
||||
vhost_https = cfg.vhost_https_port,
|
||||
listen = %cfg.listen,
|
||||
quic_port = cfg.quic_port,
|
||||
kcp_port = cfg.kcp_port,
|
||||
http_gw = cfg.http_gw_port,
|
||||
https_gw = cfg.https_gw_port,
|
||||
"starting orbien-server"
|
||||
);
|
||||
|
||||
@@ -95,22 +53,9 @@ fn load_server_config(args: &Args) -> Result<ServerConfig> {
|
||||
return ServerConfig::load(path);
|
||||
}
|
||||
|
||||
tracing::info!("using CLI flags for config");
|
||||
tracing::info!("using built-in defaults");
|
||||
let mut cfg = ServerConfig::default();
|
||||
cfg.bind_addr = args.bind_addr.clone();
|
||||
cfg.bind_port = args.bind_port;
|
||||
cfg.kcp_bind_port = args.kcp_bind_port;
|
||||
cfg.quic_bind_port = args.quic_bind_port;
|
||||
cfg.proxy_bind_addr = args.proxy_bind_addr.clone();
|
||||
cfg.vhost_http_port = args.vhost_http_port;
|
||||
cfg.vhost_https_port = args.vhost_https_port;
|
||||
cfg.sub_domain_host = args.subdomain_host.clone();
|
||||
cfg.auth.token = args.token.clone();
|
||||
cfg.web_server.addr = args.dashboard_addr.clone();
|
||||
cfg.web_server.port = args.dashboard_port;
|
||||
cfg.web_server.user = args.dashboard_user.clone();
|
||||
cfg.web_server.password = args.dashboard_pwd.clone();
|
||||
cfg.transport.tls.force = args.tls_only;
|
||||
cfg.complete();
|
||||
cfg.validate()?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
+73
-73
@@ -27,22 +27,22 @@ impl TrafficWindow {
|
||||
pub struct ServerSnapshot {
|
||||
pub total_traffic_in: u64,
|
||||
pub total_traffic_out: u64,
|
||||
pub cur_conns: usize,
|
||||
pub active_conns: usize,
|
||||
pub client_counts: usize,
|
||||
pub total_client_counts: usize,
|
||||
pub proxy_type_counts: HashMap<String, usize>,
|
||||
pub tunnel_type_counts: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ProxySnapshot {
|
||||
pub struct TunnelSnapshot {
|
||||
pub name: String,
|
||||
pub proxy_type: String,
|
||||
pub tunnel_type: String,
|
||||
pub user: String,
|
||||
pub client_id: String,
|
||||
pub session_id: String,
|
||||
pub today_traffic_in: u64,
|
||||
pub today_traffic_out: u64,
|
||||
pub cur_conns: usize,
|
||||
pub active_conns: usize,
|
||||
pub last_start_at: Option<i64>,
|
||||
pub last_close_at: Option<i64>,
|
||||
}
|
||||
@@ -55,37 +55,37 @@ pub struct TrafficPoint {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyTrafficHistory {
|
||||
pub struct TunnelTrafficHistory {
|
||||
pub name: String,
|
||||
pub unit: &'static str,
|
||||
pub granularity: &'static str,
|
||||
pub history: Vec<TrafficPoint>,
|
||||
}
|
||||
|
||||
struct ProxyStats {
|
||||
proxy_type: String,
|
||||
struct TunnelStats {
|
||||
tunnel_type: String,
|
||||
user: String,
|
||||
client_id: String,
|
||||
session_id: String,
|
||||
traffic_in: DateCounter,
|
||||
traffic_out: DateCounter,
|
||||
traffic_in_hourly: HourCounter,
|
||||
traffic_out_hourly: HourCounter,
|
||||
cur_conns: Counter,
|
||||
active_conns: Counter,
|
||||
last_start_unix: Option<i64>,
|
||||
last_close_unix: Option<i64>,
|
||||
}
|
||||
|
||||
impl ProxyStats {
|
||||
fn new(proxy_type: &str, user: &str, client_id: &str) -> Self {
|
||||
impl TunnelStats {
|
||||
fn new(tunnel_type: &str, user: &str, session_id: &str) -> Self {
|
||||
Self {
|
||||
proxy_type: proxy_type.to_string(),
|
||||
tunnel_type: tunnel_type.to_string(),
|
||||
user: user.to_string(),
|
||||
client_id: client_id.to_string(),
|
||||
session_id: session_id.to_string(),
|
||||
traffic_in: DateCounter::new(RESERVE_DAYS),
|
||||
traffic_out: DateCounter::new(RESERVE_DAYS),
|
||||
traffic_in_hourly: HourCounter::new(RESERVE_HOURS),
|
||||
traffic_out_hourly: HourCounter::new(RESERVE_HOURS),
|
||||
cur_conns: Counter::new(),
|
||||
active_conns: Counter::new(),
|
||||
last_start_unix: None,
|
||||
last_close_unix: None,
|
||||
}
|
||||
@@ -97,11 +97,11 @@ struct State {
|
||||
total_traffic_out: DateCounter,
|
||||
total_traffic_in_hourly: HourCounter,
|
||||
total_traffic_out_hourly: HourCounter,
|
||||
cur_conns: Counter,
|
||||
active_conns: Counter,
|
||||
client_counts: Counter,
|
||||
seen_clients: HashSet<String>,
|
||||
proxy_type_counts: HashMap<String, Counter>,
|
||||
proxies: HashMap<String, ProxyStats>,
|
||||
tunnel_type_counts: HashMap<String, Counter>,
|
||||
tunnels: HashMap<String, TunnelStats>,
|
||||
}
|
||||
|
||||
pub struct MemMetrics {
|
||||
@@ -116,42 +116,42 @@ impl MemMetrics {
|
||||
total_traffic_out: DateCounter::new(RESERVE_DAYS),
|
||||
total_traffic_in_hourly: HourCounter::new(RESERVE_HOURS),
|
||||
total_traffic_out_hourly: HourCounter::new(RESERVE_HOURS),
|
||||
cur_conns: Counter::new(),
|
||||
active_conns: Counter::new(),
|
||||
client_counts: Counter::new(),
|
||||
seen_clients: HashSet::new(),
|
||||
proxy_type_counts: HashMap::new(),
|
||||
proxies: HashMap::new(),
|
||||
tunnel_type_counts: HashMap::new(),
|
||||
tunnels: HashMap::new(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn server_snapshot(&self) -> ServerSnapshot {
|
||||
let g = self.state.lock().expect("metrics lock");
|
||||
let mut proxy_type_counts = HashMap::new();
|
||||
for (k, v) in &g.proxy_type_counts {
|
||||
let mut tunnel_type_counts = HashMap::new();
|
||||
for (k, v) in &g.tunnel_type_counts {
|
||||
let n = v.count().max(0) as usize;
|
||||
if n > 0 {
|
||||
proxy_type_counts.insert(k.clone(), n);
|
||||
tunnel_type_counts.insert(k.clone(), n);
|
||||
}
|
||||
}
|
||||
ServerSnapshot {
|
||||
total_traffic_in: g.total_traffic_in.today_count().max(0) as u64,
|
||||
total_traffic_out: g.total_traffic_out.today_count().max(0) as u64,
|
||||
cur_conns: g.cur_conns.count().max(0) as usize,
|
||||
active_conns: g.active_conns.count().max(0) as usize,
|
||||
client_counts: g.client_counts.count().max(0) as usize,
|
||||
total_client_counts: g.seen_clients.len(),
|
||||
proxy_type_counts,
|
||||
tunnel_type_counts,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn proxy_snapshot(&self, name: &str) -> Option<ProxySnapshot> {
|
||||
pub fn tunnel_snapshot(&self, name: &str) -> Option<TunnelSnapshot> {
|
||||
let g = self.state.lock().expect("metrics lock");
|
||||
g.proxies.get(name).map(|p| to_proxy_snapshot(name, p))
|
||||
g.tunnels.get(name).map(|p| to_tunnel_snapshot(name, p))
|
||||
}
|
||||
|
||||
pub fn proxy_traffic(&self, name: &str, window: TrafficWindow) -> Option<ProxyTrafficHistory> {
|
||||
pub fn tunnel_traffic(&self, name: &str, window: TrafficWindow) -> Option<TunnelTrafficHistory> {
|
||||
let g = self.state.lock().expect("metrics lock");
|
||||
let p = g.proxies.get(name)?;
|
||||
let p = g.tunnels.get(name)?;
|
||||
Some(match window {
|
||||
TrafficWindow::Days7 => {
|
||||
let inbound = p.traffic_in.last_days(RESERVE_DAYS);
|
||||
@@ -166,7 +166,7 @@ impl MemMetrics {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn server_traffic(&self, window: TrafficWindow) -> ProxyTrafficHistory {
|
||||
pub fn server_traffic(&self, window: TrafficWindow) -> TunnelTrafficHistory {
|
||||
let g = self.state.lock().expect("metrics lock");
|
||||
match window {
|
||||
TrafficWindow::Days7 => {
|
||||
@@ -185,15 +185,15 @@ impl MemMetrics {
|
||||
pub fn track_connection(
|
||||
self: &Arc<Self>,
|
||||
name: impl Into<String>,
|
||||
proxy_type: impl Into<String>,
|
||||
tunnel_type: impl Into<String>,
|
||||
) -> ConnGuard {
|
||||
let name = name.into();
|
||||
let proxy_type = proxy_type.into();
|
||||
self.open_connection(&name, &proxy_type);
|
||||
let tunnel_type = tunnel_type.into();
|
||||
self.open_connection(&name, &tunnel_type);
|
||||
ConnGuard {
|
||||
metrics: Arc::clone(self),
|
||||
name,
|
||||
proxy_type,
|
||||
tunnel_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,20 +201,20 @@ impl MemMetrics {
|
||||
pub struct ConnGuard {
|
||||
metrics: Arc<MemMetrics>,
|
||||
name: String,
|
||||
proxy_type: String,
|
||||
tunnel_type: String,
|
||||
}
|
||||
|
||||
impl Drop for ConnGuard {
|
||||
fn drop(&mut self) {
|
||||
self.metrics.close_connection(&self.name, &self.proxy_type);
|
||||
self.metrics.close_connection(&self.name, &self.tunnel_type);
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerMetrics for MemMetrics {
|
||||
fn new_client(&self, run_id: &str) {
|
||||
fn new_client(&self, session_id: &str) {
|
||||
let mut g = self.state.lock().expect("metrics lock");
|
||||
if !run_id.is_empty() {
|
||||
g.seen_clients.insert(run_id.to_string());
|
||||
if !session_id.is_empty() {
|
||||
g.seen_clients.insert(session_id.to_string());
|
||||
const MAX_SEEN: usize = 256;
|
||||
if g.seen_clients.len() > MAX_SEEN {
|
||||
let overflow = g.seen_clients.len() - MAX_SEEN;
|
||||
@@ -236,55 +236,55 @@ impl ServerMetrics for MemMetrics {
|
||||
.dec(1);
|
||||
}
|
||||
|
||||
fn new_proxy(&self, name: &str, proxy_type: &str, user: &str, client_id: &str) {
|
||||
fn new_tunnel(&self, name: &str, tunnel_type: &str, user: &str, session_id: &str) {
|
||||
let mut g = self.state.lock().expect("metrics lock");
|
||||
g.proxy_type_counts
|
||||
.entry(proxy_type.to_string())
|
||||
g.tunnel_type_counts
|
||||
.entry(tunnel_type.to_string())
|
||||
.or_insert_with(Counter::new)
|
||||
.inc(1);
|
||||
|
||||
let now_unix = Local::now().timestamp();
|
||||
let entry = g
|
||||
.proxies
|
||||
.tunnels
|
||||
.entry(name.to_string())
|
||||
.or_insert_with(|| ProxyStats::new(proxy_type, user, client_id));
|
||||
.or_insert_with(|| TunnelStats::new(tunnel_type, user, session_id));
|
||||
|
||||
if entry.proxy_type != proxy_type {
|
||||
*entry = ProxyStats::new(proxy_type, user, client_id);
|
||||
if entry.tunnel_type != tunnel_type {
|
||||
*entry = TunnelStats::new(tunnel_type, user, session_id);
|
||||
} else {
|
||||
entry.user = user.to_string();
|
||||
entry.client_id = client_id.to_string();
|
||||
entry.session_id = session_id.to_string();
|
||||
}
|
||||
entry.last_start_unix = Some(now_unix);
|
||||
}
|
||||
|
||||
fn close_proxy(&self, name: &str, proxy_type: &str) {
|
||||
fn close_tunnel(&self, name: &str, tunnel_type: &str) {
|
||||
let mut g = self.state.lock().expect("metrics lock");
|
||||
if let Some(counter) = g.proxy_type_counts.get(proxy_type) {
|
||||
if let Some(counter) = g.tunnel_type_counts.get(tunnel_type) {
|
||||
counter.dec(1);
|
||||
}
|
||||
if let Some(entry) = g.proxies.get_mut(name) {
|
||||
if let Some(entry) = g.tunnels.get_mut(name) {
|
||||
entry.last_close_unix = Some(Local::now().timestamp());
|
||||
}
|
||||
}
|
||||
|
||||
fn open_connection(&self, name: &str, _proxy_type: &str) {
|
||||
fn open_connection(&self, name: &str, _tunnel_type: &str) {
|
||||
let mut g = self.state.lock().expect("metrics lock");
|
||||
g.cur_conns.inc(1);
|
||||
if let Some(p) = g.proxies.get_mut(name) {
|
||||
p.cur_conns.inc(1);
|
||||
g.active_conns.inc(1);
|
||||
if let Some(p) = g.tunnels.get_mut(name) {
|
||||
p.active_conns.inc(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn close_connection(&self, name: &str, _proxy_type: &str) {
|
||||
fn close_connection(&self, name: &str, _tunnel_type: &str) {
|
||||
let mut g = self.state.lock().expect("metrics lock");
|
||||
g.cur_conns.dec(1);
|
||||
if let Some(p) = g.proxies.get_mut(name) {
|
||||
p.cur_conns.dec(1);
|
||||
g.active_conns.dec(1);
|
||||
if let Some(p) = g.tunnels.get_mut(name) {
|
||||
p.active_conns.dec(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_traffic_in(&self, name: &str, _proxy_type: &str, bytes: u64) {
|
||||
fn add_traffic_in(&self, name: &str, _tunnel_type: &str, bytes: u64) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -292,13 +292,13 @@ impl ServerMetrics for MemMetrics {
|
||||
let mut g = self.state.lock().expect("metrics lock");
|
||||
g.total_traffic_in.inc(delta);
|
||||
g.total_traffic_in_hourly.inc(delta);
|
||||
if let Some(p) = g.proxies.get_mut(name) {
|
||||
if let Some(p) = g.tunnels.get_mut(name) {
|
||||
p.traffic_in.inc(delta);
|
||||
p.traffic_in_hourly.inc(delta);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_traffic_out(&self, name: &str, _proxy_type: &str, bytes: u64) {
|
||||
fn add_traffic_out(&self, name: &str, _tunnel_type: &str, bytes: u64) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -306,28 +306,28 @@ impl ServerMetrics for MemMetrics {
|
||||
let mut g = self.state.lock().expect("metrics lock");
|
||||
g.total_traffic_out.inc(delta);
|
||||
g.total_traffic_out_hourly.inc(delta);
|
||||
if let Some(p) = g.proxies.get_mut(name) {
|
||||
if let Some(p) = g.tunnels.get_mut(name) {
|
||||
p.traffic_out.inc(delta);
|
||||
p.traffic_out_hourly.inc(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_proxy_snapshot(name: &str, p: &ProxyStats) -> ProxySnapshot {
|
||||
ProxySnapshot {
|
||||
fn to_tunnel_snapshot(name: &str, p: &TunnelStats) -> TunnelSnapshot {
|
||||
TunnelSnapshot {
|
||||
name: name.to_string(),
|
||||
proxy_type: p.proxy_type.clone(),
|
||||
tunnel_type: p.tunnel_type.clone(),
|
||||
user: p.user.clone(),
|
||||
client_id: p.client_id.clone(),
|
||||
session_id: p.session_id.clone(),
|
||||
today_traffic_in: p.traffic_in.today_count().max(0) as u64,
|
||||
today_traffic_out: p.traffic_out.today_count().max(0) as u64,
|
||||
cur_conns: p.cur_conns.count().max(0) as usize,
|
||||
active_conns: p.active_conns.count().max(0) as usize,
|
||||
last_start_at: p.last_start_unix,
|
||||
last_close_at: p.last_close_unix,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_daily_history(name: &str, inbound: &[i64], outbound: &[i64]) -> ProxyTrafficHistory {
|
||||
fn build_daily_history(name: &str, inbound: &[i64], outbound: &[i64]) -> TunnelTrafficHistory {
|
||||
let today = Local::now().date_naive();
|
||||
let n = RESERVE_DAYS.min(inbound.len()).min(outbound.len());
|
||||
let mut history = Vec::with_capacity(n);
|
||||
@@ -341,7 +341,7 @@ fn build_daily_history(name: &str, inbound: &[i64], outbound: &[i64]) -> ProxyTr
|
||||
traffic_out: outbound[age].max(0) as u64,
|
||||
});
|
||||
}
|
||||
ProxyTrafficHistory {
|
||||
TunnelTrafficHistory {
|
||||
name: name.to_string(),
|
||||
unit: "bytes",
|
||||
granularity: "day",
|
||||
@@ -349,7 +349,7 @@ fn build_daily_history(name: &str, inbound: &[i64], outbound: &[i64]) -> ProxyTr
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hourly_history(name: &str, inbound: &[i64], outbound: &[i64]) -> ProxyTrafficHistory {
|
||||
fn build_hourly_history(name: &str, inbound: &[i64], outbound: &[i64]) -> TunnelTrafficHistory {
|
||||
let now_hour = Local::now()
|
||||
.with_minute(0)
|
||||
.and_then(|t| t.with_second(0))
|
||||
@@ -365,7 +365,7 @@ fn build_hourly_history(name: &str, inbound: &[i64], outbound: &[i64]) -> ProxyT
|
||||
traffic_out: outbound[age].max(0) as u64,
|
||||
});
|
||||
}
|
||||
ProxyTrafficHistory {
|
||||
TunnelTrafficHistory {
|
||||
name: name.to_string(),
|
||||
unit: "bytes",
|
||||
granularity: "hour",
|
||||
|
||||
+10
-10
@@ -4,7 +4,7 @@ mod hour_counter;
|
||||
mod mem;
|
||||
mod traits;
|
||||
|
||||
pub use mem::{MemMetrics, ProxyTrafficHistory, TrafficWindow};
|
||||
pub use mem::{MemMetrics, TunnelTrafficHistory, TrafficWindow};
|
||||
pub use traits::ServerMetrics;
|
||||
|
||||
pub const RESERVE_DAYS: usize = 7;
|
||||
@@ -13,21 +13,21 @@ pub const RESERVE_HOURS: usize = 24;
|
||||
pub async fn join_and_record<A, B>(
|
||||
metrics: &std::sync::Arc<MemMetrics>,
|
||||
name: &str,
|
||||
proxy_type: &str,
|
||||
visitor: A,
|
||||
work: B,
|
||||
tunnel_type: &str,
|
||||
ingress: A,
|
||||
data: B,
|
||||
) -> std::io::Result<(u64, u64)>
|
||||
where
|
||||
A: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
B: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
let _guard = metrics.track_connection(name, proxy_type);
|
||||
let (to_work, from_work, err) = orbien_core::io::join_counted(visitor, work).await;
|
||||
metrics.add_traffic_in(name, proxy_type, to_work);
|
||||
metrics.add_traffic_out(name, proxy_type, from_work);
|
||||
let _guard = metrics.track_connection(name, tunnel_type);
|
||||
let (to_data, from_data, err) = orbien_core::io::join_counted(ingress, data).await;
|
||||
metrics.add_traffic_in(name, tunnel_type, to_data);
|
||||
metrics.add_traffic_out(name, tunnel_type, from_data);
|
||||
match err {
|
||||
None => Ok((to_work, from_work)),
|
||||
Some(e) if is_benign_close(&e) => Ok((to_work, from_work)),
|
||||
None => Ok((to_data, from_data)),
|
||||
Some(e) if is_benign_close(&e) => Ok((to_data, from_data)),
|
||||
Some(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
pub trait ServerMetrics: Send + Sync {
|
||||
fn new_client(&self, run_id: &str);
|
||||
fn new_client(&self, session_id: &str);
|
||||
fn close_client(&self);
|
||||
|
||||
fn new_proxy(&self, name: &str, proxy_type: &str, user: &str, client_id: &str);
|
||||
fn close_proxy(&self, name: &str, proxy_type: &str);
|
||||
fn new_tunnel(&self, name: &str, tunnel_type: &str, user: &str, session_id: &str);
|
||||
fn close_tunnel(&self, name: &str, tunnel_type: &str);
|
||||
|
||||
fn open_connection(&self, name: &str, proxy_type: &str);
|
||||
fn close_connection(&self, name: &str, proxy_type: &str);
|
||||
fn open_connection(&self, name: &str, tunnel_type: &str);
|
||||
fn close_connection(&self, name: &str, tunnel_type: &str);
|
||||
|
||||
fn add_traffic_in(&self, name: &str, proxy_type: &str, bytes: u64);
|
||||
fn add_traffic_out(&self, name: &str, proxy_type: &str, bytes: u64);
|
||||
fn add_traffic_in(&self, name: &str, tunnel_type: &str, bytes: u64);
|
||||
fn add_traffic_out(&self, name: &str, tunnel_type: &str, bytes: u64);
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
use super::vhost::{build_domains, normalize_host, HttpRoute, HttpVhost};
|
||||
use crate::access::{prepare_visitor, AccessPolicy};
|
||||
use crate::control::Control;
|
||||
use crate::metrics::ServerMetrics;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use httparse::Status;
|
||||
use orbien_core::limit::{maybe_limit, BandwidthLimiter};
|
||||
use orbien_core::msg::NewProxy;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
pub struct HttpProxy {
|
||||
pub name: String,
|
||||
pub domains: Vec<String>,
|
||||
vhost: Arc<HttpVhost>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
impl HttpProxy {
|
||||
pub async fn register(
|
||||
np: &NewProxy,
|
||||
control: Arc<Control>,
|
||||
vhost: Arc<HttpVhost>,
|
||||
sub_domain_host: &str,
|
||||
limiter: Option<Arc<BandwidthLimiter>>,
|
||||
) -> Result<Self> {
|
||||
let domains = build_domains(&np.custom_domains, &np.subdomain, sub_domain_host)?;
|
||||
let name = np.proxy_name.clone();
|
||||
let locations = np.locations.clone();
|
||||
let rewrite = np.host_header_rewrite.clone();
|
||||
|
||||
for domain in &domains {
|
||||
vhost
|
||||
.register(
|
||||
domain,
|
||||
HttpRoute {
|
||||
proxy_name: name.clone(),
|
||||
control: Arc::downgrade(&control),
|
||||
locations: locations.clone(),
|
||||
host_header_rewrite: rewrite.clone(),
|
||||
limiter: limiter.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
proxy = %name,
|
||||
domains = ?domains,
|
||||
"http proxy registered"
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
domains,
|
||||
vhost,
|
||||
closed: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn close(&self) {
|
||||
if self
|
||||
.closed
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
self.vhost.unregister_proxy(&self.name).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_vhost_http_listener(
|
||||
bind_addr: String,
|
||||
port: u16,
|
||||
vhost: Arc<HttpVhost>,
|
||||
access: Arc<AccessPolicy>,
|
||||
shutdown: Arc<Notify>,
|
||||
) -> Result<()> {
|
||||
let addr = format!("{bind_addr}:{port}");
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!(%addr, "http vhost listener ready");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.notified() => break,
|
||||
accepted = listener.accept() => {
|
||||
match accepted {
|
||||
Ok((stream, peer)) => {
|
||||
let vhost = Arc::clone(&vhost);
|
||||
let access = Arc::clone(&access);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_http_visitor(vhost, stream, peer, access).await {
|
||||
tracing::debug!(%peer, error = %e, "http visitor ended");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "http vhost accept failed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_http_visitor(
|
||||
vhost: Arc<HttpVhost>,
|
||||
stream: TcpStream,
|
||||
peer: std::net::SocketAddr,
|
||||
access: Arc<AccessPolicy>,
|
||||
) -> Result<()> {
|
||||
let mut visitor = prepare_visitor(stream, peer, &access).await?;
|
||||
let (mut head, host, path) = read_http_request_head(&mut visitor.stream).await?;
|
||||
let Some(route) = vhost.lookup(&host, &path).await else {
|
||||
tracing::debug!(peer = %visitor.peer, visitor = %visitor.visitor, %host, %path, "http no route");
|
||||
write_not_found(&mut visitor.stream).await;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(control) = route.control.upgrade() else {
|
||||
write_not_found(&mut visitor.stream).await;
|
||||
return Err(anyhow!("http proxy client gone: {}", route.proxy_name));
|
||||
};
|
||||
|
||||
if !route.host_header_rewrite.is_empty() {
|
||||
rewrite_host_header(&mut head, &route.host_header_rewrite)?;
|
||||
}
|
||||
|
||||
orbien_core::net::apply_x_forwarded_for(&mut head, &visitor.visitor.ip().to_string(), "http")?;
|
||||
|
||||
let work = match control.get_work_conn().await {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
write_bad_gateway(&mut visitor.stream).await;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let work = control
|
||||
.start_work_conn(
|
||||
work,
|
||||
&route.proxy_name,
|
||||
visitor.visitor.ip().to_string(),
|
||||
visitor.visitor.port(),
|
||||
visitor
|
||||
.local
|
||||
.map(|a| a.ip().to_string())
|
||||
.unwrap_or_default(),
|
||||
visitor.local.map(|a| a.port()).unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut work = maybe_limit(work, route.limiter.clone());
|
||||
let head_len = head.len() as u64;
|
||||
work.write_all(&head).await?;
|
||||
tracing::debug!(
|
||||
proxy = %route.proxy_name,
|
||||
%host,
|
||||
%path,
|
||||
peer = %visitor.peer,
|
||||
visitor = %visitor.visitor,
|
||||
"http joining visitor <-> work"
|
||||
);
|
||||
let _guard = control.metrics.track_connection(&route.proxy_name, "http");
|
||||
let (to_work, from_work, err) = orbien_core::io::join_counted(visitor.stream, work).await;
|
||||
control
|
||||
.metrics
|
||||
.add_traffic_in(&route.proxy_name, "http", to_work.saturating_add(head_len));
|
||||
control
|
||||
.metrics
|
||||
.add_traffic_out(&route.proxy_name, "http", from_work);
|
||||
if let Some(e) = err {
|
||||
tracing::debug!(proxy = %route.proxy_name, error = %e, "http join ended");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_http_request_head<R: AsyncRead + Unpin>(
|
||||
stream: &mut R,
|
||||
) -> Result<(Vec<u8>, String, String)> {
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
let mut tmp = [0u8; 2048];
|
||||
loop {
|
||||
let n = stream.read(&mut tmp).await?;
|
||||
if n == 0 {
|
||||
bail!("client closed before http headers completed");
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..n]);
|
||||
if buf.len() > 64 * 1024 {
|
||||
bail!("http headers too large");
|
||||
}
|
||||
|
||||
let mut headers = [httparse::EMPTY_HEADER; 64];
|
||||
let mut req = httparse::Request::new(&mut headers);
|
||||
match req.parse(&buf)? {
|
||||
Status::Complete(_) => {
|
||||
let host = req
|
||||
.headers
|
||||
.iter()
|
||||
.find(|h| h.name.eq_ignore_ascii_case("host"))
|
||||
.map(|h| String::from_utf8_lossy(h.value).into_owned())
|
||||
.ok_or_else(|| anyhow!("missing Host header"))?;
|
||||
let path = req.path.unwrap_or("/").to_string();
|
||||
return Ok((buf, normalize_host(&host), path));
|
||||
}
|
||||
Status::Partial => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrite_host_header(buf: &mut Vec<u8>, new_host: &str) -> Result<()> {
|
||||
let lower = b"host:";
|
||||
let text = String::from_utf8_lossy(buf);
|
||||
let mut out = String::new();
|
||||
let mut replaced = false;
|
||||
for line in text.split_inclusive('\n') {
|
||||
let trimmed_start = line.trim_start_matches([' ', '\t']);
|
||||
if !replaced
|
||||
&& trimmed_start.len() >= 5
|
||||
&& trimmed_start.as_bytes()[..5].eq_ignore_ascii_case(lower)
|
||||
{
|
||||
let ending = if line.ends_with("\r\n") {
|
||||
"\r\n"
|
||||
} else if line.ends_with('\n') {
|
||||
"\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
out.push_str("Host: ");
|
||||
out.push_str(new_host);
|
||||
out.push_str(ending);
|
||||
replaced = true;
|
||||
} else {
|
||||
out.push_str(line);
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
return Err(anyhow!("Host header not found for rewrite"));
|
||||
}
|
||||
*buf = out.into_bytes();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_not_found<W: AsyncWrite + Unpin>(stream: &mut W) {
|
||||
let body = "Not Found\n";
|
||||
let resp = format!(
|
||||
"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
}
|
||||
|
||||
async fn write_bad_gateway<W: AsyncWrite + Unpin>(stream: &mut W) {
|
||||
let body = "Bad Gateway\n";
|
||||
let resp = format!(
|
||||
"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
mod http;
|
||||
mod https;
|
||||
mod manager;
|
||||
mod tcp;
|
||||
mod udp;
|
||||
mod vhost;
|
||||
|
||||
pub use http::{run_vhost_http_listener, HttpProxy};
|
||||
pub use https::{run_vhost_https_listener, HttpsProxy, HttpsVhost};
|
||||
pub use manager::{format_local_addr, ProxyManager, ProxySummary, RegisteredProxy};
|
||||
pub use tcp::TcpProxy;
|
||||
pub use udp::UdpProxy;
|
||||
pub use vhost::HttpVhost;
|
||||
@@ -1,131 +0,0 @@
|
||||
use crate::control::Control;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpRoute {
|
||||
pub proxy_name: String,
|
||||
pub control: Weak<Control>,
|
||||
|
||||
pub locations: Vec<String>,
|
||||
pub host_header_rewrite: String,
|
||||
|
||||
pub limiter: Option<std::sync::Arc<orbien_core::limit::BandwidthLimiter>>,
|
||||
}
|
||||
|
||||
pub struct HttpVhost {
|
||||
routes: Mutex<HashMap<String, Vec<HttpRoute>>>,
|
||||
pub listen_port: u16,
|
||||
}
|
||||
|
||||
impl HttpVhost {
|
||||
pub fn new(listen_port: u16) -> Self {
|
||||
Self {
|
||||
routes: Mutex::new(HashMap::new()),
|
||||
listen_port,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(&self, domain: &str, route: HttpRoute) -> anyhow::Result<()> {
|
||||
let key = normalize_host(domain);
|
||||
if key.is_empty() {
|
||||
return Err(anyhow::anyhow!("empty http domain"));
|
||||
}
|
||||
let mut map = self.routes.lock().await;
|
||||
let list = map.entry(key.clone()).or_default();
|
||||
|
||||
list.retain(|r| r.proxy_name != route.proxy_name);
|
||||
list.push(route);
|
||||
tracing::info!(domain = %key, "http route registered");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unregister_proxy(&self, proxy_name: &str) {
|
||||
let mut map = self.routes.lock().await;
|
||||
map.retain(|_, list| {
|
||||
list.retain(|r| r.proxy_name != proxy_name);
|
||||
!list.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn lookup(&self, host: &str, path: &str) -> Option<HttpRoute> {
|
||||
let key = normalize_host(host);
|
||||
let map = self.routes.lock().await;
|
||||
let list = map.get(&key)?;
|
||||
pick_by_location(list, path).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn pick_by_location<'a>(list: &'a [HttpRoute], path: &str) -> Option<&'a HttpRoute> {
|
||||
let mut best: Option<(&HttpRoute, usize)> = None;
|
||||
for r in list {
|
||||
let locs = if r.locations.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
r.locations.clone()
|
||||
};
|
||||
for loc in locs {
|
||||
if loc.is_empty() || path.starts_with(&loc) {
|
||||
let score = loc.len();
|
||||
if best.map(|(_, s)| score >= s).unwrap_or(true) {
|
||||
best = Some((r, score));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(r, _)| r)
|
||||
}
|
||||
|
||||
pub fn normalize_host(host: &str) -> String {
|
||||
let host = host.trim();
|
||||
let without_port = if let Some(h) = host.strip_prefix('[') {
|
||||
if let Some(end) = h.find(']') {
|
||||
&h[..end]
|
||||
} else {
|
||||
host
|
||||
}
|
||||
} else {
|
||||
host.split(':').next().unwrap_or(host)
|
||||
};
|
||||
without_port.trim().to_ascii_lowercase()
|
||||
}
|
||||
|
||||
pub fn build_domains(
|
||||
custom_domains: &[String],
|
||||
subdomain: &str,
|
||||
sub_domain_host: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let mut out = Vec::new();
|
||||
for d in custom_domains {
|
||||
let d = d.trim();
|
||||
if !d.is_empty() {
|
||||
out.push(normalize_host(d));
|
||||
}
|
||||
}
|
||||
let sub = subdomain.trim();
|
||||
if !sub.is_empty() {
|
||||
let base = sub_domain_host.trim();
|
||||
if base.is_empty() {
|
||||
if out.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"subdomain set but server subDomainHost is empty"
|
||||
));
|
||||
}
|
||||
tracing::warn!(
|
||||
subdomain = %sub,
|
||||
"subdomain ignored: server subDomainHost is empty; using customDomains only"
|
||||
);
|
||||
} else if sub.contains('.') || sub.contains('*') {
|
||||
return Err(anyhow::anyhow!("subdomain must not contain '.' or '*'"));
|
||||
} else {
|
||||
out.push(normalize_host(&format!("{sub}.{base}")));
|
||||
}
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"http/https proxy requires customDomains and/or subdomain"
|
||||
));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -1,604 +0,0 @@
|
||||
use crate::access::AccessPolicy;
|
||||
use crate::control::Control;
|
||||
use crate::metrics::{MemMetrics, ServerMetrics};
|
||||
use crate::proxy::{run_vhost_http_listener, run_vhost_https_listener, HttpVhost, HttpsVhost};
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::auth;
|
||||
use orbien_core::config::ServerConfig;
|
||||
use orbien_core::msg::{self, Login, LoginResp, Message, NewWorkConn};
|
||||
use orbien_core::transport::{self, boxed_stream, DynStream};
|
||||
use orbien_core::VERSION;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tokio::task::JoinSet;
|
||||
use uuid::Uuid;
|
||||
|
||||
struct OfflineClientRecord {
|
||||
run_id: String,
|
||||
user: String,
|
||||
hostname: String,
|
||||
os: String,
|
||||
arch: String,
|
||||
client_ip: String,
|
||||
version: String,
|
||||
proxy_count: usize,
|
||||
disconnected_at: Instant,
|
||||
}
|
||||
|
||||
pub struct Service {
|
||||
cfg: ServerConfig,
|
||||
access: Arc<AccessPolicy>,
|
||||
controls: Arc<Mutex<HashMap<String, Arc<Control>>>>,
|
||||
offline_clients: Arc<Mutex<HashMap<String, OfflineClientRecord>>>,
|
||||
http_vhost: Option<Arc<HttpVhost>>,
|
||||
https_vhost: Option<Arc<HttpsVhost>>,
|
||||
tls_config: Arc<rustls::ServerConfig>,
|
||||
metrics: Arc<MemMetrics>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(cfg: ServerConfig) -> Result<Self> {
|
||||
let access = Arc::new(AccessPolicy::from_server_config(&cfg)?);
|
||||
let http_vhost = if cfg.vhost_http_enabled() {
|
||||
Some(Arc::new(HttpVhost::new(cfg.vhost_http_port)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let https_vhost = if cfg.vhost_https_enabled() {
|
||||
Some(Arc::new(HttpsVhost::new(cfg.vhost_https_port)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tls = &cfg.transport.tls;
|
||||
let tls_config =
|
||||
transport::new_server_tls_config(&tls.cert_file, &tls.key_file, &tls.trusted_ca_file)?;
|
||||
if tls.force {
|
||||
tracing::info!("transport.tls.force=true — non-TLS control connections rejected");
|
||||
}
|
||||
Ok(Self {
|
||||
cfg,
|
||||
access,
|
||||
controls: Arc::new(Mutex::new(HashMap::new())),
|
||||
offline_clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
http_vhost,
|
||||
https_vhost,
|
||||
tls_config,
|
||||
metrics: MemMetrics::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let this = Arc::new(self);
|
||||
|
||||
if this.cfg.quic_enabled()
|
||||
&& this.cfg.kcp_enabled()
|
||||
&& this.cfg.quic_bind_port == this.cfg.kcp_bind_port
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"quicBindPort and kcpBindPort both use UDP and must differ (got {})",
|
||||
this.cfg.quic_bind_port
|
||||
));
|
||||
}
|
||||
|
||||
let tcp_addr = format!("{}:{}", this.cfg.bind_addr, this.cfg.bind_port);
|
||||
let tcp_listener = TcpListener::bind(&tcp_addr).await?;
|
||||
tracing::info!(
|
||||
%tcp_addr,
|
||||
ws_path = transport::ORBIEN_WEBSOCKET_PATH,
|
||||
tcp_mux = this.cfg.transport.tcp_mux,
|
||||
"tcp/websocket control/work listener ready"
|
||||
);
|
||||
|
||||
let vhost_shutdown = Arc::new(Notify::new());
|
||||
let mut set = JoinSet::new();
|
||||
|
||||
if let Some(ref vhost) = this.http_vhost {
|
||||
let bind = this.cfg.proxy_bind_addr.clone();
|
||||
let port = this.cfg.vhost_http_port;
|
||||
let vhost = Arc::clone(vhost);
|
||||
let access = Arc::clone(&this.access);
|
||||
let shutdown = Arc::clone(&vhost_shutdown);
|
||||
set.spawn(
|
||||
async move { run_vhost_http_listener(bind, port, vhost, access, shutdown).await },
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref vhost) = this.https_vhost {
|
||||
let bind = this.cfg.proxy_bind_addr.clone();
|
||||
let port = this.cfg.vhost_https_port;
|
||||
let vhost = Arc::clone(vhost);
|
||||
let access = Arc::clone(&this.access);
|
||||
let shutdown = Arc::clone(&vhost_shutdown);
|
||||
set.spawn(async move {
|
||||
run_vhost_https_listener(bind, port, vhost, access, shutdown).await
|
||||
});
|
||||
}
|
||||
|
||||
if this.cfg.quic_enabled() {
|
||||
let quic_addr: SocketAddr =
|
||||
format!("{}:{}", this.cfg.bind_addr, this.cfg.quic_bind_port)
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("invalid quic bind addr: {e}"))?;
|
||||
let endpoint = transport::build_server_endpoint(
|
||||
quic_addr,
|
||||
this.cfg.transport.quic.keepalive(),
|
||||
this.cfg.transport.quic.idle_timeout(),
|
||||
this.cfg.transport.quic.max_incoming_streams,
|
||||
&this.cfg.transport.tls.cert_file,
|
||||
&this.cfg.transport.tls.key_file,
|
||||
&this.cfg.transport.tls.trusted_ca_file,
|
||||
)?;
|
||||
tracing::info!(%quic_addr, "quic control/work listener ready");
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { svc.run_quic(endpoint).await });
|
||||
}
|
||||
|
||||
if this.cfg.kcp_enabled() {
|
||||
let kcp_addr: SocketAddr = format!("{}:{}", this.cfg.bind_addr, this.cfg.kcp_bind_port)
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("invalid kcp bind addr: {e}"))?;
|
||||
let listener = transport::bind_kcp_listener(kcp_addr).await?;
|
||||
tracing::info!(
|
||||
%kcp_addr,
|
||||
tcp_mux = this.cfg.transport.tcp_mux,
|
||||
"kcp control/work listener ready"
|
||||
);
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { svc.run_kcp(listener).await });
|
||||
}
|
||||
|
||||
if this.cfg.web_server.enabled() {
|
||||
let web_cfg = this.cfg.web_server.clone();
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { crate::dashboard::run(svc, web_cfg).await });
|
||||
}
|
||||
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { svc.run_tcp(tcp_listener).await });
|
||||
|
||||
let first = set
|
||||
.join_next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no listener tasks"))?;
|
||||
vhost_shutdown.notify_waiters();
|
||||
set.abort_all();
|
||||
while set.join_next().await.is_some() {}
|
||||
|
||||
match first {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(e) if e.is_cancelled() => Ok(()),
|
||||
Err(e) => Err(anyhow!("listener task join: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_tcp(self: Arc<Self>, listener: TcpListener) -> Result<()> {
|
||||
loop {
|
||||
let (stream, peer) = listener.accept().await?;
|
||||
let svc = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = svc.handle_tcp_or_websocket(stream, peer).await {
|
||||
tracing::warn!(%peer, error = %e, "tcp/ws connection closed with error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tcp_or_websocket(
|
||||
self: Arc<Self>,
|
||||
stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
let mut peek_buf = [0u8; 16];
|
||||
let n = stream.peek(&mut peek_buf).await.unwrap_or(0);
|
||||
let physical = if transport::is_websocket_http_request(&peek_buf[..n]) {
|
||||
tracing::debug!(%peer, transport = "websocket", "upgrade");
|
||||
transport::accept_websocket(stream).await?
|
||||
} else {
|
||||
tracing::debug!(%peer, transport = "tcp", "incoming connection");
|
||||
boxed_stream(stream)
|
||||
};
|
||||
let physical = transport::check_and_enable_tls(
|
||||
physical,
|
||||
Arc::clone(&self.tls_config),
|
||||
self.cfg.transport.tls.force,
|
||||
)
|
||||
.await?;
|
||||
self.handle_physical(physical, peer).await
|
||||
}
|
||||
|
||||
async fn handle_physical(self: Arc<Self>, physical: DynStream, peer: SocketAddr) -> Result<()> {
|
||||
if self.cfg.transport.tcp_mux {
|
||||
tracing::debug!(%peer, "yamux server session started");
|
||||
let svc = Arc::clone(&self);
|
||||
transport::serve_yamux_session(physical, move |stream| {
|
||||
let svc = Arc::clone(&svc);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = svc.handle_connection(stream, peer).await {
|
||||
tracing::debug!(error = %e, "yamux stream closed with error");
|
||||
}
|
||||
});
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
|
||||
if msg.contains("unknown version: 111") || msg.contains("unknown version: 119") {
|
||||
anyhow!(
|
||||
"yamux session {peer}: {e} — transport.tcpMux mismatch: server expects yamux \
|
||||
(tcpMux=true) but peer sent a raw control frame (Login 'o'=111 / NewWorkConn 'w'=119). \
|
||||
Set the same tcpMux on orbien and orbien-server, then restart both."
|
||||
)
|
||||
} else {
|
||||
anyhow!("yamux session {peer}: {e}")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.handle_connection(physical, peer).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_kcp(self: Arc<Self>, mut listener: kcp_tokio::KcpListener) -> Result<()> {
|
||||
loop {
|
||||
let (stream, peer) = transport::accept_kcp(&mut listener).await?;
|
||||
tracing::debug!(%peer, transport = "kcp", "incoming connection");
|
||||
let svc = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
let result = async {
|
||||
let stream = transport::check_and_enable_tls(
|
||||
stream,
|
||||
Arc::clone(&svc.tls_config),
|
||||
svc.cfg.transport.tls.force,
|
||||
)
|
||||
.await?;
|
||||
svc.handle_physical(stream, peer).await
|
||||
}
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(%peer, error = %e, "kcp connection closed with error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_quic(self: Arc<Self>, endpoint: quinn::Endpoint) -> Result<()> {
|
||||
loop {
|
||||
let incoming = endpoint
|
||||
.accept()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("quic endpoint closed"))?;
|
||||
let svc = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
match incoming.await {
|
||||
Ok(conn) => {
|
||||
let peer = conn.remote_address();
|
||||
tracing::info!(%peer, "quic session accepted");
|
||||
if let Err(e) = svc.handle_quic_connection(conn).await {
|
||||
tracing::debug!(%peer, error = %e, "quic session ended");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "quic accept failed"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_quic_connection(self: Arc<Self>, conn: quinn::Connection) -> Result<()> {
|
||||
loop {
|
||||
let (send, recv) = conn.accept_bi().await?;
|
||||
let stream = transport::quic_bi(send, recv);
|
||||
let svc = Arc::clone(&self);
|
||||
let peer = conn.remote_address();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = svc.handle_connection(stream, peer).await {
|
||||
tracing::debug!(%peer, error = %e, "quic stream closed with error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
self: Arc<Self>,
|
||||
mut stream: DynStream,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
let first = msg::read_msg(&mut stream).await?;
|
||||
match first {
|
||||
Message::Login(login) => self.register_control(stream, login, peer).await,
|
||||
Message::NewWorkConn(nw) => self.register_work_conn(stream, nw).await,
|
||||
other => Err(anyhow!("unexpected first message: {:?}", other.type_byte())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_control(
|
||||
self: Arc<Self>,
|
||||
stream: DynStream,
|
||||
login: Login,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
if !auth::verify_login(&self.cfg.auth.token, &login.privilege_key, login.timestamp) {
|
||||
let mut stream = stream;
|
||||
let _ = msg::write_msg(
|
||||
&mut stream,
|
||||
&Message::LoginResp(LoginResp {
|
||||
version: VERSION.into(),
|
||||
run_id: String::new(),
|
||||
error: "authorization failed".into(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
return Err(anyhow!("authorization failed"));
|
||||
}
|
||||
|
||||
let run_id = if login.run_id.is_empty() {
|
||||
short_run_id()
|
||||
} else {
|
||||
login.run_id.clone()
|
||||
};
|
||||
|
||||
let mut stream = stream;
|
||||
msg::write_msg(
|
||||
&mut stream,
|
||||
&Message::LoginResp(LoginResp {
|
||||
version: VERSION.into(),
|
||||
run_id: run_id.clone(),
|
||||
error: String::new(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!(%run_id, %peer, pool = login.pool_count, "client logged in");
|
||||
|
||||
let max_pool = self.cfg.transport.max_pool_count.max(0) as usize;
|
||||
let pool_count = (login.pool_count.max(0) as usize).min(max_pool);
|
||||
|
||||
let client_ip = peer.ip().to_string();
|
||||
|
||||
let control = Control::new(
|
||||
run_id.clone(),
|
||||
stream,
|
||||
self.cfg.clone(),
|
||||
pool_count,
|
||||
self.http_vhost.clone(),
|
||||
self.https_vhost.clone(),
|
||||
Arc::clone(&self.access),
|
||||
login.user.clone(),
|
||||
login.hostname.clone(),
|
||||
login.os.clone(),
|
||||
login.arch.clone(),
|
||||
login.version.clone(),
|
||||
client_ip,
|
||||
Arc::clone(&self.metrics),
|
||||
);
|
||||
let control = Arc::new(control);
|
||||
|
||||
{
|
||||
let mut offline = self.offline_clients.lock().await;
|
||||
offline.remove(&run_id);
|
||||
}
|
||||
|
||||
let old = {
|
||||
let mut map = self.controls.lock().await;
|
||||
map.insert(run_id.clone(), Arc::clone(&control))
|
||||
};
|
||||
|
||||
if let Some(old) = old {
|
||||
old.shutdown().await;
|
||||
}
|
||||
|
||||
self.metrics.new_client(&run_id);
|
||||
|
||||
let controls = Arc::clone(&self.controls);
|
||||
let offline_clients = Arc::clone(&self.offline_clients);
|
||||
let metrics = Arc::clone(&self.metrics);
|
||||
let rid = run_id.clone();
|
||||
let result = Arc::clone(&control).run().await;
|
||||
control.shutdown().await;
|
||||
metrics.close_client();
|
||||
|
||||
let proxy_count = control.proxy_count().await;
|
||||
let mut map = controls.lock().await;
|
||||
if map
|
||||
.get(&rid)
|
||||
.map(|c| Arc::ptr_eq(c, &control))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
map.remove(&rid);
|
||||
}
|
||||
if !map.contains_key(&rid) {
|
||||
drop(map);
|
||||
let mut offline = offline_clients.lock().await;
|
||||
offline.insert(
|
||||
rid.clone(),
|
||||
OfflineClientRecord {
|
||||
run_id: rid,
|
||||
user: control.user.clone(),
|
||||
hostname: control.hostname.clone(),
|
||||
os: control.os.clone(),
|
||||
arch: control.arch.clone(),
|
||||
client_ip: control.client_ip.clone(),
|
||||
version: control.version.clone(),
|
||||
proxy_count,
|
||||
disconnected_at: Instant::now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn register_work_conn(self: Arc<Self>, stream: DynStream, nw: NewWorkConn) -> Result<()> {
|
||||
let control = {
|
||||
let map = self.controls.lock().await;
|
||||
map.get(&nw.run_id).cloned()
|
||||
};
|
||||
match control {
|
||||
Some(c) => {
|
||||
c.push_work_conn(stream).await;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow!("unknown run_id for work conn: {}", nw.run_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg(&self) -> &ServerConfig {
|
||||
&self.cfg
|
||||
}
|
||||
|
||||
pub fn metrics(&self) -> &Arc<MemMetrics> {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
pub async fn kick_client(&self, run_id: &str) -> Result<()> {
|
||||
let control = {
|
||||
let mut map = self.controls.lock().await;
|
||||
map.remove(run_id)
|
||||
};
|
||||
match control {
|
||||
Some(c) => {
|
||||
let proxy_count = c.proxy_count().await;
|
||||
{
|
||||
let mut offline = self.offline_clients.lock().await;
|
||||
offline.insert(
|
||||
run_id.to_string(),
|
||||
OfflineClientRecord {
|
||||
run_id: run_id.to_string(),
|
||||
user: c.user.clone(),
|
||||
hostname: c.hostname.clone(),
|
||||
os: c.os.clone(),
|
||||
arch: c.arch.clone(),
|
||||
client_ip: c.client_ip.clone(),
|
||||
version: c.version.clone(),
|
||||
proxy_count,
|
||||
disconnected_at: Instant::now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
c.kick("kicked from dashboard").await;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow!("client not online: {run_id}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dashboard_snapshot(&self) -> DashboardSnapshot {
|
||||
use crate::dashboard::model::{ClientInfo, ProxyInfo};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let controls = self.controls.lock().await;
|
||||
let offline = self.offline_clients.lock().await;
|
||||
let mut clients = Vec::with_capacity(controls.len() + offline.len());
|
||||
let mut proxies = Vec::new();
|
||||
let mut proxy_type_count: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut online_ids = std::collections::HashSet::new();
|
||||
|
||||
for (_, ctrl) in controls.iter() {
|
||||
let proxy_count = ctrl.proxy_count().await;
|
||||
online_ids.insert(ctrl.run_id.clone());
|
||||
let mut cur_conns = 0usize;
|
||||
let mut client_proxies = Vec::new();
|
||||
for s in ctrl.proxy_summaries().await {
|
||||
*proxy_type_count.entry(s.proxy_type.clone()).or_default() += 1;
|
||||
let traffic = self.metrics.proxy_snapshot(&s.name);
|
||||
let proxy_conns = traffic.as_ref().map(|t| t.cur_conns).unwrap_or(0);
|
||||
cur_conns += proxy_conns;
|
||||
client_proxies.push(ProxyInfo {
|
||||
name: s.name,
|
||||
proxy_type: s.proxy_type,
|
||||
remote_addr: s.remote_addr,
|
||||
local_addr: s.local_addr,
|
||||
client_id: ctrl.run_id.clone(),
|
||||
status: s.status,
|
||||
today_traffic_in: traffic.as_ref().map(|t| t.today_traffic_in).unwrap_or(0),
|
||||
today_traffic_out: traffic.as_ref().map(|t| t.today_traffic_out).unwrap_or(0),
|
||||
cur_conns: proxy_conns,
|
||||
last_start_time: traffic
|
||||
.as_ref()
|
||||
.and_then(|t| format_proxy_time(t.last_start_at)),
|
||||
});
|
||||
}
|
||||
clients.push(ClientInfo {
|
||||
run_id: ctrl.run_id.clone(),
|
||||
user: ctrl.user.clone(),
|
||||
hostname: ctrl.hostname.clone(),
|
||||
os: ctrl.os.clone(),
|
||||
arch: ctrl.arch.clone(),
|
||||
client_ip: ctrl.client_ip.clone(),
|
||||
version: ctrl.version.clone(),
|
||||
proxy_count,
|
||||
cur_conns,
|
||||
connected_secs: ctrl.connected_at.elapsed().as_secs(),
|
||||
status: "online".into(),
|
||||
});
|
||||
proxies.extend(client_proxies);
|
||||
}
|
||||
|
||||
for (id, rec) in offline.iter() {
|
||||
if online_ids.contains(id) {
|
||||
continue;
|
||||
}
|
||||
clients.push(ClientInfo {
|
||||
run_id: rec.run_id.clone(),
|
||||
user: rec.user.clone(),
|
||||
hostname: rec.hostname.clone(),
|
||||
os: rec.os.clone(),
|
||||
arch: rec.arch.clone(),
|
||||
client_ip: rec.client_ip.clone(),
|
||||
version: rec.version.clone(),
|
||||
proxy_count: rec.proxy_count,
|
||||
cur_conns: 0,
|
||||
connected_secs: rec.disconnected_at.elapsed().as_secs(),
|
||||
status: "offline".into(),
|
||||
});
|
||||
}
|
||||
|
||||
clients.sort_by(|a, b| {
|
||||
let ao = a.status == "online";
|
||||
let bo = b.status == "online";
|
||||
bo.cmp(&ao).then_with(|| a.run_id.cmp(&b.run_id))
|
||||
});
|
||||
proxies.sort_by(|a, b| a.name.cmp(&b.name).then(a.client_id.cmp(&b.client_id)));
|
||||
|
||||
let server_stats = self.metrics.server_snapshot();
|
||||
let total_clients = clients.len();
|
||||
|
||||
DashboardSnapshot {
|
||||
clients,
|
||||
proxies,
|
||||
proxy_type_count,
|
||||
cur_conns: server_stats.cur_conns,
|
||||
total_client_counts: total_clients,
|
||||
total_traffic_in: server_stats.total_traffic_in,
|
||||
total_traffic_out: server_stats.total_traffic_out,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DashboardSnapshot {
|
||||
pub clients: Vec<crate::dashboard::model::ClientInfo>,
|
||||
pub proxies: Vec<crate::dashboard::model::ProxyInfo>,
|
||||
pub proxy_type_count: std::collections::BTreeMap<String, usize>,
|
||||
pub cur_conns: usize,
|
||||
pub total_client_counts: usize,
|
||||
pub total_traffic_in: u64,
|
||||
pub total_traffic_out: u64,
|
||||
}
|
||||
|
||||
fn short_run_id() -> String {
|
||||
let hex = Uuid::new_v4().simple().to_string();
|
||||
hex[..16].to_owned()
|
||||
}
|
||||
|
||||
fn format_proxy_time(unix: Option<i64>) -> Option<String> {
|
||||
let ts = unix?;
|
||||
let dt = chrono::DateTime::from_timestamp(ts, 0)?;
|
||||
Some(
|
||||
dt.with_timezone(&chrono::Local)
|
||||
.format("%m-%d %H:%M:%S")
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::Service;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct DashboardSnapshot {
|
||||
pub clients: Vec<crate::dashboard::model::ClientInfo>,
|
||||
pub tunnels: Vec<crate::dashboard::model::TunnelInfo>,
|
||||
pub tunnel_type_count: BTreeMap<String, usize>,
|
||||
pub active_conns: usize,
|
||||
pub total_client_counts: usize,
|
||||
pub total_traffic_in: u64,
|
||||
pub total_traffic_out: u64,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub async fn dashboard_snapshot(&self) -> DashboardSnapshot {
|
||||
use crate::dashboard::model::{ClientInfo, TunnelInfo};
|
||||
|
||||
let controls = self.controls.lock().await;
|
||||
let offline = self.offline_clients.lock().await;
|
||||
let mut clients = Vec::with_capacity(controls.len() + offline.len());
|
||||
let mut tunnels = Vec::new();
|
||||
let mut tunnel_type_count: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut online_ids = std::collections::HashSet::new();
|
||||
|
||||
for (_, ctrl) in controls.iter() {
|
||||
let tunnel_count = ctrl.tunnel_count().await;
|
||||
online_ids.insert(ctrl.session_id.clone());
|
||||
let mut active_conns = 0usize;
|
||||
let mut client_tunnels = Vec::new();
|
||||
for s in ctrl.tunnel_summaries().await {
|
||||
*tunnel_type_count.entry(s.tunnel_type.clone()).or_default() += 1;
|
||||
let traffic = self.metrics.tunnel_snapshot(&s.name);
|
||||
let tunnel_conns = traffic.as_ref().map(|t| t.active_conns).unwrap_or(0);
|
||||
active_conns += tunnel_conns;
|
||||
client_tunnels.push(TunnelInfo {
|
||||
name: s.name,
|
||||
tunnel_type: s.tunnel_type,
|
||||
remote_addr: s.remote_addr,
|
||||
local_addr: s.local_addr,
|
||||
session_id: ctrl.session_id.clone(),
|
||||
status: s.status,
|
||||
today_traffic_in: traffic.as_ref().map(|t| t.today_traffic_in).unwrap_or(0),
|
||||
today_traffic_out: traffic.as_ref().map(|t| t.today_traffic_out).unwrap_or(0),
|
||||
active_conns: tunnel_conns,
|
||||
last_start_time: traffic
|
||||
.as_ref()
|
||||
.and_then(|t| format_tunnel_time(t.last_start_at)),
|
||||
});
|
||||
}
|
||||
clients.push(ClientInfo {
|
||||
session_id: ctrl.session_id.clone(),
|
||||
user: ctrl.user.clone(),
|
||||
hostname: ctrl.hostname.clone(),
|
||||
os: ctrl.os.clone(),
|
||||
arch: ctrl.arch.clone(),
|
||||
client_ip: ctrl.client_ip.clone(),
|
||||
version: ctrl.version.clone(),
|
||||
tunnel_count,
|
||||
active_conns,
|
||||
connected_secs: ctrl.connected_at.elapsed().as_secs(),
|
||||
status: "online".into(),
|
||||
});
|
||||
tunnels.extend(client_tunnels);
|
||||
}
|
||||
|
||||
for (id, rec) in offline.iter() {
|
||||
if online_ids.contains(id) {
|
||||
continue;
|
||||
}
|
||||
clients.push(ClientInfo {
|
||||
session_id: rec.session_id.clone(),
|
||||
user: rec.user.clone(),
|
||||
hostname: rec.hostname.clone(),
|
||||
os: rec.os.clone(),
|
||||
arch: rec.arch.clone(),
|
||||
client_ip: rec.client_ip.clone(),
|
||||
version: rec.version.clone(),
|
||||
tunnel_count: rec.tunnel_count,
|
||||
active_conns: 0,
|
||||
connected_secs: rec.disconnected_at.elapsed().as_secs(),
|
||||
status: "offline".into(),
|
||||
});
|
||||
}
|
||||
|
||||
clients.sort_by(|a, b| {
|
||||
let ao = a.status == "online";
|
||||
let bo = b.status == "online";
|
||||
bo.cmp(&ao).then_with(|| a.session_id.cmp(&b.session_id))
|
||||
});
|
||||
tunnels.sort_by(|a, b| a.name.cmp(&b.name).then(a.session_id.cmp(&b.session_id)));
|
||||
|
||||
let server_stats = self.metrics.server_snapshot();
|
||||
let total_clients = clients.len();
|
||||
|
||||
DashboardSnapshot {
|
||||
clients,
|
||||
tunnels,
|
||||
tunnel_type_count,
|
||||
active_conns: server_stats.active_conns,
|
||||
total_client_counts: total_clients,
|
||||
total_traffic_in: server_stats.total_traffic_in,
|
||||
total_traffic_out: server_stats.total_traffic_out,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_tunnel_time(unix: Option<i64>) -> Option<String> {
|
||||
let ts = unix?;
|
||||
let dt = chrono::DateTime::from_timestamp(ts, 0)?;
|
||||
Some(
|
||||
dt.with_timezone(&chrono::Local)
|
||||
.format("%m-%d %H:%M:%S")
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use super::Service;
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::msg::{self, Message};
|
||||
use orbien_core::transport::{self, boxed_stream, DynStream};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
impl Service {
|
||||
pub(super) async fn run_tcp(self: Arc<Self>, listener: TcpListener) -> Result<()> {
|
||||
loop {
|
||||
let (stream, peer) = listener.accept().await?;
|
||||
let svc = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = svc.handle_tcp_or_websocket(stream, peer).await {
|
||||
tracing::warn!(%peer, error = %e, "tcp/ws connection closed with error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tcp_or_websocket(
|
||||
self: Arc<Self>,
|
||||
stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
let mut peek_buf = [0u8; 16];
|
||||
let n = stream.peek(&mut peek_buf).await.unwrap_or(0);
|
||||
let physical = if transport::is_websocket_http_request(&peek_buf[..n]) {
|
||||
tracing::debug!(%peer, transport = "websocket", "upgrade");
|
||||
transport::accept_websocket(stream).await?
|
||||
} else {
|
||||
tracing::debug!(%peer, transport = "tcp", "incoming connection");
|
||||
boxed_stream(stream)
|
||||
};
|
||||
let physical = transport::check_and_enable_tls(
|
||||
physical,
|
||||
Arc::clone(&self.tls_config),
|
||||
self.cfg.transport.tls.force,
|
||||
)
|
||||
.await?;
|
||||
self.handle_physical(physical, peer).await
|
||||
}
|
||||
|
||||
async fn handle_physical(self: Arc<Self>, physical: DynStream, peer: SocketAddr) -> Result<()> {
|
||||
if self.cfg.transport.tcp_mux {
|
||||
tracing::debug!(%peer, "yamux server session started");
|
||||
let svc = Arc::clone(&self);
|
||||
transport::serve_yamux_session(physical, move |stream| {
|
||||
let svc = Arc::clone(&svc);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = svc.handle_connection(stream, peer).await {
|
||||
tracing::debug!(error = %e, "yamux stream closed with error");
|
||||
}
|
||||
});
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
|
||||
if msg.contains("unknown version: 65") || msg.contains("unknown version: 87") {
|
||||
anyhow!(
|
||||
"yamux session {peer}: {e} — transport.tcpMux mismatch: server expects yamux \
|
||||
(tcpMux=true) but peer sent a raw control frame (Login 'A'=65 / NewDataConn 'W'=87). \
|
||||
Set the same tcpMux on orbien and orbien-server, then restart both."
|
||||
)
|
||||
} else {
|
||||
anyhow!("yamux session {peer}: {e}")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.handle_connection(physical, peer).await
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn run_kcp(self: Arc<Self>, mut listener: kcp_tokio::KcpListener) -> Result<()> {
|
||||
loop {
|
||||
let (stream, peer) = transport::accept_kcp(&mut listener).await?;
|
||||
tracing::debug!(%peer, transport = "kcp", "incoming connection");
|
||||
let svc = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
let result = async {
|
||||
let stream = transport::check_and_enable_tls(
|
||||
stream,
|
||||
Arc::clone(&svc.tls_config),
|
||||
svc.cfg.transport.tls.force,
|
||||
)
|
||||
.await?;
|
||||
svc.handle_physical(stream, peer).await
|
||||
}
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(%peer, error = %e, "kcp connection closed with error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn run_quic(self: Arc<Self>, endpoint: quinn::Endpoint) -> Result<()> {
|
||||
loop {
|
||||
let incoming = endpoint
|
||||
.accept()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("quic endpoint closed"))?;
|
||||
let svc = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
match incoming.await {
|
||||
Ok(conn) => {
|
||||
let peer = conn.remote_address();
|
||||
tracing::info!(%peer, "quic session accepted");
|
||||
if let Err(e) = svc.handle_quic_connection(conn).await {
|
||||
tracing::debug!(%peer, error = %e, "quic session ended");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "quic accept failed"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_quic_connection(self: Arc<Self>, conn: quinn::Connection) -> Result<()> {
|
||||
loop {
|
||||
let (send, recv) = conn.accept_bi().await?;
|
||||
let stream = transport::quic_bi(send, recv);
|
||||
let svc = Arc::clone(&self);
|
||||
let peer = conn.remote_address();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = svc.handle_connection(stream, peer).await {
|
||||
tracing::debug!(%peer, error = %e, "quic stream closed with error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
self: Arc<Self>,
|
||||
mut stream: DynStream,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
let first = msg::read_msg(&mut stream).await?;
|
||||
match first {
|
||||
Message::Login(login) => self.register_control(stream, login, peer).await,
|
||||
Message::NewDataConn(nw) => self.register_data_conn(stream, nw).await,
|
||||
other => Err(anyhow!("unexpected first message: {:?}", other.type_byte())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
mod dashboard_view;
|
||||
mod ingress;
|
||||
mod session_registry;
|
||||
|
||||
use crate::access::AccessPolicy;
|
||||
use crate::control::Control;
|
||||
use crate::metrics::MemMetrics;
|
||||
use crate::tunnel::{run_http_gw_listener, run_https_gw_listener, HttpGw, HttpsGw};
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::config::ServerConfig;
|
||||
use orbien_core::transport;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
#[allow(unused_imports)] // public API re-export
|
||||
pub use dashboard_view::DashboardSnapshot;
|
||||
|
||||
struct OfflineClientRecord {
|
||||
session_id: String,
|
||||
user: String,
|
||||
hostname: String,
|
||||
os: String,
|
||||
arch: String,
|
||||
client_ip: String,
|
||||
version: String,
|
||||
tunnel_count: usize,
|
||||
disconnected_at: Instant,
|
||||
}
|
||||
|
||||
pub struct Service {
|
||||
cfg: ServerConfig,
|
||||
access: Arc<AccessPolicy>,
|
||||
controls: Arc<Mutex<HashMap<String, Arc<Control>>>>,
|
||||
offline_clients: Arc<Mutex<HashMap<String, OfflineClientRecord>>>,
|
||||
http_gw: Option<Arc<HttpGw>>,
|
||||
https_gw: Option<Arc<HttpsGw>>,
|
||||
tls_config: Arc<rustls::ServerConfig>,
|
||||
metrics: Arc<MemMetrics>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(cfg: ServerConfig) -> Result<Self> {
|
||||
let access = Arc::new(AccessPolicy::from_server_config(&cfg)?);
|
||||
let http_gw = if cfg.http_gw_enabled() {
|
||||
Some(Arc::new(HttpGw::new(cfg.http_gw_port)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let https_gw = if cfg.https_gw_enabled() {
|
||||
Some(Arc::new(HttpsGw::new(cfg.https_gw_port)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tls = &cfg.transport.tls;
|
||||
let tls_config =
|
||||
transport::new_server_tls_config(&tls.cert_file, &tls.key_file, &tls.trusted_ca_file)?;
|
||||
if tls.force {
|
||||
tracing::info!("transport.tls.force=true — non-TLS control connections rejected");
|
||||
}
|
||||
Ok(Self {
|
||||
cfg,
|
||||
access,
|
||||
controls: Arc::new(Mutex::new(HashMap::new())),
|
||||
offline_clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
http_gw,
|
||||
https_gw,
|
||||
tls_config,
|
||||
metrics: MemMetrics::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let this = Arc::new(self);
|
||||
|
||||
let tcp_addr = this.cfg.listen.clone();
|
||||
let tcp_listener = TcpListener::bind(&tcp_addr).await?;
|
||||
tracing::info!(
|
||||
%tcp_addr,
|
||||
ws_path = transport::ORBIEN_WEBSOCKET_PATH,
|
||||
tcp_mux = this.cfg.transport.tcp_mux,
|
||||
"tcp/websocket control/data listener ready"
|
||||
);
|
||||
|
||||
let gw_shutdown = Arc::new(Notify::new());
|
||||
let mut set = JoinSet::new();
|
||||
let listen_host = this
|
||||
.cfg
|
||||
.listen_host()
|
||||
.map_err(|e| anyhow!("invalid listen: {e}"))?;
|
||||
|
||||
if let Some(ref gw) = this.http_gw {
|
||||
let bind = this.cfg.proxy_addr.clone();
|
||||
let port = this.cfg.http_gw_port;
|
||||
let gw = Arc::clone(gw);
|
||||
let access = Arc::clone(&this.access);
|
||||
let shutdown = Arc::clone(&gw_shutdown);
|
||||
set.spawn(
|
||||
async move { run_http_gw_listener(bind, port, gw, access, shutdown).await },
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref gw) = this.https_gw {
|
||||
let bind = this.cfg.proxy_addr.clone();
|
||||
let port = this.cfg.https_gw_port;
|
||||
let gw = Arc::clone(gw);
|
||||
let access = Arc::clone(&this.access);
|
||||
let shutdown = Arc::clone(&gw_shutdown);
|
||||
set.spawn(async move {
|
||||
run_https_gw_listener(bind, port, gw, access, shutdown).await
|
||||
});
|
||||
}
|
||||
|
||||
if this.cfg.quic_enabled() {
|
||||
let quic_addr: SocketAddr = format!("{}:{}", listen_host, this.cfg.quic_port)
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("invalid quic bind addr: {e}"))?;
|
||||
let endpoint = transport::build_server_endpoint(
|
||||
quic_addr,
|
||||
this.cfg.transport.quic.keepalive(),
|
||||
this.cfg.transport.quic.idle_timeout(),
|
||||
this.cfg.transport.quic.max_incoming_streams,
|
||||
&this.cfg.transport.tls.cert_file,
|
||||
&this.cfg.transport.tls.key_file,
|
||||
&this.cfg.transport.tls.trusted_ca_file,
|
||||
)?;
|
||||
tracing::info!(%quic_addr, "quic control/data listener ready");
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { svc.run_quic(endpoint).await });
|
||||
}
|
||||
|
||||
if this.cfg.kcp_enabled() {
|
||||
let kcp_addr: SocketAddr = format!("{}:{}", listen_host, this.cfg.kcp_port)
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("invalid kcp bind addr: {e}"))?;
|
||||
let listener = transport::bind_kcp_listener(kcp_addr).await?;
|
||||
tracing::info!(
|
||||
%kcp_addr,
|
||||
tcp_mux = this.cfg.transport.tcp_mux,
|
||||
"kcp control/data listener ready"
|
||||
);
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { svc.run_kcp(listener).await });
|
||||
}
|
||||
|
||||
if this.cfg.dashboard.enabled() {
|
||||
let web_cfg = this.cfg.dashboard.clone();
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { crate::dashboard::run(svc, web_cfg).await });
|
||||
}
|
||||
|
||||
let svc = Arc::clone(&this);
|
||||
set.spawn(async move { svc.run_tcp(tcp_listener).await });
|
||||
|
||||
let first = set
|
||||
.join_next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no listener tasks"))?;
|
||||
gw_shutdown.notify_waiters();
|
||||
set.abort_all();
|
||||
while set.join_next().await.is_some() {}
|
||||
|
||||
match first {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(e) if e.is_cancelled() => Ok(()),
|
||||
Err(e) => Err(anyhow!("listener task join: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg(&self) -> &ServerConfig {
|
||||
&self.cfg
|
||||
}
|
||||
|
||||
pub fn metrics(&self) -> &Arc<MemMetrics> {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
pub async fn kick_client(&self, session_id: &str) -> Result<()> {
|
||||
let control = {
|
||||
let mut map = self.controls.lock().await;
|
||||
map.remove(session_id)
|
||||
};
|
||||
match control {
|
||||
Some(c) => {
|
||||
let tunnel_count = c.tunnel_count().await;
|
||||
{
|
||||
let mut offline = self.offline_clients.lock().await;
|
||||
offline.insert(
|
||||
session_id.to_string(),
|
||||
OfflineClientRecord {
|
||||
session_id: session_id.to_string(),
|
||||
user: c.user.clone(),
|
||||
hostname: c.hostname.clone(),
|
||||
os: c.os.clone(),
|
||||
arch: c.arch.clone(),
|
||||
client_ip: c.client_ip.clone(),
|
||||
version: c.version.clone(),
|
||||
tunnel_count,
|
||||
disconnected_at: Instant::now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
c.kick("kicked from dashboard").await;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow!("client not online: {session_id}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
use super::{OfflineClientRecord, Service};
|
||||
use crate::control::Control;
|
||||
use crate::metrics::ServerMetrics;
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::auth;
|
||||
use orbien_core::msg::{self, Login, LoginResp, Message, NewDataConn};
|
||||
use orbien_core::transport::DynStream;
|
||||
use orbien_core::VERSION;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
impl Service {
|
||||
pub(crate) async fn register_control(
|
||||
self: Arc<Self>,
|
||||
stream: DynStream,
|
||||
login: Login,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
if !auth::verify_login(&self.cfg.auth.token, &login.auth_digest, login.timestamp) {
|
||||
let mut stream = stream;
|
||||
let _ = msg::write_msg(
|
||||
&mut stream,
|
||||
&Message::LoginResp(LoginResp {
|
||||
version: VERSION.into(),
|
||||
session_id: String::new(),
|
||||
error: "authorization failed".into(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
return Err(anyhow!("authorization failed"));
|
||||
}
|
||||
|
||||
let session_id = if login.session_id.is_empty() {
|
||||
short_session_id()
|
||||
} else {
|
||||
login.session_id.clone()
|
||||
};
|
||||
|
||||
let mut stream = stream;
|
||||
msg::write_msg(
|
||||
&mut stream,
|
||||
&Message::LoginResp(LoginResp {
|
||||
version: VERSION.into(),
|
||||
session_id: session_id.clone(),
|
||||
error: String::new(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!(%session_id, %peer, pool = login.pool_count, "client logged in");
|
||||
|
||||
let max_pool = self.cfg.transport.max_conn_pool.max(0) as usize;
|
||||
let pool_count = (login.pool_count.max(0) as usize).min(max_pool);
|
||||
|
||||
let client_ip = peer.ip().to_string();
|
||||
|
||||
let control = Control::new(
|
||||
session_id.clone(),
|
||||
stream,
|
||||
self.cfg.clone(),
|
||||
pool_count,
|
||||
self.http_gw.clone(),
|
||||
self.https_gw.clone(),
|
||||
Arc::clone(&self.access),
|
||||
login.user.clone(),
|
||||
login.hostname.clone(),
|
||||
login.os.clone(),
|
||||
login.arch.clone(),
|
||||
login.version.clone(),
|
||||
client_ip,
|
||||
Arc::clone(&self.metrics),
|
||||
);
|
||||
let control = Arc::new(control);
|
||||
|
||||
{
|
||||
let mut offline = self.offline_clients.lock().await;
|
||||
offline.remove(&session_id);
|
||||
}
|
||||
|
||||
let old = {
|
||||
let mut map = self.controls.lock().await;
|
||||
map.insert(session_id.clone(), Arc::clone(&control))
|
||||
};
|
||||
|
||||
if let Some(old) = old {
|
||||
old.shutdown().await;
|
||||
}
|
||||
|
||||
self.metrics.new_client(&session_id);
|
||||
|
||||
let controls = Arc::clone(&self.controls);
|
||||
let offline_clients = Arc::clone(&self.offline_clients);
|
||||
let metrics = Arc::clone(&self.metrics);
|
||||
let rid = session_id.clone();
|
||||
let result = Arc::clone(&control).run().await;
|
||||
control.shutdown().await;
|
||||
metrics.close_client();
|
||||
|
||||
let tunnel_count = control.tunnel_count().await;
|
||||
let mut map = controls.lock().await;
|
||||
if map
|
||||
.get(&rid)
|
||||
.map(|c| Arc::ptr_eq(c, &control))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
map.remove(&rid);
|
||||
}
|
||||
if !map.contains_key(&rid) {
|
||||
drop(map);
|
||||
let mut offline = offline_clients.lock().await;
|
||||
offline.insert(
|
||||
rid.clone(),
|
||||
OfflineClientRecord {
|
||||
session_id: rid,
|
||||
user: control.user.clone(),
|
||||
hostname: control.hostname.clone(),
|
||||
os: control.os.clone(),
|
||||
arch: control.arch.clone(),
|
||||
client_ip: control.client_ip.clone(),
|
||||
version: control.version.clone(),
|
||||
tunnel_count,
|
||||
disconnected_at: Instant::now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) async fn register_data_conn(
|
||||
self: Arc<Self>,
|
||||
stream: DynStream,
|
||||
nw: NewDataConn,
|
||||
) -> Result<()> {
|
||||
if nw.session_id.trim().is_empty() {
|
||||
return Err(anyhow!("empty session_id for data conn"));
|
||||
}
|
||||
if !auth::verify_auth_digest(&self.cfg.auth.token, &nw.auth_digest, nw.timestamp) {
|
||||
return Err(anyhow!(
|
||||
"data conn auth failed for session_id={}",
|
||||
nw.session_id
|
||||
));
|
||||
}
|
||||
let control = {
|
||||
let map = self.controls.lock().await;
|
||||
map.get(&nw.session_id).cloned()
|
||||
};
|
||||
match control {
|
||||
Some(c) => {
|
||||
c.push_data_conn(stream).await;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow!("unknown session_id for data conn: {}", nw.session_id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn short_session_id() -> String {
|
||||
let hex = Uuid::new_v4().simple().to_string();
|
||||
hex[..16].to_owned()
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
use crate::control::Control;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpRoute {
|
||||
pub tunnel_name: String,
|
||||
pub control: Weak<Control>,
|
||||
pub location: String,
|
||||
pub host_header_rewrite: String,
|
||||
pub basic_auth_user: String,
|
||||
pub basic_auth_password: String,
|
||||
pub route_by_http_user: String,
|
||||
pub limiter: Option<std::sync::Arc<orbien_core::limit::BandwidthLimiter>>,
|
||||
}
|
||||
|
||||
type DomainIndex = HashMap<String, HashMap<String, Vec<HttpRoute>>>;
|
||||
|
||||
pub struct HttpGw {
|
||||
routes: Mutex<DomainIndex>,
|
||||
pub listen_port: u16,
|
||||
}
|
||||
|
||||
impl HttpGw {
|
||||
pub fn new(listen_port: u16) -> Self {
|
||||
Self {
|
||||
routes: Mutex::new(HashMap::new()),
|
||||
listen_port,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(&self, domain: &str, route: HttpRoute) -> anyhow::Result<()> {
|
||||
let key = normalize_host(domain);
|
||||
if key.is_empty() {
|
||||
return Err(anyhow::anyhow!("empty http domain"));
|
||||
}
|
||||
let mut map = self.routes.lock().await;
|
||||
let by_user = map.entry(key.clone()).or_default();
|
||||
let list = by_user.entry(route.route_by_http_user.clone()).or_default();
|
||||
|
||||
if let Some(existing) = list.iter().find(|r| r.location == route.location) {
|
||||
if existing.tunnel_name != route.tunnel_name {
|
||||
return Err(anyhow::anyhow!(
|
||||
"router config conflict: domain={key} location={} routeByHTTPUser={}",
|
||||
route.location,
|
||||
route.route_by_http_user
|
||||
));
|
||||
}
|
||||
list.retain(|r| r.location != route.location);
|
||||
}
|
||||
|
||||
list.push(route);
|
||||
list.sort_by(|a, b| b.location.cmp(&a.location));
|
||||
tracing::info!(domain = %key, "http route registered");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unregister_tunnel(&self, tunnel_name: &str) {
|
||||
let mut map = self.routes.lock().await;
|
||||
map.retain(|_, by_user| {
|
||||
by_user.retain(|_, list| {
|
||||
list.retain(|r| r.tunnel_name != tunnel_name);
|
||||
!list.is_empty()
|
||||
});
|
||||
!by_user.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn lookup(&self, host: &str, path: &str, route_user: &str) -> Option<HttpRoute> {
|
||||
let key = normalize_host(host);
|
||||
let map = self.routes.lock().await;
|
||||
lookup_exact_or_all_users(&map, &key, path, route_user).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_exact_or_all_users<'a>(
|
||||
map: &'a DomainIndex,
|
||||
host: &str,
|
||||
path: &str,
|
||||
route_user: &str,
|
||||
) -> Option<&'a HttpRoute> {
|
||||
if let Some(r) = match_location(map, host, path, route_user) {
|
||||
return Some(r);
|
||||
}
|
||||
if !route_user.is_empty() {
|
||||
return match_location(map, host, path, "");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn match_location<'a>(
|
||||
map: &'a DomainIndex,
|
||||
host: &str,
|
||||
path: &str,
|
||||
route_user: &str,
|
||||
) -> Option<&'a HttpRoute> {
|
||||
let list = map.get(host)?.get(route_user)?;
|
||||
for route in list {
|
||||
if path.starts_with(&route.location) {
|
||||
return Some(route);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn normalize_host(host: &str) -> String {
|
||||
let host = host.trim();
|
||||
let without_port = if let Some(h) = host.strip_prefix('[') {
|
||||
if let Some(end) = h.find(']') {
|
||||
&h[..end]
|
||||
} else {
|
||||
host
|
||||
}
|
||||
} else {
|
||||
host.split(':').next().unwrap_or(host)
|
||||
};
|
||||
without_port
|
||||
.trim()
|
||||
.trim_end_matches('.')
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
pub fn build_domains(
|
||||
domains: &[String],
|
||||
root_domain: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let root = normalize_host(root_domain);
|
||||
let entries: Vec<String> = domains
|
||||
.iter()
|
||||
.map(|d| d.trim().to_string())
|
||||
.filter(|d| !d.is_empty())
|
||||
.collect();
|
||||
|
||||
if entries.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"http/https requires at least one domain (fullDomain or subdomain prefix)"
|
||||
));
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
for entry in &entries {
|
||||
out.push(expand_domain_entry(entry, &root)?);
|
||||
}
|
||||
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
out.retain(|d| seen.insert(d.clone()));
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn expand_domain_entry(entry: &str, root: &str) -> anyhow::Result<String> {
|
||||
let e = normalize_host(entry);
|
||||
if e.is_empty() {
|
||||
return Err(anyhow::anyhow!("empty domain entry"));
|
||||
}
|
||||
if e.starts_with('.') || e.ends_with('.') || e.contains("..") {
|
||||
return Err(anyhow::anyhow!("invalid domain entry: {entry:?}"));
|
||||
}
|
||||
|
||||
if !e.contains('.') {
|
||||
if e.contains('*') {
|
||||
return Err(anyhow::anyhow!(
|
||||
"subdomain prefix must not contain '*': {entry:?}"
|
||||
));
|
||||
}
|
||||
if e.len() > 63 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"subdomain prefix too long (max 63): {entry:?}"
|
||||
));
|
||||
}
|
||||
if root.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"domain prefix {entry:?} requires server rootDomain"
|
||||
));
|
||||
}
|
||||
return Ok(normalize_host(&format!("{e}.{root}")));
|
||||
}
|
||||
|
||||
if e.contains('*') {
|
||||
return Err(anyhow::anyhow!(
|
||||
"wildcard domains are not supported: {entry:?}"
|
||||
));
|
||||
}
|
||||
|
||||
if !root.is_empty() && is_host_under_root(&e, root) {
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
Ok(e)
|
||||
}
|
||||
|
||||
fn is_host_under_root(host: &str, root: &str) -> bool {
|
||||
host == root || host.ends_with(&format!(".{root}"))
|
||||
}
|
||||
|
||||
pub fn expand_locations(locations: &[String]) -> Vec<String> {
|
||||
if locations.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
locations.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_basic_auth(header_value: &str) -> Option<(String, String)> {
|
||||
const PREFIX: &str = "Basic ";
|
||||
let value = header_value.trim();
|
||||
if value.len() < PREFIX.len() || !value[..PREFIX.len()].eq_ignore_ascii_case(PREFIX) {
|
||||
return None;
|
||||
}
|
||||
use base64::Engine;
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
.decode(value[PREFIX.len()..].trim())
|
||||
.ok()?;
|
||||
let text = String::from_utf8(decoded).ok()?;
|
||||
let (user, pass) = text.split_once(':')?;
|
||||
Some((user.to_string(), pass.to_string()))
|
||||
}
|
||||
|
||||
pub fn route_user_from_headers(
|
||||
is_proxy_request: bool,
|
||||
authorization: Option<&str>,
|
||||
proxy_authorization: Option<&str>,
|
||||
) -> String {
|
||||
if is_proxy_request {
|
||||
if let Some(proxy_auth) = proxy_authorization {
|
||||
return parse_basic_auth(proxy_auth)
|
||||
.map(|(u, _)| u)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
return authorization
|
||||
.and_then(parse_basic_auth)
|
||||
.map(|(u, _)| u)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
authorization
|
||||
.and_then(parse_basic_auth)
|
||||
.map(|(u, _)| u)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn route_basic_auth_ok(
|
||||
route: &HttpRoute,
|
||||
is_proxy_request: bool,
|
||||
authorization: Option<&str>,
|
||||
proxy_authorization: Option<&str>,
|
||||
) -> bool {
|
||||
if route.basic_auth_user.is_empty() && route.basic_auth_password.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let creds = if is_proxy_request {
|
||||
let Some(h) = proxy_authorization else {
|
||||
return false;
|
||||
};
|
||||
parse_basic_auth(h)
|
||||
} else {
|
||||
let Some(h) = authorization else {
|
||||
return false;
|
||||
};
|
||||
parse_basic_auth(h)
|
||||
};
|
||||
match creds {
|
||||
Some((u, p)) => u == route.basic_auth_user && p == route.basic_auth_password,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
use super::gw::{
|
||||
build_domains, expand_locations, normalize_host, route_basic_auth_ok, route_user_from_headers,
|
||||
HttpRoute, HttpGw,
|
||||
};
|
||||
use crate::access::{prepare_ingress, AccessPolicy};
|
||||
use crate::control::Control;
|
||||
use crate::metrics::ServerMetrics;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use httparse::Status;
|
||||
use orbien_core::limit::{maybe_limit, BandwidthLimiter};
|
||||
use orbien_core::msg::NewTunnel;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
pub struct HttpTunnel {
|
||||
pub name: String,
|
||||
pub domains: Vec<String>,
|
||||
gw: Arc<HttpGw>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
impl HttpTunnel {
|
||||
pub async fn register(
|
||||
np: &NewTunnel,
|
||||
control: Arc<Control>,
|
||||
gw: Arc<HttpGw>,
|
||||
sub_domain_host: &str,
|
||||
limiter: Option<Arc<BandwidthLimiter>>,
|
||||
) -> Result<Self> {
|
||||
let domains = build_domains(&np.domains, sub_domain_host)?;
|
||||
let name = np.tunnel_name.clone();
|
||||
let locations = expand_locations(&np.locations);
|
||||
let rewrite = np.host_header_rewrite.clone();
|
||||
let basic_auth_user = np.basic_auth_user.clone();
|
||||
let basic_auth_password = np.basic_auth_password.clone();
|
||||
let route_by_http_user = np.route_by_http_user.clone();
|
||||
|
||||
gw.unregister_tunnel(&name).await;
|
||||
|
||||
for domain in &domains {
|
||||
for location in &locations {
|
||||
gw
|
||||
.register(
|
||||
domain,
|
||||
HttpRoute {
|
||||
tunnel_name: name.clone(),
|
||||
control: Arc::downgrade(&control),
|
||||
location: location.clone(),
|
||||
host_header_rewrite: rewrite.clone(),
|
||||
basic_auth_user: basic_auth_user.clone(),
|
||||
basic_auth_password: basic_auth_password.clone(),
|
||||
route_by_http_user: route_by_http_user.clone(),
|
||||
limiter: limiter.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
tunnel = %name,
|
||||
domains = ?domains,
|
||||
locations = ?locations,
|
||||
route_by_http_user = %route_by_http_user,
|
||||
basic_auth = !basic_auth_user.is_empty() || !basic_auth_password.is_empty(),
|
||||
"http tunnel registered"
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
domains,
|
||||
gw,
|
||||
closed: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn close(&self) {
|
||||
if self
|
||||
.closed
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
self.gw.unregister_tunnel(&self.name).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_http_gw_listener(
|
||||
bind_addr: String,
|
||||
port: u16,
|
||||
gw: Arc<HttpGw>,
|
||||
access: Arc<AccessPolicy>,
|
||||
shutdown: Arc<Notify>,
|
||||
) -> Result<()> {
|
||||
let addr = format!("{bind_addr}:{port}");
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!(%addr, "http gateway listener ready");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.notified() => break,
|
||||
accepted = listener.accept() => {
|
||||
match accepted {
|
||||
Ok((stream, peer)) => {
|
||||
orbien_core::net::enable_nodelay(&stream);
|
||||
let gw = Arc::clone(&gw);
|
||||
let access = Arc::clone(&access);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_http_ingress(gw, stream, peer, access).await {
|
||||
tracing::debug!(%peer, error = %e, "http ingress ended");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "http gateway accept failed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ParsedHttpHead {
|
||||
raw: Vec<u8>,
|
||||
host: String,
|
||||
path: String,
|
||||
is_proxy_request: bool,
|
||||
authorization: Option<String>,
|
||||
proxy_authorization: Option<String>,
|
||||
}
|
||||
|
||||
async fn handle_http_ingress(
|
||||
gw: Arc<HttpGw>,
|
||||
stream: TcpStream,
|
||||
peer: std::net::SocketAddr,
|
||||
access: Arc<AccessPolicy>,
|
||||
) -> Result<()> {
|
||||
let mut ingress = prepare_ingress(stream, peer, &access).await?;
|
||||
let head = read_http_request_head(&mut ingress.stream).await?;
|
||||
|
||||
let route_user = route_user_from_headers(
|
||||
head.is_proxy_request,
|
||||
head.authorization.as_deref(),
|
||||
head.proxy_authorization.as_deref(),
|
||||
);
|
||||
|
||||
let Some(route) = gw.lookup(&head.host, &head.path, &route_user).await else {
|
||||
tracing::debug!(
|
||||
peer = %ingress.peer,
|
||||
source = %ingress.source,
|
||||
host = %head.host,
|
||||
path = %head.path,
|
||||
%route_user,
|
||||
"http no route"
|
||||
);
|
||||
write_not_found(&mut ingress.stream).await;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if !route_basic_auth_ok(
|
||||
&route,
|
||||
head.is_proxy_request,
|
||||
head.authorization.as_deref(),
|
||||
head.proxy_authorization.as_deref(),
|
||||
) {
|
||||
tracing::debug!(
|
||||
tunnel = %route.tunnel_name,
|
||||
peer = %ingress.peer,
|
||||
proxy_mode = head.is_proxy_request,
|
||||
"http basic auth failed"
|
||||
);
|
||||
if head.is_proxy_request {
|
||||
write_proxy_unauthorized(&mut ingress.stream).await;
|
||||
} else {
|
||||
write_unauthorized(&mut ingress.stream).await;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(control) = route.control.upgrade() else {
|
||||
write_not_found(&mut ingress.stream).await;
|
||||
return Err(anyhow!("http tunnel client gone: {}", route.tunnel_name));
|
||||
};
|
||||
|
||||
let mut raw = head.raw;
|
||||
if !route.host_header_rewrite.is_empty() {
|
||||
rewrite_host_header(&mut raw, &route.host_header_rewrite)?;
|
||||
}
|
||||
|
||||
orbien_core::net::apply_x_forwarded_for(&mut raw, &ingress.source.ip().to_string(), "http")?;
|
||||
|
||||
let data = match control.get_data_conn().await {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
write_bad_gateway(&mut ingress.stream).await;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let data = control
|
||||
.start_data_conn(
|
||||
data,
|
||||
&route.tunnel_name,
|
||||
ingress.source.ip().to_string(),
|
||||
ingress.source.port(),
|
||||
ingress
|
||||
.local
|
||||
.map(|a| a.ip().to_string())
|
||||
.unwrap_or_default(),
|
||||
ingress.local.map(|a| a.port()).unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut data = maybe_limit(data, route.limiter.clone());
|
||||
let head_len = raw.len() as u64;
|
||||
data.write_all(&raw).await?;
|
||||
tracing::debug!(
|
||||
tunnel = %route.tunnel_name,
|
||||
host = %head.host,
|
||||
path = %head.path,
|
||||
peer = %ingress.peer,
|
||||
source = %ingress.source,
|
||||
"http joining ingress <-> data"
|
||||
);
|
||||
let _guard = control.metrics.track_connection(&route.tunnel_name, "http");
|
||||
let (to_data, from_data, err) = orbien_core::io::join_counted(ingress.stream, data).await;
|
||||
control
|
||||
.metrics
|
||||
.add_traffic_in(&route.tunnel_name, "http", to_data.saturating_add(head_len));
|
||||
control
|
||||
.metrics
|
||||
.add_traffic_out(&route.tunnel_name, "http", from_data);
|
||||
if let Some(e) = err {
|
||||
tracing::debug!(tunnel = %route.tunnel_name, error = %e, "http join ended");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_http_request_head<R: AsyncRead + Unpin>(stream: &mut R) -> Result<ParsedHttpHead> {
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
let mut tmp = [0u8; 2048];
|
||||
loop {
|
||||
let n = stream.read(&mut tmp).await?;
|
||||
if n == 0 {
|
||||
bail!("client closed before http headers completed");
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..n]);
|
||||
if buf.len() > 64 * 1024 {
|
||||
bail!("http headers too large");
|
||||
}
|
||||
|
||||
let mut headers = [httparse::EMPTY_HEADER; 64];
|
||||
let mut req = httparse::Request::new(&mut headers);
|
||||
match req.parse(&buf)? {
|
||||
Status::Complete(_) => {
|
||||
let method = req.method.unwrap_or("").to_string();
|
||||
let target = req.path.unwrap_or("/").to_string();
|
||||
let (is_proxy_request, path) = classify_request_target(&method, &target);
|
||||
|
||||
let host = req
|
||||
.headers
|
||||
.iter()
|
||||
.find(|h| h.name.eq_ignore_ascii_case("host"))
|
||||
.map(|h| String::from_utf8_lossy(h.value).into_owned())
|
||||
.or_else(|| host_from_absolute_target(&target))
|
||||
.ok_or_else(|| anyhow!("missing Host header"))?;
|
||||
|
||||
let authorization = header_value(&req, "authorization");
|
||||
let proxy_authorization = header_value(&req, "proxy-authorization");
|
||||
|
||||
return Ok(ParsedHttpHead {
|
||||
raw: buf,
|
||||
host: normalize_host(&host),
|
||||
path,
|
||||
is_proxy_request,
|
||||
authorization,
|
||||
proxy_authorization,
|
||||
});
|
||||
}
|
||||
Status::Partial => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn header_value(req: &httparse::Request<'_, '_>, name: &str) -> Option<String> {
|
||||
req.headers
|
||||
.iter()
|
||||
.find(|h| h.name.eq_ignore_ascii_case(name))
|
||||
.map(|h| String::from_utf8_lossy(h.value).into_owned())
|
||||
}
|
||||
|
||||
fn classify_request_target(method: &str, target: &str) -> (bool, String) {
|
||||
if method.eq_ignore_ascii_case("CONNECT") {
|
||||
return (true, "/".into());
|
||||
}
|
||||
if let Some(rest) = target
|
||||
.strip_prefix("http://")
|
||||
.or_else(|| target.strip_prefix("https://"))
|
||||
.or_else(|| target.strip_prefix("HTTP://"))
|
||||
.or_else(|| target.strip_prefix("HTTPS://"))
|
||||
{
|
||||
let path = match rest.find('/') {
|
||||
Some(i) => rest[i..].to_string(),
|
||||
None => "/".into(),
|
||||
};
|
||||
return (true, path);
|
||||
}
|
||||
(false, target.to_string())
|
||||
}
|
||||
|
||||
fn host_from_absolute_target(target: &str) -> Option<String> {
|
||||
let rest = target
|
||||
.strip_prefix("http://")
|
||||
.or_else(|| target.strip_prefix("https://"))
|
||||
.or_else(|| target.strip_prefix("HTTP://"))
|
||||
.or_else(|| target.strip_prefix("HTTPS://"))?;
|
||||
let hostport = rest.split('/').next().unwrap_or(rest);
|
||||
if hostport.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hostport.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrite_host_header(buf: &mut Vec<u8>, new_host: &str) -> Result<()> {
|
||||
let lower = b"host:";
|
||||
let text = String::from_utf8_lossy(buf);
|
||||
let mut out = String::new();
|
||||
let mut replaced = false;
|
||||
for line in text.split_inclusive('\n') {
|
||||
let trimmed_start = line.trim_start_matches([' ', '\t']);
|
||||
if !replaced
|
||||
&& trimmed_start.len() >= 5
|
||||
&& trimmed_start.as_bytes()[..5].eq_ignore_ascii_case(lower)
|
||||
{
|
||||
let ending = if line.ends_with("\r\n") {
|
||||
"\r\n"
|
||||
} else if line.ends_with('\n') {
|
||||
"\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
out.push_str("Host: ");
|
||||
out.push_str(new_host);
|
||||
out.push_str(ending);
|
||||
replaced = true;
|
||||
} else {
|
||||
out.push_str(line);
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
return Err(anyhow!("Host header not found for rewrite"));
|
||||
}
|
||||
*buf = out.into_bytes();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_not_found<W: AsyncWrite + Unpin>(stream: &mut W) {
|
||||
let body = "Not Found\n";
|
||||
let resp = format!(
|
||||
"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
}
|
||||
|
||||
async fn write_unauthorized<W: AsyncWrite + Unpin>(stream: &mut W) {
|
||||
let body = "Unauthorized\n";
|
||||
let resp = format!(
|
||||
"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"Restricted\"\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
}
|
||||
|
||||
async fn write_proxy_unauthorized<W: AsyncWrite + Unpin>(stream: &mut W) {
|
||||
let body = "Proxy Authentication Required\n";
|
||||
let resp = format!(
|
||||
"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"Restricted\"\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
}
|
||||
|
||||
async fn write_bad_gateway<W: AsyncWrite + Unpin>(stream: &mut W) {
|
||||
let body = "Bad Gateway\n";
|
||||
let resp = format!(
|
||||
"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::vhost::{build_domains, normalize_host};
|
||||
use crate::access::{prepare_visitor, AccessPolicy};
|
||||
use super::gw::{build_domains, normalize_host};
|
||||
use crate::access::{prepare_ingress, AccessPolicy};
|
||||
use crate::control::Control;
|
||||
use crate::metrics;
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::limit::{maybe_limit, BandwidthLimiter};
|
||||
use orbien_core::msg::NewProxy;
|
||||
use orbien_core::msg::NewTunnel;
|
||||
use orbien_core::tls::{peek_client_hello_sni, PrefixedStream};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -14,17 +14,17 @@ use tokio::sync::{Mutex, Notify};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpsRoute {
|
||||
pub proxy_name: String,
|
||||
pub tunnel_name: String,
|
||||
pub control: Weak<Control>,
|
||||
pub limiter: Option<Arc<BandwidthLimiter>>,
|
||||
}
|
||||
|
||||
pub struct HttpsVhost {
|
||||
pub struct HttpsGw {
|
||||
routes: Mutex<HashMap<String, HttpsRoute>>,
|
||||
pub listen_port: u16,
|
||||
}
|
||||
|
||||
impl HttpsVhost {
|
||||
impl HttpsGw {
|
||||
pub fn new(listen_port: u16) -> Self {
|
||||
Self {
|
||||
routes: Mutex::new(HashMap::new()),
|
||||
@@ -38,14 +38,19 @@ impl HttpsVhost {
|
||||
return Err(anyhow!("empty https domain"));
|
||||
}
|
||||
let mut map = self.routes.lock().await;
|
||||
if let Some(existing) = map.get(&key) {
|
||||
if existing.tunnel_name != route.tunnel_name {
|
||||
return Err(anyhow!("router config conflict: domain={key} (https)"));
|
||||
}
|
||||
}
|
||||
map.insert(key.clone(), route);
|
||||
tracing::info!(domain = %key, "https route registered");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unregister_proxy(&self, proxy_name: &str) {
|
||||
pub async fn unregister_tunnel(&self, tunnel_name: &str) {
|
||||
let mut map = self.routes.lock().await;
|
||||
map.retain(|_, r| r.proxy_name != proxy_name);
|
||||
map.retain(|_, r| r.tunnel_name != tunnel_name);
|
||||
}
|
||||
|
||||
pub async fn lookup(&self, sni: &str) -> Option<HttpsRoute> {
|
||||
@@ -55,47 +60,48 @@ impl HttpsVhost {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HttpsProxy {
|
||||
pub struct HttpsTunnel {
|
||||
pub name: String,
|
||||
pub domains: Vec<String>,
|
||||
vhost: Arc<HttpsVhost>,
|
||||
gw: Arc<HttpsGw>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
impl HttpsProxy {
|
||||
impl HttpsTunnel {
|
||||
pub async fn register(
|
||||
np: &NewProxy,
|
||||
np: &NewTunnel,
|
||||
control: Arc<Control>,
|
||||
vhost: Arc<HttpsVhost>,
|
||||
gw: Arc<HttpsGw>,
|
||||
sub_domain_host: &str,
|
||||
limiter: Option<Arc<BandwidthLimiter>>,
|
||||
) -> Result<Self> {
|
||||
let domains = build_domains(&np.custom_domains, &np.subdomain, sub_domain_host)?;
|
||||
let name = np.proxy_name.clone();
|
||||
let domains = build_domains(&np.domains, sub_domain_host)?;
|
||||
let name = np.tunnel_name.clone();
|
||||
|
||||
gw.unregister_tunnel(&name).await;
|
||||
|
||||
for domain in &domains {
|
||||
vhost
|
||||
.register(
|
||||
domain,
|
||||
HttpsRoute {
|
||||
proxy_name: name.clone(),
|
||||
control: Arc::downgrade(&control),
|
||||
limiter: limiter.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
gw.register(
|
||||
domain,
|
||||
HttpsRoute {
|
||||
tunnel_name: name.clone(),
|
||||
control: Arc::downgrade(&control),
|
||||
limiter: limiter.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
proxy = %name,
|
||||
tunnel = %name,
|
||||
domains = ?domains,
|
||||
"https proxy registered (SNI passthrough)"
|
||||
"https tunnel registered (SNI passthrough)"
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
domains,
|
||||
vhost,
|
||||
gw,
|
||||
closed: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
@@ -106,21 +112,21 @@ impl HttpsProxy {
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
self.vhost.unregister_proxy(&self.name).await;
|
||||
self.gw.unregister_tunnel(&self.name).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_vhost_https_listener(
|
||||
pub async fn run_https_gw_listener(
|
||||
bind_addr: String,
|
||||
port: u16,
|
||||
vhost: Arc<HttpsVhost>,
|
||||
gw: Arc<HttpsGw>,
|
||||
access: Arc<AccessPolicy>,
|
||||
shutdown: Arc<Notify>,
|
||||
) -> Result<()> {
|
||||
let addr = format!("{bind_addr}:{port}");
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!(%addr, "https vhost listener ready (SNI mux, no TLS terminate)");
|
||||
tracing::info!(%addr, "https gateway listener ready (SNI mux, no TLS terminate)");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -128,16 +134,17 @@ pub async fn run_vhost_https_listener(
|
||||
accepted = listener.accept() => {
|
||||
match accepted {
|
||||
Ok((stream, peer)) => {
|
||||
let vhost = Arc::clone(&vhost);
|
||||
orbien_core::net::enable_nodelay(&stream);
|
||||
let gw = Arc::clone(&gw);
|
||||
let access = Arc::clone(&access);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_https_visitor(vhost, stream, peer, access).await {
|
||||
tracing::debug!(%peer, error = %e, "https visitor ended");
|
||||
if let Err(e) = handle_https_ingress(gw, stream, peer, access).await {
|
||||
tracing::debug!(%peer, error = %e, "https ingress ended");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "https vhost accept failed");
|
||||
tracing::warn!(error = %e, "https gateway accept failed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -147,18 +154,18 @@ pub async fn run_vhost_https_listener(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_https_visitor(
|
||||
vhost: Arc<HttpsVhost>,
|
||||
async fn handle_https_ingress(
|
||||
gw: Arc<HttpsGw>,
|
||||
stream: TcpStream,
|
||||
peer: std::net::SocketAddr,
|
||||
access: Arc<AccessPolicy>,
|
||||
) -> Result<()> {
|
||||
let mut visitor = prepare_visitor(stream, peer, &access).await?;
|
||||
let (sni, prefix) = peek_client_hello_sni(&mut visitor.stream).await?;
|
||||
let Some(route) = vhost.lookup(&sni).await else {
|
||||
let mut ingress = prepare_ingress(stream, peer, &access).await?;
|
||||
let (sni, prefix) = peek_client_hello_sni(&mut ingress.stream).await?;
|
||||
let Some(route) = gw.lookup(&sni).await else {
|
||||
tracing::debug!(
|
||||
peer = %visitor.peer,
|
||||
visitor = %visitor.visitor,
|
||||
peer = %ingress.peer,
|
||||
source = %ingress.source,
|
||||
%sni,
|
||||
"https no route for SNI"
|
||||
);
|
||||
@@ -167,35 +174,35 @@ async fn handle_https_visitor(
|
||||
};
|
||||
|
||||
let Some(control) = route.control.upgrade() else {
|
||||
return Err(anyhow!("https proxy client gone: {}", route.proxy_name));
|
||||
return Err(anyhow!("https tunnel client gone: {}", route.tunnel_name));
|
||||
};
|
||||
|
||||
let work = control.get_work_conn().await?;
|
||||
let work = control
|
||||
.start_work_conn(
|
||||
work,
|
||||
&route.proxy_name,
|
||||
visitor.visitor.ip().to_string(),
|
||||
visitor.visitor.port(),
|
||||
visitor
|
||||
let data = control.get_data_conn().await?;
|
||||
let data = control
|
||||
.start_data_conn(
|
||||
data,
|
||||
&route.tunnel_name,
|
||||
ingress.source.ip().to_string(),
|
||||
ingress.source.port(),
|
||||
ingress
|
||||
.local
|
||||
.map(|a| a.ip().to_string())
|
||||
.unwrap_or_default(),
|
||||
visitor.local.map(|a| a.port()).unwrap_or(0),
|
||||
ingress.local.map(|a| a.port()).unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let work = maybe_limit(work, route.limiter.clone());
|
||||
let user = PrefixedStream::new(prefix, visitor.stream);
|
||||
let data = maybe_limit(data, route.limiter.clone());
|
||||
let user = PrefixedStream::new(prefix, ingress.stream);
|
||||
|
||||
tracing::debug!(
|
||||
proxy = %route.proxy_name,
|
||||
tunnel = %route.tunnel_name,
|
||||
%sni,
|
||||
peer = %visitor.peer,
|
||||
visitor = %visitor.visitor,
|
||||
"https joining visitor <-> work (passthrough)"
|
||||
peer = %ingress.peer,
|
||||
source = %ingress.source,
|
||||
"https joining ingress <-> data (passthrough)"
|
||||
);
|
||||
let _ =
|
||||
metrics::join_and_record(&control.metrics, &route.proxy_name, "https", user, work).await;
|
||||
metrics::join_and_record(&control.metrics, &route.tunnel_name, "https", user, data).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
use super::{HttpProxy, HttpsProxy, TcpProxy, UdpProxy};
|
||||
use super::{HttpTunnel, HttpsTunnel, TcpTunnel, UdpTunnel};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub enum RegisteredProxy {
|
||||
Tcp(TcpProxy),
|
||||
Http(HttpProxy),
|
||||
Https(HttpsProxy),
|
||||
Udp(UdpProxy),
|
||||
pub enum RegisteredTunnel {
|
||||
Tcp(TcpTunnel),
|
||||
Http(HttpTunnel),
|
||||
Https(HttpsTunnel),
|
||||
Udp(UdpTunnel),
|
||||
}
|
||||
|
||||
impl RegisteredProxy {
|
||||
pub fn proxy_type(&self) -> &'static str {
|
||||
impl RegisteredTunnel {
|
||||
pub fn tunnel_type(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Tcp(_) => "tcp",
|
||||
Self::Http(_) => "http",
|
||||
@@ -28,32 +28,32 @@ impl RegisteredProxy {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProxyEntry {
|
||||
proxy: RegisteredProxy,
|
||||
struct TunnelEntry {
|
||||
tunnel: RegisteredTunnel,
|
||||
local_addr: String,
|
||||
}
|
||||
|
||||
pub struct ProxyManager {
|
||||
proxies: HashMap<String, ProxyEntry>,
|
||||
pub struct TunnelManager {
|
||||
tunnels: HashMap<String, TunnelEntry>,
|
||||
}
|
||||
|
||||
impl ProxyManager {
|
||||
impl TunnelManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
proxies: HashMap::new(),
|
||||
tunnels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
&mut self,
|
||||
name: String,
|
||||
proxy: RegisteredProxy,
|
||||
tunnel: RegisteredTunnel,
|
||||
local_addr: String,
|
||||
) -> Option<&'static str> {
|
||||
let entry = ProxyEntry { proxy, local_addr };
|
||||
if let Some(old) = self.proxies.insert(name, entry) {
|
||||
let ty = old.proxy.proxy_type();
|
||||
old.proxy.close().await;
|
||||
let entry = TunnelEntry { tunnel, local_addr };
|
||||
if let Some(old) = self.tunnels.insert(name, entry) {
|
||||
let ty = old.tunnel.tunnel_type();
|
||||
old.tunnel.close().await;
|
||||
Some(ty)
|
||||
} else {
|
||||
None
|
||||
@@ -61,9 +61,9 @@ impl ProxyManager {
|
||||
}
|
||||
|
||||
pub async fn remove(&mut self, name: &str) -> Option<&'static str> {
|
||||
if let Some(entry) = self.proxies.remove(name) {
|
||||
let ty = entry.proxy.proxy_type();
|
||||
entry.proxy.close().await;
|
||||
if let Some(entry) = self.tunnels.remove(name) {
|
||||
let ty = entry.tunnel.tunnel_type();
|
||||
entry.tunnel.close().await;
|
||||
Some(ty)
|
||||
} else {
|
||||
None
|
||||
@@ -71,27 +71,27 @@ impl ProxyManager {
|
||||
}
|
||||
|
||||
pub async fn close_all(&mut self) -> Vec<(String, &'static str)> {
|
||||
let mut closed = Vec::with_capacity(self.proxies.len());
|
||||
for (name, entry) in self.proxies.drain() {
|
||||
closed.push((name, entry.proxy.proxy_type()));
|
||||
entry.proxy.close().await;
|
||||
let mut closed = Vec::with_capacity(self.tunnels.len());
|
||||
for (name, entry) in self.tunnels.drain() {
|
||||
closed.push((name, entry.tunnel.tunnel_type()));
|
||||
entry.tunnel.close().await;
|
||||
}
|
||||
closed
|
||||
}
|
||||
|
||||
pub fn summaries(&self) -> Vec<ProxySummary> {
|
||||
self.proxies
|
||||
pub fn summaries(&self) -> Vec<TunnelSummary> {
|
||||
self.tunnels
|
||||
.iter()
|
||||
.map(|(name, entry)| {
|
||||
let (proxy_type, remote_addr) = match &entry.proxy {
|
||||
RegisteredProxy::Tcp(t) => ("tcp".into(), format!(":{}", t.remote_port)),
|
||||
RegisteredProxy::Http(h) => ("http".into(), h.domains.join(",")),
|
||||
RegisteredProxy::Https(h) => ("https".into(), h.domains.join(",")),
|
||||
RegisteredProxy::Udp(u) => ("udp".into(), format!(":{}", u.remote_port)),
|
||||
let (tunnel_type, remote_addr) = match &entry.tunnel {
|
||||
RegisteredTunnel::Tcp(t) => ("tcp".into(), format!(":{}", t.remote_port)),
|
||||
RegisteredTunnel::Http(h) => ("http".into(), h.domains.join(",")),
|
||||
RegisteredTunnel::Https(h) => ("https".into(), h.domains.join(",")),
|
||||
RegisteredTunnel::Udp(u) => ("udp".into(), format!(":{}", u.remote_port)),
|
||||
};
|
||||
ProxySummary {
|
||||
TunnelSummary {
|
||||
name: name.clone(),
|
||||
proxy_type,
|
||||
tunnel_type,
|
||||
remote_addr,
|
||||
local_addr: entry.local_addr.clone(),
|
||||
status: "online".into(),
|
||||
@@ -101,7 +101,7 @@ impl ProxyManager {
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.proxies.len()
|
||||
self.tunnels.len()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,10 +124,10 @@ pub fn format_local_addr(ip: &str, port: i32) -> String {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ProxySummary {
|
||||
pub struct TunnelSummary {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub proxy_type: String,
|
||||
pub tunnel_type: String,
|
||||
#[serde(rename = "remoteAddr")]
|
||||
pub remote_addr: String,
|
||||
#[serde(rename = "localAddr")]
|
||||
@@ -0,0 +1,13 @@
|
||||
mod http;
|
||||
mod https;
|
||||
mod manager;
|
||||
mod tcp;
|
||||
mod udp;
|
||||
mod gw;
|
||||
|
||||
pub use http::{run_http_gw_listener, HttpTunnel};
|
||||
pub use https::{run_https_gw_listener, HttpsTunnel, HttpsGw};
|
||||
pub use manager::{format_local_addr, TunnelManager, TunnelSummary, RegisteredTunnel};
|
||||
pub use tcp::TcpTunnel;
|
||||
pub use udp::UdpTunnel;
|
||||
pub use gw::HttpGw;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::access::{prepare_visitor, AccessPolicy};
|
||||
use crate::access::{prepare_ingress, AccessPolicy};
|
||||
use crate::control::Control;
|
||||
use crate::metrics;
|
||||
use anyhow::Result;
|
||||
@@ -9,7 +9,7 @@ use tokio::net::TcpListener;
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
pub struct TcpProxy {
|
||||
pub struct TcpTunnel {
|
||||
pub name: String,
|
||||
pub remote_port: u16,
|
||||
closed: Arc<AtomicBool>,
|
||||
@@ -17,7 +17,7 @@ pub struct TcpProxy {
|
||||
accept_task: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl TcpProxy {
|
||||
impl TcpTunnel {
|
||||
pub async fn start(
|
||||
name: String,
|
||||
bind_addr: String,
|
||||
@@ -28,13 +28,13 @@ impl TcpProxy {
|
||||
) -> Result<Self> {
|
||||
let addr = format!("{bind_addr}:{remote_port}");
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!(%addr, proxy = %name, "tcp proxy listening");
|
||||
tracing::info!(%addr, tunnel = %name, "tcp tunnel listening");
|
||||
|
||||
let closed = Arc::new(AtomicBool::new(false));
|
||||
let notify = Arc::new(Notify::new());
|
||||
let closed_flag = Arc::clone(&closed);
|
||||
let notify_wait = Arc::clone(¬ify);
|
||||
let proxy_name = name.clone();
|
||||
let tunnel_name = name.clone();
|
||||
let limiter_spawn = limiter.clone();
|
||||
|
||||
let control_weak = Arc::downgrade(&control);
|
||||
@@ -45,28 +45,29 @@ impl TcpProxy {
|
||||
_ = notify_wait.notified() => break,
|
||||
accepted = listener.accept() => {
|
||||
match accepted {
|
||||
Ok((user_conn, peer)) => {
|
||||
Ok((stream, peer)) => {
|
||||
orbien_core::net::enable_nodelay(&stream);
|
||||
if closed_flag.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let Some(ctl) = control_weak.upgrade() else {
|
||||
break;
|
||||
};
|
||||
let pname = proxy_name.clone();
|
||||
let pname = tunnel_name.clone();
|
||||
let lim = limiter_spawn.clone();
|
||||
let access = Arc::clone(&access);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_user_conn(
|
||||
if let Err(e) = handle_ingress(
|
||||
ctl,
|
||||
&pname,
|
||||
user_conn,
|
||||
stream,
|
||||
peer,
|
||||
lim,
|
||||
access,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(proxy = %pname, error = %e, "user conn ended");
|
||||
tracing::debug!(tunnel = %pname, error = %e, "ingress ended");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -90,7 +91,7 @@ impl TcpProxy {
|
||||
}
|
||||
|
||||
pub async fn close(&self) {
|
||||
tracing::info!(proxy = %self.name, remote_port = self.remote_port, "tcp proxy closing");
|
||||
tracing::info!(tunnel = %self.name, remote_port = self.remote_port, "tcp tunnel closing");
|
||||
self.closed.store(true, Ordering::SeqCst);
|
||||
self.notify.notify_waiters();
|
||||
if let Some(h) = self.accept_task.lock().await.take() {
|
||||
@@ -100,7 +101,7 @@ impl TcpProxy {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TcpProxy {
|
||||
impl Drop for TcpTunnel {
|
||||
fn drop(&mut self) {
|
||||
self.closed.store(true, Ordering::SeqCst);
|
||||
self.notify.notify_waiters();
|
||||
@@ -110,39 +111,39 @@ impl Drop for TcpProxy {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_user_conn(
|
||||
async fn handle_ingress(
|
||||
control: Arc<Control>,
|
||||
proxy_name: &str,
|
||||
user_conn: tokio::net::TcpStream,
|
||||
tunnel_name: &str,
|
||||
stream: tokio::net::TcpStream,
|
||||
peer: std::net::SocketAddr,
|
||||
limiter: Option<Arc<BandwidthLimiter>>,
|
||||
access: Arc<AccessPolicy>,
|
||||
) -> Result<()> {
|
||||
let visitor = prepare_visitor(user_conn, peer, &access).await?;
|
||||
let work = control.get_work_conn().await?;
|
||||
let work = control
|
||||
.start_work_conn(
|
||||
work,
|
||||
proxy_name,
|
||||
visitor.visitor.ip().to_string(),
|
||||
visitor.visitor.port(),
|
||||
visitor
|
||||
let ingress = prepare_ingress(stream, peer, &access).await?;
|
||||
let data = control.get_data_conn().await?;
|
||||
let data = control
|
||||
.start_data_conn(
|
||||
data,
|
||||
tunnel_name,
|
||||
ingress.source.ip().to_string(),
|
||||
ingress.source.port(),
|
||||
ingress
|
||||
.local
|
||||
.map(|a| a.ip().to_string())
|
||||
.unwrap_or_default(),
|
||||
visitor.local.map(|a| a.port()).unwrap_or(0),
|
||||
ingress.local.map(|a| a.port()).unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let work = maybe_limit(work, limiter);
|
||||
let data = maybe_limit(data, limiter);
|
||||
|
||||
tracing::debug!(
|
||||
proxy = %proxy_name,
|
||||
peer = %visitor.peer,
|
||||
visitor = %visitor.visitor,
|
||||
"joining visitor <-> work"
|
||||
tunnel = %tunnel_name,
|
||||
peer = %ingress.peer,
|
||||
source = %ingress.source,
|
||||
"joining ingress <-> data"
|
||||
);
|
||||
let _ =
|
||||
metrics::join_and_record(&control.metrics, proxy_name, "tcp", visitor.stream, work).await;
|
||||
metrics::join_and_record(&control.metrics, tunnel_name, "tcp", ingress.stream, data).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use crate::metrics::{MemMetrics, ServerMetrics};
|
||||
use anyhow::Result;
|
||||
use orbien_core::limit::{maybe_limit, BandwidthLimiter};
|
||||
use orbien_core::msg::{self, Message, UdpPacket};
|
||||
use orbien_core::udp::{forward_user_conn, CHANNEL_CAP, SERVER_WORK_READ_DEADLINE};
|
||||
use orbien_core::udp::{forward_user_conn, CHANNEL_CAP, SERVER_DATA_READ_DEADLINE};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -13,7 +13,7 @@ use tokio::sync::{mpsc, Mutex, Notify};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
pub struct UdpProxy {
|
||||
pub struct UdpTunnel {
|
||||
pub name: String,
|
||||
pub remote_port: u16,
|
||||
closed: Arc<AtomicBool>,
|
||||
@@ -23,7 +23,7 @@ pub struct UdpProxy {
|
||||
_udp: Arc<UdpSocket>,
|
||||
}
|
||||
|
||||
impl UdpProxy {
|
||||
impl UdpTunnel {
|
||||
pub async fn start(
|
||||
name: String,
|
||||
bind_addr: String,
|
||||
@@ -34,7 +34,7 @@ impl UdpProxy {
|
||||
) -> Result<Self> {
|
||||
let addr = format!("{bind_addr}:{remote_port}");
|
||||
let udp = Arc::new(UdpSocket::bind(&addr).await?);
|
||||
tracing::info!(%addr, proxy = %name, "udp proxy listening");
|
||||
tracing::info!(%addr, tunnel = %name, "udp tunnel listening");
|
||||
|
||||
let closed = Arc::new(AtomicBool::new(false));
|
||||
let notify = Arc::new(Notify::new());
|
||||
@@ -58,12 +58,12 @@ impl UdpProxy {
|
||||
{
|
||||
let closed_flag = Arc::clone(&closed);
|
||||
let notify_wait = Arc::clone(¬ify);
|
||||
let proxy_name = name.clone();
|
||||
let tunnel_name = name.clone();
|
||||
let control = Arc::downgrade(&control);
|
||||
tasks.push(tokio::spawn(async move {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
work_conn_loop(
|
||||
proxy_name,
|
||||
data_conn_loop(
|
||||
tunnel_name,
|
||||
control,
|
||||
limiter,
|
||||
send_rx,
|
||||
@@ -86,7 +86,7 @@ impl UdpProxy {
|
||||
}
|
||||
|
||||
pub async fn close(&self) {
|
||||
tracing::info!(proxy = %self.name, remote_port = self.remote_port, "udp proxy closing");
|
||||
tracing::info!(tunnel = %self.name, remote_port = self.remote_port, "udp tunnel closing");
|
||||
self.closed.store(true, Ordering::SeqCst);
|
||||
self.notify.notify_waiters();
|
||||
let tasks = std::mem::take(&mut *self.tasks.lock().await);
|
||||
@@ -97,7 +97,7 @@ impl UdpProxy {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UdpProxy {
|
||||
impl Drop for UdpTunnel {
|
||||
fn drop(&mut self) {
|
||||
self.closed.store(true, Ordering::SeqCst);
|
||||
self.notify.notify_waiters();
|
||||
@@ -112,8 +112,8 @@ async fn abort_wait(h: JoinHandle<()>) {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
async fn work_conn_loop(
|
||||
proxy_name: String,
|
||||
async fn data_conn_loop(
|
||||
tunnel_name: String,
|
||||
control: std::sync::Weak<Control>,
|
||||
limiter: Option<Arc<BandwidthLimiter>>,
|
||||
mut send_rx: mpsc::Receiver<UdpPacket>,
|
||||
@@ -126,17 +126,17 @@ async fn work_conn_loop(
|
||||
return;
|
||||
};
|
||||
|
||||
let work = {
|
||||
let data = {
|
||||
tokio::select! {
|
||||
_ = notify.notified() => return,
|
||||
w = control.get_work_conn() => w,
|
||||
w = control.get_data_conn() => w,
|
||||
}
|
||||
};
|
||||
|
||||
let work = match work {
|
||||
let data = match data {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
tracing::warn!(proxy = %proxy_name, error = %e, "udp get work conn failed");
|
||||
tracing::warn!(tunnel = %tunnel_name, error = %e, "udp get data conn failed");
|
||||
tokio::select! {
|
||||
_ = notify.notified() => return,
|
||||
_ = sleep(Duration::from_secs(1)) => continue,
|
||||
@@ -144,34 +144,34 @@ async fn work_conn_loop(
|
||||
}
|
||||
};
|
||||
|
||||
let work = match control
|
||||
.start_work_conn(work, &proxy_name, String::new(), 0, String::new(), 0)
|
||||
let data = match control
|
||||
.start_data_conn(data, &tunnel_name, String::new(), 0, String::new(), 0)
|
||||
.await
|
||||
{
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
tracing::warn!(proxy = %proxy_name, error = %e, "udp StartWorkConn failed");
|
||||
tracing::warn!(tunnel = %tunnel_name, error = %e, "udp StartDataConn failed");
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let work = maybe_limit(work, limiter.clone());
|
||||
let (reader, mut writer) = tokio::io::split(work);
|
||||
let data = maybe_limit(data, limiter.clone());
|
||||
let (reader, mut writer) = tokio::io::split(data);
|
||||
tracing::info!(
|
||||
proxy = %proxy_name,
|
||||
"udp work conn established"
|
||||
tunnel = %tunnel_name,
|
||||
"udp data conn established"
|
||||
);
|
||||
let metrics = Arc::clone(&control.metrics);
|
||||
let _guard = metrics.track_connection(&proxy_name, "udp");
|
||||
let _guard = metrics.track_connection(&tunnel_name, "udp");
|
||||
|
||||
let (fail_tx, mut fail_rx) = mpsc::channel::<()>(1);
|
||||
let read_tx_r = read_tx.clone();
|
||||
let fail_r = fail_tx.clone();
|
||||
let name_r = proxy_name.clone();
|
||||
let name_r = tunnel_name.clone();
|
||||
let metrics_r = Arc::clone(&metrics);
|
||||
let mut reader_task = Some(tokio::spawn(async move {
|
||||
work_reader(reader, read_tx_r, fail_r, name_r, metrics_r).await;
|
||||
data_reader(reader, read_tx_r, fail_r, name_r, metrics_r).await;
|
||||
}));
|
||||
|
||||
let reconnect = loop {
|
||||
@@ -193,21 +193,21 @@ async fn work_conn_loop(
|
||||
Some(pkt) => {
|
||||
let nbytes = pkt.content.len() as u64;
|
||||
tracing::trace!(
|
||||
proxy = %proxy_name,
|
||||
tunnel = %tunnel_name,
|
||||
len = nbytes,
|
||||
"udp packet to work"
|
||||
"udp packet to data"
|
||||
);
|
||||
if msg::write_msg(&mut writer, &Message::UdpPacket(pkt))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!(proxy = %proxy_name, "udp work write error");
|
||||
tracing::warn!(tunnel = %tunnel_name, "udp data write error");
|
||||
if let Some(h) = reader_task.take() {
|
||||
abort_wait(h).await;
|
||||
}
|
||||
break true;
|
||||
}
|
||||
metrics.add_traffic_in(&proxy_name, "udp", nbytes);
|
||||
metrics.add_traffic_in(&tunnel_name, "udp", nbytes);
|
||||
}
|
||||
None => {
|
||||
if let Some(h) = reader_task.take() {
|
||||
@@ -221,47 +221,47 @@ async fn work_conn_loop(
|
||||
};
|
||||
|
||||
if reconnect {
|
||||
tracing::info!(proxy = %proxy_name, "udp work conn lost; reconnecting");
|
||||
tracing::info!(tunnel = %tunnel_name, "udp data conn lost; reconnecting");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn work_reader<R: AsyncRead + Unpin + Send + 'static>(
|
||||
async fn data_reader<R: AsyncRead + Unpin + Send + 'static>(
|
||||
mut reader: R,
|
||||
read_tx: mpsc::Sender<UdpPacket>,
|
||||
fail_tx: mpsc::Sender<()>,
|
||||
proxy_name: String,
|
||||
tunnel_name: String,
|
||||
metrics: Arc<MemMetrics>,
|
||||
) {
|
||||
loop {
|
||||
match timeout(SERVER_WORK_READ_DEADLINE, msg::read_msg(&mut reader)).await {
|
||||
match timeout(SERVER_DATA_READ_DEADLINE, msg::read_msg(&mut reader)).await {
|
||||
Ok(Ok(Message::Ping(_))) => {
|
||||
tracing::trace!(proxy = %proxy_name, "udp work ping");
|
||||
tracing::trace!(tunnel = %tunnel_name, "udp data ping");
|
||||
}
|
||||
Ok(Ok(Message::UdpPacket(pkt))) => {
|
||||
let nbytes = pkt.content.len() as u64;
|
||||
tracing::trace!(
|
||||
proxy = %proxy_name,
|
||||
tunnel = %tunnel_name,
|
||||
len = nbytes,
|
||||
"udp packet from work"
|
||||
"udp packet from data"
|
||||
);
|
||||
metrics.add_traffic_out(&proxy_name, "udp", nbytes);
|
||||
metrics.add_traffic_out(&tunnel_name, "udp", nbytes);
|
||||
let _ = read_tx.try_send(pkt);
|
||||
}
|
||||
Ok(Ok(other)) => {
|
||||
tracing::debug!(
|
||||
proxy = %proxy_name,
|
||||
tunnel = %tunnel_name,
|
||||
ty = other.type_byte(),
|
||||
"udp work unexpected message"
|
||||
"udp data unexpected message"
|
||||
);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(proxy = %proxy_name, error = %e, "udp work read error");
|
||||
tracing::warn!(tunnel = %tunnel_name, error = %e, "udp data read error");
|
||||
let _ = fail_tx.send(()).await;
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(proxy = %proxy_name, "udp work read deadline exceeded");
|
||||
tracing::warn!(tunnel = %tunnel_name, "udp data read deadline exceeded");
|
||||
let _ = fail_tx.send(()).await;
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user