Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f37e6cbe29 | ||
|
|
c7464d53e1 | ||
|
|
03a6493147 | ||
|
|
36ef2f722d | ||
|
|
b9fda9e2c2 | ||
|
|
c5b590062c | ||
|
|
c0357b2890 | ||
|
|
4f7f7d6880 | ||
|
|
efba10f839 | ||
|
|
6ba12f35d0 | ||
|
|
6a57c23700 | ||
|
|
94b85afbc5 |
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
@@ -56,12 +56,11 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ matrix.target }}-cargo-
|
${{ runner.os }}-${{ matrix.target }}-cargo-
|
||||||
|
|
||||||
|
- name: Install cross
|
||||||
|
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
uses: actions-rs/cargo@ae10961054e4aa8bff448f48a500763b90d5c550 # v1.0.1
|
run: cross build --release --target ${{ matrix.target }}
|
||||||
with:
|
|
||||||
use-cross: true
|
|
||||||
command: build
|
|
||||||
args: --release --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Package binary
|
- name: Package binary
|
||||||
run: |
|
run: |
|
||||||
@@ -85,13 +84,42 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Update version in Cargo.toml and Cargo.lock
|
||||||
|
run: |
|
||||||
|
# Extract version from tag (remove 'v' prefix if present)
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
|
||||||
|
# Install cargo-edit for version bumping
|
||||||
|
cargo install cargo-edit
|
||||||
|
|
||||||
|
# Update Cargo.toml version
|
||||||
|
cargo set-version "$VERSION"
|
||||||
|
|
||||||
|
# Configure git
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# Commit and push changes
|
||||||
|
git add Cargo.toml Cargo.lock
|
||||||
|
git commit -m "chore: bump version to $VERSION" || echo "No changes to commit"
|
||||||
|
git push origin HEAD:main
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@c95fe1489396fe360a41fb53f90de6ddce8c4c8a # v2.2.1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: artifacts/**/*
|
files: artifacts/**/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.0.3"
|
version = "3.0.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
80
README.md
80
README.md
@@ -10,74 +10,40 @@
|
|||||||
|
|
||||||
### 🇷🇺 RU
|
### 🇷🇺 RU
|
||||||
|
|
||||||
15 февраля мы опубликовали `telemt 3` с поддержкой Middle-End Proxy, а значит:
|
18 февраля мы опубликовали `telemt 3.0.3`, он имеет:
|
||||||
|
|
||||||
- с функциональными медиа, в том числе с CDN/DC=203
|
- улучшенный механизм Middle-End Health Check
|
||||||
- с Ad-tag — показывайте спонсорский канал и собирайте статистику через официального бота
|
- высокоскоростное восстановление инициализации Middle-End
|
||||||
- с новым подходом к безопасности и асинхронности
|
- меньше задержек на hot-path
|
||||||
- с высокоточной диагностикой криптографии через `ME_DIAG`
|
- более корректную работу в Dualstack, а именно - IPv6 Middle-End
|
||||||
|
- аккуратное переподключение клиента без дрифта сессий между Middle-End
|
||||||
|
- автоматическая деградация на Direct-DC при массовой (>2 ME-DC-групп) недоступности Middle-End
|
||||||
|
- автодетект IP за NAT, при возможности - будет выполнен хендшейк с ME, при неудаче - автодеградация
|
||||||
|
- единственный известный специальный DC=203 уже добавлен в код: медиа загружаются с CDN в Direct-DC режиме
|
||||||
|
|
||||||
Для использования нужно:
|
[Здесь вы можете найти релиз](https://github.com/telemt/telemt/releases/tag/3.0.3)
|
||||||
|
|
||||||
1. Версия `telemt` ≥3.0.0
|
Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге или сетевых расследованиях - мы открыты к идеям и pull requests!
|
||||||
2. Выполнение любого из наборов условий:
|
|
||||||
- публичный IP для исходящих соединений установлен на интерфейса инстанса с `telemt`
|
|
||||||
- ЛИБО
|
|
||||||
- вы используете NAT 1:1 + включили STUN-пробинг
|
|
||||||
3. В конфиге, в секции `[general]` указать:
|
|
||||||
```toml
|
|
||||||
use_middle_proxy = true
|
|
||||||
```
|
|
||||||
|
|
||||||
Если условия из пункта 1 не выполняются:
|
|
||||||
1. Выключите ME-режим:
|
|
||||||
- установите `use_middle_proxy = false`
|
|
||||||
- ЛИБО
|
|
||||||
- Middle-End Proxy будет выключен автоматически по таймауту, но это займёт больше времени при запуске
|
|
||||||
2. В конфиге, добавьте в конец:
|
|
||||||
```toml
|
|
||||||
[dc_overrides]
|
|
||||||
"203" = "91.105.192.100:443"
|
|
||||||
```
|
|
||||||
|
|
||||||
Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге или сетевых расследованиях — мы открыты к идеям и pull requests.
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
### 🇬🇧 EN
|
### 🇬🇧 EN
|
||||||
|
|
||||||
On February 15, we released `telemt 3` with support for Middle-End Proxy, which means:
|
On February 18, we released `telemt 3.0.3`. This version introduces:
|
||||||
|
|
||||||
- functional media, including CDN/DC=203
|
- improved Middle-End Health Check method
|
||||||
- Ad-tag support – promote a sponsored channel and collect statistics via Telegram bot
|
- high-speed recovery of Middle-End init
|
||||||
- new approach to security and asynchronicity
|
- reduced latency on the hot path
|
||||||
- high-precision cryptography diagnostics via `ME_DIAG`
|
- correct Dualstack support: proper handling of IPv6 Middle-End
|
||||||
|
- *clean* client reconnection without session "drift" between Middle-End
|
||||||
|
- automatic degradation to Direct-DC mode in case of large-scale (>2 ME-DC groups) Middle-End unavailability
|
||||||
|
- automatic public IP detection behind NAT; first - Middle-End handshake is performed, otherwise automatic degradation is applied
|
||||||
|
- known special DC=203 is now handled natively: media is delivered from the CDN via Direct-DC mode
|
||||||
|
|
||||||
To use this feature, the following requirements must be met:
|
[Release is available here](https://github.com/telemt/telemt/releases/tag/3.0.3)
|
||||||
1. `telemt` version ≥ 3.0.0
|
|
||||||
2. One of the following conditions satisfied:
|
|
||||||
- the instance running `telemt` has a public IP address assigned to its network interface for outbound connections
|
|
||||||
- OR
|
|
||||||
- you are using 1:1 NAT and have STUN probing enabled
|
|
||||||
3. In the config file, under the `[general]` section, specify:
|
|
||||||
```toml
|
|
||||||
use_middle_proxy = true
|
|
||||||
````
|
|
||||||
|
|
||||||
If the conditions from step 1 are not satisfied:
|
If you have expertise in asynchronous network applications, traffic analysis, reverse engineering, or network forensics - we welcome ideas and pull requests!
|
||||||
1. Disable Middle-End mode:
|
|
||||||
- set `use_middle_proxy = false`
|
|
||||||
- OR
|
|
||||||
- Middle-End Proxy will be disabled automatically after a timeout, but this will increase startup time
|
|
||||||
|
|
||||||
2. In the config file, add the following at the end:
|
|
||||||
```toml
|
|
||||||
[dc_overrides]
|
|
||||||
"203" = "91.105.192.100:443"
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have expertise in asynchronous network applications, traffic analysis, reverse engineering, or network forensics — we welcome ideas, suggestions, and pull requests.
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -88,6 +54,8 @@ If you have expertise in asynchronous network applications, traffic analysis, re
|
|||||||
|
|
||||||
⚓ Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
|
⚓ Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
|
||||||
|
|
||||||
|
⚓ Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
|
||||||
|
|
||||||
# GOTO
|
# GOTO
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Quick Start Guide](#quick-start-guide)
|
- [Quick Start Guide](#quick-start-guide)
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ impl ProxyConfig {
|
|||||||
upstream_type: UpstreamType::Direct { interface: None },
|
upstream_type: UpstreamType::Direct { interface: None },
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
scopes: String::new(),
|
||||||
|
selected_scope: String::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -403,6 +403,10 @@ pub struct UpstreamConfig {
|
|||||||
pub weight: u16,
|
pub weight: u16,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub scopes: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub selected_scope: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ where
|
|||||||
);
|
);
|
||||||
|
|
||||||
let tg_stream = upstream_manager
|
let tg_stream = upstream_manager
|
||||||
.connect(dc_addr, Some(success.dc_idx))
|
.connect(dc_addr, Some(success.dc_idx), user.strip_prefix("scope_").filter(|s| !s.is_empty()))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
debug!(peer = %success.peer, dc_addr = %dc_addr, "Connected, performing TG handshake");
|
debug!(peer = %success.peer, dc_addr = %dc_addr, "Connected, performing TG handshake");
|
||||||
|
|||||||
@@ -167,20 +167,44 @@ impl UpstreamManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Select upstream using latency-weighted random selection.
|
/// Select upstream using latency-weighted random selection.
|
||||||
async fn select_upstream(&self, dc_idx: Option<i16>) -> Option<usize> {
|
async fn select_upstream(&self, dc_idx: Option<i16>, scope: Option<&str>) -> Option<usize> {
|
||||||
let upstreams = self.upstreams.read().await;
|
let upstreams = self.upstreams.read().await;
|
||||||
if upstreams.is_empty() {
|
if upstreams.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
// Scope filter:
|
||||||
let healthy: Vec<usize> = upstreams.iter()
|
// If scope is set: only scoped and matched items
|
||||||
|
// If scope is not set: only unscoped items
|
||||||
|
let filtered_upstreams : Vec<usize> = upstreams.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, u)| u.healthy)
|
.filter(|(_, u)| {
|
||||||
|
scope.map_or(
|
||||||
|
u.config.scopes.is_empty(),
|
||||||
|
|req_scope| {
|
||||||
|
u.config.scopes
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.any(|s| s == req_scope)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Healthy filter
|
||||||
|
let healthy: Vec<usize> = filtered_upstreams.iter()
|
||||||
|
.filter(|&&i| upstreams[i].healthy)
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if filtered_upstreams.is_empty() {
|
||||||
|
warn!(scope = scope, "No upstreams available! Using first (direct?)");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
if healthy.is_empty() {
|
if healthy.is_empty() {
|
||||||
return Some(rand::rng().gen_range(0..upstreams.len()));
|
warn!(scope = scope, "No healthy upstreams available! Using random.");
|
||||||
|
return Some(filtered_upstreams[rand::rng().gen_range(0..filtered_upstreams.len())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if healthy.len() == 1 {
|
if healthy.len() == 1 {
|
||||||
@@ -222,15 +246,20 @@ impl UpstreamManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to target through a selected upstream.
|
/// Connect to target through a selected upstream.
|
||||||
pub async fn connect(&self, target: SocketAddr, dc_idx: Option<i16>) -> Result<TcpStream> {
|
pub async fn connect(&self, target: SocketAddr, dc_idx: Option<i16>, scope: Option<&str>) -> Result<TcpStream> {
|
||||||
let idx = self.select_upstream(dc_idx).await
|
let idx = self.select_upstream(dc_idx, scope).await
|
||||||
.ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?;
|
.ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?;
|
||||||
|
|
||||||
let upstream = {
|
let mut upstream = {
|
||||||
let guard = self.upstreams.read().await;
|
let guard = self.upstreams.read().await;
|
||||||
guard[idx].config.clone()
|
guard[idx].config.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set scope for configuration copy
|
||||||
|
if let Some(s) = scope {
|
||||||
|
upstream.selected_scope = s.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
match self.connect_via_upstream(&upstream, target).await {
|
match self.connect_via_upstream(&upstream, target).await {
|
||||||
@@ -313,8 +342,12 @@ impl UpstreamManager {
|
|||||||
if let Some(e) = stream.take_error()? {
|
if let Some(e) = stream.take_error()? {
|
||||||
return Err(ProxyError::Io(e));
|
return Err(ProxyError::Io(e));
|
||||||
}
|
}
|
||||||
|
// replace socks user_id with config.selected_scope, if set
|
||||||
|
let scope: Option<&str> = Some(config.selected_scope.as_str())
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
let _user_id: Option<&str> = scope.or(user_id.as_deref());
|
||||||
|
|
||||||
connect_socks4(&mut stream, target, user_id.as_deref()).await?;
|
connect_socks4(&mut stream, target, _user_id).await?;
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
},
|
},
|
||||||
UpstreamType::Socks5 { address, interface, username, password } => {
|
UpstreamType::Socks5 { address, interface, username, password } => {
|
||||||
@@ -341,7 +374,14 @@ impl UpstreamManager {
|
|||||||
return Err(ProxyError::Io(e));
|
return Err(ProxyError::Io(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
connect_socks5(&mut stream, target, username.as_deref(), password.as_deref()).await?;
|
debug!(config = ?config, "Socks5 connection");
|
||||||
|
// replace socks user:pass with config.selected_scope, if set
|
||||||
|
let scope: Option<&str> = Some(config.selected_scope.as_str())
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
let _username: Option<&str> = scope.or(username.as_deref());
|
||||||
|
let _password: Option<&str> = scope.or(password.as_deref());
|
||||||
|
|
||||||
|
connect_socks5(&mut stream, target, _username, _password).await?;
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user