use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use ipnet::IpNet;
use serde::Deserialize;
use crate::error::{Error, Result};
use crate::protection::ProtectionMode;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub role: Role,
pub remote: Option
,
pub bind: Option,
pub carrier: Carrier,
pub protection: Protection,
pub heartbeat: u64,
pub reconnect: Option,
pub limits: Option,
pub routing: Option,
pub tun: Option,
pub socks: Option,
pub http: Option,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Client,
Server,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
pub enum Carrier {
Ssh {
transport: Transport,
},
Http {
transport: Transport,
host: String,
path: String,
tls: Option,
},
Webrtc {
transport: Transport,
},
Socks {
transport: Transport,
},
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Transport {
Tcp,
Udp,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(tag = "mode", rename_all = "lowercase", deny_unknown_fields)]
pub enum CarrierTls {
/// A real, publicly trusted certificate obtained autonomously from Let's
/// Encrypt (TLS-ALPN-01). `bind`/`remote` must use port 443 for this: the
/// CA always validates the TLS-ALPN-01 challenge against port 443 of the
/// domain's resolved address, independent of what port the service would
/// otherwise prefer.
Acme { domain: String, cache: PathBuf },
/// REALITY-style camouflage: the client sends a syntactically valid TLS
/// 1.3 ClientHello for `donor`'s SNI carrying a time-windowed HMAC tag in
/// the session_id field. A server that recognizes the tag switches to the
/// snolc protocol; anyone else (a real browser, a scanner, active probing
/// by a censor) is spliced byte-for-byte to `donor` and gets `donor`'s
/// genuine TLS session back, indistinguishable from contacting it
/// directly.
Steal {
donor: String,
/// Which TLS ClientHello shape the client sends for this
/// camouflage. `none` is the built-in hand-shaped hello (no extra
/// native dependency); the others drive a real BoringSSL handshake
/// (via `boring`) configured to match that browser's real
/// cipher/extension/ALPN shape byte-for-byte, so the same library a
/// real browser uses produces the observable bytes.
fingerprint: Fingerprint,
secret: String,
mirror: MirrorConfig,
},
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Fingerprint {
None,
Chrome131,
Firefox133,
}
/// Response caching for a mirrored (spliced) connection: identical requests
/// get replayed from memory instead of re-contacting the target. Never
/// persisted to disk.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct MirrorConfig {
pub cache: bool,
pub ttl: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Protection {
pub mode: ProtectionMode,
pub key: Option,
pub clients: Option>,
pub unknown: Option,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClientKey {
pub key: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(tag = "mode", rename_all = "lowercase", deny_unknown_fields)]
pub enum UnknownClient {
Error,
Site {
target: String,
mirror: MirrorConfig,
},
File {
path: PathBuf,
},
Service {
target: String,
mirror: MirrorConfig,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Limits {
pub connections: usize,
pub streams: usize,
pub fragments: usize,
pub memory: usize,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Tun {
pub name: String,
pub address: Vec,
pub mtu: u16,
pub auto: bool,
pub descriptor: Option,
pub include: Option>,
pub exclude: Option>,
pub strict_route: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Socks {
pub pass: Option,
pub user: Option,
pub host: IpAddr,
pub port: u16,
pub listen: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Http {
pub pass: Option,
pub user: Option,
pub host: IpAddr,
pub port: u16,
pub tls: Option,
pub listen: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct InboundTls {
pub enabled: bool,
pub cert: Option,
pub key: Option,
}
impl Config {
pub fn load(path: &Path) -> Result {
let source = fs::read_to_string(path)?;
let mut config: Self =
yaml_serde::from_str(&source).map_err(|error| Error::Config(error.to_string()))?;
config.validate()?;
if let Some(routing) = &mut config.routing
&& routing.is_relative()
{
let parent = path.parent().unwrap_or_else(|| Path::new("."));
*routing = parent.join(&*routing);
}
Ok(config)
}
pub fn validate(&self) -> Result<()> {
if self.heartbeat == 0 {
return Err(config_error("heartbeat must be greater than zero"));
}
self.carrier.validate()?;
self.protection.validate(self.role)?;
if let Some(tun) = &self.tun {
tun.validate()?;
}
if let Some(socks) = &self.socks {
validate_auth(&socks.user, &socks.pass, "socks")?;
validate_port(socks.port, "socks")?;
}
if let Some(http) = &self.http {
validate_auth(&http.user, &http.pass, "http")?;
validate_port(http.port, "http")?;
if let Some(tls) = &http.tls {
tls.validate()?;
}
}
match self.role {
Role::Client => self.validate_client(),
Role::Server => self.validate_server(),
}
}
fn validate_client(&self) -> Result<()> {
let remote = self
.remote
.as_deref()
.ok_or_else(|| config_error("client remote is required"))?;
validate_endpoint(remote, "remote")?;
if self
.carrier
.tls()
.is_some_and(CarrierTls::requires_port_443)
&& !remote.ends_with(":443")
{
return Err(config_error("acme carrier requires port 443"));
}
if self.bind.is_some() {
return Err(config_error("client cannot set bind"));
}
match self.reconnect {
None => return Err(config_error("client reconnect is required")),
Some(value) if value < -1 => {
return Err(config_error("client reconnect must be -1, 0, or positive"));
}
Some(_) => {}
}
if self.limits.is_some() {
return Err(config_error("client cannot set server limits"));
}
if !self.has_active_inbound() {
return Err(config_error("client requires an active inbound"));
}
Ok(())
}
fn validate_server(&self) -> Result<()> {
let bind = self
.bind
.as_deref()
.ok_or_else(|| config_error("server bind is required"))?;
validate_endpoint(bind, "bind")?;
if self
.carrier
.tls()
.is_some_and(CarrierTls::requires_port_443)
&& !bind.ends_with(":443")
{
return Err(config_error("acme carrier requires port 443"));
}
if self.remote.is_some() {
return Err(config_error("server cannot set remote"));
}
if self.reconnect.is_some() {
return Err(config_error("server cannot set reconnect"));
}
if self.tun.is_some() || self.socks.is_some() || self.http.is_some() {
return Err(config_error("server cannot define an inbound"));
}
let limits = self
.limits
.as_ref()
.ok_or_else(|| config_error("server limits are required"))?;
limits.validate()
}
fn has_active_inbound(&self) -> bool {
self.tun.is_some()
|| self.socks.as_ref().is_some_and(|inbound| inbound.listen)
|| self.http.as_ref().is_some_and(|inbound| inbound.listen)
}
}
impl Carrier {
fn validate(&self) -> Result<()> {
match self {
Self::Ssh { transport } | Self::Http { transport, .. } | Self::Socks { transport }
if *transport != Transport::Tcp =>
{
Err(config_error("selected carrier requires TCP"))
}
Self::Webrtc { transport } if *transport != Transport::Udp => {
Err(config_error("WebRTC requires UDP"))
}
Self::Http {
host, path, tls, ..
} => {
validate_hostname(host, "HTTP carrier host")?;
if !path.starts_with('/')
|| path
.bytes()
.any(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
{
return Err(config_error("HTTP carrier path is invalid"));
}
if let Some(tls) = tls {
tls.validate()?;
}
Ok(())
}
_ => Ok(()),
}
}
pub const fn tls(&self) -> Option<&CarrierTls> {
match self {
Self::Http { tls, .. } => tls.as_ref(),
_ => None,
}
}
}
impl CarrierTls {
fn validate(&self) -> Result<()> {
match self {
Self::Acme { domain, cache, .. } => {
validate_hostname(domain, "TLS domain")?;
if cache.as_os_str().is_empty() {
return Err(config_error("acme cache directory cannot be empty"));
}
Ok(())
}
Self::Steal {
donor,
secret,
mirror,
..
} => {
validate_hostname(donor, "TLS donor")?;
validate_key(secret, "steal secret")?;
mirror.validate()
}
}
}
/// Whether this mode requires binding/dialing port 443 specifically.
/// TLS-ALPN-01 validation always targets port 443 of the domain's
/// resolved address, regardless of the port the service would otherwise
/// run on.
pub const fn requires_port_443(&self) -> bool {
matches!(self, Self::Acme { .. })
}
}
impl MirrorConfig {
fn validate(&self) -> Result<()> {
if self.cache && self.ttl == 0 {
return Err(config_error("mirror cache ttl must be greater than zero"));
}
Ok(())
}
}
impl Protection {
fn validate(&self, role: Role) -> Result<()> {
match role {
Role::Client => {
validate_key(
self.key
.as_deref()
.ok_or_else(|| config_error("client private key is required"))?,
"client private key",
)?;
if self.clients.is_some() || self.unknown.is_some() {
return Err(config_error(
"client cannot define server authentication fields",
));
}
}
Role::Server => {
if self.key.is_some() {
return Err(config_error("server cannot define a client private key"));
}
let clients = self
.clients
.as_ref()
.ok_or_else(|| config_error("server clients are required"))?;
if clients.is_empty() {
return Err(config_error("server clients cannot be empty"));
}
for client in clients {
validate_key(&client.key, "client public key")?;
}
if let Some(unknown) = &self.unknown {
unknown.validate()?;
} else {
return Err(config_error("unknown-client behavior is required"));
}
}
}
Ok(())
}
}
impl UnknownClient {
fn validate(&self) -> Result<()> {
match self {
Self::Error | Self::File { .. } => Ok(()),
Self::Site { target, mirror } | Self::Service { target, mirror } => {
validate_endpoint(target, "unknown-client target")?;
mirror.validate()
}
}
}
}
impl Limits {
fn validate(&self) -> Result<()> {
if self.connections == 0 || self.streams == 0 || self.fragments == 0 || self.memory == 0 {
return Err(config_error("all server limits must be greater than zero"));
}
let minimum = self
.streams
.checked_mul(yamux::DEFAULT_CREDIT as usize)
.ok_or_else(|| config_error("server stream memory limit overflows"))?;
if self.memory < minimum {
return Err(config_error(
"server memory must provide 256kb for every yamux stream",
));
}
Ok(())
}
}
impl Tun {
fn validate(&self) -> Result<()> {
if self.name.is_empty() || self.name.len() > 15 || self.name.as_bytes().contains(&0) {
return Err(config_error("tun name must contain 1 to 15 non-NUL bytes"));
}
if self.mtu < 1280 {
return Err(config_error("tun MTU must be at least 1280"));
}
if !self
.address
.iter()
.any(|network| matches!(network, IpNet::V4(_)))
|| !self
.address
.iter()
.any(|network| matches!(network, IpNet::V6(_)))
{
return Err(config_error("tun requires IPv4 and IPv6 addresses"));
}
if self.descriptor.is_some_and(|descriptor| descriptor < 0) {
return Err(config_error("tun descriptor cannot be negative"));
}
#[cfg(target_os = "android")]
if self.descriptor.is_none() {
return Err(config_error("tun descriptor is required on Android"));
}
Ok(())
}
}
impl InboundTls {
fn validate(&self) -> Result<()> {
if self.enabled && (self.cert.is_none() || self.key.is_none()) {
return Err(config_error("enabled inbound TLS requires cert and key"));
}
if !self.enabled && (self.cert.is_some() || self.key.is_some()) {
return Err(config_error(
"disabled inbound TLS cannot define cert or key",
));
}
Ok(())
}
}
fn validate_auth(user: &Option, pass: &Option, name: &str) -> Result<()> {
if user.is_some() != pass.is_some() {
return Err(config_error(&format!(
"{name} user and pass must be set together"
)));
}
Ok(())
}
fn validate_port(port: u16, name: &str) -> Result<()> {
if port == 0 {
return Err(config_error(&format!("{name} port cannot be zero")));
}
Ok(())
}
fn validate_endpoint(endpoint: &str, field: &str) -> Result<()> {
let (host, port) = if endpoint.starts_with('[') {
let closing = endpoint
.find(']')
.ok_or_else(|| config_error(&format!("{field} has an invalid IPv6 address")))?;
let host = &endpoint[1..closing];
let port = endpoint
.get(closing + 1..)
.and_then(|tail| tail.strip_prefix(':'))
.ok_or_else(|| config_error(&format!("{field} has no port")))?;
(host, port)
} else {
endpoint
.rsplit_once(':')
.ok_or_else(|| config_error(&format!("{field} has no port")))?
};
if host.is_empty() {
return Err(config_error(&format!("{field} has no host")));
}
let port = port
.parse::()
.map_err(|_| config_error(&format!("{field} has an invalid port")))?;
validate_port(port, field)
}
fn validate_hostname(host: &str, field: &str) -> Result<()> {
let host = host.strip_suffix('.').unwrap_or(host);
if host.is_empty() || host.len() > 253 {
return Err(config_error(&format!("{field} is invalid")));
}
for label in host.split('.') {
if label.is_empty()
|| label.len() > 63
|| label.starts_with('-')
|| label.ends_with('-')
|| !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
{
return Err(config_error(&format!("{field} is invalid")));
}
}
Ok(())
}
fn validate_key(value: &str, field: &str) -> Result<()> {
let decoded = STANDARD
.decode(value)
.map_err(|_| config_error(&format!("{field} is not base64")))?;
if decoded.len() != 32 {
return Err(config_error(&format!("{field} must contain 32 bytes")));
}
Ok(())
}
fn config_error(message: &str) -> Error {
Error::Config(message.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
const KEY: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
fn parse(source: &str) -> Result {
let config: Config =
yaml_serde::from_str(source).map_err(|error| Error::Config(error.to_string()))?;
config.validate()?;
Ok(config)
}
#[test]
fn accepts_a_complete_client() {
let source = format!(
r#"
role: client
remote: example.com:443
bind: null
carrier:
kind: http
transport: tcp
host: example.com
path: /api/events
tls:
mode: steal
donor: example.com
fingerprint: chrome131
secret: {KEY}
mirror:
cache: true
ttl: 60
protection:
mode: chacha poly
key: {KEY}
clients: null
unknown: null
heartbeat: 15
reconnect: -1
limits: null
routing: rules.yml
tun: null
socks:
pass: null
user: null
host: 127.0.0.1
port: 1080
listen: true
http: null
"#
);
assert_eq!(parse(&source).unwrap().role, Role::Client);
}
#[test]
fn rejects_wrong_carrier_transport() {
let source = format!(
r#"
role: client
remote: example.com:443
bind: null
carrier:
kind: ssh
transport: udp
protection:
mode: no
key: {KEY}
clients: null
unknown: null
heartbeat: 15
reconnect: 0
limits: null
routing: null
tun: null
socks:
pass: null
user: null
host: 127.0.0.1
port: 1080
listen: true
http: null
"#
);
assert!(parse(&source).is_err());
}
#[test]
fn rejects_a_server_with_an_inbound() {
let source = format!(
r#"
role: server
remote: null
bind: "[::]:443"
carrier:
kind: webrtc
transport: udp
protection:
mode: AES-GCM
key: null
clients:
- key: {KEY}
unknown:
mode: error
heartbeat: 15
reconnect: null
limits:
connections: 10
streams: 50
fragments: 100
memory: 13107200
routing: null
tun: null
socks:
pass: null
user: null
host: 127.0.0.1
port: 1080
listen: false
http: null
"#
);
assert!(parse(&source).is_err());
}
#[test]
fn rejects_unknown_fields() {
let source = "role: client\ndefault: true\n";
assert!(yaml_serde::from_str::(source).is_err());
}
}