22 Commits

Author SHA1 Message Date
80575a4247 Fix Woodpecker CI pipeline: explicitly set GITEA_SERVER_URL and remove 'set' command
All checks were successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
- Added GITEA_SERVER_URL to the Release step to fix login failures.
- Removed 'set' command from the Prepare step to protect sensitive environment variables.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 16:00:45 +00:00
aff96d1a01 Release v0.1.12
Some checks failed
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/3 Pipeline failed
2026-04-09 22:37:28 +02:00
590740f40e Add wildcard SNI matching
Some checks failed
ci/woodpecker/push/build/1 Pipeline was canceled
ci/woodpecker/push/build/3 Pipeline was canceled
ci/woodpecker/push/build/2 Pipeline was canceled
ci/woodpecker/tag/build/2 Pipeline is pending
ci/woodpecker/tag/build/3 Pipeline is pending
ci/woodpecker/tag/build/1 Pipeline was canceled
2026-04-09 22:12:55 +02:00
a674895173 Make the build work
All checks were successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
2026-04-09 22:05:08 +02:00
644ca99004 Add Woodpecker build configuration 2026-04-08 22:51:01 +02:00
913e50ff1c Release version 0.1.11
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-05-01 22:04:59 +02:00
aecffa0d14 Wait for the entire TLS header to become available, even if it takes
multiple packets.

Closes: #10
2025-05-01 22:01:54 +02:00
4c2711fc81 Release version 0.1.10
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Signed-off-by: Jacob Kiers <code@kiers.eu>
2025-01-09 21:13:07 +01:00
1a9ca771ac Update based on lints
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: Jacob Kiers <code@kiers.eu>
2025-01-09 20:56:51 +01:00
b7ec67ed07 Fix SNI header parsing
When a listener is configured to deal with TLS upstreams, we use the SNI
field of the TLS ClientHello message to decide where to send the traffic.

Therefore, a buffer of 1024 bytes was used to temporarily store this
message. However, a TLS ClientHello message can be larger than that, up
to 16K bytes.

So now the first few bytes are read and manually parsed to find out how
long the message is. And then the entire ClientHello message is
retrieved.

So hopefully that will fix the issue causing the ClientHello
determination to fail.

Closes #10

Signed-off-by: Jacob Kiers <code@kiers.eu>
2025-01-09 20:56:51 +01:00
aff46b6bfb Update dependencies to latest version
This also fixes a build failure which would otherwise happen due to the
time crate.

Signed-off-by: Jacob Kiers <code@kiers.eu>
2025-01-09 20:21:34 +01:00
922ea1f030 Add help to main command
Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-12-24 17:22:58 +01:00
6300c43495 Upgrade toolchain to rust 1.79.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-06-22 15:44:13 +02:00
c21ff86ee4 Add change log
Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-06-22 15:43:58 +02:00
8d6387773a Add self update functionality
Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-06-22 15:43:58 +02:00
95149ffd9f Update .gitignore
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-06-22 11:49:12 +02:00
a140748647 Correct attribution to fourth
All checks were successful
continuous-integration/drone/push Build is passing
Search and replace accidentally also renamed the original crate, called
fourth. But attributions should be correct.

Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-06-19 21:59:19 +02:00
ad6955a30d Fix crate name and release v0.1.8
All checks were successful
continuous-integration/drone Build is passing
continuous-integration/drone/tag Build is passing
Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-06-19 21:16:30 +02:00
4592c94586 Reintroduce L4P_CONFIG environment variable
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
This points to a user-configured configuration file.

Closes #5.

Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-02-23 23:53:47 +01:00
6284870059 Rename config::config to config::config_v1
To prevent module inception, which was a clippy warning.

Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-02-23 23:34:19 +01:00
97b4bf6bbe Solve synchronization issue
The async mutex in the previous variant would fail when used in a single
threaded mode, because block_in_place() cannot be used there.

Instead, replace the code with a Arc<RwLock> inside of the
UpstreamAddress to let that class take care of its own mutability.

Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-02-23 23:31:23 +01:00
59c7128f93 Remove kcp support
Signed-off-by: Jacob Kiers <code@kiers.eu>
2024-02-23 22:49:43 +01:00
32 changed files with 2709 additions and 1288 deletions

View File

@@ -1,5 +1,5 @@
local executableName = 'l4p'; local executableName = 'l4p';
local build_image = 'img.kie.rs/jjkiers/rust-cross:rust1.71.1-zig'; local build_image = 'img.kie.rs/jjkiers/rust-crossbuild:rust1.79.0-zig0.11.0-zig';
local archs = [ local archs = [
{ target: 'aarch64-unknown-linux-musl', short: 'arm64-musl' }, { target: 'aarch64-unknown-linux-musl', short: 'arm64-musl' },

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
/.idea
/.vscode
/target /target
config.yaml config.yaml

52
.woodpecker/build.yaml Normal file
View File

@@ -0,0 +1,52 @@
when:
- event:
- push
- tag
- manual
matrix:
include:
- TARGET: x86_64-unknown-linux-musl
SHORT: amd64-musl
BIN_SUFFIX:
- TARGET: aarch64-unknown-linux-musl
SHORT: arm64-musl
BIN_SUFFIX:
- TARGET: x86_64-pc-windows-gnu
SHORT: windows
BIN_SUFFIX: .exe
steps:
- name: Prepare
image: img.kie.rs/jjkiers/rust-crossbuild:rust1.79.0-zig0.11.0-zig
commands:
- echo Using image img.kie.rs/jjkiers/rust-crossbuild:rust1.79.0-zig0.11.0-zig
- mkdir -p artifacts
- cargo --version
- rustc --version
- name: Build for ${SHORT}
image: img.kie.rs/jjkiers/rust-crossbuild:rust1.79.0-zig0.11.0-zig
commands:
- echo Building ${TARGET} \(${SHORT}\)
- cargo zigbuild --release --target ${TARGET}
- mkdir -p artifacts
- cp target/${TARGET}/release/l4p${BIN_SUFFIX} artifacts/l4p-${TARGET}${BIN_SUFFIX}
- rm -rf target/${TARGET}/release/*
depends_on:
- Prepare
- name: Release
image: img.kie.rs/jjkiers/rust-crossbuild:rust1.79.0-zig0.11.0-zig
when:
- event: tag
commands:
- ls -lah artifacts
- scripts/create_release_artifacts.sh
environment:
GITEA_SERVER_URL: https://code.kiers.eu
GITEA_SERVER_TOKEN:
from_secret: gitea_token
depends_on:
- Build for ${SHORT}

55
CHANGELOG.md Normal file
View File

@@ -0,0 +1,55 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.10] - 2025-01-09
### Fixed
* The ClientHello TLS header is now read in full before it is parsed, solving
an error where there was not enough data to fully read it. In those cases
it was not possible to determine the upstream address and therefore the proxy
would go the the default action instead.
### Changed
* Updated some dependencies to prevent the build from breaking.
## [0.1.9] - 2024-06-22
### Deprecated
The ability to run `l4p` without arguments is now deprecated. Please use
`l4p serve` going forward.
### Added
* Added self update functionality. Just run `l4p update` to use it.
* Now keeping a change log in the `CHANGELOG.md` file.
### Changed
* Updated build pipeline to generate much smaller binaries
-------
## Previous versions
[unreleased]: https://code.kiers.eu/jjkiers/layer4-proxy/compare/v0.1.9...HEAD
[0.1.10]: https://code.kiers.eu/jjkiers/layer4-proxy/compare/v0.1.9...v0.1.10
[0.1.9]: https://code.kiers.eu/jjkiers/layer4-proxy/compare/v0.1.8...v0.1.9
Types of changes:
* `Added` for new features.
* `Changed` for changes in existing functionality.
* `Deprecated` for soon-to-be removed features.
* `Removed` for now removed features.
* `Fixed` for any bug fixes.
* `Security` in case of vulnerabilities.

939
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "layer4-proxy" name = "l4p"
version = "0.1.7" version = "0.1.12"
edition = "2021" edition = "2021"
authors = ["Jacob Kiers <code@kiers.eu>"] authors = ["Jacob Kiers <code@kiers.eu>"]
license = "Apache-2.0" license = "Apache-2.0"
@@ -25,10 +25,17 @@ byte_string = "1"
bytes = "1.1" bytes = "1.1"
futures = "0.3" futures = "0.3"
log = "0.4" log = "0.4"
pico-args = "0.5.0"
pretty_env_logger = "0.5" pretty_env_logger = "0.5"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9.21" serde_yaml = "0.9.21"
time = { version = "0.3.1", features = ["local-offset", "formatting"] } time = { version = "0.3.37", features = ["local-offset", "formatting"] }
tls-parser = "0.11" tls-parser = "0.12.2"
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }
url = "2.2.2" url = "2.2.2"
psl = "2.1"
[dependencies.self_update]
version = "0.42.0"
default-features = false
features = ["rustls"]

View File

@@ -4,7 +4,7 @@
![CI](https://drone-ci.kiers.eu/api/badges/jjkiers/layer4-proxy/status.svg) ![CI](https://drone-ci.kiers.eu/api/badges/jjkiers/layer4-proxy/status.svg)
`l4p` is a layer 4 proxy implemented by Rust to listen on specific ports and transfer TCP/KCP data to remote addresses(only TCP) according to configuration. `l4p` is a layer 4 proxy implemented by Rust to listen on specific ports and transfer TCP data to remote addresses (only TCP) according to the configuration.
## Features ## Features
@@ -31,6 +31,13 @@ $ cargo install l4p
Or you can download binary file form the Release page. Or you can download binary file form the Release page.
## Features
- Listen on specific port and proxy to local or remote port
- SNI-based rule without terminating TLS connection
- Wildcard SNI matching with DNS-style longest-suffix-match
- DNS-based backend with periodic resolution
## Configuration ## Configuration
`l4p` will read yaml format configuration file from `/etc/l4p/l4p.yaml`, and you can set custom path to environment variable `L4P_CONFIG`, here is an minimal viable example: `l4p` will read yaml format configuration file from `/etc/l4p/l4p.yaml`, and you can set custom path to environment variable `L4P_CONFIG`, here is an minimal viable example:
@@ -55,9 +62,17 @@ There are two upstreams built in:
For detailed configuration, check [this example](./config.yaml.example). For detailed configuration, check [this example](./config.yaml.example).
### SNI Matching
The proxy supports both exact and wildcard SNI patterns in the `sni` config. Wildcards use DNS-style longest-suffix-match: more specific patterns take precedence. For example, with `*.example.com` and `*.api.example.com`, request `api.example.com` matches the first, while `v2.api.example.com` matches the second.
Wildcards are validated against the Public Suffix List (PSL). Known suffixes (`.com`, `.org`) require at least one label below the suffix (`*.example.com` OK, `*.com` rejected). Unknown suffixes (`.local`, `.lan`) are allowed without restriction.
Invalid wildcard patterns are rejected at config load time with clear error messages.
## Thanks ## Thanks
- [`l4p`](https://crates.io/crates/`l4p`), of which this is a heavily modified fork. - [`fourth`](https://crates.io/crates/fourth), of which this is a heavily modified fork.
## License ## License

View File

@@ -10,6 +10,9 @@ servers:
sni: sni:
api.example.org: example-api api.example.org: example-api
www.example.org: proxy www.example.org: proxy
*.example.org: wildcard-proxy # Matches any subdomain of example.org
*.dev.example.org: dev-proxy # More specific: matches v2.dev.example.org, etc.
*.local: local-upstream # Unknown suffix - allowed (no PSL restriction)
default: ban default: ban
second-server: second-server:
@@ -19,3 +22,6 @@ servers:
upstream: upstream:
proxy: "tcp://new-www.example.org:443" # Connect over IPv4 or IPv6 to new-www.example.org:443 proxy: "tcp://new-www.example.org:443" # Connect over IPv4 or IPv6 to new-www.example.org:443
example-api: "tcp6://api-v1.example.com:443" # Connect over IPv6 to api-v1.example.com:443 example-api: "tcp6://api-v1.example.com:443" # Connect over IPv6 to api-v1.example.com:443
wildcard-proxy: "tcp://wildcard.example.org:443"
dev-proxy: "tcp://dev.example.org:443"
local-upstream: "tcp://localhost:8080"

View File

@@ -15,11 +15,6 @@ servers:
listen: listen:
- "127.0.0.1:8081" - "127.0.0.1:8081"
default: remote default: remote
kcp_server:
protocol: kcp # default TCP
listen:
- "127.0.0.1:8082"
default: echo
upstream: upstream:
nginx: "tcp://127.0.0.1:8080" nginx: "tcp://127.0.0.1:8080"

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# vim: set expandtab shiftwidth=4 softtabstop=4 tabstop=4 :
set -euo pipefail
if [ -z "${CI_COMMIT_TAG:-}" ]; then
echo "No commit tag set"
exit 1
fi
DIR=$(realpath $(dirname "${BASH_SOURCE[0]}") )
echo ${DIR}
${DIR}/install_tea.sh linux-amd64 https://gitea.com/api/v1/repos/gitea/tea/releases/latest
## Log in to Gitea
TEA=$(pwd)/tea
if [ -z "${GITEA_SERVER_URL:-}" ]; then
if [ -z "${CI_FORGE_URL:-}" ]; then
echo "Cannot log in to gitea: GITEA_SERVER_URL or CI_FORGE_URL missing"
exit 1
fi
GITEA_SERVER_URL=${CI_FORGE_URL}
fi
if [ -z "${GITEA_SERVER_TOKEN:-}" ]; then
echo "Cannot log in to gitea: GITEA_SERVER_TOKEN missing"
exit 1
fi
if ! ${TEA} login ls | grep ${GITEA_SERVER_URL} 2>&1 > /dev/null || false; then
${TEA} login add
else
echo "Already logged in to ${GITEA_SERVER_URL}"
fi
## Check and create tag
if ${TEA} release ls -o json | jq -e --arg tag "${CI_COMMIT_TAG}" 'map(.["tag-_name"]) | index($tag) != null' >/dev/null; then
echo "Release ${CI_COMMIT_TAG} exists"
else
echo "Creating release ${CI_COMMIT_TAG}"
${TEA} release create -o json --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --draft
fi
find $(dirname ${DIR})/artifacts -type f -exec ${TEA} releases assets create -o json ${CI_COMMIT_TAG} {} +

39
scripts/install_tea.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -eo pipefail
if [ -x ./tea ]; then
echo "tea already installed in current directory"; exit 0
fi
platform="${1:-linux-amd64}"
src="${2:-release.json}"
# obtain JSON: if src looks like a URL fetch it, otherwise treat as filename (or default file)
if [[ "$src" =~ ^https?:// ]]; then
curl -fsSL "$src" -o /tmp/release.json.$$
json="/tmp/release.json.$$"
trap 'rm -f "$json"' EXIT
elif [ -f "$src" ]; then
json="$src"
else
echo "release JSON not found; provide a filename or URL as second arg" >&2
exit 1
fi
# read tag and find binary URL (exclude archives/checksums/sigs)
tag=$(jq -r '.tag_name' "$json")
url=$(jq -r --arg p "$platform" '.assets[]
| select(.name | test($p))
| select(.name | test("\\.(xz|zip|gz|tar|bz2|7z|sha256|sha256sum|sig|asc)$") | not)
| .browser_download_url' "$json" | head -n1)
[ -n "$url" ] || { echo "binary not found for $platform" >&2; exit 1; }
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSL "$url" -o "$tmp"
mv "$tmp" tea
chmod +x tea
echo "Downloaded tag ${tag}: $url -> ./tea"

View File

@@ -1,6 +1,7 @@
use crate::sni_matcher::SniMatcher;
use crate::upstreams::ProxyToUpstream; use crate::upstreams::ProxyToUpstream;
use crate::upstreams::Upstream; use crate::upstreams::Upstream;
use log::{debug, warn}; use log::{debug, info, warn};
use serde::Deserialize; use serde::Deserialize;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs::File; use std::fs::File;
@@ -12,14 +13,6 @@ pub struct ConfigV1 {
pub base: ParsedConfigV1, pub base: ParsedConfigV1,
} }
#[derive(Debug, Default, Deserialize, Clone)]
pub struct ParsedConfigV1 {
pub version: i32,
pub log: Option<String>,
pub servers: HashMap<String, ServerConfig>,
pub upstream: HashMap<String, Upstream>,
}
#[derive(Debug, Default, Deserialize, Clone)] #[derive(Debug, Default, Deserialize, Clone)]
pub struct BaseConfig { pub struct BaseConfig {
pub version: i32, pub version: i32,
@@ -28,6 +21,14 @@ pub struct BaseConfig {
pub upstream: HashMap<String, String>, pub upstream: HashMap<String, String>,
} }
#[derive(Debug, Default, Clone)]
pub struct ParsedConfigV1 {
pub version: i32,
pub log: Option<String>,
pub servers: HashMap<String, ParsedServerConfig>,
pub upstream: HashMap<String, Upstream>,
}
#[derive(Debug, Default, Deserialize, Clone)] #[derive(Debug, Default, Deserialize, Clone)]
pub struct ServerConfig { pub struct ServerConfig {
pub listen: Vec<String>, pub listen: Vec<String>,
@@ -36,6 +37,35 @@ pub struct ServerConfig {
pub sni: Option<HashMap<String, String>>, pub sni: Option<HashMap<String, String>>,
pub default: Option<String>, pub default: Option<String>,
} }
impl ServerConfig {
pub fn into_parsed(self) -> Result<ParsedServerConfig, Vec<String>> {
let sni = match self.sni {
Some(sni_map) => {
let matcher = SniMatcher::new(sni_map)?;
Some(matcher)
}
None => None,
};
Ok(ParsedServerConfig {
listen: self.listen,
protocol: self.protocol,
tls: self.tls,
sni,
default: self.default,
})
}
}
#[derive(Debug, Clone)]
pub struct ParsedServerConfig {
pub listen: Vec<String>,
pub protocol: Option<String>,
pub tls: Option<bool>,
pub sni: Option<SniMatcher>,
pub default: Option<String>,
}
impl TryInto<ProxyToUpstream> for &str { impl TryInto<ProxyToUpstream> for &str {
type Error = ConfigError; type Error = ConfigError;
@@ -102,12 +132,23 @@ impl ConfigV1 {
} }
} }
fn load_config(path: &str) -> Result<ParsedConfigV1, ConfigError> { /// Load and parse configuration from a YAML string.
let mut contents = String::new(); ///
let mut file = File::open(path)?; /// This public function takes raw YAML content as a string and returns a parsed,
file.read_to_string(&mut contents)?; /// validated configuration. It performs all validation including:
/// - Version checking
let base: BaseConfig = serde_yaml::from_str(&contents)?; /// - SNI pattern validation
/// - Upstream URL parsing
/// - Cross-reference validation
///
/// # Arguments
/// * `yaml_str` - The YAML configuration content as a string
///
/// # Returns
/// * `Ok(ParsedConfigV1)` - Successfully parsed and validated configuration
/// * `Err(ConfigError)` - If YAML parsing fails or validation errors occur
pub fn load_config_from_yaml(yaml_str: &str) -> Result<ParsedConfigV1, ConfigError> {
let base: BaseConfig = serde_yaml::from_str(yaml_str)?;
if base.version != 1 { if base.version != 1 {
return Err(ConfigError::Custom( return Err(ConfigError::Custom(
@@ -117,11 +158,13 @@ fn load_config(path: &str) -> Result<ParsedConfigV1, ConfigError> {
let log_level = base.log.clone().unwrap_or_else(|| "info".to_string()); let log_level = base.log.clone().unwrap_or_else(|| "info".to_string());
if !log_level.eq("disable") { if !log_level.eq("disable") {
std::env::set_var("FOURTH_LOG", log_level.clone()); unsafe {
pretty_env_logger::init_custom_env("FOURTH_LOG"); std::env::set_var("FOURTH_LOG", log_level.clone());
debug!("Set log level to {}", log_level); pretty_env_logger::init_custom_env("FOURTH_LOG");
}
} }
debug!("Set log level to {}", log_level);
debug!("Config version {}", base.version); debug!("Config version {}", base.version);
let mut parsed_upstream: HashMap<String, Upstream> = HashMap::new(); let mut parsed_upstream: HashMap<String, Upstream> = HashMap::new();
@@ -134,16 +177,50 @@ fn load_config(path: &str) -> Result<ParsedConfigV1, ConfigError> {
parsed_upstream.insert(name.to_string(), Upstream::Proxy(ups)); parsed_upstream.insert(name.to_string(), Upstream::Proxy(ups));
} }
// Convert ServerConfig to ParsedServerConfig, collecting all SNI validation errors
let mut all_errors: Vec<String> = Vec::new();
let mut parsed_servers: HashMap<String, ParsedServerConfig> = HashMap::new();
for (name, server_config) in base.servers {
match server_config.into_parsed() {
Ok(parsed) => {
parsed_servers.insert(name, parsed);
}
Err(errors) => {
for err in errors {
all_errors.push(format!("Server '{}': {}", name, err));
}
}
}
}
if !all_errors.is_empty() {
return Err(ConfigError::Custom(format!(
"Invalid SNI configuration:\n{}",
all_errors.join("\n")
)));
}
let parsed = ParsedConfigV1 { let parsed = ParsedConfigV1 {
version: base.version, version: base.version,
log: base.log, log: base.log,
servers: base.servers, servers: parsed_servers,
upstream: parsed_upstream, upstream: parsed_upstream,
}; };
verify_config(parsed) verify_config(parsed)
} }
fn load_config(path: &str) -> Result<ParsedConfigV1, ConfigError> {
let mut contents = String::new();
let mut file = File::open(path)?;
file.read_to_string(&mut contents)?;
info!("Using config file: {}", &path);
load_config_from_yaml(&contents)
}
fn verify_config(config: ParsedConfigV1) -> Result<ParsedConfigV1, ConfigError> { fn verify_config(config: ParsedConfigV1) -> Result<ParsedConfigV1, ConfigError> {
let mut used_upstreams: HashSet<String> = HashSet::new(); let mut used_upstreams: HashSet<String> = HashSet::new();
let mut upstream_names: HashSet<String> = HashSet::new(); let mut upstream_names: HashSet<String> = HashSet::new();
@@ -174,14 +251,20 @@ fn verify_config(config: ParsedConfigV1) -> Result<ParsedConfigV1, ConfigError>
listen_addresses.insert(listen.to_string()); listen_addresses.insert(listen.to_string());
} }
if server.tls.unwrap_or_default() && server.sni.is_some() { if server.tls.unwrap_or_default() {
for (_, val) in server.sni.unwrap() { if let Some(matcher) = &server.sni {
used_upstreams.insert(val.to_string()); // Collect all upstream names from the SniMatcher
for (_, upstream) in matcher.exact.iter() {
used_upstreams.insert(upstream.clone());
}
for pattern in &matcher.wildcards {
used_upstreams.insert(pattern.upstream.clone());
}
} }
} }
if server.default.is_some() { if let Some(default) = &server.default {
used_upstreams.insert(server.default.unwrap().to_string()); used_upstreams.insert(default.clone());
} }
for key in &used_upstreams { for key in &used_upstreams {
@@ -221,7 +304,48 @@ mod tests {
let config = ConfigV1::new("tests/config.yaml").unwrap(); let config = ConfigV1::new("tests/config.yaml").unwrap();
assert_eq!(config.base.version, 1); assert_eq!(config.base.version, 1);
assert_eq!(config.base.log.unwrap(), "disable"); assert_eq!(config.base.log.unwrap(), "disable");
assert_eq!(config.base.servers.len(), 5); assert_eq!(config.base.servers.len(), 3);
assert_eq!(config.base.upstream.len(), 3 + 2); // Add ban and echo upstreams assert_eq!(config.base.upstream.len(), 3 + 2); // Add ban and echo upstreams
} }
#[test]
fn test_config_hard_failure_on_invalid_sni() {
// Test that invalid SNI wildcard (*.com) causes hard failure
let config_content = r#"version: 1
log: disable
servers:
test_server:
listen:
- "127.0.0.1:8443"
protocol: tcp
tls: true
sni:
"*.com": "upstream1"
default: ban
upstream:
upstream1: tcp://127.0.0.1:9000
"#;
let result = load_config_from_yaml(config_content);
// Should fail with an error
assert!(result.is_err(), "Expected config to fail with invalid SNI");
// Verify error message contains helpful information
match result {
Err(ConfigError::Custom(msg)) => {
assert!(
msg.contains("Invalid SNI"),
"Error message should mention invalid SNI: {}",
msg
);
assert!(
msg.contains("*.com"),
"Error message should mention the invalid pattern: {}",
msg
);
}
_ => panic!("Expected ConfigError::Custom"),
}
}
} }

View File

@@ -1,3 +1,4 @@
mod config; mod config_v1;
pub(crate) use config::ConfigV1; pub(crate) use config_v1::ConfigV1;
pub(crate) use config::ParsedConfigV1; pub(crate) use config_v1::ParsedConfigV1;
pub(crate) use config_v1::ParsedServerConfig;

View File

@@ -1,15 +1,53 @@
mod config; mod config;
mod plugins;
mod servers; mod servers;
mod sni_matcher;
mod update;
mod upstreams; mod upstreams;
use crate::config::ConfigV1; use crate::config::ConfigV1;
use crate::servers::Server; use crate::servers::Server;
use std::io::{stderr, stdout, Write};
use log::{debug, error}; use log::{debug, error};
use pico_args::Arguments;
use std::path::PathBuf; use std::path::PathBuf;
fn main() { fn main() {
let mut args = Arguments::from_env();
match args.subcommand().expect("Unexpected error").as_deref() {
Some("serve") => serve(),
Some("update") => update::update(),
Some("help") => {
let _ = print_usage(&mut stdout().lock());
}
Some(cmd) => {
eprintln!("Invalid command: {cmd}");
std::process::exit(1);
}
None => {
eprintln!("Calling l4p without argument is deprecated now. Please use: l4p serve");
let _ = print_usage(&mut stderr().lock());
serve();
}
}
}
fn print_usage(out: &mut dyn Write) -> std::io::Result<()> {
writeln!(
out,
"{} v{}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
)?;
writeln!(out, "Usage:")?;
writeln!(out, "\tupdate\tUpdate l4p to the latest version")?;
writeln!(out, "\tserve\tServe the proxy")?;
writeln!(out, "\thelp\tPrint this message")?;
Ok(())
}
fn serve() {
let config_path = match find_config() { let config_path = match find_config() {
Ok(p) => p, Ok(p) => p,
Err(paths) => { Err(paths) => {
@@ -38,20 +76,28 @@ fn main() {
} }
fn find_config() -> Result<String, Vec<String>> { fn find_config() -> Result<String, Vec<String>> {
let possible_paths = ["/etc/l4p", ""]; let possible_locations = ["/etc/l4p", ""];
let possible_names = ["l4p.yaml", "config.yaml"]; let possible_names = ["l4p.yaml", "config.yaml"];
let mut tried_paths = Vec::<String>::new(); let mut tried_paths = Vec::<String>::new();
let mut possible_paths = Vec::<PathBuf>::new();
for path in possible_paths if let Ok(env_path) = std::env::var("L4P_CONFIG") {
.iter() possible_paths.push(PathBuf::from(env_path));
.flat_map(|&path| { }
possible_names
.iter() possible_paths.append(
.map(move |&file| PathBuf::new().join(path).join(file)) &mut possible_locations
}) .iter()
.collect::<Vec<PathBuf>>() .flat_map(|&path| {
{ possible_names
.iter()
.map(move |&file| PathBuf::new().join(path).join(file))
})
.collect::<Vec<PathBuf>>(),
);
for path in possible_paths {
let path_str = path.to_string_lossy().to_string(); let path_str = path.to_string_lossy().to_string();
if path.exists() { if path.exists() {
return Ok(path_str); return Ok(path_str);

View File

@@ -1,110 +0,0 @@
use std::{io::Write, time::Duration};
use kcp::Kcp;
/// Kcp Delay Config
#[derive(Debug, Clone, Copy)]
pub struct KcpNoDelayConfig {
/// Enable nodelay
pub nodelay: bool,
/// Internal update interval (ms)
pub interval: i32,
/// ACK number to enable fast resend
pub resend: i32,
/// Disable congetion control
pub nc: bool,
}
impl Default for KcpNoDelayConfig {
fn default() -> KcpNoDelayConfig {
KcpNoDelayConfig {
nodelay: false,
interval: 100,
resend: 0,
nc: false,
}
}
}
#[allow(unused)]
impl KcpNoDelayConfig {
/// Get a fastest configuration
///
/// 1. Enable NoDelay
/// 2. Set ticking interval to be 10ms
/// 3. Set fast resend to be 2
/// 4. Disable congestion control
pub fn fastest() -> KcpNoDelayConfig {
KcpNoDelayConfig {
nodelay: true,
interval: 10,
resend: 2,
nc: true,
}
}
/// Get a normal configuration
///
/// 1. Disable NoDelay
/// 2. Set ticking interval to be 40ms
/// 3. Disable fast resend
/// 4. Enable congestion control
pub fn normal() -> KcpNoDelayConfig {
KcpNoDelayConfig {
nodelay: false,
interval: 40,
resend: 0,
nc: false,
}
}
}
/// Kcp Config
#[derive(Debug, Clone, Copy)]
pub struct KcpConfig {
/// Max Transmission Unit
pub mtu: usize,
/// nodelay
pub nodelay: KcpNoDelayConfig,
/// Send window size
pub wnd_size: (u16, u16),
/// Session expire duration, default is 90 seconds
pub session_expire: Duration,
/// Flush KCP state immediately after write
pub flush_write: bool,
/// Flush ACKs immediately after input
pub flush_acks_input: bool,
/// Stream mode
pub stream: bool,
}
impl Default for KcpConfig {
fn default() -> KcpConfig {
KcpConfig {
mtu: 1400,
nodelay: KcpNoDelayConfig::normal(),
wnd_size: (256, 256),
session_expire: Duration::from_secs(90),
flush_write: false,
flush_acks_input: false,
stream: true,
}
}
}
impl KcpConfig {
/// Applies config onto `Kcp`
#[doc(hidden)]
pub fn apply_config<W: Write>(&self, k: &mut Kcp<W>) {
k.set_mtu(self.mtu).expect("invalid MTU");
k.set_nodelay(
self.nodelay.nodelay,
self.nodelay.interval,
self.nodelay.resend,
self.nodelay.nc,
);
k.set_wndsize(self.wnd_size.0, self.wnd_size.1);
}
}

View File

@@ -1,128 +0,0 @@
use std::{
io::{self, ErrorKind},
net::SocketAddr,
sync::Arc,
time::Duration,
};
use byte_string::ByteStr;
use kcp::{Error as KcpError, KcpResult};
use log::{debug, error, trace};
use tokio::{
net::{ToSocketAddrs, UdpSocket},
sync::mpsc,
task::JoinHandle,
time,
};
use crate::plugins::kcp::{config::KcpConfig, session::KcpSessionManager, stream::KcpStream};
#[allow(unused)]
pub struct KcpListener {
udp: Arc<UdpSocket>,
accept_rx: mpsc::Receiver<(KcpStream, SocketAddr)>,
task_watcher: JoinHandle<()>,
}
impl Drop for KcpListener {
fn drop(&mut self) {
self.task_watcher.abort();
}
}
impl KcpListener {
pub async fn bind<A: ToSocketAddrs>(config: KcpConfig, addr: A) -> KcpResult<KcpListener> {
let udp = UdpSocket::bind(addr).await?;
let udp = Arc::new(udp);
let server_udp = udp.clone();
let (accept_tx, accept_rx) = mpsc::channel(1024 /* backlogs */);
let task_watcher = tokio::spawn(async move {
let (close_tx, mut close_rx) = mpsc::channel(64);
let mut sessions = KcpSessionManager::new();
let mut packet_buffer = [0u8; 65536];
loop {
tokio::select! {
conv = close_rx.recv() => {
let conv = conv.expect("close_tx closed unexpectly");
sessions.close_conv(conv);
trace!("session conv: {} removed", conv);
}
recv_res = udp.recv_from(&mut packet_buffer) => {
match recv_res {
Err(err) => {
error!("udp.recv_from failed, error: {}", err);
time::sleep(Duration::from_secs(1)).await;
}
Ok((n, peer_addr)) => {
let packet = &mut packet_buffer[..n];
log::trace!("received peer: {}, {:?}", peer_addr, ByteStr::new(packet));
let mut conv = kcp::get_conv(packet);
if conv == 0 {
// Allocate a conv for client.
conv = sessions.alloc_conv();
debug!("allocate {} conv for peer: {}", conv, peer_addr);
kcp::set_conv(packet, conv);
}
let session = match sessions.get_or_create(&config, conv, &udp, peer_addr, &close_tx) {
Ok((s, created)) => {
if created {
// Created a new session, constructed a new accepted client
let stream = KcpStream::with_session(s.clone());
if let Err(..) = accept_tx.try_send((stream, peer_addr)) {
debug!("failed to create accepted stream due to channel failure");
// remove it from session
sessions.close_conv(conv);
continue;
}
}
s
},
Err(err) => {
error!("failed to create session, error: {}, peer: {}, conv: {}", err, peer_addr, conv);
continue;
}
};
// let mut kcp = session.kcp_socket().lock().await;
// if let Err(err) = kcp.input(packet) {
// error!("kcp.input failed, peer: {}, conv: {}, error: {}, packet: {:?}", peer_addr, conv, err, ByteStr::new(packet));
// }
session.input(packet).await;
}
}
}
}
}
});
Ok(KcpListener {
udp: server_udp,
accept_rx,
task_watcher,
})
}
pub async fn accept(&mut self) -> KcpResult<(KcpStream, SocketAddr)> {
match self.accept_rx.recv().await {
Some(s) => Ok(s),
None => Err(KcpError::IoError(io::Error::new(
ErrorKind::Other,
"accept channel closed unexpectly",
))),
}
}
#[allow(unused)]
pub fn local_addr(&self) -> io::Result<SocketAddr> {
self.udp.local_addr()
}
}

View File

@@ -1,14 +0,0 @@
//! Library of KCP on Tokio
pub use self::{
config::{KcpConfig, KcpNoDelayConfig},
listener::KcpListener,
stream::KcpStream,
};
mod config;
mod listener;
mod session;
mod skcp;
mod stream;
mod utils;

View File

@@ -1,256 +0,0 @@
use std::{
collections::{hash_map::Entry, HashMap},
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use byte_string::ByteStr;
use kcp::KcpResult;
use log::{error, trace};
use tokio::{
net::UdpSocket,
sync::{mpsc, Mutex},
time::{self, Instant},
};
use crate::plugins::kcp::{skcp::KcpSocket, KcpConfig};
pub struct KcpSession {
socket: Mutex<KcpSocket>,
closed: AtomicBool,
session_expire: Duration,
session_close_notifier: Option<mpsc::Sender<u32>>,
input_tx: mpsc::Sender<Vec<u8>>,
}
impl KcpSession {
fn new(
socket: KcpSocket,
session_expire: Duration,
session_close_notifier: Option<mpsc::Sender<u32>>,
input_tx: mpsc::Sender<Vec<u8>>,
) -> KcpSession {
KcpSession {
socket: Mutex::new(socket),
closed: AtomicBool::new(false),
session_expire,
session_close_notifier,
input_tx,
}
}
pub fn new_shared(
socket: KcpSocket,
session_expire: Duration,
session_close_notifier: Option<mpsc::Sender<u32>>,
) -> Arc<KcpSession> {
let is_client = session_close_notifier.is_none();
let (input_tx, mut input_rx) = mpsc::channel(64);
let udp_socket = socket.udp_socket().clone();
let session = Arc::new(KcpSession::new(
socket,
session_expire,
session_close_notifier,
input_tx,
));
{
let session = session.clone();
tokio::spawn(async move {
let mut input_buffer = [0u8; 65536];
let update_timer = time::sleep(Duration::from_millis(10));
tokio::pin!(update_timer);
loop {
tokio::select! {
// recv() then input()
// Drives the KCP machine forward
recv_result = udp_socket.recv(&mut input_buffer), if is_client => {
match recv_result {
Err(err) => {
error!("[SESSION] UDP recv failed, error: {}", err);
}
Ok(n) => {
let input_buffer = &input_buffer[..n];
trace!("[SESSION] UDP recv {} bytes, going to input {:?}", n, ByteStr::new(input_buffer));
let mut socket = session.socket.lock().await;
match socket.input(input_buffer) {
Ok(true) => {
trace!("[SESSION] UDP input {} bytes and waked sender/receiver", n);
}
Ok(false) => {}
Err(err) => {
error!("[SESSION] UDP input {} bytes error: {}, input buffer {:?}", n, err, ByteStr::new(input_buffer));
}
}
}
}
}
// bytes received from listener socket
input_opt = input_rx.recv() => {
if let Some(input_buffer) = input_opt {
let mut socket = session.socket.lock().await;
match socket.input(&input_buffer) {
Ok(..) => {
trace!("[SESSION] UDP input {} bytes from channel {:?}", input_buffer.len(), ByteStr::new(&input_buffer));
}
Err(err) => {
error!("[SESSION] UDP input {} bytes from channel failed, error: {}, input buffer {:?}",
input_buffer.len(), err, ByteStr::new(&input_buffer));
}
}
}
}
// Call update() in period
_ = &mut update_timer => {
let mut socket = session.socket.lock().await;
let is_closed = session.closed.load(Ordering::Acquire);
if is_closed && socket.can_close() {
trace!("[SESSION] KCP session closed");
break;
}
// server socket expires
if !is_client {
// If this is a server stream, close it automatically after a period of time
let last_update_time = socket.last_update_time();
let elapsed = last_update_time.elapsed();
if elapsed > session.session_expire {
if elapsed > session.session_expire * 2 {
// Force close. Client may have already gone.
trace!(
"[SESSION] force close inactive session, conv: {}, last_update: {}s ago",
socket.conv(),
elapsed.as_secs()
);
break;
}
if !is_closed {
trace!(
"[SESSION] closing inactive session, conv: {}, last_update: {}s ago",
socket.conv(),
elapsed.as_secs()
);
session.closed.store(true, Ordering::Release);
}
}
}
match socket.update() {
Ok(next_next) => {
update_timer.as_mut().reset(Instant::from_std(next_next));
}
Err(err) => {
error!("[SESSION] KCP update failed, error: {}", err);
update_timer.as_mut().reset(Instant::now() + Duration::from_millis(10));
}
}
}
}
}
{
// Close the socket.
// Wake all pending tasks and let all send/recv return EOF
let mut socket = session.socket.lock().await;
socket.close();
}
if let Some(ref notifier) = session.session_close_notifier {
let socket = session.socket.lock().await;
let _ = notifier.send(socket.conv()).await;
}
});
}
session
}
pub fn kcp_socket(&self) -> &Mutex<KcpSocket> {
&self.socket
}
pub fn close(&self) {
self.closed.store(true, Ordering::Release);
}
pub async fn input(&self, buf: &[u8]) {
self.input_tx
.send(buf.to_owned())
.await
.expect("input channel closed")
}
}
pub struct KcpSessionManager {
sessions: HashMap<u32, Arc<KcpSession>>,
next_free_conv: u32,
}
impl KcpSessionManager {
pub fn new() -> KcpSessionManager {
KcpSessionManager {
sessions: HashMap::new(),
next_free_conv: 0,
}
}
pub fn close_conv(&mut self, conv: u32) {
self.sessions.remove(&conv);
}
pub fn alloc_conv(&mut self) -> u32 {
loop {
let (mut c, _) = self.next_free_conv.overflowing_add(1);
if c == 0 {
let (nc, _) = c.overflowing_add(1);
c = nc;
}
self.next_free_conv = c;
if self.sessions.get(&self.next_free_conv).is_none() {
let conv = self.next_free_conv;
return conv;
}
}
}
pub fn get_or_create(
&mut self,
config: &KcpConfig,
conv: u32,
udp: &Arc<UdpSocket>,
peer_addr: SocketAddr,
session_close_notifier: &mpsc::Sender<u32>,
) -> KcpResult<(Arc<KcpSession>, bool)> {
match self.sessions.entry(conv) {
Entry::Occupied(occ) => Ok((occ.get().clone(), false)),
Entry::Vacant(vac) => {
let socket = KcpSocket::new(config, conv, udp.clone(), peer_addr, config.stream)?;
let session = KcpSession::new_shared(
socket,
config.session_expire,
Some(session_close_notifier.clone()),
);
trace!("created session for conv: {}, peer: {}", conv, peer_addr);
vac.insert(session.clone());
Ok((session, true))
}
}
}
}

View File

@@ -1,288 +0,0 @@
use std::{
io::{self, ErrorKind, Write},
net::SocketAddr,
sync::Arc,
task::{Context, Poll, Waker},
time::{Duration, Instant},
};
use futures::future;
use kcp::{Error as KcpError, Kcp, KcpResult};
use log::{error, trace};
use tokio::{net::UdpSocket, sync::mpsc};
use crate::plugins::kcp::{utils::now_millis, KcpConfig};
/// Writer for sending packets to the underlying UdpSocket
struct UdpOutput {
socket: Arc<UdpSocket>,
target_addr: SocketAddr,
delay_tx: mpsc::UnboundedSender<Vec<u8>>,
}
impl UdpOutput {
/// Create a new Writer for writing packets to UdpSocket
pub fn new(socket: Arc<UdpSocket>, target_addr: SocketAddr) -> UdpOutput {
let (delay_tx, mut delay_rx) = mpsc::unbounded_channel::<Vec<u8>>();
{
let socket = socket.clone();
tokio::spawn(async move {
while let Some(buf) = delay_rx.recv().await {
if let Err(err) = socket.send_to(&buf, target_addr).await {
error!("[SEND] UDP delayed send failed, error: {}", err);
}
}
});
}
UdpOutput {
socket,
target_addr,
delay_tx,
}
}
}
impl Write for UdpOutput {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.socket.try_send_to(buf, self.target_addr) {
Ok(n) => Ok(n),
Err(ref err) if err.kind() == ErrorKind::WouldBlock => {
// send return EAGAIN
// ignored as packet was lost in transmission
trace!(
"[SEND] UDP send EAGAIN, packet.size: {} bytes, delayed send",
buf.len()
);
self.delay_tx
.send(buf.to_owned())
.expect("channel closed unexpectly");
Ok(buf.len())
}
Err(err) => Err(err),
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub struct KcpSocket {
kcp: Kcp<UdpOutput>,
last_update: Instant,
socket: Arc<UdpSocket>,
flush_write: bool,
flush_ack_input: bool,
sent_first: bool,
pending_sender: Option<Waker>,
pending_receiver: Option<Waker>,
closed: bool,
}
impl KcpSocket {
pub fn new(
c: &KcpConfig,
conv: u32,
socket: Arc<UdpSocket>,
target_addr: SocketAddr,
stream: bool,
) -> KcpResult<KcpSocket> {
let output = UdpOutput::new(socket.clone(), target_addr);
let mut kcp = if stream {
Kcp::new_stream(conv, output)
} else {
Kcp::new(conv, output)
};
c.apply_config(&mut kcp);
// Ask server to allocate one
if conv == 0 {
kcp.input_conv();
}
kcp.update(now_millis())?;
Ok(KcpSocket {
kcp,
last_update: Instant::now(),
socket,
flush_write: c.flush_write,
flush_ack_input: c.flush_acks_input,
sent_first: false,
pending_sender: None,
pending_receiver: None,
closed: false,
})
}
/// Call every time you got data from transmission
pub fn input(&mut self, buf: &[u8]) -> KcpResult<bool> {
match self.kcp.input(buf) {
Ok(..) => {}
Err(KcpError::ConvInconsistent(expected, actual)) => {
trace!(
"[INPUT] Conv expected={} actual={} ignored",
expected,
actual
);
return Ok(false);
}
Err(err) => return Err(err),
}
self.last_update = Instant::now();
if self.flush_ack_input {
self.kcp.flush_ack()?;
}
Ok(self.try_wake_pending_waker())
}
/// Call if you want to send some data
pub fn poll_send(&mut self, cx: &mut Context<'_>, mut buf: &[u8]) -> Poll<KcpResult<usize>> {
if self.closed {
return Ok(0).into();
}
// If:
// 1. Have sent the first packet (asking for conv)
// 2. Too many pending packets
if self.sent_first
&& (self.kcp.wait_snd() >= self.kcp.snd_wnd() as usize || self.kcp.waiting_conv())
{
trace!(
"[SEND] waitsnd={} sndwnd={} excceeded or waiting conv={}",
self.kcp.wait_snd(),
self.kcp.snd_wnd(),
self.kcp.waiting_conv()
);
self.pending_sender = Some(cx.waker().clone());
return Poll::Pending;
}
if !self.sent_first && self.kcp.waiting_conv() && buf.len() > self.kcp.mss() as usize {
buf = &buf[..self.kcp.mss() as usize];
}
let n = self.kcp.send(buf)?;
self.sent_first = true;
self.last_update = Instant::now();
if self.flush_write {
self.kcp.flush()?;
}
Ok(n).into()
}
/// Call if you want to send some data
#[allow(dead_code)]
pub async fn send(&mut self, buf: &[u8]) -> KcpResult<usize> {
future::poll_fn(|cx| self.poll_send(cx, buf)).await
}
#[allow(dead_code)]
pub fn try_recv(&mut self, buf: &mut [u8]) -> KcpResult<usize> {
if self.closed {
return Ok(0);
}
self.kcp.recv(buf)
}
pub fn poll_recv(&mut self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<KcpResult<usize>> {
if self.closed {
return Ok(0).into();
}
match self.kcp.recv(buf) {
Ok(n) => Ok(n).into(),
Err(KcpError::RecvQueueEmpty) => {
self.pending_receiver = Some(cx.waker().clone());
Poll::Pending
}
Err(err) => Err(err).into(),
}
}
#[allow(dead_code)]
pub async fn recv(&mut self, buf: &mut [u8]) -> KcpResult<usize> {
future::poll_fn(|cx| self.poll_recv(cx, buf)).await
}
pub fn flush(&mut self) -> KcpResult<()> {
self.kcp.flush()?;
self.last_update = Instant::now();
Ok(())
}
fn try_wake_pending_waker(&mut self) -> bool {
let mut waked = false;
if self.pending_sender.is_some()
&& self.kcp.wait_snd() < self.kcp.snd_wnd() as usize
&& !self.kcp.waiting_conv()
{
let waker = self.pending_sender.take().unwrap();
waker.wake();
waked = true;
}
if self.pending_receiver.is_some() {
if let Ok(peek) = self.kcp.peeksize() {
if peek > 0 {
let waker = self.pending_receiver.take().unwrap();
waker.wake();
waked = true;
}
}
}
waked
}
pub fn update(&mut self) -> KcpResult<Instant> {
let now = now_millis();
self.kcp.update(now)?;
let next = self.kcp.check(now);
self.try_wake_pending_waker();
Ok(Instant::now() + Duration::from_millis(next as u64))
}
pub fn close(&mut self) {
self.closed = true;
if let Some(w) = self.pending_sender.take() {
w.wake();
}
if let Some(w) = self.pending_receiver.take() {
w.wake();
}
}
pub fn udp_socket(&self) -> &Arc<UdpSocket> {
&self.socket
}
pub fn can_close(&self) -> bool {
self.kcp.wait_snd() == 0
}
pub fn conv(&self) -> u32 {
self.kcp.conv()
}
pub fn peek_size(&self) -> KcpResult<usize> {
self.kcp.peeksize()
}
pub fn last_update_time(&self) -> Instant {
self.last_update
}
}

View File

@@ -1,183 +0,0 @@
use std::{
io::{self, ErrorKind},
net::{IpAddr, SocketAddr},
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use futures::{future, ready};
use kcp::{Error as KcpError, KcpResult};
use log::trace;
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::UdpSocket,
};
use crate::plugins::kcp::{config::KcpConfig, session::KcpSession, skcp::KcpSocket};
pub struct KcpStream {
session: Arc<KcpSession>,
recv_buffer: Vec<u8>,
recv_buffer_pos: usize,
recv_buffer_cap: usize,
}
impl Drop for KcpStream {
fn drop(&mut self) {
self.session.close();
}
}
#[allow(unused)]
impl KcpStream {
pub async fn connect(config: &KcpConfig, addr: SocketAddr) -> KcpResult<KcpStream> {
let udp = match addr.ip() {
IpAddr::V4(..) => UdpSocket::bind("0.0.0.0:0").await?,
IpAddr::V6(..) => UdpSocket::bind("[::]:0").await?,
};
let udp = Arc::new(udp);
let socket = KcpSocket::new(config, 0, udp, addr, config.stream)?;
let session = KcpSession::new_shared(socket, config.session_expire, None);
Ok(KcpStream::with_session(session))
}
pub(crate) fn with_session(session: Arc<KcpSession>) -> KcpStream {
KcpStream {
session,
recv_buffer: Vec::new(),
recv_buffer_pos: 0,
recv_buffer_cap: 0,
}
}
pub fn poll_send(&mut self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<KcpResult<usize>> {
// Mutex doesn't have poll_lock, spinning on it.
let socket = self.session.kcp_socket();
let mut kcp = match socket.try_lock() {
Ok(guard) => guard,
Err(..) => {
cx.waker().wake_by_ref();
return Poll::Pending;
}
};
kcp.poll_send(cx, buf)
}
pub async fn send(&mut self, buf: &[u8]) -> KcpResult<usize> {
future::poll_fn(|cx| self.poll_send(cx, buf)).await
}
pub fn poll_recv(&mut self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<KcpResult<usize>> {
loop {
// Consumes all data in buffer
if self.recv_buffer_pos < self.recv_buffer_cap {
let remaining = self.recv_buffer_cap - self.recv_buffer_pos;
let copy_length = remaining.min(buf.len());
buf.copy_from_slice(
&self.recv_buffer[self.recv_buffer_pos..self.recv_buffer_pos + copy_length],
);
self.recv_buffer_pos += copy_length;
return Ok(copy_length).into();
}
// Mutex doesn't have poll_lock, spinning on it.
let socket = self.session.kcp_socket();
let mut kcp = match socket.try_lock() {
Ok(guard) => guard,
Err(..) => {
cx.waker().wake_by_ref();
return Poll::Pending;
}
};
// Try to read from KCP
// 1. Read directly with user provided `buf`
match ready!(kcp.poll_recv(cx, buf)) {
Ok(n) => {
trace!("[CLIENT] recv directly {} bytes", n);
return Ok(n).into();
}
Err(KcpError::UserBufTooSmall) => {}
Err(err) => return Err(err).into(),
}
// 2. User `buf` too small, read to recv_buffer
let required_size = kcp.peek_size()?;
if self.recv_buffer.len() < required_size {
self.recv_buffer.resize(required_size, 0);
}
match ready!(kcp.poll_recv(cx, &mut self.recv_buffer)) {
Ok(n) => {
trace!("[CLIENT] recv buffered {} bytes", n);
self.recv_buffer_pos = 0;
self.recv_buffer_cap = n;
}
Err(err) => return Err(err).into(),
}
}
}
pub async fn recv(&mut self, buf: &mut [u8]) -> KcpResult<usize> {
future::poll_fn(|cx| self.poll_recv(cx, buf)).await
}
}
impl AsyncRead for KcpStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
match ready!(self.poll_recv(cx, buf.initialize_unfilled())) {
Ok(n) => {
buf.advance(n);
Ok(()).into()
}
Err(KcpError::IoError(err)) => Err(err).into(),
Err(err) => Err(io::Error::new(ErrorKind::Other, err)).into(),
}
}
}
impl AsyncWrite for KcpStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
match ready!(self.poll_send(cx, buf)) {
Ok(n) => Ok(n).into(),
Err(KcpError::IoError(err)) => Err(err).into(),
Err(err) => Err(io::Error::new(ErrorKind::Other, err)).into(),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
// Mutex doesn't have poll_lock, spinning on it.
let socket = self.session.kcp_socket();
let mut kcp = match socket.try_lock() {
Ok(guard) => guard,
Err(..) => {
cx.waker().wake_by_ref();
return Poll::Pending;
}
};
match kcp.flush() {
Ok(..) => Ok(()).into(),
Err(KcpError::IoError(err)) => Err(err).into(),
Err(err) => Err(io::Error::new(ErrorKind::Other, err)).into(),
}
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Ok(()).into()
}
}

View File

@@ -1,10 +0,0 @@
use std::time::{SystemTime, UNIX_EPOCH};
#[inline]
pub fn now_millis() -> u32 {
let start = SystemTime::now();
let since_the_epoch = start
.duration_since(UNIX_EPOCH)
.expect("time went afterwards");
(since_the_epoch.as_secs() * 1000 + since_the_epoch.subsec_millis() as u64 / 1_000_000) as u32
}

View File

@@ -1 +0,0 @@
//pub mod kcp;

View File

@@ -9,6 +9,7 @@ mod protocol;
pub(crate) mod upstream_address; pub(crate) mod upstream_address;
use crate::config::ParsedConfigV1; use crate::config::ParsedConfigV1;
use crate::sni_matcher::SniMatcher;
use crate::upstreams::Upstream; use crate::upstreams::Upstream;
use protocol::tcp; use protocol::tcp;
@@ -23,7 +24,7 @@ pub(crate) struct Proxy {
pub listen: SocketAddr, pub listen: SocketAddr,
pub protocol: String, pub protocol: String,
pub tls: bool, pub tls: bool,
pub sni: Option<HashMap<String, String>>, pub sni: Option<SniMatcher>,
pub default_action: String, pub default_action: String,
pub upstream: HashMap<String, Upstream>, pub upstream: HashMap<String, Upstream>,
} }
@@ -90,12 +91,6 @@ impl Server {
error!("Failed to start {}: {}", config.name, res.err().unwrap()); error!("Failed to start {}: {}", config.name, res.err().unwrap());
} }
} }
// "kcp" => {
// let res = kcp::proxy(config.clone()).await;
// if res.is_err() {
// error!("Failed to start {}: {}", config.name, res.err().unwrap());
// }
// }
_ => { _ => {
error!("Invalid protocol: {}", config.protocol) error!("Invalid protocol: {}", config.protocol)
} }
@@ -113,7 +108,6 @@ impl Server {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
//use crate::plugins::kcp::{KcpConfig, KcpStream};
use std::thread::{self, sleep}; use std::thread::{self, sleep};
use std::time::Duration; use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -141,6 +135,132 @@ mod tests {
} }
} }
/// Mock server for wildcard SNI test that responds with "tls_wildcard_response" on first read
#[tokio::main]
async fn tls_mock_server_wildcard() {
let server_addr: SocketAddr = "127.0.0.1:54598".parse().unwrap();
let listener = TcpListener::bind(server_addr).await.unwrap();
loop {
let (mut stream, _) = listener.accept().await.unwrap();
// Read client hello (which will be peeked but not actually read by proxy)
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await;
// Send a response to verify connection succeeded
let _ = stream.write(b"tls_wildcard_response").await;
let _ = stream.shutdown().await;
}
}
/// Mock server for SNI test that doesn't match wildcard pattern
#[tokio::main]
async fn tls_mock_server_default() {
let server_addr: SocketAddr = "127.0.0.1:54597".parse().unwrap();
let listener = TcpListener::bind(server_addr).await.unwrap();
loop {
let (mut stream, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await;
let _ = stream.write(b"tls_default_response").await;
let _ = stream.shutdown().await;
}
}
/// Helper function to build a minimal TLS ClientHello with SNI extension
/// This creates a valid TLS 1.2 ClientHello packet with the specified SNI hostname
fn build_tls_client_hello(sni_hostname: &str) -> Vec<u8> {
// TLS record header (9 bytes)
let mut hello = Vec::new();
// Record type: Handshake (0x16)
hello.push(0x16);
// Version: TLS 1.2 (0x0303)
hello.extend_from_slice(&[0x03, 0x03]);
// We'll set the record length later
let record_length_pos = hello.len();
hello.extend_from_slice(&[0x00, 0x00]); // Placeholder for record length
// Handshake message type: ClientHello (0x01)
hello.push(0x01);
// We'll set the handshake length later
let handshake_length_pos = hello.len();
hello.extend_from_slice(&[0x00, 0x00, 0x00]); // Placeholder for handshake length
// ClientHello fields
// Protocol version: TLS 1.2 (0x0303)
hello.extend_from_slice(&[0x03, 0x03]);
// Random: 32 bytes (we'll use a fixed pattern)
hello.extend_from_slice(&[0x00; 32]);
// Session ID length: 0 (no session)
hello.push(0x00);
// Cipher suites length: 2 bytes + cipher suites
hello.extend_from_slice(&[0x00, 0x02]); // Length of cipher suites list (2 bytes)
// Cipher suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002F)
hello.extend_from_slice(&[0x00, 0x2F]);
// Compression methods length: 1 byte
hello.push(0x01);
// Compression method: null (0x00)
hello.push(0x00);
// Extensions
let extensions_start = hello.len();
// SNI Extension (type 0x0000)
let mut sni_extension = Vec::new();
sni_extension.extend_from_slice(&[0x00, 0x00]); // Extension type: server_name
// Extension length (will be set later)
let ext_length_pos = sni_extension.len();
sni_extension.extend_from_slice(&[0x00, 0x00]); // Placeholder
// Server name list
let server_name_list_start = sni_extension.len();
sni_extension.extend_from_slice(&[0x00, 0x00]); // Placeholder for server name list length
// Server name: host_name(0), length, hostname
sni_extension.push(0x00); // Name type: host_name
let hostname_bytes = sni_hostname.as_bytes();
sni_extension.extend_from_slice(&[(hostname_bytes.len() >> 8) as u8, (hostname_bytes.len() & 0xFF) as u8]);
sni_extension.extend_from_slice(hostname_bytes);
// Set server name list length
let server_name_list_len = sni_extension.len() - server_name_list_start - 2;
let pos = server_name_list_start;
sni_extension[pos] = (server_name_list_len >> 8) as u8;
sni_extension[pos + 1] = (server_name_list_len & 0xFF) as u8;
// Set extension length
let ext_len = sni_extension.len() - ext_length_pos - 2;
sni_extension[ext_length_pos] = (ext_len >> 8) as u8;
sni_extension[ext_length_pos + 1] = (ext_len & 0xFF) as u8;
// Add SNI extension to hello
hello.extend_from_slice(&sni_extension);
// Set extensions total length
let extensions_length = hello.len() - extensions_start;
hello.insert(extensions_start, (extensions_length & 0xFF) as u8);
hello.insert(extensions_start, (extensions_length >> 8) as u8);
// Set handshake message length
let handshake_len = hello.len() - handshake_length_pos - 3;
hello[handshake_length_pos] = (handshake_len >> 16) as u8;
hello[handshake_length_pos + 1] = (handshake_len >> 8) as u8;
hello[handshake_length_pos + 2] = (handshake_len & 0xFF) as u8;
// Set record length
let record_len = hello.len() - record_length_pos - 2;
hello[record_length_pos] = (record_len >> 8) as u8;
hello[record_length_pos + 1] = (record_len & 0xFF) as u8;
hello
}
#[tokio::test] #[tokio::test]
async fn test_proxy() { async fn test_proxy() {
use crate::config::ConfigV1; use crate::config::ConfigV1;
@@ -176,27 +296,119 @@ mod tests {
assert_eq!(&buf, &[i]); assert_eq!(&buf, &[i]);
} }
conn.shutdown().await.unwrap(); conn.shutdown().await.unwrap();
}
// test KCP echo #[tokio::test]
// let kcp_config = KcpConfig::default(); async fn test_wildcard_sni_routing() {
// let server_addr: SocketAddr = "127.0.0.1:54959".parse().unwrap(); // Create test configuration with wildcard SNI pattern
// let mut conn = KcpStream::connect(&kcp_config, server_addr).await.unwrap(); use crate::upstreams::Upstream;
// let mut buf = [0u8; 1]; use std::collections::HashMap;
// for i in 0..=10u8 {
// conn.write(&[i]).await.unwrap(); // Start mock servers for upstreams
// conn.read(&mut buf).await.unwrap(); thread::spawn(move || {
// assert_eq!(&buf, &[i]); tls_mock_server_wildcard();
// } });
// conn.shutdown().await.unwrap(); thread::spawn(move || {
// tls_mock_server_default();
// // test KCP proxy and close mock server });
// let kcp_config = KcpConfig::default(); sleep(Duration::from_millis(500)); // wait for mock servers to start
// let server_addr: SocketAddr = "127.0.0.1:54958".parse().unwrap();
// let mut conn = KcpStream::connect(&kcp_config, server_addr).await.unwrap(); // Create inline configuration
// let mut buf = [0u8; 5]; let mut config = crate::config::ParsedConfigV1 {
// conn.write(b"by").await.unwrap(); version: 1,
// conn.read(&mut buf).await.unwrap(); log: Some("disable".to_string()),
// assert_eq!(&buf, b"hello"); servers: HashMap::new(),
// conn.shutdown().await.unwrap(); upstream: HashMap::new(),
};
// Add upstreams
config.upstream.insert(
"wildcard_upstream".to_string(),
Upstream::Proxy(crate::upstreams::ProxyToUpstream::new(
"127.0.0.1:54598".to_string(),
"tcp".to_string(),
)),
);
config.upstream.insert(
"default_upstream".to_string(),
Upstream::Proxy(crate::upstreams::ProxyToUpstream::new(
"127.0.0.1:54597".to_string(),
"tcp".to_string(),
)),
);
// Add TLS server with wildcard SNI pattern
let mut sni_map = HashMap::new();
sni_map.insert("*.api.example.com".to_string(), "wildcard_upstream".to_string());
let server_config = crate::config::ParsedServerConfig {
listen: vec!["127.0.0.1:54595".to_string()],
protocol: Some("tcp".to_string()),
tls: Some(true),
sni: Some(crate::sni_matcher::SniMatcher::new(sni_map).unwrap()),
default: Some("default_upstream".to_string()),
};
config.servers.insert("wildcard_test_server".to_string(), server_config);
// Start proxy server
let mut server = Server::new_from_v1_config(config);
thread::spawn(move || {
let _ = server.run();
});
sleep(Duration::from_secs(1)); // wait for proxy to start
// Test 1: Send ClientHello with SNI matching wildcard pattern
// Expected: Should route to wildcard_upstream (127.0.0.1:54598)
let client_hello = build_tls_client_hello("app.api.example.com");
let mut conn = tokio::net::TcpStream::connect("127.0.0.1:54595")
.await
.unwrap();
let _ = conn.write(&client_hello).await.unwrap();
let mut response = [0u8; 21];
let n = conn.read(&mut response).await.unwrap();
assert!(n > 0, "Should receive response from wildcard upstream");
assert_eq!(
&response[..n],
b"tls_wildcard_response",
"Should receive expected response from wildcard upstream"
);
conn.shutdown().await.unwrap();
// Test 2: Send ClientHello with SNI not matching any pattern
// Expected: Should route to default_upstream (127.0.0.1:54597)
let client_hello_nomatch = build_tls_client_hello("unrelated.example.com");
let mut conn = tokio::net::TcpStream::connect("127.0.0.1:54595")
.await
.unwrap();
let _ = conn.write(&client_hello_nomatch).await.unwrap();
let mut response = [0u8; 20];
let n = conn.read(&mut response).await.unwrap();
assert!(n > 0, "Should receive response from default upstream");
assert_eq!(
&response[..n],
b"tls_default_response",
"Should receive expected response from default upstream"
);
conn.shutdown().await.unwrap();
// Test 3: Send ClientHello with another SNI matching wildcard pattern
let client_hello_match2 = build_tls_client_hello("v2.api.example.com");
let mut conn = tokio::net::TcpStream::connect("127.0.0.1:54595")
.await
.unwrap();
let _ = conn.write(&client_hello_match2).await.unwrap();
let mut response = [0u8; 21];
let n = conn.read(&mut response).await.unwrap();
assert!(n > 0, "Should receive response from wildcard upstream for second match");
assert_eq!(
&response[..n],
b"tls_wildcard_response",
"Should receive expected response from wildcard upstream for second match"
);
conn.shutdown().await.unwrap();
} }
} }

View File

@@ -1,98 +0,0 @@
use crate::config::Upstream;
use crate::plugins::kcp::{KcpConfig, KcpListener, KcpStream};
use crate::servers::{copy, Proxy};
use futures::future::try_join;
use log::{debug, error, warn};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
pub async fn proxy(config: Arc<Proxy>) -> Result<(), Box<dyn std::error::Error>> {
let kcp_config = KcpConfig::default();
let mut listener = KcpListener::bind(kcp_config, config.listen).await?;
let config = config.clone();
loop {
let thread_proxy = config.clone();
match listener.accept().await {
Err(err) => {
error!("Failed to accept connection: {}", err);
return Err(Box::new(err));
}
Ok((stream, peer)) => {
tokio::spawn(async move {
match accept(stream, peer, thread_proxy).await {
Ok(_) => {}
Err(err) => {
error!("Relay thread returned an error: {}", err);
}
};
});
}
}
}
}
async fn accept(
inbound: KcpStream,
peer: SocketAddr,
proxy: Arc<Proxy>,
) -> Result<(), Box<dyn std::error::Error>> {
debug!("New connection from {:?}", peer);
let upstream_name = proxy.default_action.clone();
debug!("Upstream: {}", upstream_name);
let upstream = match proxy.upstream.get(&upstream_name) {
Some(upstream) => upstream,
None => {
warn!(
"No upstream named {:?} on server {:?}",
proxy.default, proxy.name
);
return process(inbound, proxy.upstream.get(&proxy.default_action).unwrap()).await;
// ToDo: Remove unwrap and check default option
}
};
return process(inbound, upstream).await;
}
async fn process(
mut inbound: KcpStream,
upstream: &Upstream,
) -> Result<(), Box<dyn std::error::Error>> {
match upstream {
Upstream::Ban => {
let _ = inbound.shutdown();
}
Upstream::Echo => {
let (mut ri, mut wi) = io::split(inbound);
let inbound_to_inbound = copy(&mut ri, &mut wi);
let bytes_tx = inbound_to_inbound.await;
debug!("Bytes read: {:?}", bytes_tx);
}
Upstream::Custom(custom) => match custom.protocol.as_ref() {
"tcp" => {
let outbound = TcpStream::connect(custom.addr.clone()).await?;
let (mut ri, mut wi) = io::split(inbound);
let (mut ro, mut wo) = io::split(outbound);
let inbound_to_outbound = copy(&mut ri, &mut wo);
let outbound_to_inbound = copy(&mut ro, &mut wi);
let (bytes_tx, bytes_rx) =
try_join(inbound_to_outbound, outbound_to_inbound).await?;
debug!("Bytes read: {:?} write: {:?}", bytes_tx, bytes_rx);
}
_ => {
error!("Reached unknown protocol: {:?}", custom.protocol);
}
},
};
Ok(())
}

View File

@@ -1,3 +1,2 @@
//pub mod kcp;
pub mod tcp; pub mod tcp;
pub mod tls; pub mod tls;

View File

@@ -1,4 +1,4 @@
use crate::servers::protocol::tls::get_sni; use crate::servers::protocol::tls::determine_upstream_name;
use crate::servers::Proxy; use crate::servers::Proxy;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use std::error::Error; use std::error::Error;
@@ -35,29 +35,7 @@ async fn accept(inbound: TcpStream, proxy: Arc<Proxy>) -> Result<(), Box<dyn Err
let upstream_name = match proxy.tls { let upstream_name = match proxy.tls {
false => proxy.default_action.clone(), false => proxy.default_action.clone(),
true => { true => determine_upstream_name(&inbound, &proxy).await?,
let mut hello_buf = [0u8; 1024];
inbound.peek(&mut hello_buf).await?;
let snis = get_sni(&hello_buf);
if snis.is_empty() {
proxy.default_action.clone()
} else {
match proxy.sni.clone() {
Some(sni_map) => {
let mut upstream = proxy.default_action.clone();
for sni in snis {
let m = sni_map.get(&sni);
if m.is_some() {
upstream = m.unwrap().clone();
break;
}
}
upstream
}
None => proxy.default_action.clone(),
}
}
}
}; };
debug!("Upstream: {}", upstream_name); debug!("Upstream: {}", upstream_name);

View File

@@ -1,10 +1,17 @@
use log::{debug, warn}; use crate::servers::Proxy;
use log::{debug, error, trace, warn};
use std::error::Error;
use std::io; // Import io for ErrorKind
use std::sync::Arc;
use std::time::Duration; // For potential delays
use tls_parser::{ use tls_parser::{
parse_tls_extensions, parse_tls_raw_record, parse_tls_record_with_header, TlsMessage, parse_tls_extensions, parse_tls_raw_record, parse_tls_record_with_header, TlsMessage,
TlsMessageHandshake, TlsMessageHandshake,
}; };
use tokio::net::TcpStream;
use tokio::time::timeout; // Use timeout for peek operations
pub fn get_sni(buf: &[u8]) -> Vec<String> { fn get_sni(buf: &[u8]) -> Vec<String> {
let mut snis: Vec<String> = Vec::new(); let mut snis: Vec<String> = Vec::new();
match parse_tls_raw_record(buf) { match parse_tls_raw_record(buf) {
Ok((_, ref r)) => match parse_tls_record_with_header(r.data, &r.hdr) { Ok((_, ref r)) => match parse_tls_record_with_header(r.data, &r.hdr) {
@@ -53,10 +60,247 @@ pub fn get_sni(buf: &[u8]) -> Vec<String> {
snis snis
} }
// Timeout duration for waiting for TLS Hello data
const TLS_PEEK_TIMEOUT: Duration = Duration::from_secs(5); // Adjust as needed
pub(crate) async fn determine_upstream_name(
inbound: &TcpStream,
proxy: &Arc<Proxy>,
) -> Result<String, Box<dyn Error>> {
let default_upstream = proxy.default_action.clone();
let mut header = [0u8; 9];
// --- Step 1: Peek the initial header (9 bytes) with timeout ---
match timeout(TLS_PEEK_TIMEOUT, async {
loop {
match inbound.peek(&mut header).await {
Ok(n) if n >= header.len() => return Ok::<usize, io::Error>(n), // Got enough bytes
Ok(0) => {
// Connection closed cleanly before sending enough data
trace!("Connection closed while peeking for TLS header");
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Connection closed while peeking for TLS header",
)
.into()); // Convert to Box<dyn Error>
}
Ok(_) => {
// Not enough bytes yet, yield and loop again
tokio::task::yield_now().await;
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
// Should not happen with await, but yield defensively
tokio::task::yield_now().await;
}
Err(e) => {
// Other I/O error
warn!("Error peeking for TLS header: {}", e);
return Err(e.into()); // Convert to Box<dyn Error>
}
}
}
})
.await
{
Ok(Ok(_)) => { /* Header peeked successfully */ }
Ok(Err(e)) => {
// Inner loop returned an error (e.g., EOF, IO error)
trace!("Failed to peek header (inner error): {}", e);
return Ok(default_upstream); // Fallback on error/EOF
}
Err(_) => {
// Timeout occurred
error!("Timeout waiting for TLS header");
return Ok(default_upstream); // Fallback on timeout
}
}
// --- Step 2: Calculate required size ---
let required_bytes = match client_hello_buffer_size(&header) {
Ok(size) => size,
Err(e) => {
// Header was invalid or not a ClientHello
trace!("Could not determine required buffer size: {}", e);
return Ok(default_upstream);
}
};
// Basic sanity check on size
if required_bytes > 16384 + 9 {
// TLS max record size + header approx
error!(
"Calculated required TLS buffer size is too large: {}",
required_bytes
);
return Ok(default_upstream);
}
// --- Step 3: Peek the full ClientHello with timeout ---
let mut hello_buf = vec![0; required_bytes];
match timeout(TLS_PEEK_TIMEOUT, async {
let mut total_peeked = 0;
loop {
// Peek into the portion of the buffer that hasn't been filled yet.
match inbound.peek(&mut hello_buf[total_peeked..]).await {
Ok(0) => {
// Connection closed cleanly before sending full ClientHello
trace!(
"Connection closed while peeking for full ClientHello (peeked {}/{} bytes)",
total_peeked,
required_bytes
);
return Err::<usize, io::Error>(
io::Error::new(
io::ErrorKind::UnexpectedEof,
"Connection closed while peeking for full ClientHello",
)
.into(),
);
}
Ok(n) => {
total_peeked += n;
if total_peeked >= required_bytes {
trace!("Successfully peeked {} bytes for ClientHello", total_peeked);
return Ok(total_peeked); // Got enough
} else {
// Not enough bytes yet, yield and loop again
trace!(
"Peeked {}/{} bytes for ClientHello, waiting for more...",
total_peeked,
required_bytes
);
tokio::task::yield_now().await;
}
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
tokio::task::yield_now().await;
}
Err(e) => {
warn!("Error peeking for full ClientHello: {}", e);
return Err(e.into());
}
}
}
})
.await
{
Ok(Ok(_)) => { /* Full hello peeked successfully */ }
Ok(Err(e)) => {
error!("Could not peek full ClientHello (inner error): {}", e);
return Ok(default_upstream); // Fallback on error/EOF
}
Err(_) => {
error!(
"Timeout waiting for full ClientHello (needed {} bytes)",
required_bytes
);
return Ok(default_upstream); // Fallback on timeout
}
}
// --- Step 4: Parse SNI ---
let snis = get_sni(&hello_buf);
// --- Step 5: Determine upstream based on SNI ---
if snis.is_empty() {
debug!("No SNI found in ClientHello, using default upstream.");
return Ok(default_upstream);
}
if let Some(matcher) = &proxy.sni {
for sni in snis {
if let Some(upstream) = matcher.match_sni(&sni) {
debug!(
"Found matching SNI '{}', routing to upstream: {}",
sni, upstream
);
return Ok(upstream);
} else {
trace!("SNI '{}' not found in matcher.", sni);
}
}
debug!("SNI(s) found but none matched configuration, using default upstream.");
} else {
debug!("SNI found but no SNI matcher configured, using default upstream.");
}
Ok(default_upstream)
}
fn client_hello_buffer_size(data: &[u8]) -> Result<usize, String> {
// TLS record header
// -----------------
// byte 0: rec type (should be 0x16 == Handshake)
// byte 1-2: version (should be 0x3000 < v < 0x3003)
// byte 3-4: rec len
if data.len() < 9 {
trace!("Not enough bytes to even check the TLS header.");
return Err("Not enough bytes to even check the TLS header.".into());
}
if data[0] != 0x16 {
trace!("Not a TLS handshake.");
return Err("Not a TLS handshake.".into());
}
// Check the record length
let record_length = ((data[3] as u16) << 8) | (data[4] as u16);
if record_length == 0 || record_length > 16384 {
trace!("Client send invalid header: way too long record header.");
return Err("Client send invalid header: way too long record header.".into());
}
// Handshake record header
// -----------------------
// byte 5: hs msg type (should be 0x01 == client_hello)
// byte 6-8: hs msg len
if data[5] != 0x01 {
trace!("Not a ClientHello message");
return Err("Not a ClientHello message".into());
}
// Check the handshake message length
let handshake_length =
((data[6] as usize) << 16) | ((data[7] as usize) << 8) | (data[8] as usize);
if handshake_length <= 0 || handshake_length > (record_length - 4).into() {
warn!("Invalid client hello length (fragmentation not implemented)");
return Err("Invalid client hello length (fragmentation not implemented)".into());
}
// Calculate the handshake length and return it
Ok(handshake_length + 9)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_too_little_data_end() {
let length = client_hello_buffer_size(&TOO_LITTLE_DATA_END);
if length.is_ok() {
assert!(false);
}
let msg = length.unwrap_err();
dbg!(msg);
assert!(true);
}
#[test]
fn test_too_little_data_start() {
let length = client_hello_buffer_size(&TOO_LITTLE_DATA_START);
assert!(length.is_ok());
assert_eq!(1712, length.unwrap())
}
#[test] #[test]
fn test_sni_extract() { fn test_sni_extract() {
const BUF: [u8; 517] = [ const BUF: [u8; 517] = [
@@ -99,6 +343,454 @@ mod tests {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]; ];
let sni = get_sni(&BUF); let sni = get_sni(&BUF);
assert!(sni[0] == *"www.lirui.tech"); assert_eq!(sni[0], *"www.lirui.tech");
} }
#[test]
fn test_sni_extract_tiny() {
const BUF: [u8; 1712] = [
0x16, 0x03, 0x01, 0x06, 0xab, 0x01, 0x00, 0x06, // |........|
0xa7, 0x03, 0x03, 0x84, 0x53, 0xb2, 0xd7, 0x37, // |....S..7|
0xcd, 0x27, 0xda, 0xf4, 0x70, 0xd8, 0x78, 0x26, // |.'..p.x&|
0x34, 0x7f, 0xe3, 0xa7, 0x5d, 0xfe, 0x97, 0x29, // |4...]..)|
0x89, 0x29, 0xa2, 0xd8, 0x62, 0x05, 0x7b, 0x13, // |.)..b.{.|
0xcf, 0x4b, 0x13, 0x20, 0x5b, 0x74, 0x4e, 0x23, // |.K. [tN#|
0x90, 0x08, 0x5a, 0x43, 0xbf, 0xe0, 0x0d, 0xeb, // |..ZC....|
0x8a, 0xc8, 0x4d, 0x14, 0x1e, 0x35, 0x43, 0x04, // |..M..5C.|
0x36, 0x32, 0xdc, 0x71, 0xff, 0xcc, 0xb3, 0x5b, // |62.q...[|
0x63, 0x4b, 0x2b, 0xee, 0x00, 0x20, 0xba, 0xba, // |cK+.. ..|
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, // |.......+|
0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, // |./.,.0..|
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, // |........|
0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, // |.../.5..|
0x06, 0x3e, 0x7a, 0x7a, 0x00, 0x00, 0x00, 0x0a, // |.>zz....|
0x00, 0x0c, 0x00, 0x0a, 0xda, 0xda, 0x11, 0xec, // |........|
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0xff, 0x01, // |........|
0x00, 0x01, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, // |........|
0x00, 0x00, 0x13, 0x00, 0x11, 0x00, 0x00, 0x0e, // |........|
0x68, 0x61, 0x2e, 0x68, 0x6f, 0x6d, 0x65, 0x2e, // |ha.home.|
0x6b, 0x69, 0x65, 0x2e, 0x72, 0x73, 0xfe, 0x0d, // |kie.rs..|
0x00, 0xba, 0x00, 0x00, 0x01, 0x00, 0x01, 0x11, // |........|
0x00, 0x20, 0xf7, 0xf4, 0x20, 0xc8, 0xb7, 0xeb, // |. .. ...|
0xf1, 0x2d, 0x8b, 0x30, 0x2c, 0xc8, 0x5e, 0xd3, // |.-.0,.^.|
0xa3, 0x02, 0x38, 0xf2, 0x41, 0xf7, 0x3f, 0x2d, // |..8.A.?-|
0xb4, 0xf0, 0xd7, 0x3b, 0xe5, 0x19, 0x3f, 0xc3, // |...;..?.|
0xae, 0x1f, 0x00, 0x90, 0x27, 0x8d, 0x4c, 0xc9, // |....'.L.|
0xb3, 0xd1, 0x63, 0x20, 0xe4, 0x33, 0x18, 0x56, // |..c .3.V|
0xd5, 0x9b, 0xd5, 0xf9, 0xf2, 0x94, 0x1d, 0xe4, // |........|
0xa6, 0x88, 0x47, 0xd2, 0x85, 0x4f, 0xf4, 0x30, // |..G..O.0|
0x22, 0xff, 0x67, 0x80, 0x60, 0x33, 0x17, 0xa0, // |".g.`3..|
0x4f, 0xdb, 0x98, 0x53, 0x00, 0xa4, 0xc8, 0x89, // |O..S....|
0xb8, 0x1b, 0x3f, 0xbd, 0xdf, 0xeb, 0x48, 0x1a, // |..?...H.|
0xa1, 0x33, 0xd7, 0xc1, 0x8d, 0x76, 0xf2, 0xcf, // |.3...v..|
0xbe, 0x30, 0x1d, 0xcd, 0x3a, 0xfe, 0xf1, 0xb0, // |.0..:...|
0x86, 0xbc, 0x28, 0x74, 0x78, 0xa1, 0x9a, 0x60, // |..(tx..`|
0x14, 0xfe, 0x12, 0x92, 0x4d, 0xb5, 0x9e, 0x85, // |....M...|
0x79, 0x62, 0x9c, 0x68, 0x73, 0xc6, 0x0e, 0xe5, // |yb.hs...|
0xad, 0x5b, 0xe2, 0x69, 0x00, 0xc0, 0x26, 0x24, // |.[.i..&$|
0x88, 0xfa, 0x22, 0x29, 0x36, 0x7b, 0x16, 0x59, // |..")6{.Y|
0x48, 0xbe, 0xf9, 0x1c, 0x86, 0x55, 0xcb, 0x67, // |H....U.g|
0xae, 0xb6, 0x7b, 0x69, 0x3e, 0xd0, 0x48, 0x31, // |..{i>.H1|
0x58, 0x8a, 0xd8, 0xba, 0x06, 0x21, 0xf0, 0xd4, // |X....!..|
0x4e, 0xef, 0xcf, 0x67, 0xc5, 0x63, 0x97, 0x59, // |N..g.c.Y|
0x95, 0x12, 0x47, 0x90, 0x00, 0x2d, 0x00, 0x02, // |..G..-..|
0x01, 0x01, 0x00, 0x10, 0x00, 0x0b, 0x00, 0x09, // |........|
0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, // |.http/1.|
0x31, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, // |1.......|
0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, // |........|
0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x00, // |........|
0x2b, 0x00, 0x07, 0x06, 0x0a, 0x0a, 0x03, 0x04, // |+.......|
0x03, 0x03, 0x00, 0x23, 0x00, 0x00, 0x00, 0x12, // |...#....|
0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, // |........|
0x02, 0x00, 0x33, 0x04, 0xef, 0x04, 0xed, 0xda, // |..3.....|
0xda, 0x00, 0x01, 0x00, 0x11, 0xec, 0x04, 0xc0, // |........|
0xc6, 0x12, 0x85, 0x0b, 0xba, 0x73, 0x9d, 0x00, // |.....s..|
0x29, 0x08, 0x40, 0x3a, 0xb8, 0xfc, 0x9e, 0x99, // |).@:....|
0x25, 0xbd, 0x60, 0xb6, 0x8a, 0x56, 0x51, 0xac, // |%.`..VQ.|
0x38, 0xa3, 0x15, 0x37, 0x21, 0x80, 0x86, 0x02, // |8..7!...|
0xb2, 0x10, 0x4b, 0x29, 0xeb, 0x37, 0x04, 0x47, // |..K).7.G|
0x16, 0x12, 0x0e, 0x63, 0x2d, 0x32, 0xf6, 0x2a, // |...c-2.*|
0x86, 0x09, 0x7b, 0x41, 0x28, 0x8c, 0xcf, 0xfa, // |..{A(...|
0x08, 0x2e, 0x0c, 0xb2, 0x55, 0xb4, 0xb4, 0xd2, // |....U...|
0x76, 0x38, 0x47, 0x44, 0x78, 0xf0, 0x01, 0xb6, // |v8GDx...|
0xee, 0xf0, 0x1f, 0x4b, 0xc5, 0x6b, 0xb3, 0x93, // |...K.k..|
0x4d, 0xa5, 0x25, 0x29, 0xda, 0x33, 0x1e, 0xc5, // |M.%).3..|
0x15, 0x98, 0xf5, 0x41, 0x3e, 0xd2, 0xf7, 0x82, // |...A>...|
0xd7, 0xbb, 0x56, 0xf0, 0x86, 0x29, 0xa3, 0x56, // |..V..).V|
0x25, 0xdc, 0xaa, 0x03, 0xaa, 0x28, 0xa7, 0x2b, // |%....(.+|
0xc0, 0x41, 0xca, 0x66, 0x3e, 0xcc, 0x21, 0x40, // |.A.f>.!@|
0x60, 0x34, 0x5f, 0x9f, 0x69, 0x37, 0xac, 0x30, // |`4_.i7.0|
0x06, 0x7a, 0xf9, 0x26, 0xfe, 0x3c, 0x13, 0x05, // |.z.&.<..|
0xf6, 0xbe, 0x5f, 0x0c, 0x9a, 0x43, 0x18, 0xa2, // |.._..C..|
0xd9, 0xc5, 0xa0, 0x06, 0x0b, 0x0a, 0x21, 0xf1, // |......!.|
0x6b, 0x12, 0x4a, 0x5d, 0xec, 0xf6, 0x01, 0x30, // |k.J]...0|
0xb6, 0x3b, 0x34, 0x62, 0xcd, 0x5a, 0x6a, 0x26, // |.;4b.Zj&|
0x08, 0x98, 0xc9, 0xd0, 0x8a, 0x49, 0x94, 0x07, // |.....I..|
0x48, 0x45, 0x78, 0x45, 0xae, 0x24, 0x2b, 0x83, // |HExE.$+.|
0xb6, 0x69, 0x6c, 0x20, 0x33, 0xa9, 0xc4, 0x8e, // |.il 3...|
0xe7, 0x1a, 0x90, 0x28, 0xc6, 0x3f, 0x16, 0xf2, // |...(.?..|
0xae, 0x3e, 0x22, 0x17, 0x26, 0x9c, 0x38, 0xf5, // |.>".&.8.|
0x88, 0x60, 0x79, 0x16, 0x28, 0xce, 0x05, 0x72, // |.`y.(..r|
0x2f, 0x64, 0x99, 0xdd, 0x8c, 0x5b, 0xa6, 0xe2, // |/d...[..|
0x65, 0x8a, 0xe2, 0x8d, 0xb6, 0x24, 0x9e, 0x6d, // |e....$.m|
0x5a, 0x70, 0xfd, 0xea, 0xca, 0xec, 0x77, 0x46, // |Zp....wF|
0x20, 0xa8, 0x1f, 0x78, 0xf6, 0x34, 0x52, 0x13, // | ..x.4R.|
0x97, 0xef, 0x60, 0xb9, 0xe5, 0xc6, 0x85, 0xf2, // |..`.....|
0x84, 0x64, 0xdc, 0x08, 0x07, 0xe2, 0x63, 0xa6, // |.d....c.|
0x23, 0x64, 0x54, 0xb8, 0x72, 0xac, 0x23, 0xda, // |#dT.r.#.|
0x8f, 0x73, 0xe4, 0x9b, 0x80, 0x77, 0x66, 0x3f, // |.s...wf?|
0x69, 0x34, 0xc4, 0xfb, 0x45, 0x3d, 0x1c, 0xa7, // |i4..E=..|
0x86, 0x98, 0x2e, 0xb4, 0xe0, 0x84, 0xb6, 0x47, // |.......G|
0x78, 0xeb, 0x2b, 0x10, 0x17, 0x45, 0x8a, 0xcf, // |x.+..E..|
0xea, 0xb5, 0x58, 0x42, 0x93, 0xbe, 0x4b, 0xad, // |..XB..K.|
0xfb, 0x28, 0x11, 0x12, 0xe0, 0x7c, 0x3d, 0x34, // |.(...|=4|
0x8c, 0x82, 0x07, 0x84, 0xda, 0x8b, 0x35, 0x86, // |......5.|
0x37, 0x35, 0x1d, 0x1a, 0xa2, 0xbf, 0x0a, 0xb4, // |75......|
0x8e, 0xf0, 0x91, 0xc4, 0xa8, 0x3f, 0x38, 0x03, // |.....?8.|
0x37, 0xc1, 0x9a, 0x94, 0x43, 0x09, 0x57, 0xee, // |7...C.W.|
0xaa, 0xcb, 0x3d, 0x13, 0xa2, 0x33, 0xd1, 0x04, // |..=..3..|
0x2c, 0x6c, 0xb4, 0x1c, 0x86, 0x07, 0x0c, 0x3c, // |,l.....<|
0x5c, 0xc9, 0x8c, 0xc8, 0x1a, 0x85, 0xa6, 0xdd, // |\.......|
0xd3, 0xc5, 0xae, 0x84, 0x4d, 0xfe, 0xa2, 0x99, // |....M...|
0xd3, 0x0b, 0x1f, 0x43, 0x01, 0xa6, 0x7b, 0xb2, // |...C..{.|
0x5b, 0xd5, 0xa0, 0x3e, 0xd4, 0x6c, 0x65, 0x75, // |[..>.leu|
0x55, 0x28, 0x4d, 0x1c, 0x28, 0x86, 0xda, 0x94, // |U(M.(...|
0xbe, 0x0a, 0x99, 0x61, 0xa4, 0x88, 0xd9, 0x6a, // |...a...j|
0x20, 0x1d, 0x78, 0x45, 0x5f, 0x66, 0xcc, 0x8c, // | .xE_f..|
0xe1, 0xba, 0x4c, 0x51, 0x99, 0x54, 0x27, 0x77, // |..LQ.T'w|
0xb4, 0x84, 0x61, 0x4e, 0xf9, 0x90, 0x6f, 0x19, // |..aN..o.|
0x44, 0x93, 0x27, 0x1d, 0x95, 0x82, 0x74, 0x7f, // |D.'...t.|
0x35, 0xaf, 0x04, 0xe4, 0x58, 0x41, 0x3a, 0x51, // |5...XA:Q|
0x0b, 0x22, 0x45, 0xaf, 0x44, 0x2a, 0xe9, 0xa3, // |."E.D*..|
0x71, 0x65, 0x15, 0x22, 0xea, 0x40, 0x10, 0xaf, // |qe.".@..|
0x5b, 0x27, 0xfc, 0x02, 0x00, 0x23, 0xa3, 0x70, // |['...#.p|
0xa9, 0x6c, 0xa7, 0xf7, 0x29, 0x5c, 0x75, 0x9b, // |.l..)\u.|
0x4c, 0x23, 0x14, 0x51, 0x12, 0x62, 0x71, 0xbb, // |L#.Q.bq.|
0x75, 0x64, 0x65, 0xb3, 0xaa, 0x1e, 0x10, 0x14, // |ude.....|
0xbf, 0xd0, 0x8b, 0xe0, 0xe4, 0x51, 0x6e, 0xa8, // |.....Qn.|
0x1a, 0x95, 0x21, 0xa9, 0x9f, 0xf7, 0x2a, 0xac, // |..!...*.|
0x5c, 0x1c, 0x12, 0xac, 0x9d, 0xac, 0x57, 0x14, // |\.....W.|
0x27, 0xaa, 0xa7, 0xee, 0xc3, 0x9d, 0x63, 0x48, // |'.....cH|
0x0e, 0xd7, 0xf8, 0x92, 0x9f, 0x28, 0xb9, 0x82, // |.....(..|
0x71, 0x99, 0xa1, 0xcb, 0x69, 0x0c, 0x29, 0x7d, // |q...i.)}|
0x67, 0x73, 0xae, 0x9d, 0xd7, 0xc7, 0x51, 0x7a, // |gs....Qz|
0x2c, 0x3a, 0x74, 0x89, 0x7d, 0x76, 0x35, 0xb5, // |,:t.}v5.|
0x97, 0x73, 0x4a, 0xfc, 0x29, 0x9a, 0x1a, 0x06, // |.sJ.)...|
0x2f, 0xd0, 0x89, 0x32, 0xfc, 0x3b, 0x17, 0xec, // |/..2.;..|
0x7a, 0xb5, 0x3c, 0x66, 0x0f, 0x43, 0x55, 0x41, // |z.<f.CUA|
0x49, 0x3f, 0xbf, 0xa1, 0x6f, 0x8a, 0x05, 0x76, // |I?..o..v|
0xd4, 0x02, 0x33, 0x52, 0x78, 0xc8, 0x08, 0xe9, // |..3Rx...|
0x49, 0xb8, 0x42, 0x05, 0xed, 0x34, 0x0a, 0xb1, // |I.B..4..|
0xa8, 0x32, 0x00, 0x6b, 0x00, 0x42, 0x56, 0x8a, // |.2.k.BV.|
0xe9, 0x04, 0x7a, 0xac, 0xc8, 0x72, 0x7f, 0x40, // |..z..r.@|
0x4c, 0xd6, 0xa9, 0x34, 0x0b, 0xc3, 0x63, 0x39, // |L..4..c9|
0x21, 0xbf, 0x04, 0xb0, 0x2b, 0x81, 0xf9, 0x07, // |!...+...|
0xe6, 0x15, 0x92, 0x89, 0x9b, 0x1e, 0xe6, 0x4b, // |.......K|
0x5b, 0x0b, 0x33, 0x5f, 0x89, 0x96, 0xa2, 0x74, // |[.3_...t|
0x41, 0x6b, 0x15, 0xe8, 0x8a, 0x62, 0xf5, 0x1c, // |Ak...b..|
0x37, 0x38, 0x62, 0x77, 0xd4, 0x57, 0x7b, 0x43, // |78bw.W{C|
0x42, 0x4f, 0x01, 0x9c, 0xf2, 0xe0, 0x68, 0xb7, // |BO....h.|
0xf1, 0x66, 0x93, 0xd8, 0x8e, 0x78, 0x80, 0x24, // |.f...x.$|
0x4c, 0x61, 0x11, 0xbb, 0xf2, 0x79, 0xf7, 0x96, // |La...y..|
0x02, 0x80, 0xaa, 0xc7, 0xcd, 0xbb, 0x55, 0x03, // |......U.|
0x22, 0x5e, 0xda, 0xa2, 0x44, 0x7d, 0x82, 0x41, // |"^..D}.A|
0x86, 0x9b, 0x92, 0x0a, 0xd5, 0x7e, 0xf2, 0x78, // |.....~.x|
0x84, 0x50, 0x00, 0x2d, 0x0b, 0xab, 0x92, 0x7a, // |.P.-...z|
0x96, 0x15, 0xcf, 0x5a, 0x34, 0x45, 0x35, 0xa7, // |...Z4E5.|
0x18, 0x61, 0x2b, 0x88, 0x45, 0xaa, 0xd3, 0xe2, // |.a+.E...|
0x54, 0xf9, 0xc7, 0xbb, 0xe7, 0x00, 0x86, 0xbd, // |T.......|
0x8b, 0xbb, 0x6d, 0x3b, 0x0f, 0x8d, 0xfb, 0x4d, // |..m;...M|
0x5d, 0x8b, 0x50, 0x2e, 0x68, 0x74, 0x5d, 0x03, // |].P.ht].|
0x16, 0x2a, 0x49, 0x24, 0x54, 0x5b, 0xa9, 0x34, // |.*I$T[.4|
0x25, 0x17, 0x79, 0xe3, 0xc3, 0x3a, 0x2a, 0x12, // |%.y..:*.|
0x75, 0x64, 0x16, 0xa4, 0xb7, 0x7e, 0x39, 0x5a, // |ud...~9Z|
0x4e, 0x3e, 0x53, 0x2b, 0x49, 0x1b, 0x26, 0xdf, // |N>S+I.&.|
0xfc, 0x29, 0x99, 0xcb, 0xad, 0x29, 0x2c, 0x72, // |.)...),r|
0x3f, 0xa7, 0xcb, 0x45, 0x4c, 0x14, 0xee, 0x46, // |?..EL..F|
0x74, 0x64, 0xdb, 0x4b, 0x4b, 0xa4, 0x35, 0x3c, // |td.KK.5<|
0x91, 0xc4, 0x9b, 0xb0, 0x66, 0xc6, 0x70, 0xb6, // |....f.p.|
0xf2, 0x07, 0x3b, 0xbf, 0x74, 0x72, 0xb4, 0x24, // |..;.tr.$|
0x7e, 0x87, 0xd4, 0x0a, 0x37, 0xd9, 0x49, 0x04, // |~...7.I.|
0x09, 0x36, 0xd1, 0x63, 0x88, 0xe1, 0xe8, 0x08, // |.6.c....|
0xbf, 0x17, 0xc4, 0xcd, 0xcb, 0x3c, 0xef, 0x88, // |.....<..|
0x2c, 0xf6, 0xa3, 0x6d, 0x89, 0x39, 0xc9, 0xfe, // |,..m.9..|
0x97, 0x25, 0xb3, 0x9a, 0x02, 0x40, 0xd4, 0x90, // |.%...@..|
0x28, 0x6a, 0x79, 0xbd, 0x4b, 0x8e, 0x10, 0x18, // |(jy.K...|
0xc9, 0xaf, 0xe9, 0xc0, 0x6e, 0xd5, 0xb1, 0xcf, // |....n...|
0xe8, 0xa4, 0xdc, 0x94, 0x12, 0x82, 0xfb, 0x08, // |........|
0x42, 0xd4, 0x1a, 0x76, 0xa2, 0x4b, 0x3f, 0xc3, // |B..v.K?.|
0xb4, 0x0b, 0xa3, 0x0c, 0xec, 0x19, 0x7c, 0x5f, // |......|_|
0xd5, 0x98, 0x99, 0xf4, 0x1a, 0xca, 0x83, 0xaa, // |........|
0xbd, 0x26, 0x31, 0x95, 0x77, 0x90, 0x43, 0x7a, // |.&1.w.Cz|
0x75, 0x15, 0xcb, 0x68, 0xae, 0x24, 0xc5, 0x1b, // |u..h.$..|
// Cutoff here.
0x8c, 0x49, 0xbe, 0xfc, 0x61, 0x54, 0xd7, 0x18, // |.I..aT..|
0x9d, 0x21, 0x10, 0x14, 0xe2, 0x6d, 0x5b, 0x4b, // |.!...m[K|
0xb0, 0x94, 0xaa, 0x6e, 0xd5, 0x7b, 0xba, 0x6e, // |...n.{.n|
0xe0, 0x03, 0xac, 0x9a, 0xbb, 0xe1, 0x17, 0x9b, // |........|
0x18, 0x0c, 0x33, 0xcc, 0x05, 0x91, 0x1c, 0x43, // |..3....C|
0x37, 0xd2, 0x10, 0xb7, 0xc6, 0xc7, 0x6b, 0xda, // |7.....k.|
0x87, 0x9c, 0xaf, 0x93, 0x52, 0x2f, 0x4c, 0x6e, // |....R/Ln|
0x14, 0xdb, 0x49, 0xbc, 0xeb, 0x96, 0xda, 0xb6, // |..I.....|
0x3b, 0xf8, 0xc0, 0x33, 0xba, 0x15, 0x37, 0x39, // |;..3..79|
0xe7, 0xae, 0xb6, 0x48, 0x3e, 0xd8, 0x57, 0x67, // |...H>.Wg|
0x9c, 0xb6, 0x9c, 0xc0, 0x18, 0x0e, 0x74, 0x67, // |......tg|
0xae, 0x8e, 0xc6, 0x80, 0x7f, 0x81, 0x25, 0xc4, // |......%.|
0xe9, 0x04, 0xe8, 0xd9, 0x98, 0xb6, 0x99, 0x93, // |........|
0xa1, 0xa4, 0x5e, 0x57, 0x74, 0x89, 0x30, 0x38, // |..^Wt.08|
0xa9, 0xbb, 0x99, 0x4a, 0x7e, 0x42, 0x3c, 0xd2, // |...J~B<.|
0x59, 0xb6, 0x49, 0xb0, 0xc7, 0x11, 0x57, 0x03, // |Y.I...W.|
0x6d, 0x23, 0x1b, 0x72, 0xe7, 0x24, 0xdb, 0x75, // |m#.r.$.u|
0x78, 0xd1, 0x38, 0x01, 0x46, 0xb6, 0x8c, 0x1b, // |x.8.F...|
0x41, 0xb4, 0xbd, 0xc1, 0xa2, 0x00, 0x63, 0xa5, // |A.....c.|
0x97, 0x30, 0x5d, 0xbe, 0xd1, 0x37, 0x31, 0xf1, // |.0]..71.|
0xbb, 0xc6, 0xf8, 0x81, 0x35, 0x86, 0x32, 0xa6, // |....5.2.|
0xc3, 0x35, 0x54, 0x45, 0x50, 0xdf, 0x61, 0x46, // |.5TEP.aF|
0x5b, 0x83, 0x6b, 0xac, 0x5c, 0x2d, 0xa2, 0xc3, // |[.k.\-..|
0x2e, 0x71, 0x32, 0x18, 0x41, 0x29, 0x99, 0x66, // |.q2.A).f|
0x8c, 0x50, 0x28, 0x92, 0x45, 0xae, 0x96, 0x38, // |.P(.E..8|
0xa4, 0x83, 0x94, 0x4a, 0x2f, 0x0e, 0x62, 0x13, // |...J/.b.|
0x07, 0x13, 0xc2, 0x0b, 0x84, 0xfd, 0x27, 0xab, // |......'.|
0x6c, 0xb4, 0x69, 0x0d, 0xd2, 0xdb, 0xfb, 0x8e, // |l.i.....|
0xa7, 0x09, 0x65, 0x76, 0x7e, 0x09, 0xa4, 0x7a, // |..ev~..z|
0xe9, 0xfe, 0xec, 0x52, 0x89, 0x7d, 0x07, 0x6f, // |...R.}.o|
0xff, 0xa0, 0xde, 0x8a, 0x42, 0x2d, 0xc3, 0x75, // |....B-.u|
0x05, 0x6d, 0x60, 0x76, 0xce, 0xe1, 0x6c, 0xfd, // |.m`v..l.|
0xae, 0x1f, 0x5e, 0x02, 0x94, 0x39, 0x2a, 0x55, // |..^..9*U|
0x00, 0x1d, 0x00, 0x20, 0x8d, 0x89, 0x9a, 0x19, // |... ....|
0x1d, 0x53, 0x52, 0xd5, 0xc1, 0x3e, 0x3a, 0x1d, // |.SR..>:.|
0x12, 0x15, 0xae, 0x33, 0x2e, 0x54, 0xd1, 0x6f, // |...3.T.o|
0xd6, 0xb1, 0x73, 0xd9, 0x56, 0x98, 0x6f, 0x8f, // |..s.V.o.|
0x7e, 0xf5, 0xd9, 0x75, 0x00, 0x0b, 0x00, 0x02, // |~..u....|
0x01, 0x00, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, // |........|
0x00, 0x00, 0x00, 0x9a, 0x9a, 0x00, 0x01, 0x00, // |........|
];
let sni = get_sni(&BUF);
assert_eq!(sni[0], *"ha.home.kie.rs");
}
#[test]
fn test_too_little_data() {
let sni = get_sni(&TOO_LITTLE_DATA_END);
assert_eq!(0, sni.len());
}
const TOO_LITTLE_DATA_START: [u8; 1392] = [
0x16, 0x03, 0x01, 0x06, 0xab, 0x01, 0x00, 0x06, // |........|
0xa7, 0x03, 0x03, 0x84, 0x53, 0xb2, 0xd7, 0x37, // |....S..7|
0xcd, 0x27, 0xda, 0xf4, 0x70, 0xd8, 0x78, 0x26, // |.'..p.x&|
0x34, 0x7f, 0xe3, 0xa7, 0x5d, 0xfe, 0x97, 0x29, // |4...]..)|
0x89, 0x29, 0xa2, 0xd8, 0x62, 0x05, 0x7b, 0x13, // |.)..b.{.|
0xcf, 0x4b, 0x13, 0x20, 0x5b, 0x74, 0x4e, 0x23, // |.K. [tN#|
0x90, 0x08, 0x5a, 0x43, 0xbf, 0xe0, 0x0d, 0xeb, // |..ZC....|
0x8a, 0xc8, 0x4d, 0x14, 0x1e, 0x35, 0x43, 0x04, // |..M..5C.|
0x36, 0x32, 0xdc, 0x71, 0xff, 0xcc, 0xb3, 0x5b, // |62.q...[|
0x63, 0x4b, 0x2b, 0xee, 0x00, 0x20, 0xba, 0xba, // |cK+.. ..|
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, // |.......+|
0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, // |./.,.0..|
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, // |........|
0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, // |.../.5..|
0x06, 0x3e, 0x7a, 0x7a, 0x00, 0x00, 0x00, 0x0a, // |.>zz....|
0x00, 0x0c, 0x00, 0x0a, 0xda, 0xda, 0x11, 0xec, // |........|
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0xff, 0x01, // |........|
0x00, 0x01, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, // |........|
0x00, 0x00, 0x13, 0x00, 0x11, 0x00, 0x00, 0x0e, // |........|
0x68, 0x61, 0x2e, 0x68, 0x6f, 0x6d, 0x65, 0x2e, // |ha.home.|
0x6b, 0x69, 0x65, 0x2e, 0x72, 0x73, 0xfe, 0x0d, // |kie.rs..|
0x00, 0xba, 0x00, 0x00, 0x01, 0x00, 0x01, 0x11, // |........|
0x00, 0x20, 0xf7, 0xf4, 0x20, 0xc8, 0xb7, 0xeb, // |. .. ...|
0xf1, 0x2d, 0x8b, 0x30, 0x2c, 0xc8, 0x5e, 0xd3, // |.-.0,.^.|
0xa3, 0x02, 0x38, 0xf2, 0x41, 0xf7, 0x3f, 0x2d, // |..8.A.?-|
0xb4, 0xf0, 0xd7, 0x3b, 0xe5, 0x19, 0x3f, 0xc3, // |...;..?.|
0xae, 0x1f, 0x00, 0x90, 0x27, 0x8d, 0x4c, 0xc9, // |....'.L.|
0xb3, 0xd1, 0x63, 0x20, 0xe4, 0x33, 0x18, 0x56, // |..c .3.V|
0xd5, 0x9b, 0xd5, 0xf9, 0xf2, 0x94, 0x1d, 0xe4, // |........|
0xa6, 0x88, 0x47, 0xd2, 0x85, 0x4f, 0xf4, 0x30, // |..G..O.0|
0x22, 0xff, 0x67, 0x80, 0x60, 0x33, 0x17, 0xa0, // |".g.`3..|
0x4f, 0xdb, 0x98, 0x53, 0x00, 0xa4, 0xc8, 0x89, // |O..S....|
0xb8, 0x1b, 0x3f, 0xbd, 0xdf, 0xeb, 0x48, 0x1a, // |..?...H.|
0xa1, 0x33, 0xd7, 0xc1, 0x8d, 0x76, 0xf2, 0xcf, // |.3...v..|
0xbe, 0x30, 0x1d, 0xcd, 0x3a, 0xfe, 0xf1, 0xb0, // |.0..:...|
0x86, 0xbc, 0x28, 0x74, 0x78, 0xa1, 0x9a, 0x60, // |..(tx..`|
0x14, 0xfe, 0x12, 0x92, 0x4d, 0xb5, 0x9e, 0x85, // |....M...|
0x79, 0x62, 0x9c, 0x68, 0x73, 0xc6, 0x0e, 0xe5, // |yb.hs...|
0xad, 0x5b, 0xe2, 0x69, 0x00, 0xc0, 0x26, 0x24, // |.[.i..&$|
0x88, 0xfa, 0x22, 0x29, 0x36, 0x7b, 0x16, 0x59, // |..")6{.Y|
0x48, 0xbe, 0xf9, 0x1c, 0x86, 0x55, 0xcb, 0x67, // |H....U.g|
0xae, 0xb6, 0x7b, 0x69, 0x3e, 0xd0, 0x48, 0x31, // |..{i>.H1|
0x58, 0x8a, 0xd8, 0xba, 0x06, 0x21, 0xf0, 0xd4, // |X....!..|
0x4e, 0xef, 0xcf, 0x67, 0xc5, 0x63, 0x97, 0x59, // |N..g.c.Y|
0x95, 0x12, 0x47, 0x90, 0x00, 0x2d, 0x00, 0x02, // |..G..-..|
0x01, 0x01, 0x00, 0x10, 0x00, 0x0b, 0x00, 0x09, // |........|
0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, // |.http/1.|
0x31, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, // |1.......|
0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, // |........|
0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x00, // |........|
0x2b, 0x00, 0x07, 0x06, 0x0a, 0x0a, 0x03, 0x04, // |+.......|
0x03, 0x03, 0x00, 0x23, 0x00, 0x00, 0x00, 0x12, // |...#....|
0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, // |........|
0x02, 0x00, 0x33, 0x04, 0xef, 0x04, 0xed, 0xda, // |..3.....|
0xda, 0x00, 0x01, 0x00, 0x11, 0xec, 0x04, 0xc0, // |........|
0xc6, 0x12, 0x85, 0x0b, 0xba, 0x73, 0x9d, 0x00, // |.....s..|
0x29, 0x08, 0x40, 0x3a, 0xb8, 0xfc, 0x9e, 0x99, // |).@:....|
0x25, 0xbd, 0x60, 0xb6, 0x8a, 0x56, 0x51, 0xac, // |%.`..VQ.|
0x38, 0xa3, 0x15, 0x37, 0x21, 0x80, 0x86, 0x02, // |8..7!...|
0xb2, 0x10, 0x4b, 0x29, 0xeb, 0x37, 0x04, 0x47, // |..K).7.G|
0x16, 0x12, 0x0e, 0x63, 0x2d, 0x32, 0xf6, 0x2a, // |...c-2.*|
0x86, 0x09, 0x7b, 0x41, 0x28, 0x8c, 0xcf, 0xfa, // |..{A(...|
0x08, 0x2e, 0x0c, 0xb2, 0x55, 0xb4, 0xb4, 0xd2, // |....U...|
0x76, 0x38, 0x47, 0x44, 0x78, 0xf0, 0x01, 0xb6, // |v8GDx...|
0xee, 0xf0, 0x1f, 0x4b, 0xc5, 0x6b, 0xb3, 0x93, // |...K.k..|
0x4d, 0xa5, 0x25, 0x29, 0xda, 0x33, 0x1e, 0xc5, // |M.%).3..|
0x15, 0x98, 0xf5, 0x41, 0x3e, 0xd2, 0xf7, 0x82, // |...A>...|
0xd7, 0xbb, 0x56, 0xf0, 0x86, 0x29, 0xa3, 0x56, // |..V..).V|
0x25, 0xdc, 0xaa, 0x03, 0xaa, 0x28, 0xa7, 0x2b, // |%....(.+|
0xc0, 0x41, 0xca, 0x66, 0x3e, 0xcc, 0x21, 0x40, // |.A.f>.!@|
0x60, 0x34, 0x5f, 0x9f, 0x69, 0x37, 0xac, 0x30, // |`4_.i7.0|
0x06, 0x7a, 0xf9, 0x26, 0xfe, 0x3c, 0x13, 0x05, // |.z.&.<..|
0xf6, 0xbe, 0x5f, 0x0c, 0x9a, 0x43, 0x18, 0xa2, // |.._..C..|
0xd9, 0xc5, 0xa0, 0x06, 0x0b, 0x0a, 0x21, 0xf1, // |......!.|
0x6b, 0x12, 0x4a, 0x5d, 0xec, 0xf6, 0x01, 0x30, // |k.J]...0|
0xb6, 0x3b, 0x34, 0x62, 0xcd, 0x5a, 0x6a, 0x26, // |.;4b.Zj&|
0x08, 0x98, 0xc9, 0xd0, 0x8a, 0x49, 0x94, 0x07, // |.....I..|
0x48, 0x45, 0x78, 0x45, 0xae, 0x24, 0x2b, 0x83, // |HExE.$+.|
0xb6, 0x69, 0x6c, 0x20, 0x33, 0xa9, 0xc4, 0x8e, // |.il 3...|
0xe7, 0x1a, 0x90, 0x28, 0xc6, 0x3f, 0x16, 0xf2, // |...(.?..|
0xae, 0x3e, 0x22, 0x17, 0x26, 0x9c, 0x38, 0xf5, // |.>".&.8.|
0x88, 0x60, 0x79, 0x16, 0x28, 0xce, 0x05, 0x72, // |.`y.(..r|
0x2f, 0x64, 0x99, 0xdd, 0x8c, 0x5b, 0xa6, 0xe2, // |/d...[..|
0x65, 0x8a, 0xe2, 0x8d, 0xb6, 0x24, 0x9e, 0x6d, // |e....$.m|
0x5a, 0x70, 0xfd, 0xea, 0xca, 0xec, 0x77, 0x46, // |Zp....wF|
0x20, 0xa8, 0x1f, 0x78, 0xf6, 0x34, 0x52, 0x13, // | ..x.4R.|
0x97, 0xef, 0x60, 0xb9, 0xe5, 0xc6, 0x85, 0xf2, // |..`.....|
0x84, 0x64, 0xdc, 0x08, 0x07, 0xe2, 0x63, 0xa6, // |.d....c.|
0x23, 0x64, 0x54, 0xb8, 0x72, 0xac, 0x23, 0xda, // |#dT.r.#.|
0x8f, 0x73, 0xe4, 0x9b, 0x80, 0x77, 0x66, 0x3f, // |.s...wf?|
0x69, 0x34, 0xc4, 0xfb, 0x45, 0x3d, 0x1c, 0xa7, // |i4..E=..|
0x86, 0x98, 0x2e, 0xb4, 0xe0, 0x84, 0xb6, 0x47, // |.......G|
0x78, 0xeb, 0x2b, 0x10, 0x17, 0x45, 0x8a, 0xcf, // |x.+..E..|
0xea, 0xb5, 0x58, 0x42, 0x93, 0xbe, 0x4b, 0xad, // |..XB..K.|
0xfb, 0x28, 0x11, 0x12, 0xe0, 0x7c, 0x3d, 0x34, // |.(...|=4|
0x8c, 0x82, 0x07, 0x84, 0xda, 0x8b, 0x35, 0x86, // |......5.|
0x37, 0x35, 0x1d, 0x1a, 0xa2, 0xbf, 0x0a, 0xb4, // |75......|
0x8e, 0xf0, 0x91, 0xc4, 0xa8, 0x3f, 0x38, 0x03, // |.....?8.|
0x37, 0xc1, 0x9a, 0x94, 0x43, 0x09, 0x57, 0xee, // |7...C.W.|
0xaa, 0xcb, 0x3d, 0x13, 0xa2, 0x33, 0xd1, 0x04, // |..=..3..|
0x2c, 0x6c, 0xb4, 0x1c, 0x86, 0x07, 0x0c, 0x3c, // |,l.....<|
0x5c, 0xc9, 0x8c, 0xc8, 0x1a, 0x85, 0xa6, 0xdd, // |\.......|
0xd3, 0xc5, 0xae, 0x84, 0x4d, 0xfe, 0xa2, 0x99, // |....M...|
0xd3, 0x0b, 0x1f, 0x43, 0x01, 0xa6, 0x7b, 0xb2, // |...C..{.|
0x5b, 0xd5, 0xa0, 0x3e, 0xd4, 0x6c, 0x65, 0x75, // |[..>.leu|
0x55, 0x28, 0x4d, 0x1c, 0x28, 0x86, 0xda, 0x94, // |U(M.(...|
0xbe, 0x0a, 0x99, 0x61, 0xa4, 0x88, 0xd9, 0x6a, // |...a...j|
0x20, 0x1d, 0x78, 0x45, 0x5f, 0x66, 0xcc, 0x8c, // | .xE_f..|
0xe1, 0xba, 0x4c, 0x51, 0x99, 0x54, 0x27, 0x77, // |..LQ.T'w|
0xb4, 0x84, 0x61, 0x4e, 0xf9, 0x90, 0x6f, 0x19, // |..aN..o.|
0x44, 0x93, 0x27, 0x1d, 0x95, 0x82, 0x74, 0x7f, // |D.'...t.|
0x35, 0xaf, 0x04, 0xe4, 0x58, 0x41, 0x3a, 0x51, // |5...XA:Q|
0x0b, 0x22, 0x45, 0xaf, 0x44, 0x2a, 0xe9, 0xa3, // |."E.D*..|
0x71, 0x65, 0x15, 0x22, 0xea, 0x40, 0x10, 0xaf, // |qe.".@..|
0x5b, 0x27, 0xfc, 0x02, 0x00, 0x23, 0xa3, 0x70, // |['...#.p|
0xa9, 0x6c, 0xa7, 0xf7, 0x29, 0x5c, 0x75, 0x9b, // |.l..)\u.|
0x4c, 0x23, 0x14, 0x51, 0x12, 0x62, 0x71, 0xbb, // |L#.Q.bq.|
0x75, 0x64, 0x65, 0xb3, 0xaa, 0x1e, 0x10, 0x14, // |ude.....|
0xbf, 0xd0, 0x8b, 0xe0, 0xe4, 0x51, 0x6e, 0xa8, // |.....Qn.|
0x1a, 0x95, 0x21, 0xa9, 0x9f, 0xf7, 0x2a, 0xac, // |..!...*.|
0x5c, 0x1c, 0x12, 0xac, 0x9d, 0xac, 0x57, 0x14, // |\.....W.|
0x27, 0xaa, 0xa7, 0xee, 0xc3, 0x9d, 0x63, 0x48, // |'.....cH|
0x0e, 0xd7, 0xf8, 0x92, 0x9f, 0x28, 0xb9, 0x82, // |.....(..|
0x71, 0x99, 0xa1, 0xcb, 0x69, 0x0c, 0x29, 0x7d, // |q...i.)}|
0x67, 0x73, 0xae, 0x9d, 0xd7, 0xc7, 0x51, 0x7a, // |gs....Qz|
0x2c, 0x3a, 0x74, 0x89, 0x7d, 0x76, 0x35, 0xb5, // |,:t.}v5.|
0x97, 0x73, 0x4a, 0xfc, 0x29, 0x9a, 0x1a, 0x06, // |.sJ.)...|
0x2f, 0xd0, 0x89, 0x32, 0xfc, 0x3b, 0x17, 0xec, // |/..2.;..|
0x7a, 0xb5, 0x3c, 0x66, 0x0f, 0x43, 0x55, 0x41, // |z.<f.CUA|
0x49, 0x3f, 0xbf, 0xa1, 0x6f, 0x8a, 0x05, 0x76, // |I?..o..v|
0xd4, 0x02, 0x33, 0x52, 0x78, 0xc8, 0x08, 0xe9, // |..3Rx...|
0x49, 0xb8, 0x42, 0x05, 0xed, 0x34, 0x0a, 0xb1, // |I.B..4..|
0xa8, 0x32, 0x00, 0x6b, 0x00, 0x42, 0x56, 0x8a, // |.2.k.BV.|
0xe9, 0x04, 0x7a, 0xac, 0xc8, 0x72, 0x7f, 0x40, // |..z..r.@|
0x4c, 0xd6, 0xa9, 0x34, 0x0b, 0xc3, 0x63, 0x39, // |L..4..c9|
0x21, 0xbf, 0x04, 0xb0, 0x2b, 0x81, 0xf9, 0x07, // |!...+...|
0xe6, 0x15, 0x92, 0x89, 0x9b, 0x1e, 0xe6, 0x4b, // |.......K|
0x5b, 0x0b, 0x33, 0x5f, 0x89, 0x96, 0xa2, 0x74, // |[.3_...t|
0x41, 0x6b, 0x15, 0xe8, 0x8a, 0x62, 0xf5, 0x1c, // |Ak...b..|
0x37, 0x38, 0x62, 0x77, 0xd4, 0x57, 0x7b, 0x43, // |78bw.W{C|
0x42, 0x4f, 0x01, 0x9c, 0xf2, 0xe0, 0x68, 0xb7, // |BO....h.|
0xf1, 0x66, 0x93, 0xd8, 0x8e, 0x78, 0x80, 0x24, // |.f...x.$|
0x4c, 0x61, 0x11, 0xbb, 0xf2, 0x79, 0xf7, 0x96, // |La...y..|
0x02, 0x80, 0xaa, 0xc7, 0xcd, 0xbb, 0x55, 0x03, // |......U.|
0x22, 0x5e, 0xda, 0xa2, 0x44, 0x7d, 0x82, 0x41, // |"^..D}.A|
0x86, 0x9b, 0x92, 0x0a, 0xd5, 0x7e, 0xf2, 0x78, // |.....~.x|
0x84, 0x50, 0x00, 0x2d, 0x0b, 0xab, 0x92, 0x7a, // |.P.-...z|
0x96, 0x15, 0xcf, 0x5a, 0x34, 0x45, 0x35, 0xa7, // |...Z4E5.|
0x18, 0x61, 0x2b, 0x88, 0x45, 0xaa, 0xd3, 0xe2, // |.a+.E...|
0x54, 0xf9, 0xc7, 0xbb, 0xe7, 0x00, 0x86, 0xbd, // |T.......|
0x8b, 0xbb, 0x6d, 0x3b, 0x0f, 0x8d, 0xfb, 0x4d, // |..m;...M|
0x5d, 0x8b, 0x50, 0x2e, 0x68, 0x74, 0x5d, 0x03, // |].P.ht].|
0x16, 0x2a, 0x49, 0x24, 0x54, 0x5b, 0xa9, 0x34, // |.*I$T[.4|
0x25, 0x17, 0x79, 0xe3, 0xc3, 0x3a, 0x2a, 0x12, // |%.y..:*.|
0x75, 0x64, 0x16, 0xa4, 0xb7, 0x7e, 0x39, 0x5a, // |ud...~9Z|
0x4e, 0x3e, 0x53, 0x2b, 0x49, 0x1b, 0x26, 0xdf, // |N>S+I.&.|
0xfc, 0x29, 0x99, 0xcb, 0xad, 0x29, 0x2c, 0x72, // |.)...),r|
0x3f, 0xa7, 0xcb, 0x45, 0x4c, 0x14, 0xee, 0x46, // |?..EL..F|
0x74, 0x64, 0xdb, 0x4b, 0x4b, 0xa4, 0x35, 0x3c, // |td.KK.5<|
0x91, 0xc4, 0x9b, 0xb0, 0x66, 0xc6, 0x70, 0xb6, // |....f.p.|
0xf2, 0x07, 0x3b, 0xbf, 0x74, 0x72, 0xb4, 0x24, // |..;.tr.$|
0x7e, 0x87, 0xd4, 0x0a, 0x37, 0xd9, 0x49, 0x04, // |~...7.I.|
0x09, 0x36, 0xd1, 0x63, 0x88, 0xe1, 0xe8, 0x08, // |.6.c....|
0xbf, 0x17, 0xc4, 0xcd, 0xcb, 0x3c, 0xef, 0x88, // |.....<..|
0x2c, 0xf6, 0xa3, 0x6d, 0x89, 0x39, 0xc9, 0xfe, // |,..m.9..|
0x97, 0x25, 0xb3, 0x9a, 0x02, 0x40, 0xd4, 0x90, // |.%...@..|
0x28, 0x6a, 0x79, 0xbd, 0x4b, 0x8e, 0x10, 0x18, // |(jy.K...|
0xc9, 0xaf, 0xe9, 0xc0, 0x6e, 0xd5, 0xb1, 0xcf, // |....n...|
0xe8, 0xa4, 0xdc, 0x94, 0x12, 0x82, 0xfb, 0x08, // |........|
0x42, 0xd4, 0x1a, 0x76, 0xa2, 0x4b, 0x3f, 0xc3, // |B..v.K?.|
0xb4, 0x0b, 0xa3, 0x0c, 0xec, 0x19, 0x7c, 0x5f, // |......|_|
0xd5, 0x98, 0x99, 0xf4, 0x1a, 0xca, 0x83, 0xaa, // |........|
0xbd, 0x26, 0x31, 0x95, 0x77, 0x90, 0x43, 0x7a, // |.&1.w.Cz|
0x75, 0x15, 0xcb, 0x68, 0xae, 0x24, 0xc5, 0x1b, // |u..h.$..|
];
const TOO_LITTLE_DATA_END: [u8; 312] = [
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // |........|
0x00, 0x01, 0x00, 0x06, 0x14, 0x49, 0xbc, 0x07, // |.....I..|
0xd1, 0x60, 0x00, 0x00, 0x45, 0x00, 0x01, 0x24, // |.`..E..$|
0x46, 0x04, 0x40, 0x00, 0x7f, 0x06, 0x11, 0x27, // |F.@....'|
0x4d, 0xaf, 0x56, 0xec, 0xc0, 0xa8, 0x3e, 0x65, // |M.V...>e|
0xbf, 0x7f, 0x01, 0xbb, 0xaf, 0x09, 0x43, 0xb4, // |......C.|
0xde, 0x93, 0xcc, 0x66, 0x50, 0x18, 0x02, 0x01, // |...fP...|
0xd3, 0x21, 0x00, 0x00, 0xba, 0x15, 0x37, 0x39, // |.!....79|
0xe7, 0xae, 0xb6, 0x48, 0x3e, 0xd8, 0x57, 0x67, // |...H>.Wg|
0x9c, 0xb6, 0x9c, 0xc0, 0x18, 0x0e, 0x74, 0x67, // |......tg|
0xae, 0x8e, 0xc6, 0x80, 0x7f, 0x81, 0x25, 0xc4, // |......%.|
0xe9, 0x04, 0xe8, 0xd9, 0x98, 0xb6, 0x99, 0x93, // |........|
0xa1, 0xa4, 0x5e, 0x57, 0x74, 0x89, 0x30, 0x38, // |..^Wt.08|
0xa9, 0xbb, 0x99, 0x4a, 0x7e, 0x42, 0x3c, 0xd2, // |...J~B<.|
0x59, 0xb6, 0x49, 0xb0, 0xc7, 0x11, 0x57, 0x03, // |Y.I...W.|
0x6d, 0x23, 0x1b, 0x72, 0xe7, 0x24, 0xdb, 0x75, // |m#.r.$.u|
0x78, 0xd1, 0x38, 0x01, 0x46, 0xb6, 0x8c, 0x1b, // |x.8.F...|
0x41, 0xb4, 0xbd, 0xc1, 0xa2, 0x00, 0x63, 0xa5, // |A.....c.|
0x97, 0x30, 0x5d, 0xbe, 0xd1, 0x37, 0x31, 0xf1, // |.0]..71.|
0xbb, 0xc6, 0xf8, 0x81, 0x35, 0x86, 0x32, 0xa6, // |....5.2.|
0xc3, 0x35, 0x54, 0x45, 0x50, 0xdf, 0x61, 0x46, // |.5TEP.aF|
0x5b, 0x83, 0x6b, 0xac, 0x5c, 0x2d, 0xa2, 0xc3, // |[.k.\-..|
0x2e, 0x71, 0x32, 0x18, 0x41, 0x29, 0x99, 0x66, // |.q2.A).f|
0x8c, 0x50, 0x28, 0x92, 0x45, 0xae, 0x96, 0x38, // |.P(.E..8|
0xa4, 0x83, 0x94, 0x4a, 0x2f, 0x0e, 0x62, 0x13, // |...J/.b.|
0x07, 0x13, 0xc2, 0x0b, 0x84, 0xfd, 0x27, 0xab, // |......'.|
0x6c, 0xb4, 0x69, 0x0d, 0xd2, 0xdb, 0xfb, 0x8e, // |l.i.....|
0xa7, 0x09, 0x65, 0x76, 0x7e, 0x09, 0xa4, 0x7a, // |..ev~..z|
0xe9, 0xfe, 0xec, 0x52, 0x89, 0x7d, 0x07, 0x6f, // |...R.}.o|
0xff, 0xa0, 0xde, 0x8a, 0x42, 0x2d, 0xc3, 0x75, // |....B-.u|
0x05, 0x6d, 0x60, 0x76, 0xce, 0xe1, 0x6c, 0xfd, // |.m`v..l.|
0xae, 0x1f, 0x5e, 0x02, 0x94, 0x39, 0x2a, 0x55, // |..^..9*U|
0x00, 0x1d, 0x00, 0x20, 0x8d, 0x89, 0x9a, 0x19, // |... ....|
0x1d, 0x53, 0x52, 0xd5, 0xc1, 0x3e, 0x3a, 0x1d, // |.SR..>:.|
0x12, 0x15, 0xae, 0x33, 0x2e, 0x54, 0xd1, 0x6f, // |...3.T.o|
0xd6, 0xb1, 0x73, 0xd9, 0x56, 0x98, 0x6f, 0x8f, // |..s.V.o.|
0x7e, 0xf5, 0xd9, 0x75, 0x00, 0x0b, 0x00, 0x02, // |~..u....|
0x01, 0x00, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, // |........|
0x00, 0x00, 0x00, 0x9a, 0x9a, 0x00, 0x01, 0x00, // |........|
];
} }

View File

@@ -2,14 +2,17 @@ use log::debug;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io::Result; use std::io::Result;
use std::net::SocketAddr; use std::net::SocketAddr;
use time::{Duration, Instant, OffsetDateTime}; use std::sync::Arc;
use std::sync::RwLock;
use std::time::Instant;
use time::{Duration, OffsetDateTime};
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub(crate) struct UpstreamAddress { pub(crate) struct UpstreamAddress {
address: String, address: String,
resolved_addresses: Vec<SocketAddr>, resolved_addresses: Arc<RwLock<Vec<SocketAddr>>>,
resolved_time: Option<Instant>, resolved_time: Arc<RwLock<Option<Instant>>>,
ttl: Option<Duration>, ttl: Arc<RwLock<Option<Duration>>>,
} }
impl Display for UpstreamAddress { impl Display for UpstreamAddress {
@@ -27,8 +30,10 @@ impl UpstreamAddress {
} }
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
if let Some(resolved) = self.resolved_time { let r = { *self.resolved_time.read().unwrap() };
if let Some(ttl) = self.ttl {
if let Some(resolved) = r {
if let Some(ttl) = { *self.ttl.read().unwrap() } {
return resolved.elapsed() < ttl; return resolved.elapsed() < ttl;
} }
} }
@@ -37,7 +42,7 @@ impl UpstreamAddress {
} }
fn is_resolved(&self) -> bool { fn is_resolved(&self) -> bool {
!self.resolved_addresses.is_empty() !self.resolved_addresses.read().unwrap().is_empty()
} }
fn time_remaining(&self) -> Duration { fn time_remaining(&self) -> Duration {
@@ -45,17 +50,19 @@ impl UpstreamAddress {
return Duration::seconds(0); return Duration::seconds(0);
} }
self.ttl.unwrap() - self.resolved_time.unwrap().elapsed() let rt = { *self.resolved_time.read().unwrap() };
let ttl = { *self.ttl.read().unwrap() };
ttl.unwrap() - rt.unwrap().elapsed()
} }
pub async fn resolve(&mut self, mode: ResolutionMode) -> Result<Vec<SocketAddr>> { pub async fn resolve(&self, mode: ResolutionMode) -> Result<Vec<SocketAddr>> {
if self.is_resolved() && self.is_valid() { if self.is_resolved() && self.is_valid() {
debug!( debug!(
"Already got address {:?}, still valid for {:.3}s", "Already got address {:?}, still valid for {:.3}s",
&self.resolved_addresses, &self.resolved_addresses,
self.time_remaining().as_seconds_f64() self.time_remaining().as_seconds_f64()
); );
return Ok(self.resolved_addresses.clone()); return Ok(self.resolved_addresses.read().unwrap().clone());
} }
debug!( debug!(
@@ -70,8 +77,8 @@ impl UpstreamAddress {
Err(e) => { Err(e) => {
debug!("Failed looking up {}: {}", &self.address, &e); debug!("Failed looking up {}: {}", &self.address, &e);
// Protect against DNS flooding. Cache the result for 1 second. // Protect against DNS flooding. Cache the result for 1 second.
self.resolved_time = Some(Instant::now()); *self.resolved_time.write().unwrap() = Some(Instant::now());
self.ttl = Some(Duration::seconds(3)); *self.ttl.write().unwrap() = Some(Duration::seconds(3));
return Err(e); return Err(e);
} }
}; };
@@ -103,11 +110,11 @@ impl UpstreamAddress {
.expect("Format") .expect("Format")
); );
self.resolved_addresses = addresses; *self.resolved_addresses.write().unwrap() = addresses.clone();
self.resolved_time = Some(Instant::now()); *self.resolved_time.write().unwrap() = Some(Instant::now());
self.ttl = Some(Duration::minutes(1)); *self.ttl.write().unwrap() = Some(Duration::minutes(1));
Ok(self.resolved_addresses.clone()) Ok(addresses)
} }
} }

391
src/sni_matcher.rs Normal file
View File

@@ -0,0 +1,391 @@
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SniPattern {
pub pattern: String,
pub upstream: String,
}
/// Validates and matches SNI patterns against incoming SNI values. Supports exact matches and wildcard patterns.
///
/// Rules:
/// - Wildcard patterns must start with "*." and have a valid domain suffix after it (e.g., "*.example.com")
/// - Wildcard patterns cannot have a wildcard in the middle (e.g., "*.example.*" is invalid)
/// - Wildcard patterns cannot have multiple wildcards (e.g., "*.*.example.com" is invalid)
/// - When there are two wildcard patterns that could match the same SNI, the longest suffix wins
/// (e.g., "*.example.com" vs "*.api.example.com" - "v2.api.example.com" matches "*.api.example.com")
/// - Wildcard patterns that overlap are not matched (e.g. "*.example.com" and "*.bar.example.com" - "bar.example.com" matches neither, "v2.bar.example.com" matches "*.bar.example.com")
/// - Wildcard patterns cannot be just "*." or "*" (must have a valid suffix)
/// - For known public suffixes (e.g., "com", "org"), the wildcard must have at least one label below the public suffix (e.g., "*.example.com" is valid, but "*.com", or *.co.uk is invalid)
/// - For unknown suffixes (e.g., "local", "lan"), the wildcard is allowed without restriction (e.g., "*.local" is valid)
#[derive(Debug, Clone)]
pub struct SniMatcher {
pub exact: HashMap<String, String>,
pub wildcards: Vec<SniPattern>,
}
impl SniMatcher {
pub fn new(sni_map: HashMap<String, String>) -> Result<Self, Vec<String>> {
Self::validate(&sni_map)?;
let mut exact = HashMap::new();
let mut wildcards = Vec::new();
for (pattern, upstream) in sni_map {
if pattern.starts_with("*.") {
wildcards.push(SniPattern {
pattern: pattern.clone(),
upstream,
});
} else {
exact.insert(pattern, upstream);
}
}
wildcards.sort_by(|a, b| {
let a_suffix = a.pattern.trim_start_matches("*.");
let b_suffix = b.pattern.trim_start_matches("*.");
b_suffix.len().cmp(&a_suffix.len())
});
Ok(SniMatcher { exact, wildcards })
}
/// Matches the provided SNI against the patterns in the matcher. Returns Some(upstream) if a match is found, or None if no match is found.
pub fn match_sni(&self, sni: &str) -> Option<String> {
if let Some(upstream) = self.exact.get(sni) {
return Some(upstream.clone());
}
// Try each wildcard in order (longest suffix first)
for wildcard in &self.wildcards {
let suffix = wildcard.pattern.trim_start_matches("*.");
let suffix_len = suffix.len();
let check = format!(".{}", suffix);
if !sni.ends_with(&check) {
continue;
}
// Must have at least one label before the suffix to match
let prefix = &sni[..sni.len() - suffix_len - 1];
// Check if a more specific wildcard could also match this SNI
let is_owned = {
let sni_labels = sni.matches('.').count() + 1;
self.wildcards.iter().any(|w| {
// Skip the current wildcard itself
if w.pattern == wildcard.pattern {
return false;
}
let w_suffix = w.pattern.trim_start_matches("*.");
let w_len = w_suffix.len();
// Only care about wildcards with longer suffix (more specific)
if w_len <= suffix_len {
return false;
}
let w_suffix_labels = w_suffix.matches('.').count() + 1;
if sni == w_suffix {
// Exact match to more specific suffix - owned if could potentially match
// (sni has at least as many labels as the suffix needs)
sni_labels >= w_suffix_labels
} else if sni.ends_with(&format!(".{}", w_suffix)) {
// Ends with more specific suffix - owned if SNI has enough labels
sni_labels >= w_suffix_labels + 1
} else {
false
}
})
};
if is_owned {
continue;
}
// Only return if we have a valid prefix (at least one label)
if !prefix.is_empty() {
return Some(wildcard.upstream.clone());
}
}
None
}
fn validate_wildcard_suffix(pattern: &str) -> Result<(), String> {
let suffix = pattern.trim_start_matches("*.");
let domain_str = format!("a.{}", suffix);
if let Some(ps) = psl::suffix(domain_str.as_bytes()) {
let ps_str = std::str::from_utf8(ps.as_bytes()).unwrap_or("");
if ps_str == suffix {
return Err(format!(
"Invalid wildcard pattern: {} - wildcard cannot be at the public suffix level",
pattern
));
}
}
Ok(())
}
fn validate(sni_map: &HashMap<String, String>) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
for (pattern, _upstream) in sni_map {
if pattern == "*" {
errors.push(format!(
"Invalid wildcard pattern: * - just asterisk is not allowed"
));
continue;
}
if pattern == "*." {
errors.push(format!(
"Invalid wildcard pattern: *. - empty suffix after wildcard"
));
continue;
}
if let Some(rest) = pattern.strip_prefix("*.") {
if rest.is_empty() {
errors.push(format!(
"Invalid wildcard pattern: {pattern} - empty suffix after wildcard"
));
continue;
}
if rest.contains('*') {
errors.push(format!("Invalid wildcard pattern: {pattern} - wildcard cannot be in the middle of suffix"));
continue;
}
if let Err(e) = Self::validate_wildcard_suffix(pattern) {
errors.push(e);
}
} else if pattern.contains('*') {
errors.push(format!(
"Invalid wildcard pattern: {pattern} - wildcard must be at the start"
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exact_match() {
let mut sni_map = HashMap::new();
sni_map.insert("example.com".to_string(), "upstream1".to_string());
sni_map.insert("*.example.com".to_string(), "upstream2".to_string());
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(
matcher.match_sni("example.com"),
Some("upstream1".to_string())
);
}
#[test]
fn test_wildcard_match() {
let mut sni_map = HashMap::new();
sni_map.insert("example.com".to_string(), "upstream1".to_string());
sni_map.insert("*.example.com".to_string(), "upstream2".to_string());
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(
matcher.match_sni("www.example.com"),
Some("upstream2".to_string())
);
assert_eq!(
matcher.match_sni("api.example.com"),
Some("upstream2".to_string())
);
}
#[test]
fn test_longest_suffix_match() {
let mut sni_map = HashMap::new();
sni_map.insert("*.example.com".to_string(), "wildcard1".to_string());
sni_map.insert("*.api.example.com".to_string(), "wildcard2".to_string());
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(
matcher.match_sni("v2.api.example.com"),
Some("wildcard2".to_string())
);
}
#[test]
fn test_no_match() {
let mut sni_map = HashMap::new();
sni_map.insert("example.com".to_string(), "upstream1".to_string());
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(matcher.match_sni("other.com"), None);
}
#[test]
fn test_unknown_suffix_allowed() {
let mut sni_map = HashMap::new();
sni_map.insert(
"*.private.local".to_string(),
"private_upstream".to_string(),
);
sni_map.insert(
"*.internal.net".to_string(),
"internal_upstream".to_string(),
);
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(
matcher.match_sni("server.private.local"),
Some("private_upstream".to_string())
);
assert_eq!(
matcher.match_sni("app.internal.net"),
Some("internal_upstream".to_string())
);
}
#[test]
fn test_invalid_public_suffix() {
let mut sni_map = HashMap::new();
sni_map.insert("*.com".to_string(), "invalid".to_string());
let result = SniMatcher::new(sni_map);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
assert!(errors[0].contains("*.com"));
}
#[test]
fn test_multiple_errors_collected() {
let mut sni_map = HashMap::new();
sni_map.insert("*.com".to_string(), "invalid1".to_string());
sni_map.insert("*.org".to_string(), "invalid2".to_string());
sni_map.insert("*.net".to_string(), "invalid3".to_string());
let result = SniMatcher::new(sni_map);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 3);
}
#[test]
fn test_valid_public_suffix() {
let mut sni_map = HashMap::new();
sni_map.insert("*.example.com".to_string(), "valid".to_string());
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(
matcher.match_sni("www.example.com"),
Some("valid".to_string())
);
}
#[test]
fn test_validate_static() {
let mut sni_map = HashMap::new();
sni_map.insert("*.com".to_string(), "invalid".to_string());
sni_map.insert("*.example.com".to_string(), "valid".to_string());
let result = SniMatcher::new(sni_map);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("*.com"));
}
#[test]
fn test_wildcard_not_at_start_rejected() {
let mut sni_map = HashMap::new();
sni_map.insert("foo*.example.com".to_string(), "invalid".to_string());
let result = SniMatcher::new(sni_map);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
assert!(errors[0].contains("*.example.com"));
}
#[test]
fn test_wildcard_in_middle_rejected() {
let mut sni_map = HashMap::new();
sni_map.insert("*.example.*".to_string(), "invalid".to_string());
let result = SniMatcher::new(sni_map);
assert!(result.is_err());
}
#[test]
fn test_trailing_dot_rejected() {
let mut sni_map = HashMap::new();
sni_map.insert("*.".to_string(), "invalid".to_string());
let result = SniMatcher::new(sni_map);
assert!(result.is_err());
}
#[test]
fn test_just_asterisk_rejected() {
let mut sni_map = HashMap::new();
sni_map.insert("*".to_string(), "invalid".to_string());
let result = SniMatcher::new(sni_map);
assert!(result.is_err());
}
#[test]
fn test_wildcard_requires_subdomain() {
let mut sni_map = HashMap::new();
sni_map.insert("*.example.com".to_string(), "upstream".to_string());
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(matcher.match_sni("example.com"), None);
assert_eq!(
matcher.match_sni("www.example.com"),
Some("upstream".to_string())
);
assert_eq!(
matcher.match_sni("foo.bar.example.com"),
Some("upstream".to_string())
);
}
#[test]
fn test_longest_suffix_wins_not_shortest() {
let mut sni_map = HashMap::new();
sni_map.insert("*.example.org".to_string(), "broad".to_string());
sni_map.insert("*.bar.example.org".to_string(), "narrow".to_string());
let matcher = SniMatcher::new(sni_map).unwrap();
assert_eq!(matcher.match_sni("bar.example.org"), None);
assert_eq!(
matcher.match_sni("v2.bar.example.org"),
Some("narrow".to_string())
);
assert_eq!(
matcher.match_sni("v2.example.org"),
Some("broad".to_string())
);
}
}

22
src/update.rs Normal file
View File

@@ -0,0 +1,22 @@
use self_update::cargo_crate_version;
pub(crate) fn update() {
println!("Updating to the latest version...");
let backend = self_update::backends::gitea::Update::configure()
.with_host("https://code.kiers.eu")
.repo_owner("jjkiers")
.repo_name("layer4-proxy")
.bin_name("l4p")
.show_download_progress(true)
.current_version(cargo_crate_version!())
.build()
.expect("Should initialize correctly.");
let status = backend.update_extended();
match status {
Err(e) => eprintln!("Error updating: {e}"),
Ok(_) => (),
}
}

View File

@@ -7,36 +7,25 @@ use serde::Deserialize;
use std::net::SocketAddr; use std::net::SocketAddr;
use tokio::io; use tokio::io;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::sync::Mutex;
#[derive(Debug, Default)]
struct Addr(Mutex<UpstreamAddress>);
impl Clone for Addr {
fn clone(&self) -> Self {
tokio::task::block_in_place(|| Self(Mutex::new(self.0.blocking_lock().clone())))
}
}
#[derive(Debug, Clone, Deserialize, Default)] #[derive(Debug, Clone, Deserialize, Default)]
pub struct ProxyToUpstream { pub struct ProxyToUpstream {
pub addr: String, pub addr: String,
pub protocol: String, pub protocol: String,
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
addresses: Addr, addresses: UpstreamAddress,
} }
impl ProxyToUpstream { impl ProxyToUpstream {
pub async fn resolve_addresses(&self) -> std::io::Result<Vec<SocketAddr>> { pub async fn resolve_addresses(&self) -> std::io::Result<Vec<SocketAddr>> {
let mut addr = self.addresses.0.lock().await; self.addresses.resolve((*self.protocol).into()).await
addr.resolve((*self.protocol).into()).await
} }
pub fn new(address: String, protocol: String) -> Self { pub fn new(address: String, protocol: String) -> Self {
Self { Self {
addr: address.clone(), addr: address.clone(),
protocol, protocol,
addresses: Addr(Mutex::new(UpstreamAddress::new(address))), addresses: UpstreamAddress::new(address),
} }
} }

View File

@@ -19,16 +19,6 @@ servers:
listen: listen:
- "0.0.0.0:54956" - "0.0.0.0:54956"
default: echo default: echo
kcp_server:
protocol: kcp
listen:
- "127.0.0.1:54958"
default: tester
kcp_echo_server:
protocol: kcp
listen:
- "127.0.0.1:54959"
default: echo
upstream: upstream:
web: "tcp://127.0.0.1:8080" web: "tcp://127.0.0.1:8080"