refactor: Refactor and optimize server code, redesign configuration file

This commit is contained in:
lxien
2026-08-16 09:57:24 +08:00
parent 08e71756c0
commit 864e805eb8
28 changed files with 2292 additions and 2127 deletions
+1 -1
View File
@@ -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
View File
@@ -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)
}
-512
View File
@@ -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)
}
}
+91
View File
@@ -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)
}
}
+248
View File
@@ -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(())
}
}
+263
View File
@@ -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 -4
View File
@@ -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,
}
+32 -35
View File
@@ -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>,
}
+45 -58
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
}
}
+7 -7
View File
@@ -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);
}
-266
View File
@@ -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;
}
-13
View File
@@ -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;
-131
View File
@@ -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)
}
-604
View File
@@ -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(),
)
}
+115
View File
@@ -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(),
)
}
+147
View File
@@ -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())),
}
}
}
+214
View File
@@ -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}")),
}
}
}
+163
View File
@@ -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()
}
+265
View File
@@ -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,
}
}
+400
View File
@@ -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")]
+13
View File
@@ -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(&notify);
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(&notify);
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;
}