Compare commits

...

20 Commits

Author SHA1 Message Date
Alexey 80cb1bc221
Merge branch 'main' into flow 2026-03-24 22:00:51 +03:00
Alexey 8461556b02
Update release.yml 2026-03-24 22:00:32 +03:00
Alexey cfd516edf3
Update Cargo.toml 2026-03-24 21:41:14 +03:00
Alexey 803c2c0492
Update release.yml 2026-03-24 21:40:53 +03:00
Alexey b762bd029f
Merge branch 'main' into flow 2026-03-24 21:18:54 +03:00
Alexey 761679d306
Update test.yml 2026-03-24 21:18:13 +03:00
Alexey 41668b153d
Update test.yml 2026-03-24 21:14:12 +03:00
Alexey 1d2f88ad29
Merge branch 'main' into flow 2026-03-24 21:11:11 +03:00
Alexey 80917f5abc
Update test.yml 2026-03-24 21:10:56 +03:00
Alexey dc61d300ab
Bump 2026-03-24 21:02:43 +03:00
Alexey ae16080de5
TLS Validator: Unknown SNI as WARN in Log 2026-03-24 21:01:41 +03:00
Alexey b8ca1fc166
Update Dockerfile 2026-03-24 20:55:32 +03:00
Alexey f9986944df
Update release.yml 2026-03-24 20:53:56 +03:00
Alexey cb877c2bc3
Update release profile settings for better optimization: merge pull request #574 from vladon/main
Update release profile settings for better optimization
2026-03-24 14:10:04 +03:00
Vladislav Yaroslavlev 4426082c17
Update release profile settings for better optimization 2026-03-24 14:01:49 +03:00
Alexey 22097f8c7c
Update Dockerfile 2026-03-24 11:46:49 +03:00
Alexey 1450af60a0
Update Dockerfile 2026-03-24 11:41:53 +03:00
Alexey f1cc8d65f2
Update release.yml 2026-03-24 11:12:03 +03:00
Alexey ec7e808daf
Update release.yml 2026-03-24 11:05:50 +03:00
Alexey e4b7e23e76
New TLS-Fetcher + TLS SNI Validator + Upstream-driver getProxySecret/Config + Workflow Tunings + Redesign Quotas on Atomics + Tests Swap: merge pull request #569 from telemt/flow
New TLS-Fetcher + TLS SNI Validator + Upstream-driver getProxySecret/Config + Workflow Tunings + Redesign Quotas on Atomics + Tests Swap
2026-03-24 10:56:15 +03:00
7 changed files with 285 additions and 93 deletions

View File

@ -23,7 +23,7 @@ jobs:
# GNU / glibc
# ==========================
build-gnu:
name: GNU ${{ matrix.target }}
name: GNU ${{ matrix.asset }}
runs-on: ubuntu-latest
container:
@ -35,8 +35,15 @@ jobs:
include:
- target: x86_64-unknown-linux-gnu
asset: telemt-x86_64-linux-gnu
cpu: baseline
- target: x86_64-unknown-linux-gnu
asset: telemt-x86_64-v3-linux-gnu
cpu: v3
- target: aarch64-unknown-linux-gnu
asset: telemt-aarch64-linux-gnu
cpu: generic
steps:
- uses: actions/checkout@v4
@ -72,11 +79,19 @@ jobs:
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
export CC=aarch64-linux-gnu-gcc
export CXX=aarch64-linux-gnu-g++
export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C lto=fat -C panic=abort"
else
export CC=clang
export CXX=clang++
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld"
if [ "${{ matrix.cpu }}" = "v3" ]; then
CPU_FLAGS="-C target-cpu=x86-64-v3"
else
CPU_FLAGS="-C target-cpu=x86-64"
fi
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld -C lto=fat -C panic=abort $CPU_FLAGS"
fi
cargo build --release --target ${{ matrix.target }}
@ -102,7 +117,7 @@ jobs:
# MUSL
# ==========================
build-musl:
name: MUSL ${{ matrix.target }}
name: MUSL ${{ matrix.asset }}
runs-on: ubuntu-latest
container:
@ -114,8 +129,15 @@ jobs:
include:
- target: x86_64-unknown-linux-musl
asset: telemt-x86_64-linux-musl
cpu: baseline
- target: x86_64-unknown-linux-musl
asset: telemt-x86_64-v3-linux-musl
cpu: v3
- target: aarch64-unknown-linux-musl
asset: telemt-aarch64-linux-musl
cpu: generic
steps:
- uses: actions/checkout@v4
@ -123,7 +145,43 @@ jobs:
- name: Install deps
run: |
apt-get update
apt-get install -y musl-tools pkg-config curl
apt-get install -y \
musl-tools \
pkg-config \
curl
- uses: actions/cache@v4
if: matrix.target == 'aarch64-unknown-linux-musl'
with:
path: ~/.musl-aarch64
key: musl-toolchain-aarch64-v1
- name: Install aarch64 musl toolchain
if: matrix.target == 'aarch64-unknown-linux-musl'
run: |
set -e
TOOLCHAIN_DIR="$HOME/.musl-aarch64"
ARCHIVE="aarch64-linux-musl-cross.tgz"
URL="https://github.com/telemt/telemt/releases/download/toolchains/$ARCHIVE"
if [ -x "$TOOLCHAIN_DIR/bin/aarch64-linux-musl-gcc" ]; then
echo "✅ MUSL toolchain cached"
else
echo "⬇️ Downloading MUSL toolchain..."
curl -fL \
--retry 5 \
--retry-delay 3 \
--connect-timeout 10 \
--max-time 120 \
-o "$ARCHIVE" "$URL"
mkdir -p "$TOOLCHAIN_DIR"
tar -xzf "$ARCHIVE" --strip-components=1 -C "$TOOLCHAIN_DIR"
fi
echo "$TOOLCHAIN_DIR/bin" >> $GITHUB_PATH
- name: Add rust target
run: rustup target add ${{ matrix.target }}
@ -140,10 +198,20 @@ jobs:
run: |
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
export CC=aarch64-linux-musl-gcc
export RUSTFLAGS="-C target-feature=+crt-static -C linker=aarch64-linux-musl-gcc"
export CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
export RUSTFLAGS="-C target-feature=+crt-static -C linker=aarch64-linux-musl-gcc -C lto=fat -C panic=abort"
else
export CC=musl-gcc
export RUSTFLAGS="-C target-feature=+crt-static"
export CC_x86_64_unknown_linux_musl=musl-gcc
if [ "${{ matrix.cpu }}" = "v3" ]; then
CPU_FLAGS="-C target-cpu=x86-64-v3"
else
CPU_FLAGS="-C target-cpu=x86-64"
fi
export RUSTFLAGS="-C target-feature=+crt-static -C lto=fat -C panic=abort $CPU_FLAGS"
fi
cargo build --release --target ${{ matrix.target }}
@ -194,12 +262,12 @@ jobs:
prerelease: ${{ contains(github.ref, '-') }}
# ==========================
# Docker (FROM RELEASE)
# Docker
# ==========================
docker:
name: Docker (from release)
name: Docker
runs-on: ubuntu-latest
needs: release
needs: [build-gnu, build-musl]
permissions:
contents: read
@ -208,26 +276,19 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install gh
run: apt-get update && apt-get install -y gh
- uses: actions/download-artifact@v4
with:
path: dist
- name: Extract version
id: vars
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Download binary
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Extract binaries
run: |
mkdir dist
mkdir bin
gh release download ${{ steps.vars.outputs.VERSION }} \
--repo ${{ github.repository }} \
--pattern "telemt-x86_64-linux-musl.tar.gz" \
--dir dist
tar -xzf dist/telemt-x86_64-linux-musl/telemt-x86_64-linux-musl.tar.gz -C bin
mv bin/telemt bin/telemt-amd64
tar -xzf dist/telemt-x86_64-linux-musl.tar.gz -C dist
chmod +x dist/telemt
tar -xzf dist/telemt-aarch64-linux-musl/telemt-aarch64-linux-musl.tar.gz -C bin
mv bin/telemt bin/telemt-arm64
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
@ -238,7 +299,11 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & Push
- name: Extract version
id: vars
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build & Push (multi-arch)
uses: docker/build-push-action@v6
with:
context: .
@ -248,4 +313,5 @@ jobs:
ghcr.io/${{ github.repository }}:${{ steps.vars.outputs.VERSION }}
ghcr.io/${{ github.repository }}:latest
build-args: |
BINARY=dist/telemt
BINARY_AMD64=bin/telemt-amd64
BINARY_ARM64=bin/telemt-arm64

View File

@ -54,14 +54,20 @@ jobs:
uses: actions/cache@v4
with:
path: |
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-cargo-nextest-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-nextest-
${{ runner.os }}-cargo-
- run: cargo test --verbose
- name: Install cargo-nextest
run: cargo install --locked cargo-nextest || true
- name: Run tests with nextest
run: cargo nextest run -j "$(nproc)"
# ==========================
# Clippy
@ -88,11 +94,13 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-clippy-
${{ runner.os }}-cargo-
- run: cargo clippy -- --cap-lints warn
- name: Run clippy
run: cargo clippy -j "$(nproc)" -- --cap-lints warn
# ==========================
# Udeps
@ -108,20 +116,24 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rust-src
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-cargo-udeps-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-udeps-
${{ runner.os }}-cargo-
- name: Install cargo-udeps
run: cargo install cargo-udeps || true
run: cargo install --locked cargo-udeps || true
# тоже не валит билд
- run: cargo udeps || true
- name: Run udeps
run: cargo udeps -j "$(nproc)" || true

2
Cargo.lock generated
View File

@ -2793,7 +2793,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "telemt"
version = "3.3.30"
version = "3.3.31"
dependencies = [
"aes",
"anyhow",

View File

@ -1,8 +1,11 @@
[package]
name = "telemt"
version = "3.3.30"
version = "3.3.31"
edition = "2024"
[profile.release]
codegen-units = 1
[features]
redteam_offline_expected_fail = []
@ -83,4 +86,6 @@ name = "crypto_bench"
harness = false
[profile.release]
lto = "thin"
lto = "fat"
codegen-units = 1

View File

@ -1,47 +1,78 @@
# syntax=docker/dockerfile:1
ARG BINARY
ARG TARGETARCH
ARG BINARY_AMD64
ARG BINARY_ARM64
# ==========================
# Stage: minimal
# Minimal Image
# ==========================
FROM debian:12-slim AS minimal
RUN apt-get update && apt-get install -y --no-install-recommends \
ARG TARGETARCH
ARG BINARY_AMD64
ARG BINARY_ARM64
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
binutils \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
xz-utils \
ca-certificates; \
rm -rf /var/lib/apt/lists/*
# --- Select correct binary ---
RUN set -eux; \
case "${TARGETARCH}" in \
amd64) BIN="${BINARY_AMD64}" ;; \
arm64) BIN="${BINARY_ARM64}" ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
echo "Using binary: $BIN"; \
test -f "$BIN"; \
cp "$BIN" /telemt
# --- Install UPX (arch-aware) ---
RUN set -eux; \
case "${TARGETARCH}" in \
amd64) UPX_ARCH="amd64" ;; \
arm64) UPX_ARCH="arm64" ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
\
&& curl -fL \
curl -fL \
--retry 5 \
--retry-delay 3 \
--connect-timeout 10 \
--max-time 120 \
-o /tmp/upx.tar.xz \
https://github.com/telemt/telemt/releases/download/toolchains/upx-amd64_linux.tar.xz \
&& tar -xf /tmp/upx.tar.xz -C /tmp \
&& mv /tmp/upx*/upx /usr/local/bin/upx \
&& chmod +x /usr/local/bin/upx \
&& rm -rf /tmp/upx*
"https://github.com/telemt/telemt/releases/download/toolchains/upx-${UPX_ARCH}_linux.tar.xz"; \
\
tar -xf /tmp/upx.tar.xz -C /tmp; \
install -m 0755 /tmp/upx*/upx /usr/local/bin/upx; \
rm -rf /tmp/upx*
COPY ${BINARY} /telemt
RUN strip /telemt || true
RUN upx --best --lzma /telemt || true
# --- Optimize binary ---
RUN set -eux; \
test -f /telemt; \
strip --strip-unneeded /telemt || true; \
upx --best --lzma /telemt || true
# ==========================
# Debug image
# Debug Image
# ==========================
FROM debian:12-slim AS debug
RUN apt-get update && apt-get install -y --no-install-recommends \
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates \
tzdata \
curl \
iproute2 \
busybox \
&& rm -rf /var/lib/apt/lists/*
busybox; \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
@ -54,7 +85,7 @@ ENTRYPOINT ["/app/telemt"]
CMD ["config.toml"]
# ==========================
# Production (REAL distroless)
# Production Distroless on MUSL
# ==========================
FROM gcr.io/distroless/static-debian12 AS prod

View File

@ -13,7 +13,7 @@ use std::sync::Arc;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tracing::{debug, trace, warn};
use tracing::{debug, info, trace, warn};
use zeroize::{Zeroize, Zeroizing};
use crate::config::{ProxyConfig, UnknownSniAction};
@ -28,6 +28,8 @@ use rand::RngExt;
const ACCESS_SECRET_BYTES: usize = 16;
static INVALID_SECRET_WARNED: OnceLock<Mutex<HashSet<(String, String)>>> = OnceLock::new();
const UNKNOWN_SNI_WARN_COOLDOWN_SECS: u64 = 5;
static UNKNOWN_SNI_WARN_NEXT_ALLOWED: OnceLock<Mutex<Option<Instant>>> = OnceLock::new();
#[cfg(test)]
const WARNED_SECRET_MAX_ENTRIES: usize = 64;
#[cfg(not(test))]
@ -86,6 +88,24 @@ fn auth_probe_saturation_state_lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn unknown_sni_warn_state_lock() -> std::sync::MutexGuard<'static, Option<Instant>> {
UNKNOWN_SNI_WARN_NEXT_ALLOWED
.get_or_init(|| Mutex::new(None))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn should_emit_unknown_sni_warn(now: Instant) -> bool {
let mut guard = unknown_sni_warn_state_lock();
if let Some(next_allowed) = *guard
&& now < next_allowed
{
return false;
}
*guard = Some(now + Duration::from_secs(UNKNOWN_SNI_WARN_COOLDOWN_SECS));
true
}
fn normalize_auth_probe_ip(peer_ip: IpAddr) -> IpAddr {
match peer_ip {
IpAddr::V4(ip) => IpAddr::V4(ip),
@ -412,6 +432,25 @@ fn auth_probe_test_lock() -> &'static Mutex<()> {
TEST_LOCK.get_or_init(|| Mutex::new(()))
}
#[cfg(test)]
fn unknown_sni_warn_test_lock() -> &'static Mutex<()> {
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
TEST_LOCK.get_or_init(|| Mutex::new(()))
}
#[cfg(test)]
fn clear_unknown_sni_warn_state_for_testing() {
if UNKNOWN_SNI_WARN_NEXT_ALLOWED.get().is_some() {
let mut guard = unknown_sni_warn_state_lock();
*guard = None;
}
}
#[cfg(test)]
fn should_emit_unknown_sni_warn_for_testing(now: Instant) -> bool {
should_emit_unknown_sni_warn(now)
}
#[cfg(test)]
fn clear_warned_secrets_for_testing() {
if let Some(warned) = INVALID_SECRET_WARNED.get()
@ -658,12 +697,25 @@ where
if client_sni.is_some() && matched_tls_domain.is_none() && preferred_user_hint.is_none() {
auth_probe_record_failure(peer.ip(), Instant::now());
maybe_apply_server_hello_delay(config).await;
debug!(
let sni = client_sni.as_deref().unwrap_or_default();
let log_now = Instant::now();
if should_emit_unknown_sni_warn(log_now) {
warn!(
peer = %peer,
sni = ?client_sni,
action = ?config.censorship.unknown_sni_action,
sni = %sni,
unknown_sni = true,
unknown_sni_action = ?config.censorship.unknown_sni_action,
"TLS handshake rejected by unknown SNI policy"
);
} else {
info!(
peer = %peer,
sni = %sni,
unknown_sni = true,
unknown_sni_action = ?config.censorship.unknown_sni_action,
"TLS handshake rejected by unknown SNI policy"
);
}
return match config.censorship.unknown_sni_action {
UnknownSniAction::Drop => HandshakeResult::Error(ProxyError::UnknownTlsSni),
UnknownSniAction::Mask => HandshakeResult::BadClient { reader, writer },

View File

@ -1643,6 +1643,32 @@ fn auth_probe_capacity_fresh_full_map_still_tracks_newcomer_with_bounded_evictio
);
}
#[test]
fn unknown_sni_warn_cooldown_first_event_is_warn_and_repeated_events_are_info_until_window_expires()
{
let _guard = unknown_sni_warn_test_lock()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
clear_unknown_sni_warn_state_for_testing();
let now = Instant::now();
assert!(
should_emit_unknown_sni_warn_for_testing(now),
"first unknown SNI event must be eligible for WARN emission"
);
assert!(
!should_emit_unknown_sni_warn_for_testing(now + Duration::from_secs(1)),
"events inside cooldown window must be demoted from WARN to INFO"
);
assert!(
should_emit_unknown_sni_warn_for_testing(
now + Duration::from_secs(UNKNOWN_SNI_WARN_COOLDOWN_SECS)
),
"once cooldown expires, next unknown SNI event must be WARN-eligible again"
);
}
#[test]
fn stress_auth_probe_full_map_churn_keeps_bound_and_tracks_newcomers() {
let _guard = auth_probe_test_lock()