mod alpn_value;
mod ip_mask;
mod location;
mod option_util;
mod sni_value;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::path::PathBuf;
use log::warn;
use percent_encoding::percent_decode_str;
use serde::Deserialize;
use url::Url;
use crate::hostname_util::normalize_hostname_pattern;
pub use alpn_value::AlpnValue;
pub use ip_mask::IpMask;
pub use location::{Location, NetLocation};
pub use option_util::{NoneOrOne, NoneOrSome, OneOrSome};
pub use sni_value::SniValue;
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Copy)]
pub struct TcpKeepaliveConfig {
pub idle_secs: u64,
pub interval_secs: u64,
}
impl TcpKeepaliveConfig {
pub fn default_server() -> Self {
Self {
idle_secs: 300, interval_secs: 60, }
}
pub fn default_target() -> Self {
Self {
idle_secs: 120, interval_secs: 30, }
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum TcpKeepaliveOption {
#[default]
UseDefaults,
Disabled,
Custom {
idle_secs: u64,
interval_secs: u64,
},
}
impl TcpKeepaliveOption {
fn resolve(&self, default: TcpKeepaliveConfig) -> Option<TcpKeepaliveConfig> {
match self {
TcpKeepaliveOption::UseDefaults => Some(default),
TcpKeepaliveOption::Disabled => None,
TcpKeepaliveOption::Custom {
idle_secs,
interval_secs,
} => Some(TcpKeepaliveConfig {
idle_secs: *idle_secs,
interval_secs: *interval_secs,
}),
}
}
pub fn resolve_for_server(&self) -> Option<TcpKeepaliveConfig> {
self.resolve(TcpKeepaliveConfig::default_server())
}
pub fn resolve_for_target(&self) -> Option<TcpKeepaliveConfig> {
self.resolve(TcpKeepaliveConfig::default_target())
}
}
impl<'de> Deserialize<'de> for TcpKeepaliveOption {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct TcpKeepaliveOptionVisitor;
impl<'de> serde::de::Visitor<'de> for TcpKeepaliveOptionVisitor {
type Value = TcpKeepaliveOption;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter
.write_str("true, false, null, or an object with idle_secs and interval_secs")
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v {
Ok(TcpKeepaliveOption::UseDefaults)
} else {
Ok(TcpKeepaliveOption::Disabled)
}
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(TcpKeepaliveOption::Disabled)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(TcpKeepaliveOption::Disabled)
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut idle_secs = None;
let mut interval_secs = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"idle_secs" => {
if idle_secs.is_some() {
return Err(serde::de::Error::duplicate_field("idle_secs"));
}
idle_secs = Some(map.next_value()?);
}
"interval_secs" => {
if interval_secs.is_some() {
return Err(serde::de::Error::duplicate_field("interval_secs"));
}
interval_secs = Some(map.next_value()?);
}
_ => {
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
let idle_secs =
idle_secs.ok_or_else(|| serde::de::Error::missing_field("idle_secs"))?;
let interval_secs = interval_secs
.ok_or_else(|| serde::de::Error::missing_field("interval_secs"))?;
Ok(TcpKeepaliveOption::Custom {
idle_secs,
interval_secs,
})
}
}
deserializer.deserialize_any(TcpKeepaliveOptionVisitor)
}
}
#[derive(Debug, Clone)]
pub enum Config {
ServerConfig(ServerConfig),
IpMaskGroup(IpMaskGroupConfig),
}
#[derive(Debug, Clone, Deserialize)]
pub struct IpMaskGroupConfig {
group: String,
#[serde(alias = "ip_mask")]
ip_masks: OneOrSome<IpMaskSelection>,
}
impl<'de> serde::de::Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let mut map = serde_json::Map::deserialize(deserializer)?;
if map.contains_key("group") {
let config = serde_json::from_value(serde_json::Value::Object(map))
.map_err(serde::de::Error::custom)?;
Ok(Config::IpMaskGroup(config))
} else {
if !map.contains_key("transport") {
map.insert(
"transport".to_string(),
serde_json::Value::String("tcp".to_string()),
);
}
let config = serde_json::from_value(serde_json::Value::Object(map))
.map_err(serde::de::Error::custom)?;
Ok(Config::ServerConfig(config))
}
}
}
#[derive(Debug, Clone)]
pub enum IpMaskSelection {
Literal(IpMask),
Group(String),
}
impl<'de> Deserialize<'de> for IpMaskSelection {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct IpMaskSelectionVisitor;
impl serde::de::Visitor<'_> for IpMaskSelectionVisitor {
type Value = IpMaskSelection;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an IP mask or a group name")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match value.try_into() {
Ok(ip_mask) => Ok(IpMaskSelection::Literal(ip_mask)),
Err(_) => {
Ok(IpMaskSelection::Group(value.to_string()))
}
}
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
}
deserializer.deserialize_str(IpMaskSelectionVisitor)
}
}
impl IpMaskSelection {
pub fn unwrap_literal(self) -> IpMask {
match self {
IpMaskSelection::Literal(ip_mask) => ip_mask,
IpMaskSelection::Group(_) => {
panic!("Tried to unwrap an IP group as a literal");
}
}
}
pub fn replace_groups(
selections: &mut OneOrSome<IpMaskSelection>,
groups: &HashMap<String, Vec<IpMask>>,
) -> std::io::Result<()> {
let mut ret = vec![];
for selection in selections.iter() {
match selection {
IpMaskSelection::Literal(ip_mask) => {
ret.push(IpMaskSelection::Literal(ip_mask.clone()));
}
IpMaskSelection::Group(group_name) => {
let ip_masks = groups.get(group_name.as_str()).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("No such ip group: {}", group_name),
)
})?;
ret.extend(ip_masks.iter().cloned().map(IpMaskSelection::Literal));
}
}
}
*selections = OneOrSome::Some(ret);
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
#[serde(deserialize_with = "deserialize_socket_addr", alias = "bindAddress")]
pub address: SocketAddr,
#[serde(default, alias = "iptables")]
pub use_iptables: bool,
#[serde(flatten)]
pub target_configs: TargetConfigs,
}
fn deserialize_socket_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
let mut iter = value.to_socket_addrs().map_err(|_| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Other("invalid socket address"),
&"valid socket address",
)
})?;
let socket_addr = iter.next().ok_or_else(|| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Other("unable to resolve socket address"),
&"valid socket address",
)
})?;
Ok(socket_addr)
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "transport", rename_all = "lowercase")]
pub enum TargetConfigs {
Tcp {
#[serde(default = "default_true")]
tcp_nodelay: bool,
#[serde(default)]
tcp_keepalive: TcpKeepaliveOption,
#[serde(alias = "target")]
targets: Box<OneOrSome<TcpTargetConfig>>,
},
Udp {
#[serde(alias = "target")]
targets: OneOrSome<UdpTargetConfig>,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct TcpTargetConfig {
pub allowlist: OneOrSome<IpMaskSelection>,
#[serde(default, alias = "serverTls")]
pub server_tls: Option<ServerTlsConfig>,
#[serde(default = "default_true")]
pub tcp_nodelay: bool,
#[serde(default)]
pub tcp_keepalive: TcpKeepaliveOption,
#[serde(flatten)]
pub action: TcpAction,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RawTcpActionConfig {
#[serde(alias = "location", alias = "addresses", alias = "address")]
pub locations: OneOrSome<TcpTargetLocation>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HttpTcpActionConfig {
#[serde(default)]
pub http_paths: HashMap<String, OneOrSome<HttpPathConfig>>,
pub default_http_action: HttpPathAction,
}
#[derive(Debug, Clone)]
pub enum TcpAction {
Raw(RawTcpActionConfig),
Http(HttpTcpActionConfig),
}
impl<'de> Deserialize<'de> for TcpAction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let mut map = serde_json::Map::deserialize(deserializer)?;
if !map.contains_key("protocol") {
map.insert(
"protocol".to_string(),
serde_json::Value::String(
if map.contains_key("paths") || map.contains_key("default_http_action") {
"http"
} else {
"raw"
}
.to_string(),
),
);
}
let protocol = map
.get("protocol")
.and_then(serde_json::Value::as_str)
.unwrap();
match protocol {
"raw" => {
let config = RawTcpActionConfig::deserialize(serde_json::Value::Object(map))
.map_err(serde::de::Error::custom)?;
Ok(TcpAction::Raw(config))
}
"http" => {
let config = HttpTcpActionConfig::deserialize(serde_json::Value::Object(map))
.map_err(serde::de::Error::custom)?;
Ok(TcpAction::Http(config))
}
_ => Err(serde::de::Error::unknown_variant(
protocol,
&["raw", "http"],
)),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct HttpPathConfig {
#[serde(default, deserialize_with = "deserialize_required_headers")]
pub required_request_headers: HashMap<String, HttpValueMatch>,
pub http_action: HttpPathAction,
}
fn deserialize_required_headers<'de, D>(
deserializer: D,
) -> Result<HashMap<String, HttpValueMatch>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = HashMap::<String, HttpValueMatch>::deserialize(deserializer)?;
raw.into_iter()
.map(|(key, value)| {
let key = key.to_ascii_lowercase();
let value = if key == "host" {
match value {
HttpValueMatch::Single(s) => {
let normalized =
normalize_hostname_pattern(&s).map_err(serde::de::Error::custom)?;
HttpValueMatch::Hostnames(vec![normalized])
}
HttpValueMatch::Multiple(patterns) => {
let normalized = patterns
.into_iter()
.map(|s| normalize_hostname_pattern(&s))
.collect::<Result<_, _>>()
.map_err(serde::de::Error::custom)?;
HttpValueMatch::Hostnames(normalized)
}
other => other,
}
} else {
value
};
Ok((key, value))
})
.collect()
}
#[derive(Default, Debug, Clone)]
pub enum HttpValueMatch {
#[default]
Any,
Single(String),
Multiple(Vec<String>),
Hostnames(Vec<String>),
}
impl<'de> Deserialize<'de> for HttpValueMatch {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct HttpValueMatchVisitor;
impl<'de> serde::de::Visitor<'de> for HttpValueMatchVisitor {
type Value = HttpValueMatch;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string, an array of strings, or null")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HttpValueMatch::Single(value.to_string()))
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HttpValueMatch::Single(value))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut values = Vec::new();
while let Some(value) = seq.next_element()? {
values.push(value);
}
Ok(HttpValueMatch::Multiple(values))
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HttpValueMatch::Any)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(HttpValueMatch::Any)
}
}
deserializer.deserialize_any(HttpValueMatchVisitor)
}
}
#[derive(Debug, Clone)]
pub enum HttpPathAction {
CloseConnection,
ServeMessage(HttpServeMessageConfig),
ServeDirectory(HttpServeDirectoryConfig),
Forward(HttpForwardConfig),
}
#[derive(Debug, Clone, Deserialize)]
pub struct HttpServeMessageConfig {
pub status_code: u16,
#[serde(default)]
pub status_message: Option<String>,
#[serde(default)]
pub content: String,
#[serde(default)]
pub response_headers: HashMap<String, String>,
#[serde(default)]
pub response_id_header_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HttpServeDirectoryConfig {
pub path: String,
#[serde(default)]
pub response_headers: HashMap<String, String>,
#[serde(default)]
pub response_id_header_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HttpForwardConfig {
#[serde(alias = "location", alias = "addresses", alias = "address")]
pub locations: OneOrSome<TcpTargetLocation>,
#[serde(default)]
pub replacement_path: Option<String>,
#[serde(default)]
pub request_header_patch: Option<Box<HttpHeaderPatch>>,
#[serde(default)]
pub response_header_patch: Option<Box<HttpHeaderPatch>>,
#[serde(default)]
pub request_id_header_name: Option<String>,
#[serde(default)]
pub response_id_header_name: Option<String>,
}
impl<'de> Deserialize<'de> for HttpPathAction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct HttpPathActionVisitor;
impl<'de> serde::de::Visitor<'de> for HttpPathActionVisitor {
type Value = HttpPathAction;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string 'close' or a map with 'type' field")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if value == "close" {
Ok(HttpPathAction::CloseConnection)
} else {
Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
}
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut json_map = serde_json::Map::new();
while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
json_map.insert(key, value);
}
let action_type = json_map
.get("type")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| serde::de::Error::missing_field("type"))?;
match action_type {
"close" => Ok(HttpPathAction::CloseConnection),
"serve-message" => {
let config = HttpServeMessageConfig::deserialize(
serde_json::Value::Object(json_map),
)
.map_err(serde::de::Error::custom)?;
Ok(HttpPathAction::ServeMessage(config))
}
"serve-directory" => {
let config = HttpServeDirectoryConfig::deserialize(
serde_json::Value::Object(json_map),
)
.map_err(serde::de::Error::custom)?;
Ok(HttpPathAction::ServeDirectory(config))
}
"forward" => {
let config =
HttpForwardConfig::deserialize(serde_json::Value::Object(json_map))
.map_err(serde::de::Error::custom)?;
Ok(HttpPathAction::Forward(config))
}
_ => Err(serde::de::Error::unknown_variant(
action_type,
&["close", "serve-message", "serve-directory", "http-forward"],
)),
}
}
}
deserializer.deserialize_any(HttpPathActionVisitor)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct HttpHeaderPatch {
#[serde(default)]
pub default_headers: HashMap<String, String>,
#[serde(default)]
pub overwrite_headers: HashMap<String, String>,
#[serde(default)]
pub remove_headers: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TcpTargetLocationConfig {
#[serde(flatten)]
pub location: Location,
#[serde(default)]
pub client_tls: ClientTlsConfig,
}
#[derive(Debug, Clone)]
pub enum TcpTargetLocation {
OnlyAddress(NetLocation),
OnlyPath(PathBuf),
Config(TcpTargetLocationConfig),
}
impl<'de> Deserialize<'de> for TcpTargetLocation {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct TcpTargetLocationVisitor;
impl<'de> serde::de::Visitor<'de> for TcpTargetLocationVisitor {
type Value = TcpTargetLocation;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(
"a string, an object with 'address' and 'port', or a location config",
)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if let Ok(net_location) = NetLocation::try_from(value) {
return Ok(TcpTargetLocation::OnlyAddress(net_location));
}
Ok(TcpTargetLocation::OnlyPath(PathBuf::from(value)))
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let config = TcpTargetLocationConfig::deserialize(
serde::de::value::MapAccessDeserializer::new(map),
)?;
Ok(TcpTargetLocation::Config(config))
}
}
deserializer.deserialize_any(TcpTargetLocationVisitor)
}
}
impl TcpTargetLocation {
pub fn into_components(self) -> (Location, ClientTlsConfig) {
match self {
TcpTargetLocation::OnlyAddress(net_location) => {
(Location::Address(net_location), ClientTlsConfig::default())
}
TcpTargetLocation::OnlyPath(path_buf) => {
(Location::Path(path_buf), ClientTlsConfig::default())
}
TcpTargetLocation::Config(TcpTargetLocationConfig {
location,
client_tls,
}) => (location, client_tls),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum TlsMode {
#[default]
Terminate, Passthrough, }
impl<'de> Deserialize<'de> for TlsMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"terminate" => Ok(TlsMode::Terminate),
"passthrough" => Ok(TlsMode::Passthrough),
_ => Err(serde::de::Error::unknown_variant(
&s,
&["terminate", "passthrough"],
)),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerTlsConfig {
#[serde(default)]
pub mode: TlsMode,
#[serde(default, alias = "sni_hostname")]
pub sni_hostnames: NoneOrSome<SniValue>,
#[serde(default, alias = "alpn_protocol")]
pub alpn_protocols: NoneOrSome<AlpnValue>,
#[serde(default)]
pub cert: Option<String>,
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub optional: bool,
#[serde(default, alias = "client_ca_cert")]
pub client_ca_certs: NoneOrSome<String>,
#[serde(default, alias = "client_fingerprint")]
pub client_fingerprints: NoneOrSome<String>,
}
impl ServerTlsConfig {
pub fn is_passthrough(&self) -> bool {
self.mode == TlsMode::Passthrough
}
pub fn is_terminate(&self) -> bool {
self.mode == TlsMode::Terminate
}
pub fn validate(&self) -> Result<(), String> {
if self.is_terminate() {
if self.cert.is_none() || self.key.is_none() {
return Err("cert and key are required for TLS terminate mode".to_string());
}
} else if self.is_passthrough()
&& (!self.client_fingerprints.is_empty() || !self.client_ca_certs.is_empty())
{
return Err(
"client_fingerprints and client_ca_certs are not valid for TLS passthrough mode"
.to_string(),
);
}
Ok(())
}
pub fn validate_with_action(&self, action: &TcpAction) -> Result<(), String> {
if self.is_passthrough() {
if let TcpAction::Raw(RawTcpActionConfig { locations }) = action {
for location in locations.iter() {
let (_, client_tls) = location.clone().into_components();
if client_tls.is_enabled() {
return Err(
"client_tls cannot be enabled in TLS passthrough mode (would cause TLS-in-TLS). Use terminate mode or disable client_tls.".to_string()
);
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub enum ClientTlsConfig {
#[default]
Disabled,
Enabled {
verify: bool,
key: Option<String>,
cert: Option<String>,
sni_hostname: NoneOrOne<String>,
alpn_protocols: NoneOrSome<String>,
server_fingerprints: NoneOrSome<String>,
},
}
impl<'de> Deserialize<'de> for ClientTlsConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct ClientTlsConfigVisitor;
impl<'de> serde::de::Visitor<'de> for ClientTlsConfigVisitor {
type Value = ClientTlsConfig;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter
.write_str("a boolean, the string 'no-verify', or a client TLS config object")
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v {
Ok(ClientTlsConfig::Enabled {
verify: true,
key: None,
cert: None,
sni_hostname: NoneOrOne::default(),
alpn_protocols: NoneOrSome::default(),
server_fingerprints: NoneOrSome::default(),
})
} else {
Err(serde::de::Error::custom(
"client_tls cannot be false; omit the field or use an object",
))
}
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if value == "no-verify" {
Ok(ClientTlsConfig::Enabled {
verify: false,
key: None,
cert: None,
sni_hostname: NoneOrOne::default(),
alpn_protocols: NoneOrSome::default(),
server_fingerprints: NoneOrSome::default(),
})
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(value),
&"'no-verify' or a client TLS config object",
))
}
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut verify = None;
let mut key = None;
let mut cert = None;
let mut sni_hostname = None;
let mut alpn_protocols = None;
let mut server_fingerprints = None;
while let Some(field_key) = map.next_key::<String>()? {
match field_key.as_str() {
"verify" => {
if verify.is_some() {
return Err(serde::de::Error::duplicate_field("verify"));
}
verify = Some(map.next_value()?);
}
"key" => {
if key.is_some() {
return Err(serde::de::Error::duplicate_field("key"));
}
key = Some(map.next_value()?);
}
"cert" => {
if cert.is_some() {
return Err(serde::de::Error::duplicate_field("cert"));
}
cert = Some(map.next_value()?);
}
"sni_hostname" | "sni" => {
if sni_hostname.is_some() {
return Err(serde::de::Error::duplicate_field("sni_hostname"));
}
sni_hostname = Some(map.next_value()?);
}
"alpn_protocols" | "alpn_protocol" | "alpn" => {
if alpn_protocols.is_some() {
return Err(serde::de::Error::duplicate_field("alpn_protocols"));
}
alpn_protocols = Some(map.next_value()?);
}
"server_fingerprints" | "server_fingerprint" => {
if server_fingerprints.is_some() {
return Err(serde::de::Error::duplicate_field(
"server_fingerprints",
));
}
server_fingerprints = Some(map.next_value()?);
}
_ => {
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
Ok(ClientTlsConfig::Enabled {
verify: verify.unwrap_or(true),
key,
cert,
sni_hostname: sni_hostname.unwrap_or_default(),
alpn_protocols: alpn_protocols.unwrap_or_default(),
server_fingerprints: server_fingerprints.unwrap_or_default(),
})
}
}
deserializer.deserialize_any(ClientTlsConfigVisitor)
}
}
impl ClientTlsConfig {
pub fn is_enabled(&self) -> bool {
matches!(self, ClientTlsConfig::Enabled { .. })
}
pub fn should_verify(&self) -> bool {
match self {
ClientTlsConfig::Enabled { verify, .. } => *verify,
ClientTlsConfig::Disabled => false,
}
}
pub fn has_client_cert(&self) -> bool {
matches!(
self,
ClientTlsConfig::Enabled {
key: Some(_),
cert: Some(_),
..
}
)
}
pub fn key(&self) -> Option<&String> {
match self {
ClientTlsConfig::Enabled { key, .. } => key.as_ref(),
ClientTlsConfig::Disabled => None,
}
}
pub fn cert(&self) -> Option<&String> {
match self {
ClientTlsConfig::Enabled { cert, .. } => cert.as_ref(),
ClientTlsConfig::Disabled => None,
}
}
pub fn sni_hostname(&self) -> &NoneOrOne<String> {
match self {
ClientTlsConfig::Enabled { sni_hostname, .. } => sni_hostname,
ClientTlsConfig::Disabled => &NoneOrOne::Unspecified,
}
}
pub fn alpn_protocols(&self) -> &NoneOrSome<String> {
match self {
ClientTlsConfig::Enabled { alpn_protocols, .. } => alpn_protocols,
ClientTlsConfig::Disabled => &NoneOrSome::Unspecified,
}
}
pub fn server_fingerprints(&self) -> &NoneOrSome<String> {
match self {
ClientTlsConfig::Enabled {
server_fingerprints,
..
} => server_fingerprints,
ClientTlsConfig::Disabled => &NoneOrSome::Unspecified,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct UdpTargetConfig {
#[serde(alias = "address", alias = "locations", alias = "location")]
pub addresses: OneOrSome<NetLocation>,
pub allowlist: OneOrSome<IpMaskSelection>,
pub association_timeout_secs: Option<u32>,
}
fn deserialize_configs(mut config_str: String, filename: &str) -> std::io::Result<Vec<Config>> {
let path = std::path::Path::new(filename);
let extension = path.extension().and_then(std::ffi::OsStr::to_str);
let is_json = match extension {
Some("json") => {
let trimmed_str = config_str.trim();
if trimmed_str.starts_with('{') && trimmed_str.ends_with('}') {
config_str = format!("[\n{}\n]\n", config_str);
}
true
}
Some("yaml") | Some("yml") => false,
_ => {
let trimmed_str = config_str.trim();
if trimmed_str.starts_with("//") {
true
} else if trimmed_str.starts_with('{') && trimmed_str.ends_with('}') {
config_str = format!("[\n{}\n]\n", config_str);
true
} else if trimmed_str.starts_with('-') {
false
} else {
warn!("Could not detect config format, assuming YAML.");
false
}
}
};
if is_json {
config_str = config_str
.split('\n')
.filter(|s| !s.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
}
if config_str.contains("serverTls") {
eprintln!("WARNING: serverTls is deprecated and has been renamed to server_tls. This will be removed in future versions.");
}
if config_str.contains("bindAddress") {
eprintln!("WARNING: bindAddress is deprecated and has been renamed to address. This will be removed in future versions.");
}
if config_str.contains("addresses") {
eprintln!("WARNING: addresses is deprecated and has been renamed to locations. This will be removed in future versions.");
}
if is_json {
serde_json::from_str(&config_str).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("failed to parse config JSON: {}", e),
)
})
} else {
serde_yaml::from_str(&config_str).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("failed to parse config YAML: {}", e),
)
})
}
}
pub async fn load_server_configs(
config_paths: Vec<String>,
config_urls: Vec<String>,
) -> std::io::Result<Vec<ServerConfig>> {
let mut groups: HashMap<String, Vec<IpMask>> = HashMap::new();
groups.insert(String::from("all"), vec![IpMask::all()]);
let mut server_configs = vec![];
for config_path in config_paths {
let config_str = tokio::fs::read_to_string(&config_path).await?;
let configs = deserialize_configs(config_str, &config_path)?;
for config in configs {
match config {
Config::ServerConfig(server_config) => {
server_configs.push(server_config);
}
Config::IpMaskGroup(IpMaskGroupConfig {
group,
mut ip_masks,
}) => {
if IpMask::try_from(group.as_str()).is_ok() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid IP group name, looks like an IP mask: {}", group),
));
}
IpMaskSelection::replace_groups(&mut ip_masks, &groups)?;
let ip_masks = ip_masks
.into_iter()
.map(IpMaskSelection::unwrap_literal)
.collect::<Vec<_>>();
if groups.insert(group.clone(), ip_masks).is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("duplicate IP group name: {}", group),
));
}
}
}
}
}
for server_config in server_configs.iter_mut() {
match server_config.target_configs {
TargetConfigs::Tcp {
ref mut targets, ..
} => {
let mut expanded_targets = Vec::new();
for target in targets.iter() {
if let Some(ref server_tls) = target.server_tls {
if server_tls.optional {
warn!(
"Auto-migrating 'optional: true' at {} - creating two explicit targets",
server_config.address
);
let mut tls_target = target.clone();
if let Some(ref mut tls_config) = tls_target.server_tls {
tls_config.optional = false;
}
expanded_targets.push(tls_target);
let mut non_tls_target = target.clone();
non_tls_target.server_tls = None;
expanded_targets.push(non_tls_target);
} else {
expanded_targets.push(target.clone());
}
} else {
expanded_targets.push(target.clone());
}
}
**targets = OneOrSome::Some(expanded_targets);
for target in targets.iter_mut() {
IpMaskSelection::replace_groups(&mut target.allowlist, &groups)?;
}
}
TargetConfigs::Udp { ref mut targets } => {
for target in targets.iter_mut() {
IpMaskSelection::replace_groups(&mut target.allowlist, &groups)?;
}
}
}
}
for config_url in config_urls {
server_configs.push(load_url(&config_url).await?);
}
Ok(server_configs)
}
pub async fn load_url(config_url: &str) -> std::io::Result<ServerConfig> {
let url = Url::parse(config_url).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Failed to parse url: {}", e),
)
})?;
let host_str = url.host_str().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"No host specified in URL format",
)
})?;
let port = url.port().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"No port specified in URL format",
)
})?;
let address = tokio::net::lookup_host((host_str, port))
.await?
.next()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Unable to resolve bind address",
)
})?;
match url.scheme() {
"tcp" => {
let mut locations = vec![];
for (query_key, query_value) in url.query_pairs().into_owned() {
let query_value = percent_decode_str(&query_value)
.decode_utf8()
.unwrap()
.into_owned();
match query_key.as_str() {
"target" | "target-address" => {
locations.push(TcpTargetLocation::Config(TcpTargetLocationConfig {
location: Location::Address(query_value.as_str().try_into()?),
client_tls: ClientTlsConfig::default(),
}));
}
"target-path" => {
locations.push(TcpTargetLocation::Config(TcpTargetLocationConfig {
location: Location::Path(PathBuf::from(query_value)),
client_tls: ClientTlsConfig::default(),
}));
}
unknown_key => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown TCP URL query key: {}", unknown_key),
));
}
}
}
if locations.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"No target locations specified in URL query",
));
}
let tcp_target_config = TcpTargetConfig {
allowlist: OneOrSome::One(IpMaskSelection::Literal(IpMask::all())),
server_tls: None,
tcp_nodelay: true,
tcp_keepalive: TcpKeepaliveOption::default(),
action: TcpAction::Raw(RawTcpActionConfig {
locations: OneOrSome::Some(locations),
}),
};
Ok(ServerConfig {
address,
use_iptables: false,
target_configs: TargetConfigs::Tcp {
tcp_nodelay: true,
tcp_keepalive: TcpKeepaliveOption::default(),
targets: Box::new(OneOrSome::One(tcp_target_config)),
},
})
}
"udp" => {
let mut addresses = vec![];
for (query_key, query_value) in url.query_pairs().into_owned() {
let query_value = percent_decode_str(&query_value)
.decode_utf8()
.unwrap()
.into_owned();
match query_key.as_str() {
"target" | "target-address" => {
addresses.push(query_value.as_str().try_into()?);
}
unknown_key => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown UDP URL query key: {}", unknown_key),
));
}
}
}
if addresses.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"No target addresses specified in URL query",
));
}
let udp_target_config = UdpTargetConfig {
addresses: OneOrSome::Some(addresses),
allowlist: OneOrSome::One(IpMaskSelection::Literal(IpMask::all())),
association_timeout_secs: None,
};
Ok(ServerConfig {
address,
use_iptables: false,
target_configs: TargetConfigs::Udp {
targets: OneOrSome::One(udp_target_config),
},
})
}
unknown_scheme => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unknown URL scheme: {}", unknown_scheme),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tcp_target_forms_deserialize_from_json_and_yaml() {
let target = serde_json::json!({
"allowlist": "127.0.0.1/32",
"location": "127.0.0.1:9001"
});
for (field, targets, expected_count) in [
("target", target.clone(), 1),
("targets", serde_json::json!([target.clone(), target]), 2),
("targets", serde_json::json!([]), 0),
] {
let config = serde_json::json!([{
"address": "0.0.0.0:9000",
"transport": "tcp",
field: targets
}]);
for (filename, contents) in [
("config.json", serde_json::to_string(&config).unwrap()),
("config.yaml", serde_yaml::to_string(&config).unwrap()),
] {
let result = deserialize_configs(contents, filename);
if expected_count == 0 {
assert!(result.is_err(), "{filename} accepted empty targets");
continue;
}
let Config::ServerConfig(server) = result.unwrap().pop().unwrap() else {
panic!("expected server config");
};
let TargetConfigs::Tcp { targets, .. } = server.target_configs else {
panic!("expected TCP targets");
};
assert_eq!(targets.into_vec().len(), expected_count);
}
}
}
#[tokio::test]
async fn tcp_url_builds_single_target() {
let server = load_url("tcp://0.0.0.0:9000?target=127.0.0.1:9001")
.await
.unwrap();
let TargetConfigs::Tcp { targets, .. } = server.target_configs else {
panic!("expected TCP targets");
};
assert_eq!(targets.into_vec().len(), 1);
}
fn deser_path_config(yaml: &str) -> HttpPathConfig {
serde_yaml::from_str(yaml).unwrap()
}
fn get_header<'a>(config: &'a HttpPathConfig, key: &str) -> &'a HttpValueMatch {
config.required_request_headers.get(key).unwrap()
}
fn host_yaml(host_value: &str) -> String {
format!(
"required_request_headers:\n host: {}\nhttp_action:\n type: close\n",
host_value
)
}
fn deser_host(host_value: &str) -> HttpValueMatch {
let c: HttpPathConfig = serde_yaml::from_str(&host_yaml(host_value)).unwrap();
c.required_request_headers.get("host").unwrap().clone()
}
fn assert_host_rejected(host_value: &str) {
assert!(
serde_yaml::from_str::<HttpPathConfig>(&host_yaml(host_value)).is_err(),
"expected rejection for host: {}",
host_value
);
}
#[test]
fn host_string_becomes_hostnames() {
let m = deser_host("example.com");
assert!(matches!(m, HttpValueMatch::Hostnames(v) if v == ["example.com"]));
}
#[test]
fn host_list_becomes_hostnames() {
let c = deser_path_config(
r#"
required_request_headers:
host:
- a.com
- "*.b.com"
http_action:
type: close
"#,
);
let m = get_header(&c, "host");
assert!(matches!(m, HttpValueMatch::Hostnames(v) if v == &["a.com", "*.b.com"]));
}
#[test]
fn host_null_stays_any() {
let m = deser_host("~");
assert!(matches!(m, HttpValueMatch::Any));
}
#[test]
fn host_key_is_case_insensitive() {
let c = deser_path_config(
r#"
required_request_headers:
Host: example.com
http_action:
type: close
"#,
);
assert!(c.required_request_headers.contains_key("host"));
assert!(!c.required_request_headers.contains_key("Host"));
let m = get_header(&c, "host");
assert!(matches!(m, HttpValueMatch::Hostnames(v) if v == &["example.com"]));
}
#[test]
fn non_host_key_stays_single() {
let c = deser_path_config(
r#"
required_request_headers:
x-custom: foo
http_action:
type: close
"#,
);
let m = get_header(&c, "x-custom");
assert!(matches!(m, HttpValueMatch::Single(v) if v == "foo"));
}
#[test]
fn non_host_key_stays_multiple() {
let c = deser_path_config(
r#"
required_request_headers:
x-custom:
- foo
- bar
http_action:
type: close
"#,
);
let m = get_header(&c, "x-custom");
assert!(matches!(m, HttpValueMatch::Multiple(v) if v == &["foo", "bar"]));
}
#[test]
fn all_keys_lowercased() {
let c = deser_path_config(
r#"
required_request_headers:
X-Custom: foo
Accept-Language: en
http_action:
type: close
"#,
);
assert!(c.required_request_headers.contains_key("x-custom"));
assert!(c.required_request_headers.contains_key("accept-language"));
assert!(!c.required_request_headers.contains_key("X-Custom"));
assert!(!c.required_request_headers.contains_key("Accept-Language"));
}
#[test]
fn missing_headers_is_empty() {
let c = deser_path_config(
r#"
http_action:
type: close
"#,
);
assert!(c.required_request_headers.is_empty());
}
#[test]
fn host_trailing_dot_normalized() {
let m = deser_host("\"example.com.\"");
assert!(matches!(m, HttpValueMatch::Hostnames(v) if v == ["example.com"]));
}
#[test]
fn host_empty_string_rejected() {
assert_host_rejected("\"\"");
}
#[test]
fn host_consecutive_dots_rejected() {
assert_host_rejected("\"example..com\"");
}
#[test]
fn host_star_dot_rejected() {
assert_host_rejected("\"*.\"");
}
#[test]
fn host_bare_dot_rejected() {
assert_host_rejected("\".\"");
}
#[test]
fn host_control_char_rejected() {
assert_host_rejected(&format!("\"example{}.com\"", '\x01'));
}
#[test]
fn host_list_with_bad_entry_rejected() {
assert_host_rejected("\n - good.com\n - \"bad..com\"");
}
#[test]
fn host_wildcard_trailing_dot_normalized() {
let m = deser_host("\"*.example.com.\"");
assert!(matches!(m, HttpValueMatch::Hostnames(v) if v == ["*.example.com"]));
}
#[test]
fn host_valid_patterns_still_accepted() {
let c = deser_path_config(
r#"
required_request_headers:
host:
- example.com
- "*.example.com"
- ".example.com"
- "*"
http_action:
type: close
"#,
);
let m = get_header(&c, "host");
assert!(matches!(m, HttpValueMatch::Hostnames(v) if v.len() == 4));
}
}