sdm-xray v0.1.0: stealth transport server installer + clients

This commit is contained in:
ded_ikar
2026-08-16 02:08:24 +05:00
commit a3a8e951c5
13 changed files with 3013 additions and 0 deletions
Executable
+360
View File
@@ -0,0 +1,360 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means (a) that the initial Contributor has attached the notice
described in Exhibit B to the Covered Software; or (b) that the
Covered Software was made available under the terms of version 1.1
or earlier of the License, but not also under the terms of a
Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software; or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under this
License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
Executable
+50
View File
@@ -0,0 +1,50 @@
# sdm-xray
Собственный анти-DPI туннель: форк [Xray-core](https://github.com/XTLS/Xray-core) с кастомным
транспортом **`stealth`**. Клиент → своя VPS, трафик максимально похож на браузерный HTTPS.
## Принцип
«Адаптивность, а не протокол»: транспорт меняет поведение (identity) быстрее, чем DPI успевает
его описывать. Всё в одном процессе, одним JSON-конфигом, без панелей и внешних сервисов.
Что умеет:
- Свой транспорт поверх HTTP/2 (TLS 1.3 + uTLS chrome), мультиплексирование на одном канале
- Ротация туннельных путей на каждое соединение + ретрай со свежей identity при сбое
- Анти-пробиг: сервер зеркалит реальный сайт (по умолчанию wikipedia) — зонд видит
настоящий многостраничный сайт на вашем домене (редиректы, HTML/CSS-ссылки переписаны)
- Padding/pacing под браузер, опции: ротация портов, rate-limit, decoy, REALITY
- По умолчанию только порт 443 (как браузер)
## Установка сервера (свежая Ubuntu VPS)
Домен должен указывать A-записью на IP VPS, порт 80 открыт.
```bash
bash <(curl -fsSL https://git.my-handbook.ru/ded_ikar/sdm-xray/raw/main/install.sh) ваш-домен.ru
```
Скрипт: ставит бинарь + systemd, генерит UUID/ключ, получает Let's Encrypt, поднимает
туннель на 443 с зеркалом. В конце печатает готовый клиентский конфиг.
## Клиенты
- **Windows**: скачать `xray-stealth-client-windows-amd64.exe` из Releases, сохранить
`client-stealth.json` (из вывода install.sh), запустить `xray-stealth-client-windows-amd64.exe run -c client-stealth.json`,
прокси SOCKS на `127.0.0.1:10808`.
- **Роутер**: `xray-stealth-client-linux-arm64` — положить в `/tmp`, тот же конфиг,
SOCKS 127.0.0.1:10808 или TUN + policy routing.
## Параметры `stealthSettings`
`key`, `padMax`, `padProb`, `paceMs`, `paceJitterMs`, `path`/`paths`, `ports` (опция),
`bps` (опция), `target` (зеркало), `decoy`/`decoyEverySecs`/`decoyTarget` (опция),
`padProfile` ("browser"|"uniform"), `retries`.
## Сборка из исходников
Нужны Go 1.26, protoc, protoc-gen-go.
```bash
GOOS=linux GOARCH=amd64 go build -o xray-server-amd64 ./main # сервер
GOOS=linux GOARCH=arm64 go build -o xray-client-arm64 ./main # роутер
GOOS=windows GOARCH=amd64 go build -o xray-client-win.exe ./main # Windows
```
Транспорт: `stealth/transport/internet/stealth/`. Изменённые файлы upstream:
`stealth/infra/conf/transport_internet.go`, `stealth/infra/conf/transport_method.go`.
## Лицензия и атрибуция
MPL-2.0. Форк Xray-core (XTLS) — https://github.com/XTLS/Xray-core. Новые файлы транспорта —
наш код; изменённые файлы upstream распространяются под MPL-2.0 (исходники приложены в `stealth/`).
Executable
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env bash
# sdm-xray — установка сервера stealth-туннеля на свежую Ubuntu VPS.
# Использование:
# bash <(curl -fsSL https://git.my-handbook.ru/ded_ikar/sdm-xray/raw/main/install.sh) <domain>
# где <domain> — ваш домен, A-запись которого указывает на IP этой VPS.
set -euo pipefail
DOMAIN="${1:-}"
if [ -z "$DOMAIN" ]; then
echo "Usage: install.sh <domain>"
echo " domain — домен, указывающий на IP этой VPS (нужен для Let's Encrypt)"
exit 1
fi
GITEA="https://git.my-handbook.ru/ded_ikar/sdm-xray"
VERSION="v0.1.0"
BIN="xray-stealth-server-linux-amd64"
INSTALL_DIR="/usr/local/xray"
DATA_DIR="/etc/xray"
WEBROOT="/var/www/html"
VPS_IP="$(curl -fsSL --max-time 10 https://api.ipify.org 2>/dev/null || echo UNKNOWN)"
echo "=== sdm-xray installer v${VERSION} ==="
echo "domain: $DOMAIN vps-ip: $VPS_IP"
export DEBIAN_FRONTEND=noninteractive
echo "=== apt: deps ==="
apt-get update -qq
apt-get install -y -qq curl openssl jq nginx certbot >/dev/null 2>&1
echo "=== download server binary + geo files ==="
mkdir -p "$INSTALL_DIR" "$DATA_DIR" "$WEBROOT"
curl -fsSL -o "$INSTALL_DIR/xray" "$GITEA/releases/download/$VERSION/$BIN"
curl -fsSL -o "$INSTALL_DIR/geoip.dat" "$GITEA/releases/download/$VERSION/geoip.dat"
curl -fsSL -o "$INSTALL_DIR/geosite.dat" "$GITEA/releases/download/$VERSION/geosite.dat"
chmod 755 "$INSTALL_DIR/xray"
"$INSTALL_DIR/xray" version | head -1
echo "=== generate secrets ==="
UUID="$(cat /proc/sys/kernel/random/uuid)"
KEY="$(openssl rand -hex 8)"
echo "$UUID" > "$DATA_DIR/uuid"
echo "$KEY" > "$DATA_DIR/key"
echo "=== nginx on 80 (acme) ==="
mkdir -p "$WEBROOT/.well-known/acme-challenge"
cat > /etc/nginx/sites-available/sdm <<'NGX'
server {
listen 80;
listen [::]:80;
server_name DOMAIN;
root /var/www/html;
index index.html;
location /.well-known/acme-challenge/ {
alias /var/www/html/.well-known/acme-challenge/;
}
location / {
return 301 https://$host$request_uri;
}
}
NGX
sed -i "s/DOMAIN/$DOMAIN/" /etc/nginx/sites-available/sdm
ln -sf /etc/nginx/sites-available/sdm /etc/nginx/sites-enabled/sdm
rm -f /etc/nginx/sites-enabled/default
nginx -t >/dev/null 2>&1 && systemctl reload nginx
echo "=== Let's Encrypt cert ==="
certbot certonly --webroot -w "$WEBROOT" -d "$DOMAIN" \
--register-unsafely-without-email --agree-tos --no-eff-email -n \
--cert-name "$DOMAIN" >/dev/null 2>&1 || true
CERT_DIR="/etc/letsencrypt/live/$DOMAIN"
if [ ! -f "$CERT_DIR/fullchain.pem" ]; then
echo "ERROR: сертификат не получен. Проверьте, что $DOMAIN -> $VPS_IP и порт 80 открыт."
exit 1
fi
echo "=== xray config ==="
cat > "$DATA_DIR/config.json" <<EOF
{
"log": {"loglevel": "warning"},
"routing": {
"rules": [
{"port": "443", "network": "udp", "outboundTag": "block"},
{"ip": ["geoip:private"], "outboundTag": "block"}
]
},
"inbounds": [
{
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {"clients": [{"id": "$UUID", "level": 0}], "decryption": "none"},
"streamSettings": {
"network": "stealth",
"stealthSettings": {
"key": "$KEY", "padMax": 2048, "padProb": 50, "paceMs": 0, "paceJitterMs": 0,
"paths": ["/api/t04d12b46/", "/api/v2/telemetry/", "/collect/", "/ingest/"],
"ports": [443], "bps": 0, "target": "https://en.wikipedia.org",
"padProfile": "browser", "retries": 3
},
"security": "tls",
"tlsSettings": {
"certificates": [
{"certificateFile": "$CERT_DIR/fullchain.pem", "keyFile": "$CERT_DIR/privkey.pem"}
]
}
},
"sniffing": {"enabled": true, "destOverride": ["http","tls","quic"]}
}
],
"outbounds": [
{"protocol": "freedom", "tag": "direct"},
{"protocol": "blackhole", "tag": "block"}
]
}
EOF
echo "=== systemd ==="
cat > /etc/systemd/system/xray.service <<'UNIT'
[Unit]
Description=sdm-xray stealth tunnel
After=network.target nss-lookup.target
[Service]
ExecStart=/usr/local/xray/xray run -c /etc/xray/config.json
Restart=on-failure
RestartPreventExitStatus=23
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable xray >/dev/null 2>&1 || true
systemctl restart xray
sleep 1
if ! systemctl is-active xray >/dev/null 2>&1; then
echo "ERROR: xray не запустился:"; journalctl -u xray -n 20 --no-pager || true; exit 1
fi
echo
echo "=== INSTALLED OK ==="
echo "uuid: $UUID"
echo "key: $KEY"
echo
echo "=== client config (client-stealth.json) ==="
cat <<CFG
{
"log": {"loglevel": "warning"},
"inbounds": [
{"listen": "127.0.0.1", "port": 10808, "protocol": "socks"},
{"listen": "127.0.0.1", "port": 10809, "protocol": "http"}
],
"outbounds": [
{
"protocol": "vless",
"settings": {"vnext": [{"address": "$DOMAIN", "port": 443, "users": [{"id": "$UUID", "encryption": "none"}]}]},
"streamSettings": {
"network": "stealth",
"stealthSettings": {"key": "$KEY", "padMax": 2048, "padProb": 50, "paceMs": 0, "paceJitterMs": 0, "paths": ["/api/t04d12b46/", "/api/v2/telemetry/", "/collect/", "/ingest/"], "ports": [443], "bps": 0},
"security": "tls",
"tlsSettings": {"serverName": "$DOMAIN", "fingerprint": "chrome"}
}
},
{"protocol": "freedom", "tag": "direct"}
]
}
CFG
echo
echo "=== client binaries ==="
echo " Windows: $GITEA/releases/download/$VERSION/xray-stealth-client-windows-amd64.exe"
echo " Router: $GITEA/releases/download/$VERSION/xray-stealth-client-linux-arm64"
+9
View File
@@ -0,0 +1,9 @@
# stealth — кастомный транспорт
Изменения относительно upstream Xray-core:
- `transport/internet/stealth/` — новый транспорт (framing/conn/dialer/hub), наш код.
- `infra/conf/transport_internet.go`, `infra/conf/transport_method.go` — изменённые файлы
upstream (регистрация сети "stealth" + параметры). Остальной исходник — Xray-core,
см. https://github.com/XTLS/Xray-core.
MPL-2.0. Сборка: Go 1.26, `go build ./main`.
+341
View File
@@ -0,0 +1,341 @@
package conf
import (
"os"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
)
type TransportProtocol string
// Build implements Buildable.
func (p TransportProtocol) Build() (string, error) {
switch strings.ToLower(string(p)) {
case "raw", "tcp":
return "tcp", nil
case "xhttp", "splithttp":
return "splithttp", nil
case "kcp", "mkcp":
return "mkcp", nil
case "grpc":
errors.PrintNonRemovalDeprecatedFeatureWarning("gRPC transport (with unnecessary costs, etc.)", "XHTTP stream-up H2")
return "grpc", nil
case "ws", "websocket":
errors.PrintNonRemovalDeprecatedFeatureWarning("WebSocket transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3")
return "websocket", nil
case "httpupgrade":
errors.PrintNonRemovalDeprecatedFeatureWarning("HTTPUpgrade transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3")
return "httpupgrade", nil
case "h2", "h3", "http":
return "", errors.PrintRemovedFeatureError("HTTP transport (without header padding, etc.)", "XHTTP stream-one H2 & H3")
case "quic":
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
case "hysteria":
return "hysteria", nil
case "stealth":
return "stealth", nil
default:
return "", errors.New("Config: unknown transport protocol: ", p)
}
}
type StreamConfig struct {
Address *Address `json:"address"`
Port uint16 `json:"port"`
Method *TransportProtocol `json:"method"`
Network *TransportProtocol `json:"network"`
Security string `json:"security"`
FinalMask *FinalMask `json:"finalmask"`
TLSSettings *TLSConfig `json:"tlsSettings"`
REALITYSettings *REALITYConfig `json:"realitySettings"`
RAWSettings *TCPConfig `json:"rawSettings"`
TCPSettings *TCPConfig `json:"tcpSettings"`
XHTTPSettings *SplitHTTPConfig `json:"xhttpSettings"`
SplitHTTPSettings *SplitHTTPConfig `json:"splithttpSettings"`
KCPSettings *KCPConfig `json:"kcpSettings"`
GRPCSettings *GRPCConfig `json:"grpcSettings"`
WSSettings *WebSocketConfig `json:"wsSettings"`
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
StealthSettings *StealthConfig `json:"stealthSettings"`
SocketSettings *SocketConfig `json:"sockopt"`
}
// Build implements Buildable.
func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
config := &internet.StreamConfig{
Port: uint32(c.Port),
ProtocolName: "tcp",
}
if c.Address != nil {
config.Address = c.Address.Build()
}
if c.Method != nil {
c.Network = c.Method
}
if c.Network != nil {
protocol, err := c.Network.Build()
if err != nil {
return nil, err
}
config.ProtocolName = protocol
}
switch strings.ToLower(c.Security) {
case "", "none":
case "tls":
tlsSettings := c.TLSSettings
if tlsSettings == nil {
tlsSettings = &TLSConfig{}
}
ts, err := tlsSettings.Build()
if err != nil {
return nil, errors.New("Failed to build TLS config.").Base(err)
}
tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type
case "reality":
if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" && config.ProtocolName != "stealth" {
return nil, errors.New("REALITY only supports RAW, XHTTP, gRPC and stealth for now.")
}
if c.REALITYSettings == nil {
return nil, errors.New(`REALITY: Empty "realitySettings".`)
}
ts, err := c.REALITYSettings.Build()
if err != nil {
return nil, errors.New("Failed to build REALITY config.").Base(err)
}
tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type
case "xtls":
return nil, errors.PrintRemovedFeatureError(`Legacy XTLS`, `xtls-rprx-vision with TLS or REALITY`)
default:
return nil, errors.New(`Unknown security "` + c.Security + `".`)
}
if c.RAWSettings != nil {
c.TCPSettings = c.RAWSettings
}
if c.TCPSettings != nil {
ts, err := c.TCPSettings.Build()
if err != nil {
return nil, errors.New("Failed to build RAW config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "tcp",
Settings: serial.ToTypedMessage(ts),
})
}
if c.XHTTPSettings != nil {
c.SplitHTTPSettings = c.XHTTPSettings
}
if c.SplitHTTPSettings != nil {
hs, err := c.SplitHTTPSettings.Build()
if err != nil {
return nil, errors.New("Failed to build XHTTP config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "splithttp",
Settings: serial.ToTypedMessage(hs),
})
}
if c.StealthSettings != nil {
hs, err := c.StealthSettings.Build()
if err != nil {
return nil, errors.New("Failed to build stealth config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "stealth",
Settings: serial.ToTypedMessage(hs),
})
}
if c.KCPSettings != nil {
ts, err := c.KCPSettings.Build()
if err != nil {
return nil, errors.New("Failed to build mKCP config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "mkcp",
Settings: serial.ToTypedMessage(ts),
})
}
if c.GRPCSettings != nil {
gs, err := c.GRPCSettings.Build()
if err != nil {
return nil, errors.New("Failed to build gRPC config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "grpc",
Settings: serial.ToTypedMessage(gs),
})
}
if c.WSSettings != nil {
ts, err := c.WSSettings.Build()
if err != nil {
return nil, errors.New("Failed to build WebSocket config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "websocket",
Settings: serial.ToTypedMessage(ts),
})
}
if c.HTTPUPGRADESettings != nil {
hs, err := c.HTTPUPGRADESettings.Build()
if err != nil {
return nil, errors.New("Failed to build HTTPUpgrade config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "httpupgrade",
Settings: serial.ToTypedMessage(hs),
})
}
if c.HysteriaSettings != nil {
hs, err := c.HysteriaSettings.Build()
if err != nil {
return nil, errors.New("Failed to build Hysteria config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "hysteria",
Settings: serial.ToTypedMessage(hs),
})
}
if c.SocketSettings != nil {
ss, err := c.SocketSettings.Build()
if err != nil {
return nil, errors.New("Failed to build sockopt.").Base(err)
}
config.SocketSettings = ss
}
if c.FinalMask != nil {
for _, mask := range c.FinalMask.Tcp {
u, err := mask.Build(true)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u))
}
for _, mask := range c.FinalMask.Udp {
u, err := mask.Build(false)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u))
}
if c.FinalMask.QuicParams != nil {
profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile)
switch profile {
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
if profile == "" {
profile = string(bbr.ProfileStandard)
}
default:
return nil, errors.New("unknown bbr profile")
}
up, err := c.FinalMask.QuicParams.BrutalUp.Bps()
if err != nil {
return nil, err
}
down, err := c.FinalMask.QuicParams.BrutalDown.Bps()
if err != nil {
return nil, err
}
if up > 0 && up < 65536 {
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
}
if down > 0 && down < 65536 {
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
}
c.FinalMask.QuicParams.Congestion = strings.ToLower(c.FinalMask.QuicParams.Congestion)
switch c.FinalMask.QuicParams.Congestion {
case "", "brutal", "reno", "bbr":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
}
default:
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if (c.FinalMask.QuicParams.UdpHop.Interval.From != 0 && c.FinalMask.QuicParams.UdpHop.Interval.From < 5) || (c.FinalMask.QuicParams.UdpHop.Interval.To != 0 && c.FinalMask.QuicParams.UdpHop.Interval.To < 5) {
return nil, errors.New("Interval must be at least 5")
}
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxStreamReceiveWindow > 0 && c.FinalMask.QuicParams.MaxStreamReceiveWindow < 16384 {
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.InitConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.InitConnectionReceiveWindow < 16384 {
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.MaxConnectionReceiveWindow < 16384 {
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxIdleTimeout != 0 && (c.FinalMask.QuicParams.MaxIdleTimeout < 4 || c.FinalMask.QuicParams.MaxIdleTimeout > 120) {
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
}
if c.FinalMask.QuicParams.KeepAlivePeriod != 0 && (c.FinalMask.QuicParams.KeepAlivePeriod < 2 || c.FinalMask.QuicParams.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if c.FinalMask.QuicParams.MaxIncomingStreams != 0 && c.FinalMask.QuicParams.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if c.FinalMask.QuicParams.Debug {
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
}
config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
UdpHop: &internet.UdpHop{
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
IntervalMax: int64(c.FinalMask.QuicParams.UdpHop.Interval.To),
},
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
}
}
}
return config, nil
}
type ProxyConfig struct {
Tag string `json:"tag"`
// TransportLayerProxy: For compatibility.
TransportLayerProxy bool `json:"transportLayer"`
}
// Build implements Buildable.
func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) {
if v.Tag == "" {
return nil, errors.New("Proxy tag is not set.")
}
return &internet.ProxyConfig{
Tag: v.Tag,
TransportLayerProxy: v.TransportLayerProxy,
}, nil
}
+856
View File
@@ -0,0 +1,856 @@
package conf
import (
"context"
"encoding/json"
"math/big"
"net/url"
"sort"
"strconv"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/grpc"
"github.com/xtls/xray-core/transport/internet/headers/http"
"github.com/xtls/xray-core/transport/internet/headers/noop"
"github.com/xtls/xray-core/transport/internet/httpupgrade"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/kcp"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/stealth"
"github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/websocket"
"google.golang.org/protobuf/proto"
)
type NoOpConnectionAuthenticator struct{}
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
return new(noop.ConnectionConfig), nil
}
type AuthenticatorRequest struct {
Version string `json:"version"`
Method string `json:"method"`
Path StringList `json:"path"`
Headers map[string]*StringList `json:"headers"`
}
func sortMapKeys(m map[string]*StringList) []string {
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
config := &http.RequestConfig{
Uri: []string{"/"},
Header: []*http.Header{
{
Name: "Host",
Value: []string{"www.baidu.com", "www.bing.com"},
},
{
Name: "User-Agent",
Value: []string{utils.ChromeUA},
},
{
Name: "Sec-CH-UA",
Value: []string{utils.ChromeUACH},
},
{
Name: "Sec-CH-UA-Mobile",
Value: []string{"?0"},
},
{
Name: "Sec-CH-UA-Platform",
Value: []string{"Windows"},
},
{
Name: "Sec-Fetch-Mode",
Value: []string{"no-cors", "cors", "same-origin"},
},
{
Name: "Sec-Fetch-Dest",
Value: []string{"empty"},
},
{
Name: "Sec-Fetch-Site",
Value: []string{"none"},
},
{
Name: "Sec-Fetch-User",
Value: []string{"?1"},
},
{
Name: "Accept-Encoding",
Value: []string{"gzip, deflate"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Method) > 0 {
config.Method = &http.Method{Value: v.Method}
}
if len(v.Path) > 0 {
config.Uri = append([]string(nil), v.Path...)
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type AuthenticatorResponse struct {
Version string `json:"version"`
Status string `json:"status"`
Reason string `json:"reason"`
Headers map[string]*StringList `json:"headers"`
}
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
config := &http.ResponseConfig{
Header: []*http.Header{
{
Name: "Content-Type",
Value: []string{"application/octet-stream", "video/mpeg"},
},
{
Name: "Transfer-Encoding",
Value: []string{"chunked"},
},
{
Name: "Connection",
Value: []string{"keep-alive"},
},
{
Name: "Pragma",
Value: []string{"no-cache"},
},
{
Name: "Cache-Control",
Value: []string{"private", "no-cache"},
},
},
}
if len(v.Version) > 0 {
config.Version = &http.Version{Value: v.Version}
}
if len(v.Status) > 0 || len(v.Reason) > 0 {
config.Status = &http.Status{
Code: "200",
Reason: "OK",
}
if len(v.Status) > 0 {
config.Status.Code = v.Status
}
if len(v.Reason) > 0 {
config.Status.Reason = v.Reason
}
}
if len(v.Headers) > 0 {
config.Header = make([]*http.Header, 0, len(v.Headers))
headerNames := sortMapKeys(v.Headers)
for _, key := range headerNames {
value := v.Headers[key]
if value == nil {
return nil, errors.New("empty HTTP header value: " + key).AtError()
}
config.Header = append(config.Header, &http.Header{
Name: key,
Value: append([]string(nil), (*value)...),
})
}
}
return config, nil
}
type Authenticator struct {
Request AuthenticatorRequest `json:"request"`
Response AuthenticatorResponse `json:"response"`
}
func (v *Authenticator) Build() (proto.Message, error) {
config := new(http.Config)
requestConfig, err := v.Request.Build()
if err != nil {
return nil, err
}
config.Request = requestConfig
responseConfig, err := v.Response.Build()
if err != nil {
return nil, err
}
config.Response = responseConfig
return config, nil
}
var tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{
"none": func() interface{} { return new(NoOpConnectionAuthenticator) },
"http": func() interface{} { return new(Authenticator) },
}, "type", "")
type TCPConfig struct {
HeaderConfig json.RawMessage `json:"header"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
}
// Build implements Buildable.
func (c *TCPConfig) Build() (proto.Message, error) {
config := new(tcp.Config)
if len(c.HeaderConfig) > 0 {
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
if err != nil {
return nil, errors.New("invalid TCP header config").Base(err).AtError()
}
ts, err := headerConfig.(Buildable).Build()
if err != nil {
return nil, errors.New("invalid TCP header config").Base(err).AtError()
}
config.HeaderSettings = serial.ToTypedMessage(ts)
}
if c.AcceptProxyProtocol {
config.AcceptProxyProtocol = c.AcceptProxyProtocol
}
return config, nil
}
type SplitHTTPConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Mode string `json:"mode"`
Headers map[string]string `json:"headers"`
XPaddingBytes Int32Range `json:"xPaddingBytes"`
XPaddingObfsMode bool `json:"xPaddingObfsMode"`
XPaddingKey string `json:"xPaddingKey"`
XPaddingHeader string `json:"xPaddingHeader"`
XPaddingPlacement string `json:"xPaddingPlacement"`
XPaddingMethod string `json:"xPaddingMethod"`
UplinkHTTPMethod string `json:"uplinkHTTPMethod"`
SessionIDPlacement string `json:"sessionIDPlacement"`
SessionIDKey string `json:"sessionIDKey"`
SessionIDTable string `json:"sessionIDTable"`
SessionIDLength Int32Range `json:"sessionIDLength"`
SeqPlacement string `json:"seqPlacement"`
SeqKey string `json:"seqKey"`
UplinkDataPlacement string `json:"uplinkDataPlacement"`
UplinkDataKey string `json:"uplinkDataKey"`
UplinkChunkSize Int32Range `json:"uplinkChunkSize"`
NoGRPCHeader bool `json:"noGRPCHeader"`
NoSSEHeader bool `json:"noSSEHeader"`
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"`
Xmux XmuxConfig `json:"xmux"`
DownloadSettings *StreamConfig `json:"downloadSettings"`
Extra json.RawMessage `json:"extra"`
}
type XmuxConfig struct {
MaxConcurrency Int32Range `json:"maxConcurrency"`
MaxConnections Int32Range `json:"maxConnections"`
CMaxReuseTimes Int32Range `json:"cMaxReuseTimes"`
HMaxRequestTimes Int32Range `json:"hMaxRequestTimes"`
HMaxReusableSecs Int32Range `json:"hMaxReusableSecs"`
HKeepAlivePeriod int64 `json:"hKeepAlivePeriod"`
}
func newRangeConfig(input Int32Range) *splithttp.RangeConfig {
return &splithttp.RangeConfig{
From: input.From,
To: input.To,
}
}
// Build implements Buildable.
func (c *SplitHTTPConfig) Build() (proto.Message, error) {
if c.Extra != nil {
var extra SplitHTTPConfig
if err := json.Unmarshal(c.Extra, &extra); err != nil {
return nil, errors.New(`Failed to unmarshal "extra".`).Base(err)
}
extra.Host = c.Host
extra.Path = c.Path
extra.Mode = c.Mode
c = &extra
}
switch c.Mode {
case "":
c.Mode = "auto"
case "auto", "packet-up", "stream-up", "stream-one":
default:
return nil, errors.New("unsupported mode: " + c.Mode)
}
// Priority (client): host > serverName > address
for k := range c.Headers {
if strings.ToLower(k) == "host" {
return nil, errors.New(`"headers" can't contain "host"`)
}
}
if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) {
return nil, errors.New("xPaddingBytes cannot be disabled")
}
if c.XPaddingKey == "" {
c.XPaddingKey = "x_padding"
}
if c.XPaddingHeader == "" {
c.XPaddingHeader = "X-Padding"
}
switch c.XPaddingPlacement {
case "":
c.XPaddingPlacement = "queryInHeader"
case "cookie", "header", "query", "queryInHeader":
default:
return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement)
}
switch c.XPaddingMethod {
case "":
c.XPaddingMethod = "repeat-x"
case "repeat-x", "tokenish":
default:
return nil, errors.New("unsupported padding method: " + c.XPaddingMethod)
}
switch c.UplinkDataPlacement {
case "":
c.UplinkDataPlacement = splithttp.PlacementAuto
case splithttp.PlacementAuto, splithttp.PlacementBody:
case splithttp.PlacementCookie, splithttp.PlacementHeader:
if c.Mode != "packet-up" {
return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode")
}
default:
return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement)
}
if c.UplinkHTTPMethod == "" {
c.UplinkHTTPMethod = "POST"
}
c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod)
if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" {
return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode")
}
switch c.SessionIDPlacement {
case "":
c.SessionIDPlacement = "path"
case "path", "cookie", "header", "query":
default:
return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement)
}
switch c.SeqPlacement {
case "":
c.SeqPlacement = "path"
case "path", "cookie", "header", "query":
default:
return nil, errors.New("unsupported seq placement: " + c.SeqPlacement)
}
if c.SessionIDPlacement != "path" && c.SessionIDKey == "" {
switch c.SessionIDPlacement {
case "cookie", "query":
c.SessionIDKey = "x_session"
case "header":
c.SessionIDKey = "X-Session"
}
}
if c.SessionIDTable != "" {
if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok {
c.SessionIDTable = predefined
}
room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To)
// 2.1B possiblities should be enough
if room.Cmp(big.NewInt(2<<30)) < 0 {
return nil, errors.New("sessionIDTable or sessionIDLength is too small")
}
if c.SessionIDLength.From <= 0 {
return nil, errors.New("sessionIDLength.from must be greater than 0")
}
for i := 0; i < len(c.SessionIDTable); i++ {
if c.SessionIDTable[i] >= 0x80 {
return nil, errors.New("sessionIDTable must contain only ASCII characters")
}
}
}
if c.SeqPlacement != "path" && c.SeqKey == "" {
switch c.SeqPlacement {
case "cookie", "query":
c.SeqKey = "x_seq"
case "header":
c.SeqKey = "X-Seq"
}
}
if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" {
switch c.UplinkDataPlacement {
case splithttp.PlacementCookie:
c.UplinkDataKey = "x_data"
case splithttp.PlacementAuto, splithttp.PlacementHeader:
c.UplinkDataKey = "X-Data"
}
}
if c.ServerMaxHeaderBytes < 0 {
return nil, errors.New("invalid negative value of maxHeaderBytes")
}
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 3
c.Xmux.MaxConnections.To = 3
c.Xmux.HMaxRequestTimes.From = 600
c.Xmux.HMaxRequestTimes.To = 900
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
}
config := &splithttp.Config{
Host: c.Host,
Path: c.Path,
Mode: c.Mode,
Headers: c.Headers,
XPaddingBytes: newRangeConfig(c.XPaddingBytes),
XPaddingObfsMode: c.XPaddingObfsMode,
XPaddingKey: c.XPaddingKey,
XPaddingHeader: c.XPaddingHeader,
XPaddingPlacement: c.XPaddingPlacement,
XPaddingMethod: c.XPaddingMethod,
UplinkHTTPMethod: c.UplinkHTTPMethod,
SessionIDPlacement: c.SessionIDPlacement,
SeqPlacement: c.SeqPlacement,
SessionIDKey: c.SessionIDKey,
SeqKey: c.SeqKey,
UplinkDataPlacement: c.UplinkDataPlacement,
UplinkDataKey: c.UplinkDataKey,
UplinkChunkSize: newRangeConfig(c.UplinkChunkSize),
NoGRPCHeader: c.NoGRPCHeader,
NoSSEHeader: c.NoSSEHeader,
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
ScMaxBufferedPosts: c.ScMaxBufferedPosts,
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
ServerMaxHeaderBytes: c.ServerMaxHeaderBytes,
SessionIDTable: c.SessionIDTable,
SessionIDLength: newRangeConfig(c.SessionIDLength),
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
},
}
if c.DownloadSettings != nil {
if c.Mode == "stream-one" {
return nil, errors.New(`Can not use "downloadSettings" in "stream-one" mode.`)
}
var err error
if config.DownloadSettings, err = c.DownloadSettings.Build(); err != nil {
return nil, errors.New(`Failed to build "downloadSettings".`).Base(err)
}
}
return config, nil
}
func roomSize(tableSize int, min, max int32) *big.Int {
base := big.NewInt(int64(tableSize))
sum := new(big.Int)
term := new(big.Int)
for k := min; k <= max; k++ {
term.Exp(base, big.NewInt(int64(k)), nil)
sum.Add(sum, term)
}
return sum
}
type KCPConfig struct {
Mtu *uint32 `json:"mtu"`
Tti *uint32 `json:"tti"`
UpCap *uint32 `json:"uplinkCapacity"`
DownCap *uint32 `json:"downlinkCapacity"`
CwndMultiplier *uint32 `json:"cwndMultiplier"`
MaxSendingWindow *uint32 `json:"maxSendingWindow"`
HeaderConfig json.RawMessage `json:"header"`
Seed *string `json:"seed"`
}
// Build implements Buildable.
func (c *KCPConfig) Build() (proto.Message, error) {
if c.HeaderConfig != nil || c.Seed != nil {
return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm")
}
config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config)
if c.Mtu != nil {
config.Mtu = *c.Mtu
}
if c.Tti != nil {
config.Tti = *c.Tti
}
if c.UpCap != nil {
config.UplinkCapacity = *c.UpCap
}
if c.DownCap != nil {
config.DownlinkCapacity = *c.DownCap
}
if c.CwndMultiplier != nil {
config.CwndMultiplier = *c.CwndMultiplier
}
if c.MaxSendingWindow != nil {
config.MaxSendingWindow = *c.MaxSendingWindow
}
if config.Mtu < 21 {
return nil, errors.New("Mtu must be at least 21").AtError()
}
if config.Tti < 10 || config.Tti > 1000 {
return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
}
if config.CwndMultiplier < 1 {
return nil, errors.New("CwndMultiplier must be at least 1").AtError()
}
if config.GetSendingBufferSize() == 0 {
return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
}
return config, nil
}
type GRPCConfig struct {
Authority string `json:"authority"`
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
IdleTimeout int32 `json:"idle_timeout"`
HealthCheckTimeout int32 `json:"health_check_timeout"`
PermitWithoutStream bool `json:"permit_without_stream"`
InitialWindowsSize int32 `json:"initial_windows_size"`
UserAgent string `json:"user_agent"`
}
func (g *GRPCConfig) Build() (proto.Message, error) {
if g.IdleTimeout <= 0 {
g.IdleTimeout = 0
}
if g.HealthCheckTimeout <= 0 {
g.HealthCheckTimeout = 0
}
if g.InitialWindowsSize < 0 {
// default window size of gRPC-go
g.InitialWindowsSize = 0
}
return &grpc.Config{
Authority: g.Authority,
ServiceName: g.ServiceName,
MultiMode: g.MultiMode,
IdleTimeout: g.IdleTimeout,
HealthCheckTimeout: g.HealthCheckTimeout,
PermitWithoutStream: g.PermitWithoutStream,
InitialWindowsSize: g.InitialWindowsSize,
UserAgent: g.UserAgent,
}, nil
}
type WebSocketConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
HeartbeatPeriod uint32 `json:"heartbeatPeriod"`
}
// Build implements Buildable.
func (c *WebSocketConfig) Build() (proto.Message, error) {
path := c.Path
var ed uint32
if u, err := url.Parse(path); err == nil {
if q := u.Query(); q.Get("ed") != "" {
Ed, _ := strconv.Atoi(q.Get("ed"))
ed = uint32(Ed)
q.Del("ed")
u.RawQuery = q.Encode()
path = u.String()
}
}
// Priority (client): host > serverName > address
for k, v := range c.Headers {
if strings.ToLower(k) == "host" {
errors.PrintDeprecatedFeatureWarning(`"host" in "headers"`, `independent "host"`)
if c.Host == "" {
c.Host = v
}
delete(c.Headers, k)
}
}
config := &websocket.Config{
Path: path,
Host: c.Host,
Header: c.Headers,
AcceptProxyProtocol: c.AcceptProxyProtocol,
Ed: ed,
HeartbeatPeriod: c.HeartbeatPeriod,
}
return config, nil
}
type HttpUpgradeConfig struct {
Host string `json:"host"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
}
// Build implements Buildable.
func (c *HttpUpgradeConfig) Build() (proto.Message, error) {
path := c.Path
var ed uint32
if u, err := url.Parse(path); err == nil {
if q := u.Query(); q.Get("ed") != "" {
Ed, _ := strconv.Atoi(q.Get("ed"))
ed = uint32(Ed)
q.Del("ed")
u.RawQuery = q.Encode()
path = u.String()
}
}
// Priority (client): host > serverName > address
for k := range c.Headers {
if strings.ToLower(k) == "host" {
return nil, errors.New(`"headers" can't contain "host"`)
}
}
config := &httpupgrade.Config{
Path: path,
Host: c.Host,
Header: c.Headers,
AcceptProxyProtocol: c.AcceptProxyProtocol,
Ed: ed,
}
return config, nil
}
const (
Byte = 1
Kilobyte = 1024 * Byte
Megabyte = 1024 * Kilobyte
Gigabyte = 1024 * Megabyte
Terabyte = 1024 * Gigabyte
)
type Bandwidth string
func (b Bandwidth) Bps() (uint64, error) {
s := strings.TrimSpace(strings.ToLower(string(b)))
if s == "" {
return 0, nil
}
idx := len(s)
for i, c := range s {
if (c < '0' || c > '9') && c != '.' {
idx = i
break
}
}
numStr := s[:idx]
unit := strings.TrimSpace(s[idx:])
val, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, err
}
mul := uint64(1)
switch unit {
case "", "b", "bps":
mul = Byte
case "k", "kb", "kbps":
mul = Kilobyte
case "m", "mb", "mbps":
mul = Megabyte
case "g", "gb", "gbps":
mul = Gigabyte
case "t", "tb", "tbps":
mul = Terabyte
default:
return 0, errors.New("unsupported unit: " + unit)
}
return uint64(val*float64(mul)) / 8, nil
}
type UdpHop struct {
PortList PortList `json:"ports"`
Interval Int32Range `json:"interval"`
}
type Masquerade struct {
Type string `json:"type"`
Dir string `json:"dir"`
Url string `json:"url"`
RewriteHost bool `json:"rewriteHost"`
Insecure bool `json:"insecure"`
Content string `json:"content"`
Headers map[string]string `json:"headers"`
StatusCode int32 `json:"statusCode"`
}
type HysteriaConfig struct {
Version int32 `json:"version"`
Auth string `json:"auth"`
Congestion *string `json:"congestion"`
Up *Bandwidth `json:"up"`
Down *Bandwidth `json:"down"`
UdpHop *UdpHop `json:"udphop"`
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
Masquerade Masquerade `json:"masquerade"`
}
func (c *HysteriaConfig) Build() (proto.Message, error) {
if c.Version != 2 {
return nil, errors.New("version != 2")
}
if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil {
errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams")
}
if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) {
return nil, errors.New("UdpIdleTimeout must be between 2 and 600")
}
config := &hysteria.Config{}
config.Auth = c.Auth
config.UdpIdleTimeout = c.UdpIdleTimeout
config.MasqType = c.Masquerade.Type
config.MasqFile = c.Masquerade.Dir
config.MasqUrl = c.Masquerade.Url
config.MasqUrlRewriteHost = c.Masquerade.RewriteHost
config.MasqUrlInsecure = c.Masquerade.Insecure
config.MasqString = c.Masquerade.Content
config.MasqStringHeaders = c.Masquerade.Headers
config.MasqStringStatusCode = c.Masquerade.StatusCode
if config.UdpIdleTimeout == 0 {
config.UdpIdleTimeout = 60
}
return config, nil
}
func readFileOrString(f string, s []string) ([]byte, error) {
if len(f) > 0 {
return filesystem.ReadCert(f)
}
if len(s) > 0 {
return []byte(strings.Join(s, "\n")), nil
}
return nil, errors.New("both file and bytes are empty.")
}
// StealthConfig configures the stealth transport.
type StealthConfig struct {
Key string `json:"key"`
PadMax int64 `json:"padMax"`
PadProb int64 `json:"padProb"`
PaceMs int64 `json:"paceMs"`
PaceJitterMs int64 `json:"paceJitterMs"`
Path string `json:"path"`
Paths []string `json:"paths"`
Ports []uint32 `json:"ports"`
Bps int64 `json:"bps"`
Target string `json:"target"`
Decoy bool `json:"decoy"`
DecoyEveryS int64 `json:"decoyEverySecs"`
DecoyTarget string `json:"decoyTarget"`
PadProfile string `json:"padProfile"`
Retries int64 `json:"retries"`
}
// Build implements Buildable.
func (c *StealthConfig) Build() (proto.Message, error) {
return &stealth.Config{
Key: c.Key,
PadMax: c.PadMax,
PadProb: c.PadProb,
PaceMs: c.PaceMs,
PaceJitterMs: c.PaceJitterMs,
Path: c.Path,
Paths: c.Paths,
Ports: c.Ports,
Bps: c.Bps,
Target: c.Target,
Decoy: c.Decoy,
DecoyTarget: c.DecoyTarget,
DecoyEverySecs: c.DecoyEveryS,
PadProfile: c.PadProfile,
Retries: c.Retries,
}, nil
}
@@ -0,0 +1,175 @@
package stealth
import (
"net/url"
"strings"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/transport/internet"
)
const protocolName = "stealth"
const authOK = "ok"
const chromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
func init() {
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
return new(Config)
}))
}
func (c *Config) GetNormalizedPaths() []string {
if len(c.Paths) > 0 {
out := make([]string, 0, len(c.Paths))
for _, p := range c.Paths {
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
out = append(out, p)
}
return out
}
return []string{c.GetNormalizedPath()}
}
func (c *Config) NormalizedPadProb() int {
if c.PadProb <= 0 {
return 50
}
if c.PadProb > 100 {
return 100
}
return int(c.PadProb)
}
func (c *Config) GetNormalizedPorts() []uint32 {
if len(c.Ports) == 0 {
return nil
}
return c.Ports
}
func (c *Config) NormalizedBps() int {
if c.Bps <= 0 {
return 0
}
if c.Bps > 1<<30 {
return 1 << 30
}
return int(c.Bps)
}
func (c *Config) DecoyEnabled() bool {
return c.Decoy
}
func (c *Config) DecoyEvery() time.Duration {
n := c.DecoyEverySecs
if n <= 0 {
n = 60
}
if n > 86400 {
n = 86400
}
return time.Duration(n) * time.Second
}
func (c *Config) NormalizedDecoyTarget() string {
t := strings.TrimSpace(c.DecoyTarget)
if t == "" {
return "https://www.cloudflare.com/cdn-cgi/trace"
}
if !strings.Contains(t, "://") {
t = "https://" + t
}
return t
}
func (c *Config) GetNormalizedTarget() *url.URL {
t := strings.TrimSpace(c.Target)
if t == "" {
return nil
}
if !strings.Contains(t, "://") {
t = "https://" + t
}
u, err := url.Parse(t)
if err != nil || u.Host == "" {
return nil
}
return u
}
func (c *Config) GetNormalizedPath() string {
path := c.Path
if path == "" {
return "/"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path
}
func (c *Config) GetNormalizedRetries() int {
n := c.Retries
if n == 0 {
return 3
}
if n < 0 {
return 0
}
if n > 10 {
return 10
}
return int(n)
}
func (c *Config) NormalizedPadProfile() string {
switch c.PadProfile {
case "uniform":
return "uniform"
default:
return "browser"
}
}
func (c *Config) NormalizedPadMax() int {
if c.PadMax <= 0 {
return 0
}
if c.PadMax > 4096 {
return 4096
}
return int(c.PadMax)
}
func (c *Config) NormalizedPaceJitterMs() time.Duration {
if c.PaceJitterMs <= 0 {
return 0
}
if c.PaceJitterMs > 5000 {
return 5000
}
return time.Duration(c.PaceJitterMs) * time.Millisecond
}
func (c *Config) NormalizedPaceMs() time.Duration {
if c.PaceMs <= 0 {
return 0
}
if c.PaceMs > 5000 {
return 5000
}
return time.Duration(c.PaceMs) * time.Millisecond
}
@@ -0,0 +1,268 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.12
// protoc v3.21.12
// source: transport/internet/stealth/config.proto
package stealth
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Shared auth secret (both sides must match).
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Max random padding bytes appended after each data frame (0 = disabled).
PadMax int64 `protobuf:"varint,2,opt,name=pad_max,json=padMax,proto3" json:"pad_max,omitempty"`
// Minimum interval between frame writes in ms (0 = disabled).
PaceMs int64 `protobuf:"varint,3,opt,name=pace_ms,json=paceMs,proto3" json:"pace_ms,omitempty"`
// HTTP/2 request path (default "/").
Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"`
// Max random extra delay added to pacing (ms, 0 = disabled).
PaceJitterMs int64 `protobuf:"varint,5,opt,name=pace_jitter_ms,json=paceJitterMs,proto3" json:"pace_jitter_ms,omitempty"`
// Multiple tunnel paths for identity rotation; falls back to [path].
Paths []string `protobuf:"bytes,6,rep,name=paths,proto3" json:"paths,omitempty"`
// Percent chance (0-100) to append a PAD frame after a data frame (default 50).
PadProb int64 `protobuf:"varint,7,opt,name=pad_prob,json=padProb,proto3" json:"pad_prob,omitempty"`
// Candidate ports for identity rotation (client picks one per connection).
// Overrides the outbound port; the server must listen on these ports.
Ports []uint32 `protobuf:"varint,8,rep,packed,name=ports,proto3" json:"ports,omitempty"`
// Optional upload rate limit in bytes/sec (0 = unlimited).
Bps int64 `protobuf:"varint,9,opt,name=bps,proto3" json:"bps,omitempty"`
// Reverse-proxy target for non-tunnel requests (a real website to mirror).
// Empty = serve the built-in landing page only.
Target string `protobuf:"bytes,10,opt,name=target,proto3" json:"target,omitempty"`
// Client-side decoy: periodically make a real request to a benign CDN
// (direct, not through the tunnel) to mask aggregated activity. Default off.
Decoy bool `protobuf:"varint,11,opt,name=decoy,proto3" json:"decoy,omitempty"`
// Decoy interval in seconds (default 60).
DecoyEverySecs int64 `protobuf:"varint,12,opt,name=decoy_every_secs,json=decoyEverySecs,proto3" json:"decoy_every_secs,omitempty"`
// Decoy target URL (default Cloudflare trace endpoint).
DecoyTarget string `protobuf:"bytes,13,opt,name=decoy_target,json=decoyTarget,proto3" json:"decoy_target,omitempty"`
// Padding profile: "browser" (default, biased toward small pads) or "uniform".
PadProfile string `protobuf:"bytes,14,opt,name=pad_profile,json=padProfile,proto3" json:"pad_profile,omitempty"`
// Dial retries with a fresh identity (new path/port) on failure (default 3).
Retries int64 `protobuf:"varint,15,opt,name=retries,proto3" json:"retries,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_transport_internet_stealth_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_stealth_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_stealth_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Config) GetPadMax() int64 {
if x != nil {
return x.PadMax
}
return 0
}
func (x *Config) GetPaceMs() int64 {
if x != nil {
return x.PaceMs
}
return 0
}
func (x *Config) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *Config) GetPaceJitterMs() int64 {
if x != nil {
return x.PaceJitterMs
}
return 0
}
func (x *Config) GetPaths() []string {
if x != nil {
return x.Paths
}
return nil
}
func (x *Config) GetPadProb() int64 {
if x != nil {
return x.PadProb
}
return 0
}
func (x *Config) GetPorts() []uint32 {
if x != nil {
return x.Ports
}
return nil
}
func (x *Config) GetBps() int64 {
if x != nil {
return x.Bps
}
return 0
}
func (x *Config) GetTarget() string {
if x != nil {
return x.Target
}
return ""
}
func (x *Config) GetDecoy() bool {
if x != nil {
return x.Decoy
}
return false
}
func (x *Config) GetDecoyEverySecs() int64 {
if x != nil {
return x.DecoyEverySecs
}
return 0
}
func (x *Config) GetDecoyTarget() string {
if x != nil {
return x.DecoyTarget
}
return ""
}
func (x *Config) GetPadProfile() string {
if x != nil {
return x.PadProfile
}
return ""
}
func (x *Config) GetRetries() int64 {
if x != nil {
return x.Retries
}
return 0
}
var File_transport_internet_stealth_config_proto protoreflect.FileDescriptor
const file_transport_internet_stealth_config_proto_rawDesc = "" +
"\n" +
"'transport/internet/stealth/config.proto\x12\x1fxray.transport.internet.stealth\"\x95\x03\n" +
"\x06Config\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x17\n" +
"\apad_max\x18\x02 \x01(\x03R\x06padMax\x12\x17\n" +
"\apace_ms\x18\x03 \x01(\x03R\x06paceMs\x12\x12\n" +
"\x04path\x18\x04 \x01(\tR\x04path\x12$\n" +
"\x0epace_jitter_ms\x18\x05 \x01(\x03R\fpaceJitterMs\x12\x14\n" +
"\x05paths\x18\x06 \x03(\tR\x05paths\x12\x19\n" +
"\bpad_prob\x18\a \x01(\x03R\apadProb\x12\x14\n" +
"\x05ports\x18\b \x03(\rR\x05ports\x12\x10\n" +
"\x03bps\x18\t \x01(\x03R\x03bps\x12\x16\n" +
"\x06target\x18\n" +
" \x01(\tR\x06target\x12\x14\n" +
"\x05decoy\x18\v \x01(\bR\x05decoy\x12(\n" +
"\x10decoy_every_secs\x18\f \x01(\x03R\x0edecoyEverySecs\x12!\n" +
"\fdecoy_target\x18\r \x01(\tR\vdecoyTarget\x12\x1f\n" +
"\vpad_profile\x18\x0e \x01(\tR\n" +
"padProfile\x12\x18\n" +
"\aretries\x18\x0f \x01(\x03R\aretriesB6Z4github.com/xtls/xray-core/transport/internet/stealthb\x06proto3"
var (
file_transport_internet_stealth_config_proto_rawDescOnce sync.Once
file_transport_internet_stealth_config_proto_rawDescData []byte
)
func file_transport_internet_stealth_config_proto_rawDescGZIP() []byte {
file_transport_internet_stealth_config_proto_rawDescOnce.Do(func() {
file_transport_internet_stealth_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_stealth_config_proto_rawDesc), len(file_transport_internet_stealth_config_proto_rawDesc)))
})
return file_transport_internet_stealth_config_proto_rawDescData
}
var file_transport_internet_stealth_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_stealth_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.stealth.Config
}
var file_transport_internet_stealth_config_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_transport_internet_stealth_config_proto_init() }
func file_transport_internet_stealth_config_proto_init() {
if File_transport_internet_stealth_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_stealth_config_proto_rawDesc), len(file_transport_internet_stealth_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_transport_internet_stealth_config_proto_goTypes,
DependencyIndexes: file_transport_internet_stealth_config_proto_depIdxs,
MessageInfos: file_transport_internet_stealth_config_proto_msgTypes,
}.Build()
File_transport_internet_stealth_config_proto = out.File
file_transport_internet_stealth_config_proto_goTypes = nil
file_transport_internet_stealth_config_proto_depIdxs = nil
}
@@ -0,0 +1,40 @@
syntax = "proto3";
package xray.transport.internet.stealth;
option go_package = "github.com/xtls/xray-core/transport/internet/stealth";
message Config {
// Shared auth secret (both sides must match).
string key = 1;
// Max random padding bytes appended after each data frame (0 = disabled).
int64 pad_max = 2;
// Minimum interval between frame writes in ms (0 = disabled).
int64 pace_ms = 3;
// HTTP/2 request path (default "/").
string path = 4;
// Max random extra delay added to pacing (ms, 0 = disabled).
int64 pace_jitter_ms = 5;
// Multiple tunnel paths for identity rotation; falls back to [path].
repeated string paths = 6;
// Percent chance (0-100) to append a PAD frame after a data frame (default 50).
int64 pad_prob = 7;
// Candidate ports for identity rotation (client picks one per connection).
// Overrides the outbound port; the server must listen on these ports.
repeated uint32 ports = 8;
// Optional upload rate limit in bytes/sec (0 = unlimited).
int64 bps = 9;
// Reverse-proxy target for non-tunnel requests (a real website to mirror).
// Empty = serve the built-in landing page only.
string target = 10;
// Client-side decoy: periodically make a real request to a benign CDN
// (direct, not through the tunnel) to mask aggregated activity. Default off.
bool decoy = 11;
// Decoy interval in seconds (default 60).
int64 decoy_every_secs = 12;
// Decoy target URL (default Cloudflare trace endpoint).
string decoy_target = 13;
// Padding profile: "browser" (default, biased toward small pads) or "uniform".
string pad_profile = 14;
// Dial retries with a fresh identity (new path/port) on failure (default 3).
int64 retries = 15;
}
+160
View File
@@ -0,0 +1,160 @@
package stealth
import (
"context"
"crypto/rand"
"io"
mathrand "math/rand"
"net"
"sync"
"time"
"golang.org/x/time/rate"
)
type h2Conn struct {
reader io.Reader
writer io.Writer
cancel context.CancelFunc
flush func()
done chan struct{}
padMax int
padProb int
padUni bool
paceMs time.Duration
paceJit time.Duration
limiter *rate.Limiter
mu sync.Mutex
lastWrite time.Time
pending []byte
localAddr net.Addr
remote net.Addr
}
func newH2Conn(reader io.Reader, writer io.Writer, cancel context.CancelFunc, flush func(), cfg *Config, local, remote net.Addr) *h2Conn {
return &h2Conn{
reader: reader,
writer: writer,
cancel: cancel,
flush: flush,
done: make(chan struct{}),
padMax: cfg.NormalizedPadMax(),
padProb: cfg.NormalizedPadProb(),
padUni: cfg.NormalizedPadProfile() == "uniform",
paceMs: cfg.NormalizedPaceMs(),
paceJit: cfg.NormalizedPaceJitterMs(),
limiter: newLimiter(cfg),
localAddr: local,
remote: remote,
}
}
func newLimiter(cfg *Config) *rate.Limiter {
if bps := cfg.NormalizedBps(); bps > 0 {
return rate.NewLimiter(rate.Limit(bps), bps)
}
return nil
}
// padSize returns a browser-like (mostly small, occasional large) or uniform size.
func (c *h2Conn) padSize() int {
if c.padMax <= 0 {
return 0
}
if c.padUni {
return mathrand.Intn(c.padMax + 1)
}
if mathrand.Intn(100) < 80 {
return mathrand.Intn(c.padMax/8 + 1)
}
return mathrand.Intn(c.padMax + 1)
}
func (c *h2Conn) Read(p []byte) (int, error) {
for {
if len(c.pending) > 0 {
n := copy(p, c.pending)
c.pending = c.pending[n:]
return n, nil
}
ftype, payload, err := readFrame(c.reader)
if err != nil {
return 0, err
}
switch ftype {
case frameTypeData:
n := copy(p, payload)
if n < len(payload) {
c.pending = append(c.pending[:0], payload[n:]...)
}
return n, nil
case frameTypeClose:
return 0, io.EOF
default:
// padding and unknown frames are skipped
}
}
}
func (c *h2Conn) Write(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
select {
case <-c.done:
return 0, net.ErrClosed
default:
}
if c.limiter != nil {
if err := c.limiter.WaitN(context.Background(), len(p)); err != nil {
return 0, err
}
}
if c.paceMs > 0 {
d := c.lastWrite.Add(c.paceMs).Sub(time.Now())
if c.paceJit > 0 {
d += time.Duration(mathrand.Int63n(int64(c.paceJit)))
}
if d > 0 {
time.Sleep(d)
}
}
if err := writeFrame(c.writer, frameTypeData, p); err != nil {
return 0, err
}
if c.padMax > 0 && mathrand.Intn(100) < c.padProb {
pad := make([]byte, c.padSize())
_, _ = rand.Read(pad)
if err := writeFrame(c.writer, frameTypePad, pad); err != nil {
return 0, err
}
}
if c.flush != nil {
c.flush()
}
c.lastWrite = time.Now()
return len(p), nil
}
func (c *h2Conn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
select {
case <-c.done:
return nil
default:
close(c.done)
}
if c.cancel != nil {
c.cancel()
}
return nil
}
// Done returns a channel closed when the connection is closed.
func (c *h2Conn) Done() <-chan struct{} { return c.done }
func (c *h2Conn) LocalAddr() net.Addr { return c.localAddr }
func (c *h2Conn) RemoteAddr() net.Addr { return c.remote }
func (c *h2Conn) SetDeadline(t time.Time) error { return nil }
func (c *h2Conn) SetReadDeadline(t time.Time) error { return nil }
func (c *h2Conn) SetWriteDeadline(t time.Time) error { return nil }
@@ -0,0 +1,205 @@
package stealth
import (
"context"
gotls "crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
mathrand "math/rand"
"sync"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/net/http2"
)
var (
clientCache = make(map[string]*http.Client)
clientCacheMu sync.Mutex
clientCacheAt = make(map[string]time.Time)
decoyOnce sync.Once
)
func dialTLS(ctx context.Context, dest xnet.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
if err != nil {
return nil, err
}
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
return reality.UClient(conn, config, ctx, dest)
}
if config := tls.ConfigFromStreamSettings(streamSettings); config != nil {
tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
if fingerprint := tls.GetFingerprint(config.Fingerprint); fingerprint != nil {
uconn := tls.UClient(conn, tlsConfig, fingerprint)
if err := uconn.(*tls.UConn).HandshakeContext(ctx); err != nil {
conn.Close()
return nil, errors.New("stealth: TLS handshake failed").Base(err)
}
conn = uconn
} else {
tconn := tls.Client(conn, tlsConfig)
if err := tconn.(*tls.Conn).HandshakeContext(ctx); err != nil {
conn.Close()
return nil, errors.New("stealth: TLS handshake failed").Base(err)
}
conn = tconn
}
}
return conn, nil
}
func getHTTPClient(dest xnet.Destination, streamSettings *internet.MemoryStreamConfig) *http.Client {
cfg := streamSettings.ProtocolSettings.(*Config)
fingerprint := ""
if tc := tls.ConfigFromStreamSettings(streamSettings); tc != nil {
fingerprint = tc.Fingerprint
}
key := dest.NetAddr() + "|" + cfg.Key + "|" + cfg.GetNormalizedPath() + "|" + fingerprint
clientCacheMu.Lock()
defer clientCacheMu.Unlock()
if c, ok := clientCache[key]; ok {
return c
}
transport := &http2.Transport{
DialTLSContext: func(ctxInner context.Context, network string, addr string, tlsCfg *gotls.Config) (net.Conn, error) {
return dialTLS(ctxInner, dest, streamSettings)
},
IdleConnTimeout: xnet.ConnIdleTimeout,
}
client := &http.Client{Transport: transport}
clientCache[key] = client
clientCacheAt[key] = time.Now()
return client
}
func openStream(ctx context.Context, dest xnet.Destination, streamSettings *internet.MemoryStreamConfig, client *http.Client) (stat.Connection, error) {
cfg := streamSettings.ProtocolSettings.(*Config)
paths := cfg.GetNormalizedPaths()
path := paths[mathrand.Intn(len(paths))]
streamCtx, cancel := context.WithCancel(ctx)
pr, pw := io.Pipe()
var localAddr, remoteAddr net.Addr
streamCtx = httptrace.WithClientTrace(streamCtx, &httptrace.ClientTrace{
GotConn: func(ci httptrace.GotConnInfo) {
if ci.Conn != nil {
localAddr = ci.Conn.LocalAddr()
remoteAddr = ci.Conn.RemoteAddr()
}
},
})
target := "https://" + dest.NetAddr() + path
req, err := http.NewRequestWithContext(streamCtx, http.MethodPost, target, pr)
if err != nil {
cancel()
pw.Close()
return nil, err
}
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("User-Agent", chromeUA)
req.ContentLength = -1
// the auth frame must reach the server before it replies 200
go func() {
_ = writeFrame(pw, frameTypeAuth, []byte(cfg.Key))
}()
resp, err := client.Do(req)
if err != nil {
cancel()
pw.Close()
return nil, errors.New("stealth: request failed").Base(err)
}
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
pw.Close()
cancel()
return nil, fmt.Errorf("stealth: auth rejected (status %d)", resp.StatusCode)
}
if localAddr == nil {
localAddr = &net.TCPAddr{IP: net.IPv4zero}
}
if remoteAddr == nil {
remoteAddr = localAddr
}
cancelStream := func() {
pw.Close()
cancel()
}
c := newH2Conn(resp.Body, pw, cancelStream, nil, cfg, localAddr, remoteAddr)
return stat.Connection(c), nil
}
func startDecoy(cfg *Config) {
decoyOnce.Do(func() {
if !cfg.DecoyEnabled() {
return
}
interval := cfg.DecoyEvery()
target := cfg.NormalizedDecoyTarget()
client := &http.Client{Timeout: 15 * time.Second}
go func() {
for {
time.Sleep(interval)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err == nil {
req.Header.Set("User-Agent", chromeUA)
resp, err := client.Do(req)
if err == nil {
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
cancel()
}
}()
})
}
func Dial(ctx context.Context, dest xnet.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
cfg, _ := streamSettings.ProtocolSettings.(*Config)
retries := 3
if cfg != nil {
retries = cfg.GetNormalizedRetries()
startDecoy(cfg)
}
var lastErr error
for attempt := 0; attempt <= retries; attempt++ {
effDest := dest
if cfg != nil {
if ports := cfg.GetNormalizedPorts(); len(ports) > 0 {
effDest.Port = xnet.Port(ports[mathrand.Intn(len(ports))])
}
}
errors.LogInfo(ctx, "stealth dialing to ", effDest, " (attempt ", attempt+1, ")")
client := getHTTPClient(effDest, streamSettings)
conn, err := openStream(ctx, effDest, streamSettings, client)
if err == nil {
return conn, nil
}
lastErr = err
errors.LogInfo(ctx, "stealth attempt failed, retrying with a new identity: ", err)
}
return nil, lastErr
}
func init() {
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}
@@ -0,0 +1,59 @@
package stealth
import (
"encoding/binary"
"errors"
"io"
)
const (
frameTypeData byte = 0x01
frameTypePad byte = 0x02
frameTypeAuth byte = 0xFE
frameTypeClose byte = 0xFF
)
const maxFrameSize = 64 * 1024
func writeFrame(w io.Writer, ftype byte, payload []byte) error {
n := 1 + len(payload)
if n > maxFrameSize {
return errors.New("stealth: frame too large")
}
var hdr [4]byte
binary.BigEndian.PutUint32(hdr[:], uint32(n))
if _, err := w.Write(hdr[:]); err != nil {
return err
}
if _, err := w.Write([]byte{ftype}); err != nil {
return err
}
if len(payload) > 0 {
if _, err := w.Write(payload); err != nil {
return err
}
}
return nil
}
func readFrame(r io.Reader) (byte, []byte, error) {
var hdr [4]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return 0, nil, err
}
n := binary.BigEndian.Uint32(hdr[:])
if n == 0 || n > maxFrameSize {
return 0, nil, errors.New("stealth: bad frame length")
}
var ftype [1]byte
if _, err := io.ReadFull(r, ftype[:]); err != nil {
return 0, nil, err
}
payload := make([]byte, n-1)
if len(payload) > 0 {
if _, err := io.ReadFull(r, payload); err != nil {
return 0, nil, err
}
}
return ftype[0], payload, nil
}
+317
View File
@@ -0,0 +1,317 @@
package stealth
import (
"bytes"
"context"
gotls "crypto/tls"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strconv"
"strings"
"time"
goreality "github.com/xtls/reality"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/net/http2"
)
const landingPage = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>dedikar.online</title>
<style>
body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #f6f7f9; color: #222; }
.wrap { max-width: 720px; margin: 0 auto; padding: 64px 24px; }
h1 { font-size: 28px; margin-bottom: 8px; }
p { line-height: 1.6; color: #444; }
.card { background: #fff; border: 1px solid #e3e5e8; border-radius: 10px; padding: 24px; margin-top: 24px; }
.muted { color: #888; font-size: 13px; }
</style>
</head>
<body>
<div class="wrap">
<h1>dedikar.online</h1>
<p>Personal infrastructure and utility services.</p>
<div class="card">
<p>Status: operational.</p>
<p class="muted">This is a private host. If you reached this page by accident, nothing to see here.</p>
</div>
</div>
</body>
</html>
`
type mirrorHostKey struct{}
var urlAttrRe = regexp.MustCompile(`(?i)(href|src|action|poster|data-src|data-lazy-src|srcset|content)\s*=\s*("([^"]*)"|'([^']*)')`)
func isCSSResponse(resp *http.Response) bool {
return strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/css")
}
func isHTMLResponse(resp *http.Response) bool {
return strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html")
}
func rewriteURLRef(v, targetHost, ourHost string) string {
v = strings.ReplaceAll(v, "https://"+targetHost, "https://"+ourHost)
v = strings.ReplaceAll(v, "http://"+targetHost, "https://"+ourHost)
v = strings.ReplaceAll(v, "//"+targetHost, "//"+ourHost)
return v
}
func rewriteHTMLRefs(body []byte, targetHost, ourHost string) []byte {
if targetHost == "" || ourHost == "" || strings.EqualFold(targetHost, ourHost) {
return body
}
return urlAttrRe.ReplaceAllFunc(body, func(m []byte) []byte {
sm := urlAttrRe.FindSubmatch(m)
var val []byte
if len(sm) > 3 && sm[3] != nil {
val = sm[3]
} else if len(sm) > 4 && sm[4] != nil {
val = sm[4]
}
if val == nil {
return m
}
nv := []byte(rewriteURLRef(string(val), targetHost, ourHost))
return bytes.Replace(m, val, nv, 1)
})
}
var cssURLRe = regexp.MustCompile(`(?i)url\s*\(\s*(?:'([^']*)'|"([^"]*)"|([^'")\s]+))\s*\)`)
func rewriteCSSRefs(body []byte, targetHost, ourHost string) []byte {
if targetHost == "" || ourHost == "" || strings.EqualFold(targetHost, ourHost) {
return body
}
return cssURLRe.ReplaceAllFunc(body, func(m []byte) []byte {
sm := cssURLRe.FindSubmatch(m)
var val []byte
if len(sm) > 1 && sm[1] != nil {
val = sm[1]
} else if len(sm) > 2 && sm[2] != nil {
val = sm[2]
} else if len(sm) > 3 && sm[3] != nil {
val = sm[3]
}
if val == nil {
return m
}
nv := []byte(rewriteURLRef(string(val), targetHost, ourHost))
return bytes.Replace(m, val, nv, 1)
})
}
func rewriteBodyRefs(body []byte, targetHost, ourHost string) []byte {
return rewriteCSSRefs(rewriteHTMLRefs(body, targetHost, ourHost), targetHost, ourHost)
}
type Listener struct {
server *http.Server
listener net.Listener
config *Config
paths []string
addConn internet.ConnHandler
proxy *httputil.ReverseProxy
target *url.URL
}
func ListenStealth(ctx context.Context, address xnet.Address, port xnet.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
errors.LogInfo(ctx, "stealth listening on ", address, ":", port)
cfg, ok := streamSettings.ProtocolSettings.(*Config)
if !ok {
return nil, errors.New("stealth: invalid stream settings")
}
listener, err := internet.ListenSystem(ctx, &net.TCPAddr{IP: address.IP(), Port: int(port)}, streamSettings.SocketSettings)
if err != nil {
return nil, err
}
l := &Listener{
listener: listener,
config: cfg,
paths: cfg.GetNormalizedPaths(),
addConn: handler,
}
if target := cfg.GetNormalizedTarget(); target != nil {
// custom director: NewSingleHostReverseProxy in go>=1.26 does not
// rewrite req.Host, which makes real sites reject the request
rp := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.Host = target.Host
req.Header.Set("Accept-Encoding", "identity")
},
ModifyResponse: func(resp *http.Response) error {
host, _ := resp.Request.Context().Value(mirrorHostKey{}).(string)
if host == "" {
return nil
}
loc := resp.Header.Get("Location")
if loc != "" {
u, err := url.Parse(loc)
if err == nil && u.Host != "" {
// keep redirects inside our domain so the mirror feels seamless
if strings.EqualFold(u.Host, target.Host) {
u.Scheme = "https"
u.Host = host
resp.Header.Set("Location", u.String())
}
}
}
// rewrite absolute links in HTML/CSS so browsing stays on our domain
if isHTMLResponse(resp) || isCSSResponse(resp) {
if body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20)); err == nil && len(body) < 8<<20 {
var nb []byte
if isCSSResponse(resp) {
nb = rewriteCSSRefs(body, target.Host, host)
} else {
nb = rewriteBodyRefs(body, target.Host, host)
}
if !bytes.Equal(nb, body) {
resp.Body.Close()
resp.Body = io.NopCloser(bytes.NewReader(nb))
resp.ContentLength = int64(len(nb))
resp.Header.Set("Content-Length", strconv.Itoa(len(nb)))
resp.Header.Del("Content-Encoding")
resp.Header.Del("Transfer-Encoding")
}
}
}
return nil
},
}
l.proxy = rp
l.target = target
errors.LogInfo(ctx, "stealth mirroring real site at ", target.String())
}
// wrap the raw listener with TLS or REALITY; the inner stream is served as
// plaintext HTTP/1.1 + h2c (same pattern as XHTTP)
if config := tls.ConfigFromStreamSettings(streamSettings); config != nil {
if tlsConfig := config.GetTLSConfig(); tlsConfig != nil {
tlsConfig.NextProtos = []string{"h2"}
if len(tlsConfig.Certificates) > 0 {
certs := tlsConfig.Certificates
tlsConfig.GetCertificate = func(*gotls.ClientHelloInfo) (*gotls.Certificate, error) {
return &certs[0], nil
}
}
listener = gotls.NewListener(listener, tlsConfig)
errors.LogInfo(ctx, "stealth TLS listener enabled")
}
}
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
listener = goreality.NewListener(listener, config.GetREALITYConfig())
errors.LogInfo(ctx, "stealth REALITY listener enabled")
}
protocols := new(http.Protocols)
protocols.SetHTTP1(true)
protocols.SetUnencryptedHTTP2(true)
server := &http.Server{
Handler: l,
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 0,
Protocols: protocols,
}
_ = http2.ConfigureServer(server, &http2.Server{})
l.server = server
go func() {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
errors.LogWarningInner(context.Background(), err, "stealth: http server stopped")
}
}()
return l, nil
}
func (l *Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || !containsString(l.paths, r.URL.Path) {
// probes and normal browsing see a real website (reverse-proxied),
// falling back to a built-in landing page when no target is set
if l.proxy != nil && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
l.proxy.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), mirrorHostKey{}, r.Host)))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, landingPage)
return
}
ftype, payload, err := readFrame(r.Body)
if err != nil || ftype != frameTypeAuth || string(payload) != l.config.Key {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(http.StatusOK)
var flush func()
if flusher, ok := w.(http.Flusher); ok {
flush = flusher.Flush
flusher.Flush()
}
var local, remote net.Addr
if v, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
local = v
}
if raddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr); err == nil {
remote = raddr
}
if local == nil {
local = &net.TCPAddr{IP: net.IPv4zero}
}
if remote == nil {
remote = local
}
conn := newH2Conn(r.Body, w, nil, flush, l.config, local, remote)
l.addConn(stat.Connection(conn))
select {
case <-conn.Done():
case <-r.Context().Done():
}
_ = conn.Close()
}
func containsString(list []string, s string) bool {
for _, v := range list {
if v == s {
return true
}
}
return false
}
func (l *Listener) Addr() net.Addr {
return l.listener.Addr()
}
func (l *Listener) Close() error {
_ = l.server.Close()
return l.listener.Close()
}
func init() {
common.Must(internet.RegisterTransportListener(protocolName, ListenStealth))
}