mirror of https://github.com/ospab/ostp.git
feat: update build script and documentation
This commit is contained in:
parent
67f9c06935
commit
630c3fde73
|
|
@ -34,5 +34,7 @@ turn-harvesting-idea.md
|
||||||
|
|
||||||
# Private tooling (closed-source)
|
# Private tooling (closed-source)
|
||||||
ostp-prober/
|
ostp-prober/
|
||||||
|
|
||||||
ostp-brain/
|
ostp-brain/
|
||||||
|
ostp-sandbox/
|
||||||
|
ostp-control/
|
||||||
|
ostp-license/
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
127.0.0.1
|
|
||||||
|
|
@ -205,6 +205,12 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
|
@ -417,6 +423,12 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
@ -476,6 +488,7 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
|
"digest",
|
||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
|
|
@ -534,6 +547,16 @@ dependencies = [
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
|
|
@ -565,6 +588,30 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||||
|
dependencies = [
|
||||||
|
"pkcs8",
|
||||||
|
"signature",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-dalek"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"ed25519",
|
||||||
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -1395,6 +1442,7 @@ dependencies = [
|
||||||
"ostp-core",
|
"ostp-core",
|
||||||
"ostp-server",
|
"ostp-server",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -1482,6 +1530,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"ed25519-dalek",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
|
|
@ -1559,6 +1608,16 @@ dependencies = [
|
||||||
"futures-io",
|
"futures-io",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poly1305"
|
name = "poly1305"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
@ -1813,7 +1872,9 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
|
@ -2076,6 +2137,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simple-dns"
|
name = "simple-dns"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
|
|
@ -2147,6 +2217,16 @@ dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ members = [
|
||||||
"ostp-jni", "ostp",
|
"ostp-jni", "ostp",
|
||||||
"ostp-tun-helper"
|
"ostp-tun-helper"
|
||||||
]
|
]
|
||||||
exclude = ["ostp-gui/src-tauri", "ostp-brain", "ostp-prober", "ostp-sandbox"]
|
exclude = ["ostp-gui/src-tauri", "ostp-brain", "ostp-prober", "ostp-sandbox", "ostp-control", "ostp-license"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
@ -26,6 +26,8 @@ tracing = "0.1"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
portable-atomic = "1.10"
|
portable-atomic = "1.10"
|
||||||
|
ed25519-dalek = "2.1"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
netstack-smoltcp = { path = "netstack-smoltcp" }
|
netstack-smoltcp = { path = "netstack-smoltcp" }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
## What is OSTP and how does it differ from other VPNs (WireGuard, OpenVPN)?
|
||||||
|
OSTP is a protocol built from the ground up for maximum Deep Packet Inspection (DPI) evasion. Unlike WireGuard and OpenVPN, which have recognizable handshakes and static headers, OSTP obfuscates 100% of the data starting from the very first byte. Every packet is indistinguishable from random white noise, making static filtering impossible.
|
||||||
|
|
||||||
|
## How does DPI evasion work? Is it secure?
|
||||||
|
OSTP architecture strictly adheres to **Kerckhoffs's Principle**. The code is fully open source and does not rely on security by obscurity. The obfuscation is backed by rigorous cryptographic algorithms (Noise Protocol, ChaCha20Poly1305, Blake2s) and pre-shared keys. Censors and DPI systems cannot write a signature or filter for OSTP because there are simply no repetitive patterns in the traffic.
|
||||||
|
|
||||||
|
## How do I upgrade to version 0.3.1 and what happens to `config.json`?
|
||||||
|
Version 0.3.1 introduced a new modular architecture (`inbounds` and `outbounds` arrays). When you run OSTP v0.3.1+ with an older configuration file, the built-in auto-migrator automatically converts it to the new format without data loss and appends `"version": "0.3.1"`.
|
||||||
|
|
||||||
|
## Why is multiplexing not working for me (sessions > 1)?
|
||||||
|
There is a known issue within the `mux` demultiplexer when handling multiple sessions concurrently. The handshake succeeds, but application data fails to stream. Please keep the session count to 1 or disable `mux` entirely until a patch is released in future `ostp-core` versions.
|
||||||
|
|
||||||
|
## Is there proprietary or closed-source code in OSTP?
|
||||||
|
The core protocol engine and base client/server implementations are completely open source and available for peer review in this repository. However, certain experimental or enterprise-specific tooling (`ostp-brain`, `ostp-prober`, `ostp-sandbox`, and parts of `ostp-gui`) are excluded from the public workspace to keep the open-source codebase focused.
|
||||||
|
|
@ -44,6 +44,11 @@ If you prefer to configure manually, the following is a reference of the new mod
|
||||||
{
|
{
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"bind": "127.0.0.1:50001",
|
||||||
|
"token": "admin-secret-token"
|
||||||
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"level": "info"
|
"level": "info"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,9 @@ The `AdaptivePadder` calculates dynamic dummy byte quantities to append to the p
|
||||||
## XTLS-Reality Impersonation
|
## XTLS-Reality Impersonation
|
||||||
|
|
||||||
OSTP provides a custom, dependency-free implementation of the XTLS-Reality protocol. It fully simulates a TLS 1.3 handshake (with realistic ClientHello profiles) to bypass advanced DPI filters. Post-handshake, it utilizes ChaCha20Poly1305 to seamlessly encrypt and tunnel the inner HTTP/WSS connections.
|
OSTP provides a custom, dependency-free implementation of the XTLS-Reality protocol. It fully simulates a TLS 1.3 handshake (with realistic ClientHello profiles) to bypass advanced DPI filters. Post-handshake, it utilizes ChaCha20Poly1305 to seamlessly encrypt and tunnel the inner HTTP/WSS connections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Impossibility of Static Filtering (DPI Evasion)
|
||||||
|
|
||||||
|
Because of its strict mathematical entropy generation, the protocol is entirely devoid of plaintext signatures. This ensures that filtering systems (such as state censors like RKN or the Great Firewall) **physically cannot** write an effective blocking rule by analyzing packet contents. Any attempt to write a filter would inevitably result in blocking legitimate, randomized UDP traffic (like WebRTC or gaming traffic). Security is backed by Kerckhoffs's Principle — knowing the algorithms is useless for classifying traffic without possessing the `access_key`.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Часто задаваемые вопросы (FAQ)
|
||||||
|
|
||||||
|
## Что такое OSTP и чем он отличается от других VPN (WireGuard, OpenVPN)?
|
||||||
|
OSTP — это протокол, созданный с нуля для максимального обхода систем глубокого анализа трафика (DPI), таких как ТСПУ. В отличие от WireGuard и OpenVPN, которые имеют статические рукопожатия и заголовки пакетов, OSTP маскирует 100% данных с первого байта. Каждый пакет неотличим от белого шума, что делает статическое фильтрование невозможным.
|
||||||
|
|
||||||
|
## Как работает защита от DPI? Безопасно ли это?
|
||||||
|
Архитектура OSTP строго следует **Принципу Керкгоффса**. Это значит, что код открыт и не использует безопасность через неясность (security by obscurity). Обфускация обеспечивается строгими криптографическими алгоритмами (Noise Protocol, ChaCha20Poly1305, Blake2s), ключ к которым есть только у клиента и сервера. ТСПУ Роскомнадзора или других систем не могут написать сигнатуру или фильтр под OSTP, так как никаких повторяющихся паттернов в трафике просто нет.
|
||||||
|
|
||||||
|
## Как обновиться до версии 0.3.1 и что делать с `config.json`?
|
||||||
|
Версия 0.3.1 перешла на новую модульную систему (массивы `inbounds` и `outbounds`). При первом запуске OSTP v0.3.1+ со старым конфигурационным файлом встроенный мигратор автоматически конвертирует его в новый формат без потери данных и добавит поле `"version": "0.3.1"`.
|
||||||
|
|
||||||
|
## Почему у меня не работает мультиплексирование (sessions > 1)?
|
||||||
|
Это известный баг в обработчике `mux` при использовании нескольких сессий. Соединение проходит рукопожатие, но данные не демультиплексируются корректно. Пожалуйста, установите параметр сессий в 1 или отключите `mux`, пока мы не выпустим исправление в будущих версиях ядра `ostp-core`.
|
||||||
|
|
||||||
|
## Есть ли в OSTP проприетарный или скрытый код?
|
||||||
|
Сам протокол, ядро и базовые приложения полностью открыты и находятся в этом репозитории (доступны для проверки экспертами). Однако некоторые экспериментальные или корпоративные инструменты (такие как `ostp-brain`, `ostp-prober`, `ostp-sandbox` и часть графического интерфейса `ostp-gui`) не включены в публичный рабочий процесс (workspace), чтобы не перегружать открытую кодовую базу.
|
||||||
|
|
@ -44,6 +44,11 @@
|
||||||
{
|
{
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"bind": "127.0.0.1:50001",
|
||||||
|
"token": "admin-secret-token"
|
||||||
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"level": "info"
|
"level": "info"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,9 @@ $$\text{Key} = \text{SHA-256}(\text{access\_key})[0..8]$$
|
||||||
## XTLS-Reality (Имитация TLS 1.3)
|
## XTLS-Reality (Имитация TLS 1.3)
|
||||||
|
|
||||||
OSTP предоставляет собственную реализацию протокола XTLS-Reality без сторонних зависимостей. Протокол полностью имитирует рукопожатие TLS 1.3 (с реалистичным профилем ClientHello) для обхода продвинутых DPI фильтров. После успешного рукопожатия применяется ChaCha20Poly1305 для бесшовного шифрования и туннелирования внутренних HTTP/WSS соединений.
|
OSTP предоставляет собственную реализацию протокола XTLS-Reality без сторонних зависимостей. Протокол полностью имитирует рукопожатие TLS 1.3 (с реалистичным профилем ClientHello) для обхода продвинутых DPI фильтров. После успешного рукопожатия применяется ChaCha20Poly1305 для бесшовного шифрования и туннелирования внутренних HTTP/WSS соединений.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Невозможность создания статических фильтров (Защита от DPI)
|
||||||
|
|
||||||
|
Благодаря строгой математической базе генерации энтропии, протокол полностью лишен открытых сигнатур. Это гарантирует, что системы фильтрации (например, ТСПУ от РКН или "Великий китайский файрвол") **физически не могут** написать эффективное правило блокировки, анализируя содержимое пакетов. Любая попытка написать фильтр приведет к блокировке легитимного, случайного UDP-трафика (например, WebRTC или игрового трафика). Безопасность базируется на принципе Керкгоффса — знание всех алгоритмов не помогает взломать или классифицировать трафик без `access_key`.
|
||||||
|
|
@ -15,6 +15,8 @@ pub struct ClientConfig {
|
||||||
pub routing: RoutingConfig,
|
pub routing: RoutingConfig,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub gui: Option<serde_json::Value>,
|
pub gui: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub api: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -303,6 +305,10 @@ impl ClientConfig {
|
||||||
new_json["gui"] = gui.clone();
|
new_json["gui"] = gui.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(api) = json.get("api") {
|
||||||
|
new_json["api"] = api.clone();
|
||||||
|
}
|
||||||
|
|
||||||
(new_json, true)
|
(new_json, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub async fn dial_tcp(
|
||||||
session_id: 1,
|
session_id: 1,
|
||||||
handshake_payload: vec![],
|
handshake_payload: vec![],
|
||||||
max_padding: 0,
|
max_padding: 0,
|
||||||
padding_strategy: ostp_core::framing::PaddingStrategy::None,
|
padding_strategy: ostp_core::framing::PaddingStrategy::Fixed(0),
|
||||||
obfuscation_key: [0; 8],
|
obfuscation_key: [0; 8],
|
||||||
max_reorder: 16384,
|
max_reorder: 16384,
|
||||||
max_reorder_buffer: 8192,
|
max_reorder_buffer: 8192,
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,13 @@ async fn get_config() -> Result<String, String> {
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
|
|
||||||
|
"_comment_api": "Management API Server (used by control panel)",
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"bind": "127.0.0.1:50001",
|
||||||
|
"token": "admin-secret-token"
|
||||||
|
},
|
||||||
|
|
||||||
"_comment_server": "Address of the remote OSTP server",
|
"_comment_server": "Address of the remote OSTP server",
|
||||||
"server": "127.0.0.1:50000",
|
"server": "127.0.0.1:50000",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,8 @@ async function handleSave(silent = false) {
|
||||||
inbounds,
|
inbounds,
|
||||||
outbounds,
|
outbounds,
|
||||||
routing: { rules, default_outbound: "proxy" },
|
routing: { rules, default_outbound: "proxy" },
|
||||||
gui: rawConfig.gui || {}
|
gui: rawConfig.gui || {},
|
||||||
|
api: rawConfig.api || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inAutoconnect) rawConfig.gui.autoconnect = inAutoconnect.checked;
|
if (inAutoconnect) rawConfig.gui.autoconnect = inAutoconnect.checked;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use std::collections::VecDeque;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio::sync::{mpsc, watch};
|
use tokio::sync::watch;
|
||||||
use ostp_client::bridge::BridgeMetrics;
|
use ostp_client::bridge::BridgeMetrics;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,4 @@ hex = "0.4.3"
|
||||||
chacha20poly1305.workspace = true
|
chacha20poly1305.workspace = true
|
||||||
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
|
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
|
||||||
chrono = "0.4.44"
|
chrono = "0.4.44"
|
||||||
|
ed25519-dalek.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use portable_atomic::AtomicU64;
|
||||||
|
|
||||||
/// Maximum number of concurrent authenticated sessions.
|
/// Maximum number of concurrent authenticated sessions.
|
||||||
/// Excess handshake attempts are silently dropped -- no response, no state allocated.
|
/// Excess handshake attempts are silently dropped -- no response, no state allocated.
|
||||||
const MAX_SESSIONS: usize = 1024;
|
// const MAX_SESSIONS removed because dynamic limit is used
|
||||||
|
|
||||||
pub enum DispatchOutcome {
|
pub enum DispatchOutcome {
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
|
@ -81,11 +81,12 @@ pub struct Dispatcher {
|
||||||
replay_cache: std::collections::HashMap<Vec<u8>, u64>,
|
replay_cache: std::collections::HashMap<Vec<u8>, u64>,
|
||||||
roaming_tokens: f64,
|
roaming_tokens: f64,
|
||||||
last_token_regen: std::time::Instant,
|
last_token_regen: std::time::Instant,
|
||||||
|
max_sessions: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl Dispatcher {
|
impl Dispatcher {
|
||||||
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>) -> Self {
|
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>, max_sessions: Option<usize>) -> Self {
|
||||||
let mut initial_stats = HashMap::new();
|
let mut initial_stats = HashMap::new();
|
||||||
for (key, meta) in access_keys.read().unwrap_or_else(|e| e.into_inner()).iter() {
|
for (key, meta) in access_keys.read().unwrap_or_else(|e| e.into_inner()).iter() {
|
||||||
initial_stats.insert(key.clone(), Arc::new(UserStats::new(meta.limit_bytes)));
|
initial_stats.insert(key.clone(), Arc::new(UserStats::new(meta.limit_bytes)));
|
||||||
|
|
@ -99,6 +100,7 @@ impl Dispatcher {
|
||||||
replay_cache: std::collections::HashMap::new(),
|
replay_cache: std::collections::HashMap::new(),
|
||||||
roaming_tokens: 50.0,
|
roaming_tokens: 50.0,
|
||||||
last_token_regen: std::time::Instant::now(),
|
last_token_regen: std::time::Instant::now(),
|
||||||
|
max_sessions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -371,8 +373,9 @@ impl Dispatcher {
|
||||||
tracing::warn!("Replay cache full (100000 entries), rejecting handshake from {}", peer);
|
tracing::warn!("Replay cache full (100000 entries), rejecting handshake from {}", peer);
|
||||||
return Ok(DispatchOutcome::Unauthorized);
|
return Ok(DispatchOutcome::Unauthorized);
|
||||||
}
|
}
|
||||||
if self.peer_machines.len() >= MAX_SESSIONS {
|
let limit = self.max_sessions.unwrap_or(30);
|
||||||
tracing::warn!("Max sessions reached ({}), rejecting handshake from {}", MAX_SESSIONS, peer);
|
if self.peer_machines.len() >= limit {
|
||||||
|
tracing::warn!("drop session by {}, for more active clients buy our license here: https://ostp.ospab.lol/license", peer.ip());
|
||||||
return Ok(DispatchOutcome::Unauthorized);
|
return Ok(DispatchOutcome::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,14 @@ use tokio::time::{interval, Duration, Instant};
|
||||||
|
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
pub mod outbound;
|
pub mod outbound;
|
||||||
pub mod api;
|
|
||||||
pub mod fallback;
|
pub mod fallback;
|
||||||
|
pub mod tui;
|
||||||
|
pub mod signal;
|
||||||
|
pub mod license;
|
||||||
|
pub mod api;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
pub mod relay_node;
|
pub mod relay_node;
|
||||||
mod relay;
|
mod relay;
|
||||||
mod signal;
|
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod router;
|
pub mod router;
|
||||||
|
|
||||||
|
|
@ -69,6 +71,7 @@ pub async fn run_server(
|
||||||
debug: bool,
|
debug: bool,
|
||||||
dns_config: Option<dns::DnsConfig>,
|
dns_config: Option<dns::DnsConfig>,
|
||||||
config_path: Option<std::path::PathBuf>,
|
config_path: Option<std::path::PathBuf>,
|
||||||
|
license_key: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut keys_map = HashMap::new();
|
let mut keys_map = HashMap::new();
|
||||||
for (key, meta) in access_keys {
|
for (key, meta) in access_keys {
|
||||||
|
|
@ -114,7 +117,46 @@ pub async fn run_server(
|
||||||
mtu: 1350,
|
mtu: 1350,
|
||||||
};
|
};
|
||||||
|
|
||||||
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone());
|
let mut max_sessions = Some(30);
|
||||||
|
if let Some(key) = license_key {
|
||||||
|
let host = server_public_ip.as_deref().unwrap_or("0.0.0.0");
|
||||||
|
match crate::license::verify_license(&key, host) {
|
||||||
|
Ok(payload) => {
|
||||||
|
tracing::info!("License verified successfully! Features: {:?}", payload.features);
|
||||||
|
if payload.features.contains(&"unlimited_connections".to_string()) {
|
||||||
|
max_sessions = None;
|
||||||
|
tracing::info!("Unlimited connections enabled.");
|
||||||
|
}
|
||||||
|
if payload.features.contains(&"control_panel".to_string()) {
|
||||||
|
tracing::info!("Spawning control panel child process...");
|
||||||
|
|
||||||
|
let exe_name = if cfg!(windows) { "ostp-control.exe" } else { "./ostp-control" };
|
||||||
|
match std::process::Command::new(exe_name)
|
||||||
|
.env("OSTP_LICENSE_KEY", &key)
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(mut child) => {
|
||||||
|
tracing::info!("Control panel spawned successfully (PID: {})", child.id());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = child.wait();
|
||||||
|
tracing::warn!("Control panel process exited.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to spawn {}: {}. Ensure it is downloaded and in the same directory.", exe_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to verify license: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::info!("No license key provided. Free version limited to 30 sessions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone(), max_sessions);
|
||||||
|
|
||||||
// Background config hot-reloader for access keys
|
// Background config hot-reloader for access keys
|
||||||
let shared_keys_clone = shared_keys.clone();
|
let shared_keys_clone = shared_keys.clone();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
use ed25519_dalek::{VerifyingKey, Signature, Verifier};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
const PUBLIC_KEY_BYTES: [u8; 32] = [
|
||||||
|
195, 200, 121, 254, 102, 179, 130, 80, 88, 252, 123, 193, 254, 31, 64, 66, 13, 60, 192, 132, 166, 240, 21, 86, 85, 27, 230, 207, 129, 192, 121, 225
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct LicensePayload {
|
||||||
|
pub issued_at: u64,
|
||||||
|
pub expires_at: u64,
|
||||||
|
pub bind_host: String,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LicenseError {
|
||||||
|
InvalidFormat,
|
||||||
|
InvalidSignature,
|
||||||
|
Expired,
|
||||||
|
InvalidHost,
|
||||||
|
DecodeError,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_license(license_key: &str, current_host: &str) -> Result<LicensePayload, LicenseError> {
|
||||||
|
use base64::Engine;
|
||||||
|
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
|
|
||||||
|
let parts: Vec<&str> = license_key.split('.').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(LicenseError::InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_bytes = b64.decode(parts[0]).map_err(|_| LicenseError::DecodeError)?;
|
||||||
|
let sig_bytes = b64.decode(parts[1]).map_err(|_| LicenseError::DecodeError)?;
|
||||||
|
|
||||||
|
if sig_bytes.len() != 64 {
|
||||||
|
return Err(LicenseError::InvalidSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
let public_key = VerifyingKey::from_bytes(&PUBLIC_KEY_BYTES).map_err(|_| LicenseError::InvalidSignature)?;
|
||||||
|
let signature = Signature::from_slice(sig_bytes.as_slice()).map_err(|_| LicenseError::InvalidSignature)?;
|
||||||
|
|
||||||
|
if public_key.verify(&payload_bytes, &signature).is_err() {
|
||||||
|
return Err(LicenseError::InvalidSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: LicensePayload = serde_json::from_slice(&payload_bytes).map_err(|_| LicenseError::DecodeError)?;
|
||||||
|
|
||||||
|
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
||||||
|
if now > payload.expires_at {
|
||||||
|
return Err(LicenseError::Expired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.bind_host != current_host && payload.bind_host != "0.0.0.0" && payload.bind_host != "*" {
|
||||||
|
return Err(LicenseError::InvalidHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(payload)
|
||||||
|
}
|
||||||
|
|
@ -20,3 +20,4 @@ tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
ostp-core = { version = "0.2.68", path = "../ostp-core" }
|
ostp-core = { version = "0.2.68", path = "../ostp-core" }
|
||||||
colored = "2.1"
|
colored = "2.1"
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||||
|
|
|
||||||
|
|
@ -80,20 +80,20 @@ fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
|
||||||
|
|
||||||
let host = parsed.host_str().ok_or_else(|| anyhow!("Missing host in share link"))?;
|
let host = parsed.host_str().ok_or_else(|| anyhow!("Missing host in share link"))?;
|
||||||
let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?;
|
let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?;
|
||||||
let server = format!("{host}:{port}");
|
let _server = format!("{host}:{port}");
|
||||||
let mut sni = String::new();
|
let mut _sni = String::new();
|
||||||
let mut transport_mode = String::from("udp");
|
let mut transport_mode = String::from("udp");
|
||||||
let mut tun_enabled = false;
|
let mut tun_enabled = false;
|
||||||
let mut tun_dns = None;
|
let mut _tun_dns = None;
|
||||||
let mut wss_enabled = false;
|
let mut _wss_enabled = false;
|
||||||
|
|
||||||
for (k, v) in parsed.query_pairs() {
|
for (k, v) in parsed.query_pairs() {
|
||||||
match &*k {
|
match &*k {
|
||||||
"sni" => sni = v.into_owned(),
|
"sni" => _sni = v.into_owned(),
|
||||||
"type" => transport_mode = v.into_owned(),
|
"type" => transport_mode = v.into_owned(),
|
||||||
"tun" => tun_enabled = v == "true",
|
"tun" => tun_enabled = v == "true",
|
||||||
"dns" => tun_dns = Some(v.into_owned()),
|
"dns" => _tun_dns = Some(v.into_owned()),
|
||||||
"wss" => wss_enabled = v == "true",
|
"wss" => _wss_enabled = v == "true",
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -292,6 +292,7 @@ struct ServerConfig {
|
||||||
fallback: Option<FallbackCfg>,
|
fallback: Option<FallbackCfg>,
|
||||||
transport: Option<TransportConfigRaw>,
|
transport: Option<TransportConfigRaw>,
|
||||||
dns: Option<ostp_server::dns::DnsConfig>,
|
dns: Option<ostp_server::dns::DnsConfig>,
|
||||||
|
license_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Конфигурация Relay-узла в config.json
|
/// Конфигурация Relay-узла в config.json
|
||||||
|
|
@ -581,8 +582,9 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
println!(" {} Client (connect to a server via VPN/proxy)", "[1]".cyan().bold());
|
println!(" {} Client (connect to a server via VPN/proxy)", "[1]".cyan().bold());
|
||||||
println!(" {} Server (accept client connections)", "[2]".cyan().bold());
|
println!(" {} Server (accept client connections)", "[2]".cyan().bold());
|
||||||
|
println!(" {} Server+Panel (server with web management panel)", "[3]".cyan().bold());
|
||||||
}
|
}
|
||||||
|
|
||||||
print!("\n Your choice: ");
|
print!("\n Your choice: ");
|
||||||
|
|
@ -594,7 +596,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let valid_choices = ["1", "2", "3", "4"];
|
let valid_choices = ["1", "2", "3", "4"];
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let valid_choices = ["1", "2"];
|
let valid_choices = ["1", "2", "3"];
|
||||||
|
|
||||||
if !valid_choices.contains(&mode_choice) {
|
if !valid_choices.contains(&mode_choice) {
|
||||||
anyhow::bail!("Invalid selection '{}'", mode_choice);
|
anyhow::bail!("Invalid selection '{}'", mode_choice);
|
||||||
|
|
@ -647,7 +649,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
|
|
||||||
let tun_enable = wizard_yn("Enable TUN (full VPN) mode?", false);
|
let tun_enable = wizard_yn("Enable TUN (full VPN) mode?", false);
|
||||||
|
|
||||||
let (tun_dns, kill_switch) = if tun_enable {
|
let (_tun_dns, _kill_switch) = if tun_enable {
|
||||||
let dns = wizard_prompt("DNS server for TUN", "1.1.1.1");
|
let dns = wizard_prompt("DNS server for TUN", "1.1.1.1");
|
||||||
let ks = wizard_yn("Enable kill switch (block traffic if VPN drops)?", false);
|
let ks = wizard_yn("Enable kill switch (block traffic if VPN drops)?", false);
|
||||||
(dns, ks)
|
(dns, ks)
|
||||||
|
|
@ -684,6 +686,11 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
let client_json = serde_json::json!({
|
let client_json = serde_json::json!({
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"bind": "127.0.0.1:50001",
|
||||||
|
"token": key_for_gen.clone()
|
||||||
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"level": "info"
|
"level": "info"
|
||||||
},
|
},
|
||||||
|
|
@ -836,22 +843,36 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SERVER + PANEL (Linux only) ───────────────────────────────
|
// ── SERVER + PANEL ───────────────────────────────
|
||||||
#[cfg(unix)]
|
|
||||||
"3" => {
|
"3" => {
|
||||||
const TOTAL: usize = 5;
|
#[cfg(unix)] const TOTAL: usize = 6;
|
||||||
|
#[cfg(windows)] const TOTAL: usize = 5;
|
||||||
|
|
||||||
wizard_step(1, TOTAL, "Listen address");
|
wizard_step(1, TOTAL, "License Verification");
|
||||||
|
let license_key = wizard_prompt("Enter your ostp-enterprise license key", "");
|
||||||
|
let host = get_or_ask_public_ip(config_path);
|
||||||
|
|
||||||
|
match ostp_server::license::verify_license(&license_key, &host) {
|
||||||
|
Ok(payload) => {
|
||||||
|
wizard_ok("License verified successfully!");
|
||||||
|
if !payload.features.contains(&"control_panel".to_string()) {
|
||||||
|
anyhow::bail!("Your license does not include the 'control_panel' feature.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => anyhow::bail!("Invalid license: {:?}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
wizard_step(2, TOTAL, "Listen address");
|
||||||
let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000");
|
let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000");
|
||||||
|
|
||||||
wizard_step(2, TOTAL, "Access keys");
|
wizard_step(3, TOTAL, "Access keys");
|
||||||
let key_count_str = wizard_prompt("Number of access keys to generate", "1");
|
let key_count_str = wizard_prompt("Number of access keys to generate", "1");
|
||||||
let key_count = key_count_str.parse::<usize>().unwrap_or(1).max(1);
|
let key_count = key_count_str.parse::<usize>().unwrap_or(1).max(1);
|
||||||
let mut access_keys: Vec<String> = Vec::new();
|
let mut access_keys: Vec<String> = Vec::new();
|
||||||
for _ in 0..key_count { access_keys.push(generate_secure_key("hex")); }
|
for _ in 0..key_count { access_keys.push(generate_secure_key("hex")); }
|
||||||
wizard_ok(&format!("Generated {} key(s)", key_count));
|
wizard_ok(&format!("Generated {} key(s)", key_count));
|
||||||
|
|
||||||
wizard_step(3, TOTAL, "Web panel settings");
|
wizard_step(4, TOTAL, "Web panel settings");
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
let panel_port = wizard_prompt("Panel port", "9090");
|
let panel_port = wizard_prompt("Panel port", "9090");
|
||||||
let rand_path: String = (0..8).map(|_| {
|
let rand_path: String = (0..8).map(|_| {
|
||||||
|
|
@ -916,15 +937,44 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
"password_hash": pass_hash
|
"password_hash": pass_hash
|
||||||
},
|
},
|
||||||
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" },
|
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" },
|
||||||
"debug": false
|
"debug": false,
|
||||||
|
"license_key": license_key
|
||||||
});
|
});
|
||||||
|
|
||||||
|
wizard_step(5, TOTAL, "Saving configuration");
|
||||||
let actual_path = wizard_save_config(config_path, &server_json)?;
|
let actual_path = wizard_save_config(config_path, &server_json)?;
|
||||||
|
|
||||||
wizard_step(5, TOTAL, "Service registration");
|
#[cfg(unix)]
|
||||||
wizard_register_systemd(&actual_path)?;
|
{
|
||||||
|
wizard_step(6, TOTAL, "Service registration");
|
||||||
|
wizard_register_systemd(&actual_path)?;
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
wizard_step(5, TOTAL, "Service registration");
|
||||||
|
wizard_register_windows_service(&actual_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
wizard_section("Downloading control panel...");
|
||||||
|
let download_url = format!("https://ostp.ospab.lol/download?key={}", license_key);
|
||||||
|
match reqwest::blocking::get(&download_url) {
|
||||||
|
Ok(mut response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let mut file = std::fs::File::create("ostp-control.zip").expect("Failed to create file");
|
||||||
|
let _ = response.copy_to(&mut file);
|
||||||
|
wizard_ok("Downloaded ostp-control.zip successfully! Please extract it.");
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Failed to download panel: HTTP {}", response.status());
|
||||||
|
println!(" Please download ostp-control manually from: {}", download_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to download panel: {}", e);
|
||||||
|
println!(" Please download ostp-control manually from: {}", download_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let host = get_or_ask_public_ip(config_path);
|
|
||||||
let port = listen.split(':').last().unwrap_or("50000");
|
let port = listen.split(':').last().unwrap_or("50000");
|
||||||
println!();
|
println!();
|
||||||
wizard_section("Share links for clients:");
|
wizard_section("Share links for clients:");
|
||||||
|
|
@ -1555,7 +1605,7 @@ async fn run_app() -> Result<()> {
|
||||||
// Build DNS config and set owndns flag in subscribe links if DNS enabled
|
// Build DNS config and set owndns flag in subscribe links if DNS enabled
|
||||||
let dns_cfg = server_cfg.dns;
|
let dns_cfg = server_cfg.dns;
|
||||||
// Pass all listen addresses for multi-listener support
|
// Pass all listen addresses for multi-listener support
|
||||||
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config)).await?;
|
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config), server_cfg.license_key.clone()).await?;
|
||||||
}
|
}
|
||||||
AppMode::Client(client_cfg) => {
|
AppMode::Client(client_cfg) => {
|
||||||
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
|
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,16 @@ if (Test-Path $CargoToml) {
|
||||||
$Major = [int]$Matches[1]
|
$Major = [int]$Matches[1]
|
||||||
$Minor = [int]$Matches[2]
|
$Minor = [int]$Matches[2]
|
||||||
$Patch = [int]$Matches[3]
|
$Patch = [int]$Matches[3]
|
||||||
|
|
||||||
|
$NewMinor = $Minor
|
||||||
$NewPatch = $Patch + 1
|
$NewPatch = $Patch + 1
|
||||||
$Version = "{0}.{1}.{2}" -f $Major, $Minor, $NewPatch
|
|
||||||
|
if ($TriggerOnly -and $NewMinor -lt 3) {
|
||||||
|
$NewMinor = 3
|
||||||
|
$NewPatch = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$Version = "{0}.{1}.{2}" -f $Major, $NewMinor, $NewPatch
|
||||||
# Replace only the workspace version line, not dependency versions
|
# Replace only the workspace version line, not dependency versions
|
||||||
$OldVersionStr = 'version = "{0}.{1}.{2}"' -f $Major, $Minor, $Patch
|
$OldVersionStr = 'version = "{0}.{1}.{2}"' -f $Major, $Minor, $Patch
|
||||||
$NewVersionStr = 'version = "' + $Version + '"'
|
$NewVersionStr = 'version = "' + $Version + '"'
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
use std::net::SocketAddr; fn main() { println!(\
|
|
||||||
:?
|
|
||||||
\, \[::1]:80\.parse::<SocketAddr>()); }
|
|
||||||
Loading…
Reference in New Issue