feat(fptn): integrate FPTN client component and runtime provider

This commit is contained in:
Dushnilin
2026-09-15 14:57:05 +03:00
parent 03c51d7606
commit 73cdde17bb
26 changed files with 880 additions and 14 deletions
+7 -2
View File
@@ -20,7 +20,7 @@
**Tachyon** is an advanced, autonomous network routing, proxy orchestration, and anti-censorship engine designed specifically for **OpenWrt** routers (fully supporting **OpenWrt 23.05, 24.10, 25.x, and SNAPSHOT** builds). Direct fork of **[Forkop by @ushan0v](https://github.com/ushan0v/forkop)** (formerly **Podkop Plus**).
Tachyon combines the power of **sing-box**, local hardware DPI bypass engines (**Zapret v1 / Zapret v2 / ByeDPI**), an interactive combinatorial **DPI Strategy Fuzzer**, a hardened **Telegram control bot**, and a cutting-edge **AI Stack** (autonomous **AI Doctor v2.5**, offline local diagnostics, **HTTP REST Agent API / OpenAPI 3.0**, and **Model Context Protocol (MCP)** server for autonomous AI agents).
Tachyon combines the power of **sing-box**, high-speed **FPTN** (Fast Packet Tunnel Network), local hardware DPI bypass engines (**Zapret v1 / Zapret v2 / ByeDPI**), an interactive combinatorial **DPI Strategy Fuzzer**, a hardened **Telegram control bot**, and a cutting-edge **AI Stack** (autonomous **AI Doctor v2.5**, offline local diagnostics, **HTTP REST Agent API / OpenAPI 3.0**, and **Model Context Protocol (MCP)** server for autonomous AI agents).
The entire backend logic is written in **ucode** — OpenWrt's native, high-performance C scripting language — delivering instant response times with minimal RAM footprint (starting from 128 MB RAM devices).
@@ -41,7 +41,7 @@ Tachyon intercepts network flows via kernel **nftables** and dispatches requests
2. **Zapret v1 (`nfqws`)**: Basic TCP/UDP packet desynchronization (`fake`, `disorder`, `split2`) directly on router without VPS.
3. **Zapret v2 (`nfqws2`)**: Advanced multi-vector DPI evasion (`multisplit`, `seqovl`, `wsize`, PAWS `tcp_ts`, authentic `blobs`) for YouTube 4K, Discord, and streaming.
4. **ByeDPI (`ciadpi`)**: Local SOCKS5 desync engine with HTTP/TLS SNI payload fragmentation.
5. **Encrypted Proxy Tunnel (sing-box)**: Censored endpoints and private traffic are routed through modern protocols (VLESS Reality, Hysteria2, WireGuard, AmneziaWG).
5. **Encrypted Proxy & Tunnel (sing-box / FPTN)**: Censored endpoints and private traffic are routed through modern secure protocols (VLESS Reality, Hysteria2, WireGuard, AmneziaWG) and high-speed **FPTN** tunnel (`tun-fptn` over WebSocket/TLS with web traffic masquerading).
6. **Smart DNS Pipeline**: Isolated DNS processing via FakeIP (`198.18.0.0/15`), DoH/DoT/DoQ with anti-hijack transparent redirection and automated failover (DNS Failover).
<p align="center">
@@ -52,6 +52,10 @@ Tachyon intercepts network flows via kernel **nftables** and dispatches requests
### 🛡️ 1. Multi-Protocol Proxying & Smart DNS Stack
* **sing-box Engine (v1.11+)**: Native support for modern proxy protocols — **VLESS (Reality / gRPC / WS)**, **VMess**, **Shadowsocks**, **Trojan**, **Hysteria2**, and **WireGuard / AmneziaWG**.
* **High-Speed FPTN Engine (`fptn-client-cli`)**:
* Native integration of Fast Packet Tunnel Network: L3 packet tunneling over WebSocket/TLS with effective HTTPS camouflage to bypass restrictive protocol blocks.
* Dedicated `tun-fptn` interface, isolated routing table (`4249`), and seamless selective routing via nftables rules.
* Full managed component lifecycle: router platform autodetection, download and installation from official releases via LuCI, commit-level updates, and safe rollback support.
* **Cloudflare WARP & AmneziaWG Generator (`generate_warp`)**: Instant generation of working WireGuard and AmneziaWG profiles directly on the router.
* **Multi-Dimensional Selective Routing**:
* **By Domains & IP Subnets**: Route only target traffic through proxies or desync engines.
@@ -328,6 +332,7 @@ Tachyon stands on the shoulders of incredible open-source projects:
* 📦 **[sing-box](https://github.com/SagerNet/sing-box)** — Universal proxy engine.
* 🚀 **[zapret (bol-van)](https://github.com/bol-van/zapret2)** — DPI desync framework (`nfqws` / `nfqws2`).
* 🌐 **[ByeDPI](https://github.com/hrbrmstr/byedpi)** — Local SOCKS desync proxy.
* 🛡️ **[FPTN (fptn-project)](https://github.com/fptn-project/fptn)** — High-speed VPN & packet tunnel over WebSocket/TLS with DPI evasion.
<p align="center">
<img src="assets/readme/divider_stream.svg" width="100%" alt="divider" />
+7 -2
View File
@@ -20,7 +20,7 @@
**Tachyon** — это высокопроизводительное, автономное и бескомпромиссное решение для оркестрации сетевого трафика, проксирования и обхода цензуры на роутерах под управлением **OpenWrt** (полная совместимость с **OpenWrt 23.05, 24.10, 25.x и SNAPSHOT**). Прямой форк проекта **[Forkop от @ushan0v](https://github.com/ushan0v/forkop)** (ранее **Podkop Plus**).
Tachyon объединяет ядро **sing-box**, средства локального аппаратного обхода DPI (**Zapret v1 / Zapret v2 / ByeDPI**), интерактивный комбинаторный **DPI Strategy Fuzzer**, защищённый **Telegram-бот управления**, а также инновационный **AI Stack** (автономный **AI Doctor v2.5**, офлайн-диагностику, **HTTP REST Agent API / OpenAPI 3.0** и **MCP Server** для подключения ИИ-агентов).
Tachyon объединяет ядро **sing-box**, высокоскоростной протокол **FPTN** (Fast Packet Tunnel Network), средства локального аппаратного обхода DPI (**Zapret v1 / Zapret v2 / ByeDPI**), интерактивный комбинаторный **DPI Strategy Fuzzer**, защищённый **Telegram-бот управления**, а также инновационный **AI Stack** (автономный **AI Doctor v2.5**, офлайн-диагностику, **HTTP REST Agent API / OpenAPI 3.0** и **MCP Server** для подключения ИИ-агентов).
Вся внутренняя логика реализована на скриптовом движке **ucode** — нативном C-интерпретаторе OpenWrt, обеспечивающем ультранизкое потребление RAM (от 128 МБ ОЗУ) и мгновенный отклик.
@@ -41,7 +41,7 @@ Tachyon перехватывает сетевой стек через ядро *
2. **Zapret v1 (`nfqws`)**: Базовая десинхронизация TCP/UDP (`fake`, `disorder`, `split2`) прямо на роутере без VPS.
3. **Zapret v2 (`nfqws2`)**: Адаптивный многовекторный обход ТСПУ (`multisplit`, `seqovl`, `wsize`, PAWS `tcp_ts`, аутентичные `blobs`) для YouTube 4K, Discord и стриминга.
4. **ByeDPI (`ciadpi`)**: Локальный SOCKS5-десинхронизатор с фрагментацией полезной нагрузки HTTP/TLS SNI.
5. **Зашифрованный прокси-туннель (sing-box)**: Заблокированные ресурсы и приватный трафик направляются через защищённые протоколы (VLESS Reality, Hysteria2, WireGuard, AmneziaWG).
5. **Туннелирование и прокси (sing-box / FPTN)**: Заблокированные ресурсы и приватный трафик направляются через защищённые протоколы (VLESS Reality, Hysteria2, WireGuard, AmneziaWG) и высокоскоростной туннель **FPTN** (`tun-fptn` поверх WebSocket/TLS с маскировкой под веб-трафик).
6. **Smart DNS Pipeline**: Изолированная обработка DNS через FakeIP (`198.18.0.0/15`), DoH/DoT/DoQ с защитой от перехвата провайдером и автоматическим отказоустойчивым переключением (DNS Failover).
<p align="center">
@@ -52,6 +52,10 @@ Tachyon перехватывает сетевой стек через ядро *
### 🛡️ 1. Мультипротокольное проксирование и умный DNS-стек
* **Ядро sing-box Engine (v1.11+)**: Нативная поддержка современных защищённых протоколов — **VLESS (Reality / gRPC / WS)**, **VMess**, **Shadowsocks**, **Trojan**, **Hysteria2**, **WireGuard / AmneziaWG**.
* **Высокоскоростной туннель FPTN Engine (`fptn-client-cli`)**:
* Нативная интеграция Fast Packet Tunnel Network: L3-туннелирование IP-пакетов поверх WebSocket/TLS с эффективной маскировкой под стандартный HTTPS-трафик для обхода жестких протокольных блокировок.
* Выделенный сетевой интерфейс `tun-fptn`, таблица маршрутизации (`4249`) и бесшовная селективная маршрутизация по правилам nftables.
* Управление компонентом: автоопределение платформы роутера, скачивание и установка из официальных релизов прямо из LuCI, отслеживание обновлений и безопасный откат (Rollback).
* **Генератор профилей Cloudflare WARP / AmneziaWG (`generate_warp`)**: Быстрое создание готовых конфигураций WireGuard и AmneziaWG прямо на роутере без сторонних скриптов.
* **Многомерная селективная маршрутизация**:
* **По доменам и IP-сетям**: Направляйте через прокси или десинк только целевой трафик.
@@ -328,6 +332,7 @@ Tachyon опирается на фундаментальные разработ
* 📦 **[sing-box](https://github.com/SagerNet/sing-box)** — универсальная прокси-платформа.
* 🚀 **[zapret (bol-van)](https://github.com/bol-van/zapret2)** — средства локального обхода DPI (`nfqws` / `nfqws2`).
* 🌐 **[ByeDPI](https://github.com/hrbrmstr/byedpi)** — локальный SOCKS-прокси для десинка пакетов.
* 🛡️ **[FPTN (fptn-project)](https://github.com/fptn-project/fptn)** — высокоскоростной VPN/туннель пакетов через WebSocket/TLS с обходом блокировок.
<p align="center">
<img src="assets/readme/divider_stream.svg" width="100%" alt="divider" />
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "fe-app-tachyon",
"version": "1.3.27",
"version": "1.3.28",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fe-app-tachyon",
"version": "1.3.27",
"version": "1.3.28",
"license": "MIT",
"devDependencies": {
"@babel/parser": "7.28.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "fe-app-tachyon",
"version": "1.3.27",
"version": "1.3.28",
"license": "MIT",
"type": "module",
"scripts": {
@@ -43,6 +43,11 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
'olcrtc:install_version': 'olcrtcInstall',
'olcrtc:remove': 'olcrtcRemove',
'olcrtc:rollback': 'olcrtcRollback',
'fptn:check_update': 'fptnCheck',
'fptn:install': 'fptnInstall',
'fptn:install_version': 'fptnInstall',
'fptn:remove': 'fptnRemove',
'fptn:rollback': 'fptnRollback',
'tailscale:check_update': 'tailscaleCheck',
'tailscale:install': 'tailscaleInstall',
'tailscale:install_version': 'tailscaleInstall',
@@ -245,6 +245,10 @@ export interface StoreType {
tailscale_installed: number;
tailscale_backup_version?: string;
tailscale_backup_time?: number;
fptn_version: string;
fptn_installed: number;
fptn_backup_version?: string;
fptn_backup_time?: number;
server_inbounds_enabled_count: number;
direct_bypass_enabled?: number;
direct_bypass_address?: string;
@@ -289,6 +293,10 @@ export interface StoreType {
olcrtcInstall: { loading: boolean };
olcrtcRemove: { loading: boolean };
olcrtcRollback: { loading: boolean };
fptnCheck: { loading: boolean };
fptnInstall: { loading: boolean };
fptnRemove: { loading: boolean };
fptnRollback: { loading: boolean };
tailscaleCheck: { loading: boolean };
tailscaleInstall: { loading: boolean };
tailscaleRemove: { loading: boolean };
@@ -30,6 +30,8 @@ const UNKNOWN_SYSTEM_INFO: StoreType['diagnosticsSystemInfo'] = {
olcrtc_installed: 0,
tailscale_version: _('unknown'),
tailscale_installed: 0,
fptn_version: _('unknown'),
fptn_installed: 0,
server_inbounds_enabled_count: -1,
openwrt_version: _('unknown'),
device_model: _('unknown'),
@@ -45,6 +45,10 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
fptnCheck: { loading: false },
fptnInstall: { loading: false },
fptnRemove: { loading: false },
fptnRollback: { loading: false },
tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
@@ -111,6 +111,10 @@ export const initialDiagnosticStore: Pick<
tailscale_installed: 0,
tailscale_backup_version: '',
tailscale_backup_time: 0,
fptn_version: 'loading',
fptn_installed: 0,
fptn_backup_version: '',
fptn_backup_time: 0,
server_inbounds_enabled_count: -1,
direct_bypass_enabled: 0,
direct_bypass_address: '',
@@ -195,6 +199,10 @@ export const initialDiagnosticStore: Pick<
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
fptnCheck: { loading: false },
fptnInstall: { loading: false },
fptnRemove: { loading: false },
fptnRollback: { loading: false },
tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
@@ -212,6 +220,7 @@ export const initialDiagnosticStore: Pick<
byedpi: { status: null, latest_version: '', release_url: '' },
wdtt: { status: null, latest_version: '', release_url: '' },
olcrtc: { status: null, latest_version: '', release_url: '' },
fptn: { status: null, latest_version: '', release_url: '' },
tailscale: { status: null, latest_version: '', release_url: '' },
direct_bypass: { status: null, latest_version: '', release_url: '' },
torrserver_direct: { status: null, latest_version: '', release_url: '' },
@@ -74,6 +74,8 @@ function getComponentCardTitle(component: Tachyon.ComponentName): string {
return 'WDTT';
case 'olcrtc':
return 'OlcRTC';
case 'fptn':
return 'FPTN';
case 'tailscale':
return 'Tailscale';
default:
@@ -100,6 +102,8 @@ function getComponentCurrentVersion(
return sys.wdtt_version;
case 'olcrtc':
return sys.olcrtc_version;
case 'fptn':
return sys.fptn_version;
case 'tailscale':
return sys.tailscale_version;
default:
@@ -561,6 +565,18 @@ function patchSystemInfoAfterMutation(result: Tachyon.ComponentActionResult) {
}
}
if (result.component === 'fptn') {
nextSystemInfo.providerInfoLoaded = true;
if (result.action === 'remove') {
nextSystemInfo.fptn_installed = 0;
nextSystemInfo.fptn_version = 'not installed';
} else {
nextSystemInfo.fptn_installed = 1;
nextSystemInfo.fptn_version = version;
}
}
if (result.component === 'direct_bypass') {
nextSystemInfo.direct_bypass_enabled = result.action === 'enable' ? 1 : 0;
}
@@ -582,7 +598,8 @@ function patchSystemInfoAfterMutation(result: Tachyon.ComponentActionResult) {
result.component === 'zapret2' ||
result.component === 'byedpi' ||
result.component === 'wdtt' ||
result.component === 'olcrtc'
result.component === 'olcrtc' ||
result.component === 'fptn'
) {
notifyActionProvidersAvailabilityChanged(normalizedSystemInfo);
}
@@ -1130,6 +1147,8 @@ function getComponentBackupVersion(component: Tachyon.ComponentName): string {
return sys.wdtt_backup_version || '';
case 'olcrtc':
return sys.olcrtc_backup_version || '';
case 'fptn':
return sys.fptn_backup_version || '';
case 'tailscale':
return sys.tailscale_backup_version || '';
default:
@@ -1159,7 +1178,14 @@ function getOptionalComponentActions({
removeKey,
rollbackKey,
}: {
component: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc' | 'tailscale';
component:
| 'zapret'
| 'zapret2'
| 'byedpi'
| 'wdtt'
| 'olcrtc'
| 'fptn'
| 'tailscale';
installed: boolean;
checkKey: UpdatesActionKey;
installKey: UpdatesActionKey;
@@ -1197,6 +1223,7 @@ const COMPONENT_REPO_URLS: Record<Tachyon.ComponentName, string> = {
byedpi: 'https://github.com/DPITrickster/ByeDPI-OpenWrt',
wdtt: 'https://github.com/SpaceNeuroX/qwdtt-openwrt',
olcrtc: 'https://github.com/alekvol/openwrt-olcrtc',
fptn: 'https://github.com/fptn-project/fptn',
tailscale: 'https://openwrt.org/packages/pkgdata/tailscale',
direct_bypass: '',
torrserver_direct: '',
@@ -1212,6 +1239,7 @@ function getComponentCards(): ComponentCard[] {
const byedpiInstalled = Boolean(systemInfo.byedpi_installed);
const wdttInstalled = Boolean(systemInfo.wdtt_installed);
const olcrtcInstalled = Boolean(systemInfo.olcrtc_installed);
const fptnInstalled = Boolean(systemInfo.fptn_installed);
const tailscaleInstalled = Boolean(systemInfo.tailscale_installed);
const singBoxInstalled = !isNotInstalled(systemInfo.sing_box_version);
const singBoxStable =
@@ -1339,6 +1367,14 @@ function getComponentCards(): ComponentCard[] {
removeKey: 'olcrtcRemove',
rollbackKey: 'olcrtcRollback',
});
const fptnActions = getOptionalComponentActions({
component: 'fptn',
installed: fptnInstalled,
checkKey: 'fptnCheck',
installKey: 'fptnInstall',
removeKey: 'fptnRemove',
rollbackKey: 'fptnRollback',
});
const tailscaleActions = getOptionalComponentActions({
component: 'tailscale',
installed: tailscaleInstalled,
@@ -1537,6 +1573,21 @@ function getComponentCards(): ComponentCard[] {
actions: olcrtcActions,
supportsVersions: true,
},
{
component: 'fptn',
column: 1,
title: 'FPTN',
version: systemInfoLoading
? _('Loading...')
: fptnInstalled
? systemInfo.fptn_version
: _('Not installed'),
latestVersion: getLatestVersion('fptn'),
releaseUrl: getGitHubReleaseUrl('fptn'),
repoUrl: COMPONENT_REPO_URLS.fptn,
actions: fptnActions,
supportsVersions: true,
},
{
component: 'tailscale',
column: 1,
+5
View File
@@ -651,6 +651,10 @@ export namespace Tachyon {
olcrtc_installed: 0 | 1;
tailscale_version: string;
tailscale_installed: 0 | 1;
fptn_version: string;
fptn_installed: 0 | 1;
fptn_backup_version?: string;
fptn_backup_time?: number;
direct_bypass_enabled?: 0 | 1;
direct_bypass_address?: string;
direct_bypass_port?: string;
@@ -743,6 +747,7 @@ export namespace Tachyon {
| 'byedpi'
| 'wdtt'
| 'olcrtc'
| 'fptn'
| 'tailscale'
| 'direct_bypass'
| 'torrserver_direct';
@@ -5997,6 +5997,10 @@ var initialDiagnosticStore = {
tailscale_installed: 0,
tailscale_backup_version: "",
tailscale_backup_time: 0,
fptn_version: "loading",
fptn_installed: 0,
fptn_backup_version: "",
fptn_backup_time: 0,
server_inbounds_enabled_count: -1,
direct_bypass_enabled: 0,
direct_bypass_address: "",
@@ -6081,6 +6085,10 @@ var initialDiagnosticStore = {
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
fptnCheck: { loading: false },
fptnInstall: { loading: false },
fptnRemove: { loading: false },
fptnRollback: { loading: false },
tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
@@ -6098,6 +6106,7 @@ var initialDiagnosticStore = {
byedpi: { status: null, latest_version: "", release_url: "" },
wdtt: { status: null, latest_version: "", release_url: "" },
olcrtc: { status: null, latest_version: "", release_url: "" },
fptn: { status: null, latest_version: "", release_url: "" },
tailscale: { status: null, latest_version: "", release_url: "" },
direct_bypass: { status: null, latest_version: "", release_url: "" },
torrserver_direct: { status: null, latest_version: "", release_url: "" }
@@ -6483,6 +6492,11 @@ var componentActionKeyMap = {
"olcrtc:install_version": "olcrtcInstall",
"olcrtc:remove": "olcrtcRemove",
"olcrtc:rollback": "olcrtcRollback",
"fptn:check_update": "fptnCheck",
"fptn:install": "fptnInstall",
"fptn:install_version": "fptnInstall",
"fptn:remove": "fptnRemove",
"fptn:rollback": "fptnRollback",
"tailscale:check_update": "tailscaleCheck",
"tailscale:install": "tailscaleInstall",
"tailscale:install_version": "tailscaleInstall",
@@ -6666,6 +6680,10 @@ function getEmptyUpdatesActions() {
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
fptnCheck: { loading: false },
fptnInstall: { loading: false },
fptnRemove: { loading: false },
fptnRollback: { loading: false },
tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
@@ -11170,6 +11188,8 @@ var UNKNOWN_SYSTEM_INFO = {
olcrtc_installed: 0,
tailscale_version: _("unknown"),
tailscale_installed: 0,
fptn_version: _("unknown"),
fptn_installed: 0,
server_inbounds_enabled_count: -1,
openwrt_version: _("unknown"),
device_model: _("unknown")
@@ -20984,6 +21004,8 @@ function getComponentCardTitle(component) {
return "WDTT";
case "olcrtc":
return "OlcRTC";
case "fptn":
return "FPTN";
case "tailscale":
return "Tailscale";
default:
@@ -21007,6 +21029,8 @@ function getComponentCurrentVersion(component) {
return sys.wdtt_version;
case "olcrtc":
return sys.olcrtc_version;
case "fptn":
return sys.fptn_version;
case "tailscale":
return sys.tailscale_version;
default:
@@ -21327,6 +21351,16 @@ function patchSystemInfoAfterMutation(result) {
nextSystemInfo.olcrtc_version = version;
}
}
if (result.component === "fptn") {
nextSystemInfo.providerInfoLoaded = true;
if (result.action === "remove") {
nextSystemInfo.fptn_installed = 0;
nextSystemInfo.fptn_version = "not installed";
} else {
nextSystemInfo.fptn_installed = 1;
nextSystemInfo.fptn_version = version;
}
}
if (result.component === "direct_bypass") {
nextSystemInfo.direct_bypass_enabled = result.action === "enable" ? 1 : 0;
}
@@ -21338,7 +21372,7 @@ function patchSystemInfoAfterMutation(result) {
store.set({
diagnosticsSystemInfo: normalizedSystemInfo
});
if (result.component === "zapret" || result.component === "zapret2" || result.component === "byedpi" || result.component === "wdtt" || result.component === "olcrtc") {
if (result.component === "zapret" || result.component === "zapret2" || result.component === "byedpi" || result.component === "wdtt" || result.component === "olcrtc" || result.component === "fptn") {
notifyActionProvidersAvailabilityChanged(normalizedSystemInfo);
}
}
@@ -21756,6 +21790,8 @@ function getComponentBackupVersion(component) {
return sys.wdtt_backup_version || "";
case "olcrtc":
return sys.olcrtc_backup_version || "";
case "fptn":
return sys.fptn_backup_version || "";
case "tailscale":
return sys.tailscale_backup_version || "";
default:
@@ -21806,6 +21842,7 @@ var COMPONENT_REPO_URLS = {
byedpi: "https://github.com/DPITrickster/ByeDPI-OpenWrt",
wdtt: "https://github.com/SpaceNeuroX/qwdtt-openwrt",
olcrtc: "https://github.com/alekvol/openwrt-olcrtc",
fptn: "https://github.com/fptn-project/fptn",
tailscale: "https://openwrt.org/packages/pkgdata/tailscale",
direct_bypass: "",
torrserver_direct: ""
@@ -21820,6 +21857,7 @@ function getComponentCards() {
const byedpiInstalled = Boolean(systemInfo.byedpi_installed);
const wdttInstalled = Boolean(systemInfo.wdtt_installed);
const olcrtcInstalled = Boolean(systemInfo.olcrtc_installed);
const fptnInstalled = Boolean(systemInfo.fptn_installed);
const tailscaleInstalled = Boolean(systemInfo.tailscale_installed);
const singBoxInstalled = !isNotInstalled(systemInfo.sing_box_version);
const singBoxStable = singBoxInstalled && !systemInfo.sing_box_extended && !systemInfo.sing_box_tiny;
@@ -21934,6 +21972,14 @@ function getComponentCards() {
removeKey: "olcrtcRemove",
rollbackKey: "olcrtcRollback"
});
const fptnActions = getOptionalComponentActions({
component: "fptn",
installed: fptnInstalled,
checkKey: "fptnCheck",
installKey: "fptnInstall",
removeKey: "fptnRemove",
rollbackKey: "fptnRollback"
});
const tailscaleActions = getOptionalComponentActions({
component: "tailscale",
installed: tailscaleInstalled,
@@ -22077,6 +22123,17 @@ function getComponentCards() {
actions: olcrtcActions,
supportsVersions: true
},
{
component: "fptn",
column: 1,
title: "FPTN",
version: systemInfoLoading ? _("Loading...") : fptnInstalled ? systemInfo.fptn_version : _("Not installed"),
latestVersion: getLatestVersion("fptn"),
releaseUrl: getGitHubReleaseUrl("fptn"),
repoUrl: COMPONENT_REPO_URLS.fptn,
actions: fptnActions,
supportsVersions: true
},
{
component: "tailscale",
column: 1,
+116
View File
@@ -1417,6 +1417,43 @@ function resolve_olcrtc_release(arch, tag) {
return null;
}
function resolve_fptn_release(arch, tag) {
let asset_ext = is_apk() ? "apk" : "ipk";
let release_series = trim(helper_output("openwrt-release-series", [ "/etc/openwrt_release" ]));
let releases_json = (tag != null && tag != "") ?
fetch_github_release_by_tag_json("fptn-project", "fptn", tag) :
fetch_github_releases_json("fptn-project", "fptn", "30");
if (releases_json != "") {
let resolved = trim(helper_output_input(releases_json, "fptn-select-asset", [ release_series, asset_ext, arch.candidates ]));
let fields = split(resolved, "\t");
if (length(fields) >= 4) {
let ver = extract_arch_package_version(fields[1], fields[0]);
let m = match(ver, /^([0-9]+\.[0-9]+\.[0-9]+)/);
if (m) ver = m[1];
return {
arch: fields[0],
package_name: fields[1],
package_url: fields[2],
release_url: fields[3],
version: ver
};
}
}
if (tag != null && tag != "") {
let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH");
let tag_clean = replace(tag, /^v/, "");
let pkg_name = "fptn-client-" + tag_clean + "-openwrt-" + (release_series != "" ? release_series + ".x" : "24.10.x") + "-" + distrib_arch + "." + asset_ext;
return {
arch: distrib_arch,
package_name: pkg_name,
package_url: "https://github.com/fptn-project/fptn/releases/download/" + tag + "/" + pkg_name,
release_url: "https://github.com/fptn-project/fptn/releases/tag/" + tag,
version: tag_clean
};
}
return null;
}
function download_direct_package(release) {
let package_file = tmp_dir + "/" + release.package_name;
if (!download_with_retry(release.package_url, package_file, release.package_name) || !file_nonempty(package_file))
@@ -1688,6 +1725,47 @@ function install_olcrtc(action, target_tag) {
action_success("olcrtc", action, "OlcRTC package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
}
function install_fptn(action, target_tag) {
init_tmp_dir() || action_fail("fptn", action, "Failed to create temporary directory");
let arch = resolve_arch_candidates();
if (arch == null)
action_fail("fptn", action, "Failed to detect package architecture");
let release = null;
retry_resolve("Resolving FPTN package", function() {
release = resolve_fptn_release(arch, target_tag);
return release != null;
});
if (release == null)
action_fail("fptn", action, "Failed to resolve FPTN package for this router architecture");
let runtime_module = LIB_DIR + "/providers/fptn/runtime.uc";
let installed = provider_installed(runtime_module);
let current_version = provider_package_version(runtime_module);
if (action == "check_update") {
if (!installed)
action_success("fptn", action, "FPTN is not installed", current_version, release.version, 0, "", release.release_url || "");
check_success("fptn", current_version, release.version, release.release_url || "");
}
let pkg = download_direct_package(release);
if (pkg == null)
action_fail("fptn", action, "Failed to download FPTN package");
run_logged("Updating package lists before FPTN package installation", pkg_list_update_command());
if (!run_logged("Installing FPTN package " + pkg.name, pkg_install_files_command([ pkg.file ])))
action_fail("fptn", action, "Failed to install FPTN package", current_version, pkg.version);
disable_standalone_service("fptn");
disable_standalone_service("fptn-client");
restart_tachyon_after_successful_change();
clear_version_caches();
current_version = provider_package_version(runtime_module);
if (current_version == "")
current_version = pkg.version || "unknown";
action_success("fptn", action, "FPTN package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
}
function install_tailscale(action) {
let component = "tailscale";
let label = "Tailscale";
@@ -3071,6 +3149,8 @@ function normalize_component_name(component) {
component = as_string(component);
if (component == "sing-box" || component == "singbox")
return "sing_box";
if (component == "fptn-client" || component == "fptn_client")
return "fptn";
if (component == "tachyon")
return "tachyon";
if (component == "direct-bypass" || component == "directbypass" || component == "direct_proxy")
@@ -3236,6 +3316,24 @@ function create_component_backup(component) {
write_file(meta_file, sprintf("%J\n", meta));
return true;
}
else if (component == "fptn") {
let bin = "/usr/bin/fptn-client-cli";
if (!file_exists(bin)) bin = "/usr/bin/fptn-client";
if (!file_exists(bin)) return true;
let st = fs.stat(bin);
let size = (st && st.size) ? st.size : 0;
if (!check_free_disk_space("/etc", size)) return false;
ensure_dir(bdir);
command_success_from_args([ "cp", "-p", bin, bdir + "/fptn-client-cli" ]);
let version = provider_package_version(LIB_DIR + "/providers/fptn/runtime.uc");
let meta = {
component: "fptn",
version: version,
timestamp: now_seconds()
};
write_file(meta_file, sprintf("%J\n", meta));
return true;
}
return true;
}
@@ -3314,6 +3412,16 @@ function rollback_component(component) {
restart_tachyon_after_successful_change();
action_success("olcrtc", "rollback", "OlcRTC rolled back to " + backup_version, backup_version, "", 1);
}
else if (component == "fptn") {
let backup_bin = bdir + "/fptn-client-cli";
if (!file_exists(backup_bin)) action_fail("fptn", "rollback", "FPTN backup binary is missing");
remove_file("/usr/bin/fptn-client-cli");
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/fptn-client-cli" ]);
command_success_from_args([ "chmod", "0755", "/usr/bin/fptn-client-cli" ]);
clear_version_caches();
restart_tachyon_after_successful_change();
action_success("fptn", "rollback", "FPTN rolled back to " + backup_version, backup_version, "", 1);
}
else if (component == "zapret") {
let backup_bin = bdir + "/nfqws";
if (!file_exists(backup_bin)) action_fail("zapret", "rollback", "Zapret backup binary is missing");
@@ -3393,6 +3501,8 @@ function list_component_releases(component, count) {
owner = "SpaceNeuroX"; repo = "qwdtt-openwrt";
} else if (component == "olcrtc") {
owner = "alekvol"; repo = "openwrt-olcrtc";
} else if (component == "fptn") {
owner = "fptn-project"; repo = "fptn";
} else {
print("[]\n"); return;
}
@@ -3449,6 +3559,8 @@ function install_component_version(component, tag) {
install_wdtt("install", tag);
} else if (component == "olcrtc") {
install_olcrtc("install", tag);
} else if (component == "fptn") {
install_fptn("install", tag);
} else {
action_fail(component, "install_version", "Component " + component + " does not support version installation");
}
@@ -3595,6 +3707,10 @@ function component_action(component, action, extra) {
install_olcrtc(action);
else if (component == "olcrtc" && action == "remove")
remove_optional_component("olcrtc", "olcrtc", "OlcRTC", LIB_DIR + "/providers/olcrtc/runtime.uc");
else if (component == "fptn" && (action == "check_update" || action == "install"))
install_fptn(action);
else if (component == "fptn" && action == "remove")
remove_optional_component("fptn", "fptn-client", "FPTN", LIB_DIR + "/providers/fptn/runtime.uc");
else if (component == "tailscale" && (action == "check_update" || action == "install"))
install_tailscale(action);
else if (component == "tailscale" && action == "remove")
+47 -1
View File
@@ -388,7 +388,7 @@ function openwrt_release_series(path) {
function updates_arch_package_version(package_name, package_arch) {
let version = str_remove_suffix(str_remove_suffix(package_name, ".ipk"), ".apk");
let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-", "wdtt_", "wdtt-", "olcrtc_", "olcrtc-"];
let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-", "fptn-client-", "fptn-client_", "fptn_", "fptn-", "wdtt_", "wdtt-", "olcrtc_", "olcrtc-"];
for (let prefix in prefixes) {
if (str_startswith(version, prefix)) {
@@ -1011,6 +1011,50 @@ function byedpi_select_asset(series, asset_ext, arch_candidates) {
}
}
function fptn_asset_matches(name, series, arch, ext) {
if (!str_startswith(name, "fptn-client-") && !str_startswith(name, "fptn-"))
return false;
if (!str_endswith(name, "." + ext))
return false;
if (series != "" && !str_contains(name, series))
return false;
return str_contains(name, "-" + arch + "." + ext) || str_contains(name, "_" + arch + "." + ext);
}
function select_fptn_asset_from_release(release, series, asset_ext, arch_candidates) {
for (let arch in split(as_string(arch_candidates), " ")) {
if (arch == "")
continue;
for (let asset in array_or_empty(release.assets)) {
if (type(asset) != "object")
continue;
let name = as_string(asset.name || "");
let url = as_string(asset.browser_download_url || "");
if (url != "" && fptn_asset_matches(name, series, arch, asset_ext)) {
print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n");
return true;
}
}
}
return false;
}
function fptn_select_asset(series, asset_ext, arch_candidates) {
let releases = releases_array_or_wrapped(read_stdin_json());
for (let pass = 0; pass < 2; pass++) {
let current_series = pass == 0 ? as_string(series) : "";
for (let release in releases) {
if (type(release) != "object")
continue;
if (release.draft === true || (length(releases) > 1 && release.prerelease === true))
continue;
if (select_fptn_asset_from_release(release, current_series, asset_ext, arch_candidates))
return;
}
}
}
function wdtt_select_asset(series, asset_ext, arch_candidates) {
let releases = releases_array_or_wrapped(read_stdin_json());
@@ -1494,6 +1538,8 @@ else if (mode == "wdtt-select-asset")
wdtt_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "olcrtc-select-asset")
olcrtc_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "fptn-select-asset")
fptn_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "sing-box-extended-release-tag")
sing_box_extended_release_tag();
else if (mode == "sing-box-lx-release-tag")
+4 -2
View File
@@ -622,7 +622,7 @@ function normalize_component_name(component) {
function valid_component_name(component) {
component = normalize_component_name(component);
return component == "tachyon" || component == "sing_box" || component == "zapret" ||
component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "tailscale";
component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "fptn" || component == "tailscale";
}
function settings_component_auto_update_enabled(settings) {
@@ -1601,7 +1601,7 @@ function component_update_check_cache() {
let results = [];
if (enabled) {
for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi", "wdtt", "olcrtc" ]) {
for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi", "wdtt", "olcrtc", "fptn" ]) {
let value = read_json_file(component_update_check_cache_path(component));
if (component_update_check_result_cacheable(value))
push(results, value);
@@ -2031,6 +2031,8 @@ function automatic_component_check_names() {
push(result, "wdtt");
if (module_success([ LIB_DIR + "/providers/olcrtc/runtime.uc", "installed" ]))
push(result, "olcrtc");
if (module_success([ LIB_DIR + "/providers/fptn/runtime.uc", "installed" ]))
push(result, "fptn");
return result;
}
+17 -1
View File
@@ -340,6 +340,10 @@ function is_olcrtc_action(action) {
return as_string(action) == "olcrtc";
}
function is_fptn_action(action) {
return as_string(action) == "fptn";
}
function wdtt_sections() {
let result = [];
let sections = uci_core.section_objects(CONFIG_NAME, "section");
@@ -360,6 +364,16 @@ function olcrtc_sections() {
return result;
}
function fptn_sections() {
let result = [];
let sections = uci_core.section_objects(CONFIG_NAME, "section");
for (let section in sections) {
if (bool_option(section, "enabled", true) && option(section, "action", "") == "fptn")
push(result, section);
}
return result;
}
function normalize_action(action) {
action = as_string(action);
return is_connections_action(action) ? "connection" : action;
@@ -1634,6 +1648,8 @@ return {
cli_delete_section,
wdtt_sections,
olcrtc_sections,
fptn_sections,
is_wdtt_action,
is_olcrtc_action
is_olcrtc_action,
is_fptn_action
};
+11
View File
@@ -27,6 +27,8 @@ function binary_installed(package_name) {
return fs.stat("/opt/zapret/nfq/nfqws") != null || fs.stat("/opt/zapret/nfqws") != null;
if (package_name == "byedpi")
return fs.stat("/usr/bin/ciadpi") != null;
if (package_name == "fptn" || package_name == "fptn-client")
return fs.stat("/usr/bin/fptn-client-cli") != null || fs.stat("/usr/bin/fptn-client") != null;
if (package_name == "sing-box" || package_name == "sing-box-extended" || package_name == "sing-box-lx")
return fs.stat("/usr/bin/sing-box") != null;
return false;
@@ -151,6 +153,15 @@ function binary_version(package_name) {
let m = match(out, /([0-9a-zA-Z._-]+)/);
if (m) return m[1];
}
if ((package_name == "fptn" || package_name == "fptn-client")) {
for (let p in [ "/usr/bin/fptn-client-cli", "/usr/bin/fptn-client" ]) {
if (fs.stat(p) != null) {
let out = command_output_args([ p, "--version" ]);
let m = match(out, /version[ \t]*([0-9a-zA-Z._-]+)/i) || match(out, /([0-9]+\.[0-9a-zA-Z._-]+)/);
if (m) return m[1];
}
}
}
if ((package_name == "sing-box" || package_name == "sing-box-extended" || package_name == "sing-box-lx") && fs.stat("/usr/bin/sing-box") != null) {
let out = command_output_args([ "/usr/bin/sing-box", "version" ]);
let m = match(out, /version[ \t]*([0-9a-zA-Z._-]+)/i);
@@ -59,6 +59,7 @@ const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
const BYEDPI_RUNTIME_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
const WDTT_RUNTIME_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
const OLCRTC_RUNTIME_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
const FPTN_RUNTIME_UC = LIB_DIR + "/providers/fptn/runtime.uc";
const TAILSCALE_RUNTIME_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc";
const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc";
@@ -1148,6 +1149,8 @@ function build_system_info() {
let byedpi_version = byedpi_installed ? provider_version(BYEDPI_RUNTIME_UC) : "not installed";
let tailscale_installed = provider_installed(TAILSCALE_RUNTIME_UC) ? 1 : 0;
let tailscale_version = tailscale_installed ? provider_version(TAILSCALE_RUNTIME_UC) : "not installed";
let fptn_installed = provider_installed(FPTN_RUNTIME_UC) ? 1 : 0;
let fptn_version = fptn_installed ? provider_version(FPTN_RUNTIME_UC) : "not installed";
let device_model = first_line_value("/tmp/sysinfo/model", "unknown");
let direct_bypass_enabled = bool_option(settings(), "direct_bypass_enabled", false) ? 1 : 0;
@@ -1177,6 +1180,7 @@ function build_system_info() {
let zapret2_meta = read_backup_meta("zapret2");
let byedpi_meta = read_backup_meta("byedpi");
let tailscale_meta = read_backup_meta("tailscale");
let fptn_meta = read_backup_meta("fptn");
return {
tachyon_version: TACHYON_VERSION,
@@ -1208,6 +1212,10 @@ function build_system_info() {
tailscale_installed,
tailscale_backup_version: tailscale_meta ? as_string(tailscale_meta.version) : "",
tailscale_backup_time: tailscale_meta ? int(tailscale_meta.timestamp || 0) : 0,
fptn_version,
fptn_installed,
fptn_backup_version: fptn_meta ? as_string(fptn_meta.version) : "",
fptn_backup_time: fptn_meta ? int(fptn_meta.timestamp || 0) : 0,
direct_bypass_enabled,
direct_bypass_address,
direct_bypass_port,
@@ -0,0 +1,39 @@
let fs = require("fs");
function config(ctx) {
let env_get = function(key, fallback) {
let val = getenv(key);
return val != null ? val : fallback;
};
let bin = env_get("TACHYON_FPTN_BIN", "/usr/bin/fptn-client-cli");
if (fs.stat(bin) == null && fs.stat("/usr/bin/fptn-client") != null)
bin = "/usr/bin/fptn-client";
let state_dir = env_get("TACHYON_FPTN_STATE_DIR", "/var/run/tachyon/fptn");
return {
kind: "fptn",
action: "fptn",
binary: bin,
tun_interface: env_get("TACHYON_FPTN_TUN", "tun-fptn"),
route_table: env_get("TACHYON_FPTN_ROUTE_TABLE", "4249"),
fwmark: env_get("TACHYON_FPTN_FWMARK", "0x00300000"),
mark_mask: env_get("TACHYON_FPTN_MARK_MASK", "0x00ff0000"),
rule_priority: env_get("TACHYON_FPTN_RULE_PRIORITY", "102"),
state_dir: state_dir,
pid_file: state_dir + "/fptn.pid",
log_file: state_dir + "/fptn.log",
package_name: "fptn",
runtime_path: (ctx && ctx.lib_dir ? ctx.lib_dir : "/usr/lib/tachyon") + "/providers/fptn/runtime.uc",
config_name: "fptn",
status_label: "fptn",
check_prefix: "fptn"
};
}
function validator() {
return require("providers.fptn.validator");
}
return { config, validator };
@@ -0,0 +1,291 @@
#!/usr/bin/env ucode
let fs = require("fs");
let common = require("core.common");
let uci_core = require("core.uci");
let connections = require("config.connections");
let as_string = common.as_string;
let bool_value = common.bool_value;
let write_json = common.write_json;
let shell_quote = common.shell_quote;
let command_from_args = common.command_from_args;
let command_output = common.command_output;
let command_status = common.command_status;
let command_success_from_args = common.command_success_from_args;
let object_or_empty = common.object_or_empty;
let option = common.option;
let bool_option = common.bool_option;
let section_name = common.section_name;
let remove_file = common.remove_file;
const MODE = as_string(getenv("TACHYON_CONFIG_NAME")) || "tachyon";
const LIB_DIR = as_string(getenv("TACHYON_LIB_DIR")) || "/usr/lib/tachyon";
const CONFIG_NAME = MODE;
let cfg = require("providers.fptn.common").config({ lib_dir: LIB_DIR });
function log_message(msg, level) {
level = level || "info";
command_status("logger -t tachyon-fptn -p " + (level == "fatal" ? "user.err" : level == "warn" ? "user.warning" : "user.info") + " " + shell_quote(msg));
}
function provider_available() {
let st = fs.stat(cfg.binary);
return st != null && st.mode != null && (int(st.mode) & 73) != 0;
}
function enabled_sections() {
if (connections && connections.fptn_sections)
return connections.fptn_sections();
let result = [];
let sections = uci_core.section_objects(CONFIG_NAME, "section");
for (let s in sections) {
if (bool_option(s, "enabled", true) && option(s, "action", "") == "fptn")
push(result, s);
}
return result;
}
function enabled_rule_count() {
return length(enabled_sections());
}
function running_pid() {
let pid_data = trim(as_string(fs.readfile(cfg.pid_file) || ""));
if (pid_data == "" || match(pid_data, /[^0-9]/))
return null;
let pid = int(pid_data);
if (pid <= 0)
return null;
if (fs.stat("/proc/" + pid) != null)
return pid;
remove_file(cfg.pid_file);
return null;
}
function package_version() {
if (!provider_available())
return "";
let out = trim(command_output(shell_quote(cfg.binary) + " --version 2>&1 || true"));
let m = match(out, /version[ \t]*([0-9a-zA-Z._-]+)/i) || match(out, /([0-9]+\.[0-9a-zA-Z._-]+)/);
if (m)
return m[1];
let pkgs = require("core.packages");
if (pkgs && pkgs.version)
return pkgs.version("fptn") || pkgs.version("fptn-client") || "";
return "";
}
function remove_kernel_routing() {
let p = cfg.rule_priority || "102";
let mark_spec = cfg.fwmark + "/" + cfg.mark_mask;
for (let i = 0; i < 5; i++) {
if (command_status("ip rule del fwmark " + mark_spec + " table " + cfg.route_table + " priority " + p + " 2>/dev/null") != 0)
break;
}
command_status("ip route flush table " + cfg.route_table + " 2>/dev/null; true");
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0)
command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " down 2>/dev/null; true");
}
function install_kernel_routing() {
remove_kernel_routing();
command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " up 2>/dev/null; true");
let route_ok = command_status("ip route replace default dev " + shell_quote(cfg.tun_interface) + " table " + cfg.route_table + " 2>/dev/null") == 0;
if (!route_ok) {
log_message("Failed to add default route dev " + cfg.tun_interface + " to table " + cfg.route_table, "warn");
return false;
}
let p = cfg.rule_priority || "102";
let mark_spec = cfg.fwmark + "/" + cfg.mark_mask;
let rule_ok = command_status("ip rule add fwmark " + mark_spec + " table " + cfg.route_table + " priority " + p + " 2>/dev/null") == 0;
if (!rule_ok) {
log_message("Failed to add ip rule for table " + cfg.route_table, "warn");
return false;
}
return true;
}
function stop_runtime() {
let pid = running_pid();
if (pid != null) {
log_message("Stopping FPTN daemon (PID " + pid + ")", "info");
command_status("kill -15 " + pid + " 2>/dev/null; true");
let stopped = false;
for (let i = 0; i < 20; i++) {
if (fs.stat("/proc/" + pid) == null) {
stopped = true;
break;
}
system("sleep 0.1 2>/dev/null || sleep 1");
}
if (!stopped && fs.stat("/proc/" + pid) != null) {
log_message("FPTN daemon did not stop gracefully; sending SIGKILL", "warn");
command_status("kill -9 " + pid + " 2>/dev/null; true");
}
}
remove_file(cfg.pid_file);
remove_kernel_routing();
return true;
}
function start_runtime() {
let sections = enabled_sections();
if (length(sections) == 0) {
stop_runtime();
return true;
}
if (!provider_available()) {
log_message("Cannot start FPTN: binary " + cfg.binary + " not found or not executable", "warn");
return false;
}
let section = sections[0];
let token = as_string(option(section, "access_token", ""));
if (token == "") {
log_message("Cannot start FPTN: access_token not configured for section " + section_name(section), "warn");
return false;
}
stop_runtime();
command_status("mkdir -p " + shell_quote(cfg.state_dir) + " 2>/dev/null");
let cmd_args = [
cfg.binary,
"--access-token", token,
"--tun-interface-name", cfg.tun_interface,
"--enable-split-tunnel", "false"
];
let sni = as_string(option(section, "sni", ""));
if (sni != "") {
push(cmd_args, "--sni");
push(cmd_args, sni);
}
let bypass_method = as_string(option(section, "bypass_method", ""));
if (bypass_method != "") {
push(cmd_args, "--bypass-method");
push(cmd_args, bypass_method);
}
let preferred_server = as_string(option(section, "preferred_server", ""));
if (preferred_server != "") {
push(cmd_args, "--preferred-server");
push(cmd_args, preferred_server);
}
let cmd_str = command_from_args(cmd_args) + " >> " + shell_quote(cfg.log_file) + " 2>&1 & echo $! > " + shell_quote(cfg.pid_file);
log_message("Starting FPTN client on interface " + cfg.tun_interface, "info");
system(cmd_str);
let started = false;
for (let i = 0; i < 30; i++) {
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0) {
started = true;
break;
}
let p = running_pid();
if (p == null && i > 5)
break;
system("sleep 0.1 2>/dev/null || sleep 1");
}
if (!started) {
log_message("FPTN interface " + cfg.tun_interface + " did not come up", "warn");
return false;
}
install_kernel_routing();
log_message("FPTN client successfully started on " + cfg.tun_interface + " (table " + cfg.route_table + ")", "info");
return true;
}
function status_json() {
let installed = provider_available();
let pid = running_pid();
let running = pid != null;
let rule_count = enabled_rule_count();
let ver = package_version();
write_json({
installed: installed,
configured: rule_count > 0,
enabled_rule_count: rule_count,
service_running: running,
pid: pid,
version: ver,
binary: cfg.binary,
tun_interface: cfg.tun_interface,
route_table: cfg.route_table,
log_file: cfg.log_file,
ready: installed && running && rule_count > 0,
status_message: running ? "FPTN is running" : (installed ? "FPTN is installed but not running" : "FPTN is not installed")
});
return true;
}
function check_json() {
write_json({
fptn_installed: provider_available() ? 1 : 0,
fptn_version: package_version(),
binary: cfg.binary
});
return true;
}
function module_exports() {
return {
provider_available: provider_available,
package_version: package_version,
start_runtime: start_runtime,
stop_runtime: stop_runtime,
status_json: status_json,
check_json: check_json
};
}
if (sourcepath(1) != null && sourcepath(1) != "")
return module_exports();
let mode = as_string(ARGV[0]);
if (mode == "start-runtime")
exit(start_runtime() ? 0 : 1);
else if (mode == "stop-runtime")
exit(stop_runtime() ? 0 : 1);
else if (mode == "restart-runtime") {
stop_runtime();
exit(start_runtime() ? 0 : 1);
}
else if (mode == "status")
status_json();
else if (mode == "check")
check_json();
else if (mode == "installed" || mode == "provider-available")
exit(provider_available() ? 0 : 1);
else if (mode == "package-version" || mode == "version")
printf("%s\n", package_version());
else if (mode == "enabled-rule-count")
printf("%d\n", enabled_rule_count());
else {
warn("Usage: providers/fptn/runtime.uc <start-runtime|stop-runtime|restart-runtime|status|check|installed|version>\n");
exit(1);
}
@@ -0,0 +1,23 @@
let common = require("core.common");
let as_string = common.as_string;
function is_valid_token(token) {
token = trim(as_string(token));
return length(token) > 0;
}
function validate_section(section) {
if (!section)
return { valid: false, error: "Section is missing" };
let token = as_string(section.access_token || "");
if (!is_valid_token(token))
return { valid: false, error: "Missing or invalid FPTN access token" };
return { valid: true };
}
return {
is_valid_token,
validate_section
};
@@ -122,6 +122,7 @@ const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
const TAILSCALE_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
const WDTT_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
const OLCRTC_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
const FPTN_UC = LIB_DIR + "/providers/fptn/runtime.uc";
const PARENTAL_QUOTA_UC = LIB_DIR + "/service/parental_quota.uc";
const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
@@ -943,6 +944,7 @@ function start_main() {
module_success(ZAPRET2_UC, [ "start-runtime" ]);
module_success(WDTT_UC, [ "start-runtime" ]);
module_success(OLCRTC_UC, [ "start-runtime" ]);
module_success(FPTN_UC, [ "start-runtime" ]);
module_success(PARENTAL_QUOTA_UC, [ "install-cron" ]);
module_success(PARENTAL_QUOTA_UC, [ "tick" ]);
@@ -1036,6 +1038,8 @@ function stop_main() {
log_message("WDTT stop failed (non-fatal)", "warn");
if (!module_success(OLCRTC_UC, [ "stop-runtime" ]))
log_message("OlcRTC stop failed (non-fatal)", "warn");
if (!module_success(FPTN_UC, [ "stop-runtime" ]))
log_message("FPTN stop failed (non-fatal)", "warn");
if (!module_success(TAILSCALE_UC, [ "stop-runtime" ]))
log_message("Tailscale stop failed (non-fatal)", "warn");
module_success(PARENTAL_QUOTA_UC, [ "remove-cron" ]);
@@ -1230,6 +1234,7 @@ function parse_reload_plan(output) {
needs_byedpi_restart: 0,
needs_wdtt_restart: 0,
needs_olcrtc_restart: 0,
needs_fptn_restart: 0,
needs_dnsmasq_configure: 0,
needs_dnsmasq_restore: 0,
needs_cron_refresh: 0,
@@ -1268,6 +1273,7 @@ function reload_actions_summary(plan) {
actions = append_reload_action(actions, plan.needs_byedpi_restart, "ByeDPI");
actions = append_reload_action(actions, plan.needs_wdtt_restart, "WDTT");
actions = append_reload_action(actions, plan.needs_olcrtc_restart, "OlcRTC");
actions = append_reload_action(actions, plan.needs_fptn_restart, "FPTN");
actions = append_reload_action(actions, plan.needs_dnsmasq_configure || plan.needs_dnsmasq_restore, "dnsmasq");
actions = append_reload_action(actions, plan.needs_cron_refresh, "scheduled jobs");
actions = append_reload_action(actions, plan.needs_list_update, "remote lists");
@@ -1462,6 +1468,8 @@ function reload(reason) {
module_success(WDTT_UC, [ "stop-runtime" ]);
if (plan.needs_olcrtc_restart == 1)
module_success(OLCRTC_UC, [ "stop-runtime" ]);
if (plan.needs_fptn_restart == 1)
module_success(FPTN_UC, [ "stop-runtime" ]);
if (plan.needs_hosts_update == 1 && module_success(STATE_UC, [ "has-hosts-list-update-sources" ]))
module_success(HOSTS_UC, [ "list-update" ]);
@@ -1534,6 +1542,8 @@ function reload(reason) {
module_success(WDTT_UC, [ "start-runtime" ]);
if (plan.needs_olcrtc_restart == 1)
module_success(OLCRTC_UC, [ "start-runtime" ]);
if (plan.needs_fptn_restart == 1)
module_success(FPTN_UC, [ "start-runtime" ]);
if (plan.needs_byedpi_restart == 1)
module_success(BYEDPI_UC, [ "start-runtime" ]);
// Native Tailscale must be up before sing-box so tailnet routes win over
+8
View File
@@ -65,6 +65,7 @@ function emit_reload_plan(previous, current, context) {
byedpi_runtime: current.byedpi_runtime != previous.byedpi_runtime,
wdtt_runtime: current.wdtt_runtime != previous.wdtt_runtime,
olcrtc_runtime: current.olcrtc_runtime != previous.olcrtc_runtime,
fptn_runtime: current.fptn_runtime != previous.fptn_runtime,
list: current.list != previous.list,
hosts_list: current.hosts_list != previous.hosts_list,
cron: current.cron != previous.cron
@@ -78,6 +79,7 @@ function emit_reload_plan(previous, current, context) {
byedpi_restart: false,
wdtt_restart: false,
olcrtc_restart: false,
fptn_restart: false,
dnsmasq_configure: false,
dnsmasq_restore: false,
cron_refresh: false,
@@ -122,6 +124,9 @@ function emit_reload_plan(previous, current, context) {
if (changed.olcrtc_runtime)
needs.olcrtc_restart = true;
if (changed.fptn_runtime)
needs.fptn_restart = true;
if (changed.dnsmasq) {
if (!current.dont_touch_dhcp)
needs.dnsmasq_configure = true;
@@ -187,6 +192,7 @@ function emit_reload_plan(previous, current, context) {
emit_bool("changed_byedpi_runtime", changed.byedpi_runtime);
emit_bool("changed_wdtt_runtime", changed.wdtt_runtime);
emit_bool("changed_olcrtc_runtime", changed.olcrtc_runtime);
emit_bool("changed_fptn_runtime", changed.fptn_runtime);
emit_bool("changed_cron", changed.cron);
emit_bool("changed_list", changed.list);
emit_bool("changed_hosts_list", changed.hosts_list);
@@ -198,6 +204,7 @@ function emit_reload_plan(previous, current, context) {
emit_bool("needs_byedpi_restart", needs.byedpi_restart);
emit_bool("needs_wdtt_restart", needs.wdtt_restart);
emit_bool("needs_olcrtc_restart", needs.olcrtc_restart);
emit_bool("needs_fptn_restart", needs.fptn_restart);
emit_bool("needs_dnsmasq_configure", needs.dnsmasq_configure);
emit_bool("needs_dnsmasq_restore", needs.dnsmasq_restore);
emit_bool("needs_cron_refresh", needs.cron_refresh);
@@ -293,6 +300,7 @@ function plan_state_from_file(path) {
byedpi_runtime: as_string(state.byedpi_runtime_signature),
wdtt_runtime: as_string(state.wdtt_runtime_signature),
olcrtc_runtime: as_string(state.olcrtc_runtime_signature),
fptn_runtime: as_string(state.fptn_runtime_signature),
list: as_string(state.list_signature),
hosts_list: as_string(state.hosts_list_signature),
cron: as_string(state.cron_signature),
+26
View File
@@ -132,6 +132,7 @@ const RELOAD_STATE_FIELDS = [
"byedpi_runtime_signature",
"wdtt_runtime_signature",
"olcrtc_runtime_signature",
"fptn_runtime_signature",
"list_signature",
"hosts_list_signature",
"cron_signature",
@@ -1854,6 +1855,24 @@ function olcrtc_runtime_signature_body(sections) {
return body;
}
function fptn_runtime_signature_body(sections) {
let body = "";
for (let section in sections) {
section = object_or_empty(section);
if (!section_action_is_enabled(section, "fptn"))
continue;
let name = section_name(section);
body = signature_add_value(body, "fptn." + name + ".access_token", option(section, "access_token", ""));
body = signature_add_value(body, "fptn." + name + ".sni", option(section, "sni", ""));
body = signature_add_value(body, "fptn." + name + ".bypass_method", option(section, "bypass_method", ""));
body = signature_add_value(body, "fptn." + name + ".preferred_server", option(section, "preferred_server", ""));
}
return body;
}
function reload_state_values_from_sources(format, settings, sections, servers, dnsmasq, legacy_dnsmasq_present, mwan3_active_value, schedules, profiles, guest_modes) {
return {
format: as_string(format),
@@ -1868,6 +1887,7 @@ function reload_state_values_from_sources(format, settings, sections, servers, d
byedpi_runtime_signature: signature_hash(byedpi_runtime_signature_body(sections)),
wdtt_runtime_signature: signature_hash(wdtt_runtime_signature_body(sections)),
olcrtc_runtime_signature: signature_hash(olcrtc_runtime_signature_body(sections)),
fptn_runtime_signature: signature_hash(fptn_runtime_signature_body(sections)),
list_signature: signature_hash(list_update_signature_body(sections)),
hosts_list_signature: signature_hash(hosts_list_signature_body(sections)),
cron_signature: signature_hash(cron_signature_body(settings, sections)),
@@ -2276,6 +2296,12 @@ else if (mode == "olcrtc-runtime-signature-fixture") {
let data = fixture_data(ARGV[1]);
exit(print_signature_hash(olcrtc_runtime_signature_body(fixture_section_list(data))) ? 0 : 1);
}
else if (mode == "fptn-runtime-signature")
exit(print_signature_hash(fptn_runtime_signature_body(uci_sections("section"))) ? 0 : 1);
else if (mode == "fptn-runtime-signature-fixture") {
let data = fixture_data(ARGV[1]);
exit(print_signature_hash(fptn_runtime_signature_body(fixture_section_list(data))) ? 0 : 1);
}
else if (mode == "dont-touch-dhcp")
print(dont_touch_dhcp_value(uci_settings()), "\n");
else if (mode == "dont-touch-dhcp-fixture") {
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
UPDATER_UC="$ROOT_DIR/tachyon/files/usr/lib/components/updater.uc"
PACKAGES_UC="$ROOT_DIR/tachyon/files/usr/lib/core/packages.uc"
FPTN_RUNTIME_UC="$ROOT_DIR/tachyon/files/usr/lib/providers/fptn/runtime.uc"
STATE_UC="$ROOT_DIR/tachyon/files/usr/lib/service/state.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
}
ucode_run() {
ucode -L "$TACHYON_LIB" "$@"
}
# 1. Test updater.uc asset selection for FPTN
cat >"$WORK_DIR/releases.json" <<'EOF_JSON'
[
{
"tag_name": "v0.4.5",
"name": "0.4.5",
"html_url": "https://github.com/fptn-project/fptn/releases/tag/v0.4.5",
"assets": [
{
"name": "fptn-client-0.4.5-openwrt-23.05.x-x86_64.ipk",
"browser_download_url": "https://github.com/fptn-project/fptn/releases/download/v0.4.5/fptn-client-0.4.5-openwrt-23.05.x-x86_64.ipk"
},
{
"name": "fptn-client-0.4.5-openwrt-24.10.x-x86_64.apk",
"browser_download_url": "https://github.com/fptn-project/fptn/releases/download/v0.4.5/fptn-client-0.4.5-openwrt-24.10.x-x86_64.apk"
},
{
"name": "fptn-client-0.4.5-openwrt-24.10.x-aarch64_cortex-a53.apk",
"browser_download_url": "https://github.com/fptn-project/fptn/releases/download/v0.4.5/fptn-client-0.4.5-openwrt-24.10.x-aarch64_cortex-a53.apk"
}
]
}
]
EOF_JSON
SELECTED_APK="$(cat "$WORK_DIR/releases.json" | ucode_run "$UPDATER_UC" fptn-select-asset "24.10" "apk" "aarch64_cortex-a53 aarch64_generic")"
ARCH_MATCH="$(echo "$SELECTED_APK" | awk '{print $1}')"
NAME_MATCH="$(echo "$SELECTED_APK" | awk '{print $2}')"
assert_eq "aarch64_cortex-a53" "$ARCH_MATCH" "fptn asset arch match"
assert_eq "fptn-client-0.4.5-openwrt-24.10.x-aarch64_cortex-a53.apk" "$NAME_MATCH" "fptn asset name match"
# 2. Test package version extraction
PARSED_VER="$(ucode_run "$UPDATER_UC" updates-arch-package-version "fptn-client-0.4.5-openwrt-24.10.x-x86_64.ipk" "x86_64")"
assert_eq "0.4.5-openwrt-24.10.x" "$PARSED_VER" "fptn package version strip"
# 3. Test packages.uc binary detection and version
MOCK_BIN="$WORK_DIR/fptn-client-cli"
cat >"$MOCK_BIN" <<'EOF_BIN'
#!/usr/bin/env sh
echo "fptn-client 0.4.5"
EOF_BIN
chmod 0755 "$MOCK_BIN"
export TACHYON_FPTN_BIN="$MOCK_BIN"
FPTN_VER="$(TACHYON_LIB_DIR="$TACHYON_LIB" ucode_run "$FPTN_RUNTIME_UC" version)"
assert_eq "0.4.5" "$FPTN_VER" "fptn runtime version command"
FPTN_INSTALLED="$(TACHYON_LIB_DIR="$TACHYON_LIB" ucode_run "$FPTN_RUNTIME_UC" installed && echo "1" || echo "0")"
assert_eq "1" "$FPTN_INSTALLED" "fptn runtime installed check"
# 4. Test state.uc fptn runtime signature fixture
cat >"$WORK_DIR/state_fixture.json" <<'EOF_FIXTURE'
{
"section": [
{
".name": "sec1",
"enabled": "1",
"action": "fptn",
"access_token": "secret_token_123",
"sni": "my.domain.com"
}
]
}
EOF_FIXTURE
SIG_1="$(ucode_run "$STATE_UC" fptn-runtime-signature-fixture "$WORK_DIR/state_fixture.json")"
[ -n "$SIG_1" ] || fail "fptn signature should not be empty"
cat >"$WORK_DIR/state_fixture2.json" <<'EOF_FIXTURE2'
{
"section": [
{
".name": "sec1",
"enabled": "1",
"action": "fptn",
"access_token": "secret_token_diff",
"sni": "my.domain.com"
}
]
}
EOF_FIXTURE2
SIG_2="$(ucode_run "$STATE_UC" fptn-runtime-signature-fixture "$WORK_DIR/state_fixture2.json")"
[ "$SIG_1" != "$SIG_2" ] || fail "fptn signature should differ when token changes"
echo "fptn component tests passed"
+1
View File
@@ -1457,6 +1457,7 @@ zapret2_runtime_signature=$(state_ucode zapret2-runtime-signature-fixture "$WORK
byedpi_runtime_signature=$(state_ucode byedpi-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
wdtt_runtime_signature=$(state_ucode wdtt-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
olcrtc_runtime_signature=$(state_ucode olcrtc-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
fptn_runtime_signature=$(state_ucode fptn-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
list_signature=$(state_ucode list-update-signature-fixture "$WORK_DIR/reload-state-signatures.json")
hosts_list_signature=$(state_ucode hosts-list-signature-fixture "$WORK_DIR/reload-state-signatures.json")
cron_signature=$(state_ucode cron-signature-fixture "$WORK_DIR/reload-state-signatures.json")