feat(tailscale): нативный режим tailscaled и компонент Tailscale
- новый компонент Tailscale: установка пакета из фида, карточка в Обновлениях - режим native для секций Tailscale: системный tailscaled вместо sing-box endpoint - доступ к тейлнету с LAN без клиента: маршруты 100.64.0.0/10 и MagicDNS через dnsmasq - masquerade и exit-node (opt-in) с обходом tproxy по зарезервированной маске - validator: требование tailscaled и auth-key для native; bypass-правила в mangle - тесты: генерация конфига, смоук рантайма с моками, обновлена сигнатура состояния
This commit is contained in:
@@ -3726,6 +3726,30 @@ msgid "Tachyon-managed nfqws2 runtime is ready"
|
|||||||
msgstr "NFQWS2 Tachyon готов"
|
msgstr "NFQWS2 Tachyon готов"
|
||||||
|
|
||||||
msgid "Tailscale auth key"
|
msgid "Tailscale auth key"
|
||||||
|
msgid "Tailscale mode"
|
||||||
|
msgstr "Режим Tailscale"
|
||||||
|
|
||||||
|
msgid "sing-box endpoint (userspace, default)"
|
||||||
|
msgstr "Endpoint в sing-box (userspace, по умолчанию)"
|
||||||
|
|
||||||
|
msgid "Native tailscaled (tailnet reachable from LAN without client)"
|
||||||
|
msgstr "Нативный tailscaled (тейлнет доступен из LAN без клиента)"
|
||||||
|
|
||||||
|
msgid "Masquerade LAN traffic into tailnet"
|
||||||
|
msgstr "Маскарадинг LAN-трафика в тейлнет"
|
||||||
|
|
||||||
|
msgid "SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes."
|
||||||
|
msgstr "Подменять LAN-адреса при выходе через tailscale0, чтобы пиры могли отвечать. Нужно, если не все пиры принимают маршруты."
|
||||||
|
|
||||||
|
msgid "Use tailnet exit node for LAN"
|
||||||
|
msgstr "Использовать exit-node тейлнета для LAN"
|
||||||
|
|
||||||
|
msgid "Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines."
|
||||||
|
msgstr "Направлять весь не-локальный LAN-трафик через exit-node тейлнета вместо прокси-движков."
|
||||||
|
|
||||||
|
msgid "Tailscale"
|
||||||
|
msgstr "Tailscale"
|
||||||
|
|
||||||
msgstr "Ключ аутентификации Tailscale"
|
msgstr "Ключ аутентификации Tailscale"
|
||||||
|
|
||||||
msgid "Tailscale control URL"
|
msgid "Tailscale control URL"
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
|
|||||||
'byedpi:check_update': 'byedpiCheck',
|
'byedpi:check_update': 'byedpiCheck',
|
||||||
'byedpi:install': 'byedpiInstall',
|
'byedpi:install': 'byedpiInstall',
|
||||||
'byedpi:remove': 'byedpiRemove',
|
'byedpi:remove': 'byedpiRemove',
|
||||||
|
'tailscale:check_update': 'tailscaleCheck',
|
||||||
|
'tailscale:install': 'tailscaleInstall',
|
||||||
|
'tailscale:remove': 'tailscaleRemove',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getComponentActionKey(
|
export function getComponentActionKey(
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ export interface StoreType {
|
|||||||
zapret2_installed: number;
|
zapret2_installed: number;
|
||||||
byedpi_version: string;
|
byedpi_version: string;
|
||||||
byedpi_installed: number;
|
byedpi_installed: number;
|
||||||
|
tailscale_version: string;
|
||||||
|
tailscale_installed: number;
|
||||||
server_inbounds_enabled_count: number;
|
server_inbounds_enabled_count: number;
|
||||||
openwrt_version: string;
|
openwrt_version: string;
|
||||||
device_model: string;
|
device_model: string;
|
||||||
@@ -241,6 +243,9 @@ export interface StoreType {
|
|||||||
byedpiCheck: { loading: boolean };
|
byedpiCheck: { loading: boolean };
|
||||||
byedpiInstall: { loading: boolean };
|
byedpiInstall: { loading: boolean };
|
||||||
byedpiRemove: { loading: boolean };
|
byedpiRemove: { loading: boolean };
|
||||||
|
tailscaleCheck: { loading: boolean };
|
||||||
|
tailscaleInstall: { loading: boolean };
|
||||||
|
tailscaleRemove: { loading: boolean };
|
||||||
};
|
};
|
||||||
updatesChecks: Record<
|
updatesChecks: Record<
|
||||||
Tachyon.ComponentName,
|
Tachyon.ComponentName,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const UNKNOWN_SYSTEM_INFO: StoreType['diagnosticsSystemInfo'] = {
|
|||||||
zapret2_installed: 0,
|
zapret2_installed: 0,
|
||||||
byedpi_version: _('unknown'),
|
byedpi_version: _('unknown'),
|
||||||
byedpi_installed: 0,
|
byedpi_installed: 0,
|
||||||
|
tailscale_version: _('unknown'),
|
||||||
|
tailscale_installed: 0,
|
||||||
server_inbounds_enabled_count: -1,
|
server_inbounds_enabled_count: -1,
|
||||||
openwrt_version: _('unknown'),
|
openwrt_version: _('unknown'),
|
||||||
device_model: _('unknown'),
|
device_model: _('unknown'),
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
|
|||||||
byedpiCheck: { loading: false },
|
byedpiCheck: { loading: false },
|
||||||
byedpiInstall: { loading: false },
|
byedpiInstall: { loading: false },
|
||||||
byedpiRemove: { loading: false },
|
byedpiRemove: { loading: false },
|
||||||
|
tailscaleCheck: { loading: false },
|
||||||
|
tailscaleInstall: { loading: false },
|
||||||
|
tailscaleRemove: { loading: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1651,7 +1651,8 @@ async function renderSectionsWidget() {
|
|||||||
outbounds: section.outbounds.map((outbound) => ({
|
outbounds: section.outbounds.map((outbound) => ({
|
||||||
...outbound,
|
...outbound,
|
||||||
latency:
|
latency:
|
||||||
SERVICE_TYPES.has(outbound.type) || !customProxyLatencies.has(outbound.code)
|
SERVICE_TYPES.has(outbound.type) ||
|
||||||
|
!customProxyLatencies.has(outbound.code)
|
||||||
? outbound.latency
|
? outbound.latency
|
||||||
: customProxyLatencies.get(outbound.code)!,
|
: customProxyLatencies.get(outbound.code)!,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ export const initialDiagnosticStore: Pick<
|
|||||||
zapret2_installed: 0,
|
zapret2_installed: 0,
|
||||||
byedpi_version: 'loading',
|
byedpi_version: 'loading',
|
||||||
byedpi_installed: 0,
|
byedpi_installed: 0,
|
||||||
|
tailscale_version: 'loading',
|
||||||
|
tailscale_installed: 0,
|
||||||
server_inbounds_enabled_count: -1,
|
server_inbounds_enabled_count: -1,
|
||||||
openwrt_version: 'loading',
|
openwrt_version: 'loading',
|
||||||
device_model: 'loading',
|
device_model: 'loading',
|
||||||
@@ -158,6 +160,9 @@ export const initialDiagnosticStore: Pick<
|
|||||||
byedpiCheck: { loading: false },
|
byedpiCheck: { loading: false },
|
||||||
byedpiInstall: { loading: false },
|
byedpiInstall: { loading: false },
|
||||||
byedpiRemove: { loading: false },
|
byedpiRemove: { loading: false },
|
||||||
|
tailscaleCheck: { loading: false },
|
||||||
|
tailscaleInstall: { loading: false },
|
||||||
|
tailscaleRemove: { loading: false },
|
||||||
},
|
},
|
||||||
updatesChecks: {
|
updatesChecks: {
|
||||||
tachyon: { status: null, latest_version: '', release_url: '' },
|
tachyon: { status: null, latest_version: '', release_url: '' },
|
||||||
@@ -165,5 +170,6 @@ export const initialDiagnosticStore: Pick<
|
|||||||
zapret: { status: null, latest_version: '', release_url: '' },
|
zapret: { status: null, latest_version: '', release_url: '' },
|
||||||
zapret2: { status: null, latest_version: '', release_url: '' },
|
zapret2: { status: null, latest_version: '', release_url: '' },
|
||||||
byedpi: { status: null, latest_version: '', release_url: '' },
|
byedpi: { status: null, latest_version: '', release_url: '' },
|
||||||
|
tailscale: { status: null, latest_version: '', release_url: '' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ function getComponentCardTitle(component: Tachyon.ComponentName): string {
|
|||||||
return 'Zapret2';
|
return 'Zapret2';
|
||||||
case 'byedpi':
|
case 'byedpi':
|
||||||
return 'ByeDPI';
|
return 'ByeDPI';
|
||||||
|
case 'tailscale':
|
||||||
|
return 'Tailscale';
|
||||||
default:
|
default:
|
||||||
return String(component);
|
return String(component);
|
||||||
}
|
}
|
||||||
@@ -82,6 +84,8 @@ function getComponentCurrentVersion(
|
|||||||
return sys.zapret2_version;
|
return sys.zapret2_version;
|
||||||
case 'byedpi':
|
case 'byedpi':
|
||||||
return sys.byedpi_version;
|
return sys.byedpi_version;
|
||||||
|
case 'tailscale':
|
||||||
|
return sys.tailscale_version;
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -1003,6 +1007,8 @@ function getComponentInstallKey(
|
|||||||
return 'zapret2Install';
|
return 'zapret2Install';
|
||||||
case 'byedpi':
|
case 'byedpi':
|
||||||
return 'byedpiInstall';
|
return 'byedpiInstall';
|
||||||
|
case 'tailscale':
|
||||||
|
return 'tailscaleInstall';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1038,7 +1044,7 @@ function getOptionalComponentActions({
|
|||||||
installKey,
|
installKey,
|
||||||
removeKey,
|
removeKey,
|
||||||
}: {
|
}: {
|
||||||
component: 'zapret' | 'zapret2' | 'byedpi';
|
component: 'zapret' | 'zapret2' | 'byedpi' | 'tailscale';
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
checkKey: UpdatesActionKey;
|
checkKey: UpdatesActionKey;
|
||||||
installKey: UpdatesActionKey;
|
installKey: UpdatesActionKey;
|
||||||
@@ -1066,6 +1072,7 @@ const COMPONENT_REPO_URLS: Record<Tachyon.ComponentName, string> = {
|
|||||||
zapret: 'https://github.com/remittor/zapret-openwrt',
|
zapret: 'https://github.com/remittor/zapret-openwrt',
|
||||||
zapret2: 'https://github.com/Dushnilin/zapret2-openwrt',
|
zapret2: 'https://github.com/Dushnilin/zapret2-openwrt',
|
||||||
byedpi: 'https://github.com/DPITrickster/ByeDPI-OpenWrt',
|
byedpi: 'https://github.com/DPITrickster/ByeDPI-OpenWrt',
|
||||||
|
tailscale: 'https://openwrt.org/packages/pkgdata/tailscale',
|
||||||
};
|
};
|
||||||
|
|
||||||
function getComponentCards(): ComponentCard[] {
|
function getComponentCards(): ComponentCard[] {
|
||||||
@@ -1076,6 +1083,7 @@ function getComponentCards(): ComponentCard[] {
|
|||||||
const zapretInstalled = Boolean(systemInfo.zapret_installed);
|
const zapretInstalled = Boolean(systemInfo.zapret_installed);
|
||||||
const zapret2Installed = Boolean(systemInfo.zapret2_installed);
|
const zapret2Installed = Boolean(systemInfo.zapret2_installed);
|
||||||
const byedpiInstalled = Boolean(systemInfo.byedpi_installed);
|
const byedpiInstalled = Boolean(systemInfo.byedpi_installed);
|
||||||
|
const tailscaleInstalled = Boolean(systemInfo.tailscale_installed);
|
||||||
const singBoxInstalled = !isNotInstalled(systemInfo.sing_box_version);
|
const singBoxInstalled = !isNotInstalled(systemInfo.sing_box_version);
|
||||||
const singBoxStable =
|
const singBoxStable =
|
||||||
singBoxInstalled &&
|
singBoxInstalled &&
|
||||||
@@ -1176,6 +1184,13 @@ function getComponentCards(): ComponentCard[] {
|
|||||||
installKey: 'byedpiInstall',
|
installKey: 'byedpiInstall',
|
||||||
removeKey: 'byedpiRemove',
|
removeKey: 'byedpiRemove',
|
||||||
});
|
});
|
||||||
|
const tailscaleActions = getOptionalComponentActions({
|
||||||
|
component: 'tailscale',
|
||||||
|
installed: tailscaleInstalled,
|
||||||
|
checkKey: 'tailscaleCheck',
|
||||||
|
installKey: 'tailscaleInstall',
|
||||||
|
removeKey: 'tailscaleRemove',
|
||||||
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -1253,6 +1268,20 @@ function getComponentCards(): ComponentCard[] {
|
|||||||
repoUrl: COMPONENT_REPO_URLS.byedpi,
|
repoUrl: COMPONENT_REPO_URLS.byedpi,
|
||||||
actions: byedpiActions,
|
actions: byedpiActions,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'tailscale',
|
||||||
|
column: 1,
|
||||||
|
title: 'Tailscale',
|
||||||
|
version: systemInfoLoading
|
||||||
|
? _('Loading...')
|
||||||
|
: tailscaleInstalled
|
||||||
|
? systemInfo.tailscale_version
|
||||||
|
: _('Not installed'),
|
||||||
|
latestVersion: getLatestVersion('tailscale'),
|
||||||
|
releaseUrl: getGitHubReleaseUrl('tailscale'),
|
||||||
|
repoUrl: COMPONENT_REPO_URLS.tailscale,
|
||||||
|
actions: tailscaleActions,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -551,6 +551,8 @@ export namespace Tachyon {
|
|||||||
zapret2_installed: 0 | 1;
|
zapret2_installed: 0 | 1;
|
||||||
byedpi_version: string;
|
byedpi_version: string;
|
||||||
byedpi_installed: 0 | 1;
|
byedpi_installed: 0 | 1;
|
||||||
|
tailscale_version: string;
|
||||||
|
tailscale_installed: 0 | 1;
|
||||||
openwrt_version: string;
|
openwrt_version: string;
|
||||||
device_model: string;
|
device_model: string;
|
||||||
generated_at?: number;
|
generated_at?: number;
|
||||||
@@ -627,7 +629,8 @@ export namespace Tachyon {
|
|||||||
| 'sing_box'
|
| 'sing_box'
|
||||||
| 'zapret'
|
| 'zapret'
|
||||||
| 'zapret2'
|
| 'zapret2'
|
||||||
| 'byedpi';
|
| 'byedpi'
|
||||||
|
| 'tailscale';
|
||||||
|
|
||||||
export type ComponentAction =
|
export type ComponentAction =
|
||||||
| 'check_update'
|
| 'check_update'
|
||||||
|
|||||||
@@ -5253,6 +5253,8 @@ var initialDiagnosticStore = {
|
|||||||
zapret2_installed: 0,
|
zapret2_installed: 0,
|
||||||
byedpi_version: "loading",
|
byedpi_version: "loading",
|
||||||
byedpi_installed: 0,
|
byedpi_installed: 0,
|
||||||
|
tailscale_version: "loading",
|
||||||
|
tailscale_installed: 0,
|
||||||
server_inbounds_enabled_count: -1,
|
server_inbounds_enabled_count: -1,
|
||||||
openwrt_version: "loading",
|
openwrt_version: "loading",
|
||||||
device_model: "loading"
|
device_model: "loading"
|
||||||
@@ -5316,14 +5318,18 @@ var initialDiagnosticStore = {
|
|||||||
zapret2Remove: { loading: false },
|
zapret2Remove: { loading: false },
|
||||||
byedpiCheck: { loading: false },
|
byedpiCheck: { loading: false },
|
||||||
byedpiInstall: { loading: false },
|
byedpiInstall: { loading: false },
|
||||||
byedpiRemove: { loading: false }
|
byedpiRemove: { loading: false },
|
||||||
|
tailscaleCheck: { loading: false },
|
||||||
|
tailscaleInstall: { loading: false },
|
||||||
|
tailscaleRemove: { loading: false }
|
||||||
},
|
},
|
||||||
updatesChecks: {
|
updatesChecks: {
|
||||||
tachyon: { status: null, latest_version: "", release_url: "" },
|
tachyon: { status: null, latest_version: "", release_url: "" },
|
||||||
sing_box: { status: null, latest_version: "", release_url: "" },
|
sing_box: { status: null, latest_version: "", release_url: "" },
|
||||||
zapret: { status: null, latest_version: "", release_url: "" },
|
zapret: { status: null, latest_version: "", release_url: "" },
|
||||||
zapret2: { status: null, latest_version: "", release_url: "" },
|
zapret2: { status: null, latest_version: "", release_url: "" },
|
||||||
byedpi: { status: null, latest_version: "", release_url: "" }
|
byedpi: { status: null, latest_version: "", release_url: "" },
|
||||||
|
tailscale: { status: null, latest_version: "", release_url: "" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5680,7 +5686,10 @@ var componentActionKeyMap = {
|
|||||||
"zapret2:remove": "zapret2Remove",
|
"zapret2:remove": "zapret2Remove",
|
||||||
"byedpi:check_update": "byedpiCheck",
|
"byedpi:check_update": "byedpiCheck",
|
||||||
"byedpi:install": "byedpiInstall",
|
"byedpi:install": "byedpiInstall",
|
||||||
"byedpi:remove": "byedpiRemove"
|
"byedpi:remove": "byedpiRemove",
|
||||||
|
"tailscale:check_update": "tailscaleCheck",
|
||||||
|
"tailscale:install": "tailscaleInstall",
|
||||||
|
"tailscale:remove": "tailscaleRemove"
|
||||||
};
|
};
|
||||||
function getComponentActionKey(component, action) {
|
function getComponentActionKey(component, action) {
|
||||||
return componentActionKeyMap[`${component}:${action}`];
|
return componentActionKeyMap[`${component}:${action}`];
|
||||||
@@ -5792,7 +5801,10 @@ function getEmptyUpdatesActions() {
|
|||||||
zapret2Remove: { loading: false },
|
zapret2Remove: { loading: false },
|
||||||
byedpiCheck: { loading: false },
|
byedpiCheck: { loading: false },
|
||||||
byedpiInstall: { loading: false },
|
byedpiInstall: { loading: false },
|
||||||
byedpiRemove: { loading: false }
|
byedpiRemove: { loading: false },
|
||||||
|
tailscaleCheck: { loading: false },
|
||||||
|
tailscaleInstall: { loading: false },
|
||||||
|
tailscaleRemove: { loading: false }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getEmptyDiagnosticsActions() {
|
function getEmptyDiagnosticsActions() {
|
||||||
@@ -9715,6 +9727,8 @@ var UNKNOWN_SYSTEM_INFO = {
|
|||||||
zapret2_installed: 0,
|
zapret2_installed: 0,
|
||||||
byedpi_version: _("unknown"),
|
byedpi_version: _("unknown"),
|
||||||
byedpi_installed: 0,
|
byedpi_installed: 0,
|
||||||
|
tailscale_version: _("unknown"),
|
||||||
|
tailscale_installed: 0,
|
||||||
server_inbounds_enabled_count: -1,
|
server_inbounds_enabled_count: -1,
|
||||||
openwrt_version: _("unknown"),
|
openwrt_version: _("unknown"),
|
||||||
device_model: _("unknown")
|
device_model: _("unknown")
|
||||||
@@ -16572,6 +16586,8 @@ function getComponentCardTitle(component) {
|
|||||||
return "Zapret2";
|
return "Zapret2";
|
||||||
case "byedpi":
|
case "byedpi":
|
||||||
return "ByeDPI";
|
return "ByeDPI";
|
||||||
|
case "tailscale":
|
||||||
|
return "Tailscale";
|
||||||
default:
|
default:
|
||||||
return String(component);
|
return String(component);
|
||||||
}
|
}
|
||||||
@@ -16589,6 +16605,8 @@ function getComponentCurrentVersion(component) {
|
|||||||
return sys.zapret2_version;
|
return sys.zapret2_version;
|
||||||
case "byedpi":
|
case "byedpi":
|
||||||
return sys.byedpi_version;
|
return sys.byedpi_version;
|
||||||
|
case "tailscale":
|
||||||
|
return sys.tailscale_version;
|
||||||
default:
|
default:
|
||||||
return void 0;
|
return void 0;
|
||||||
}
|
}
|
||||||
@@ -17257,6 +17275,8 @@ function getComponentInstallKey(component) {
|
|||||||
return "zapret2Install";
|
return "zapret2Install";
|
||||||
case "byedpi":
|
case "byedpi":
|
||||||
return "byedpiInstall";
|
return "byedpiInstall";
|
||||||
|
case "tailscale":
|
||||||
|
return "tailscaleInstall";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getComponentInstallAction(component) {
|
function getComponentInstallAction(component) {
|
||||||
@@ -17300,7 +17320,8 @@ var COMPONENT_REPO_URLS = {
|
|||||||
sing_box: "https://github.com/SagerNet/sing-box",
|
sing_box: "https://github.com/SagerNet/sing-box",
|
||||||
zapret: "https://github.com/remittor/zapret-openwrt",
|
zapret: "https://github.com/remittor/zapret-openwrt",
|
||||||
zapret2: "https://github.com/Dushnilin/zapret2-openwrt",
|
zapret2: "https://github.com/Dushnilin/zapret2-openwrt",
|
||||||
byedpi: "https://github.com/DPITrickster/ByeDPI-OpenWrt"
|
byedpi: "https://github.com/DPITrickster/ByeDPI-OpenWrt",
|
||||||
|
tailscale: "https://openwrt.org/packages/pkgdata/tailscale"
|
||||||
};
|
};
|
||||||
function getComponentCards() {
|
function getComponentCards() {
|
||||||
const systemInfo = normalizeSingBoxVariantFields(
|
const systemInfo = normalizeSingBoxVariantFields(
|
||||||
@@ -17310,6 +17331,7 @@ function getComponentCards() {
|
|||||||
const zapretInstalled = Boolean(systemInfo.zapret_installed);
|
const zapretInstalled = Boolean(systemInfo.zapret_installed);
|
||||||
const zapret2Installed = Boolean(systemInfo.zapret2_installed);
|
const zapret2Installed = Boolean(systemInfo.zapret2_installed);
|
||||||
const byedpiInstalled = Boolean(systemInfo.byedpi_installed);
|
const byedpiInstalled = Boolean(systemInfo.byedpi_installed);
|
||||||
|
const tailscaleInstalled = Boolean(systemInfo.tailscale_installed);
|
||||||
const singBoxInstalled = !isNotInstalled(systemInfo.sing_box_version);
|
const singBoxInstalled = !isNotInstalled(systemInfo.sing_box_version);
|
||||||
const singBoxStable = singBoxInstalled && !systemInfo.sing_box_extended && !systemInfo.sing_box_tiny;
|
const singBoxStable = singBoxInstalled && !systemInfo.sing_box_extended && !systemInfo.sing_box_tiny;
|
||||||
const singBoxExtended = Boolean(systemInfo.sing_box_extended) && !systemInfo.sing_box_compressed && !systemInfo.sing_box_lx;
|
const singBoxExtended = Boolean(systemInfo.sing_box_extended) && !systemInfo.sing_box_compressed && !systemInfo.sing_box_lx;
|
||||||
@@ -17398,6 +17420,13 @@ function getComponentCards() {
|
|||||||
installKey: "byedpiInstall",
|
installKey: "byedpiInstall",
|
||||||
removeKey: "byedpiRemove"
|
removeKey: "byedpiRemove"
|
||||||
});
|
});
|
||||||
|
const tailscaleActions = getOptionalComponentActions({
|
||||||
|
component: "tailscale",
|
||||||
|
installed: tailscaleInstalled,
|
||||||
|
checkKey: "tailscaleCheck",
|
||||||
|
installKey: "tailscaleInstall",
|
||||||
|
removeKey: "tailscaleRemove"
|
||||||
|
});
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: "tachyon",
|
component: "tachyon",
|
||||||
@@ -17451,6 +17480,16 @@ function getComponentCards() {
|
|||||||
releaseUrl: getGitHubReleaseUrl("byedpi"),
|
releaseUrl: getGitHubReleaseUrl("byedpi"),
|
||||||
repoUrl: COMPONENT_REPO_URLS.byedpi,
|
repoUrl: COMPONENT_REPO_URLS.byedpi,
|
||||||
actions: byedpiActions
|
actions: byedpiActions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: "tailscale",
|
||||||
|
column: 1,
|
||||||
|
title: "Tailscale",
|
||||||
|
version: systemInfoLoading ? _("Loading...") : tailscaleInstalled ? systemInfo.tailscale_version : _("Not installed"),
|
||||||
|
latestVersion: getLatestVersion("tailscale"),
|
||||||
|
releaseUrl: getGitHubReleaseUrl("tailscale"),
|
||||||
|
repoUrl: COMPONENT_REPO_URLS.tailscale,
|
||||||
|
actions: tailscaleActions
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,11 +538,11 @@ function populateProtocolValues(option, capabilities) {
|
|||||||
|
|
||||||
resetOptionValues(option);
|
resetOptionValues(option);
|
||||||
|
|
||||||
if (normalized.singBoxTailscale) {
|
// Tailscale is offered unconditionally: the native tailscaled mode does not
|
||||||
Object.entries(TAILSCALE_PROTOCOL_LABELS).forEach(([value, label]) => {
|
// depend on the sing-box build, only on the installable Tailscale component.
|
||||||
addOptionValue(option, value, label);
|
Object.entries(TAILSCALE_PROTOCOL_LABELS).forEach(([value, label]) => {
|
||||||
});
|
addOptionValue(option, value, label);
|
||||||
}
|
});
|
||||||
|
|
||||||
Object.entries(BASE_PROTOCOL_LABELS).forEach(([value, label]) => {
|
Object.entries(BASE_PROTOCOL_LABELS).forEach(([value, label]) => {
|
||||||
addOptionValue(option, value, label);
|
addOptionValue(option, value, label);
|
||||||
@@ -3277,6 +3277,46 @@ function createServerContent(section, options = {}) {
|
|||||||
o.rmempty = true;
|
o.rmempty = true;
|
||||||
addTailscaleDepends(o);
|
addTailscaleDepends(o);
|
||||||
|
|
||||||
|
o = section.option(form.ListValue, "tailscale_mode", _("Tailscale mode"));
|
||||||
|
o.value(
|
||||||
|
"singbox",
|
||||||
|
_("sing-box endpoint (userspace, default)"),
|
||||||
|
);
|
||||||
|
o.value(
|
||||||
|
"native",
|
||||||
|
_("Native tailscaled (tailnet reachable from LAN without client)"),
|
||||||
|
);
|
||||||
|
o.default = "singbox";
|
||||||
|
o.modalonly = true;
|
||||||
|
o.rmempty = false;
|
||||||
|
addTailscaleDepends(o);
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
"tailscale_masquerade",
|
||||||
|
_("Masquerade LAN traffic into tailnet"),
|
||||||
|
_(
|
||||||
|
"SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.default = "1";
|
||||||
|
o.modalonly = true;
|
||||||
|
o.rmempty = false;
|
||||||
|
o.depends({ protocol: "tailscale", tailscale_mode: "native" });
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
"tailscale_use_exit_node",
|
||||||
|
_("Use tailnet exit node for LAN"),
|
||||||
|
_(
|
||||||
|
"Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.default = "0";
|
||||||
|
o.modalonly = true;
|
||||||
|
o.rmempty = false;
|
||||||
|
o.depends({ protocol: "tailscale", tailscale_mode: "native" });
|
||||||
|
|
||||||
// AWG Server Options
|
// AWG Server Options
|
||||||
o = section.option(
|
o = section.option(
|
||||||
form.Value,
|
form.Value,
|
||||||
|
|||||||
@@ -3726,6 +3726,30 @@ msgid "Tachyon-managed nfqws2 runtime is ready"
|
|||||||
msgstr "NFQWS2 Tachyon готов"
|
msgstr "NFQWS2 Tachyon готов"
|
||||||
|
|
||||||
msgid "Tailscale auth key"
|
msgid "Tailscale auth key"
|
||||||
|
msgid "Tailscale mode"
|
||||||
|
msgstr "Режим Tailscale"
|
||||||
|
|
||||||
|
msgid "sing-box endpoint (userspace, default)"
|
||||||
|
msgstr "Endpoint в sing-box (userspace, по умолчанию)"
|
||||||
|
|
||||||
|
msgid "Native tailscaled (tailnet reachable from LAN without client)"
|
||||||
|
msgstr "Нативный tailscaled (тейлнет доступен из LAN без клиента)"
|
||||||
|
|
||||||
|
msgid "Masquerade LAN traffic into tailnet"
|
||||||
|
msgstr "Маскарадинг LAN-трафика в тейлнет"
|
||||||
|
|
||||||
|
msgid "SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes."
|
||||||
|
msgstr "Подменять LAN-адреса при выходе через tailscale0, чтобы пиры могли отвечать. Нужно, если не все пиры принимают маршруты."
|
||||||
|
|
||||||
|
msgid "Use tailnet exit node for LAN"
|
||||||
|
msgstr "Использовать exit-node тейлнета для LAN"
|
||||||
|
|
||||||
|
msgid "Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines."
|
||||||
|
msgstr "Направлять весь не-локальный LAN-трафик через exit-node тейлнета вместо прокси-движков."
|
||||||
|
|
||||||
|
msgid "Tailscale"
|
||||||
|
msgstr "Tailscale"
|
||||||
|
|
||||||
msgstr "Ключ аутентификации Tailscale"
|
msgstr "Ключ аутентификации Tailscale"
|
||||||
|
|
||||||
msgid "Tailscale control URL"
|
msgid "Tailscale control URL"
|
||||||
|
|||||||
@@ -200,11 +200,18 @@ config settings 'settings'
|
|||||||
# option enabled '1'
|
# option enabled '1'
|
||||||
# option protocol 'tailscale'
|
# option protocol 'tailscale'
|
||||||
# option routing_mode 'rules'
|
# option routing_mode 'rules'
|
||||||
|
# # tailscale_mode: singbox (userspace endpoint inside sing-box, default)
|
||||||
|
# # or native (system tailscaled: tailnet reachable from LAN without client,
|
||||||
|
# # requires Tailscale component installed on the Updates tab)
|
||||||
|
# option tailscale_mode 'singbox'
|
||||||
# option tailscale_control_url 'https://controlplane.tailscale.com'
|
# option tailscale_control_url 'https://controlplane.tailscale.com'
|
||||||
# option tailscale_hostname 'tachyon-server-tailscale-example'
|
# option tailscale_hostname 'tachyon-server-tailscale-example'
|
||||||
# option tailscale_auth_key 'tskey-auth-...'
|
# option tailscale_auth_key 'tskey-auth-...'
|
||||||
# option tailscale_accept_routes '0'
|
# option tailscale_accept_routes '0'
|
||||||
# option tailscale_advertise_exit_node '1'
|
# option tailscale_advertise_exit_node '1'
|
||||||
|
# # Native mode only:
|
||||||
|
# #option tailscale_masquerade '1'
|
||||||
|
# #option tailscale_use_exit_node '0'
|
||||||
# #list tailscale_advertise_routes '192.168.1.0/24'
|
# #list tailscale_advertise_routes '192.168.1.0/24'
|
||||||
|
|
||||||
config telegram 'telegram'
|
config telegram 'telegram'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const COMPONENT_LOCK_DIR = getenv("UPDATES_LOCK_DIR") || RUNTIME_STATE_DIR + "/c
|
|||||||
const TMP_STALE_TTL_MINUTES = getenv("UPDATES_TMP_STALE_TTL_MINUTES") || "30";
|
const TMP_STALE_TTL_MINUTES = getenv("UPDATES_TMP_STALE_TTL_MINUTES") || "30";
|
||||||
const TMP_FILE_STALE_TTL_MINUTES = getenv("UPDATES_TMP_FILE_STALE_TTL_MINUTES") || "10";
|
const TMP_FILE_STALE_TTL_MINUTES = getenv("UPDATES_TMP_FILE_STALE_TTL_MINUTES") || "10";
|
||||||
const SB_MANAGED_SERVICE_MARKER = getenv("SB_MANAGED_SERVICE_MARKER") || constants.SB_MANAGED_SERVICE_MARKER || "Tachyon managed sing-box service for binary variants";
|
const SB_MANAGED_SERVICE_MARKER = getenv("SB_MANAGED_SERVICE_MARKER") || constants.SB_MANAGED_SERVICE_MARKER || "Tachyon managed sing-box service for binary variants";
|
||||||
|
const TAILSCALE_PACKAGE_URL = getenv("TAILSCALE_PACKAGE_URL") || "https://openwrt.org/packages/pkgdata/tailscale";
|
||||||
|
|
||||||
let as_string = common.as_string;
|
let as_string = common.as_string;
|
||||||
let shell_quote = common.shell_quote;
|
let shell_quote = common.shell_quote;
|
||||||
@@ -1302,6 +1303,35 @@ function install_byedpi(action) {
|
|||||||
action_success("byedpi", action, "ByeDPI package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
|
action_success("byedpi", action, "ByeDPI package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function install_tailscale(action) {
|
||||||
|
let component = "tailscale";
|
||||||
|
let label = "Tailscale";
|
||||||
|
let runtime_module = LIB_DIR + "/providers/tailscale/runtime.uc";
|
||||||
|
init_tmp_dir() || action_fail(component, action, "Failed to create temporary directory");
|
||||||
|
|
||||||
|
if (action == "check_update") {
|
||||||
|
if (!pkg_is_installed("tailscale")) {
|
||||||
|
run_logged("Refreshing package index", pkg_list_update_command());
|
||||||
|
let available_version = available_package_version("tailscale");
|
||||||
|
action_success(component, action, label + " is not installed", "", available_version, 0, "", TAILSCALE_PACKAGE_URL);
|
||||||
|
}
|
||||||
|
run_logged("Refreshing package index", pkg_list_update_command());
|
||||||
|
check_success(component, installed_package_version("tailscale"), available_package_version("tailscale"), TAILSCALE_PACKAGE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!run_logged("Refreshing package index", pkg_list_update_command()))
|
||||||
|
updates_log("Package index refresh failed; trying to install from the cached index", "warn");
|
||||||
|
if (!run_logged("Installing " + label + " package", pkg_install_name_command("tailscale")))
|
||||||
|
action_fail(component, "install", "Failed to install " + label + " package from the feed");
|
||||||
|
disable_standalone_service("tailscale");
|
||||||
|
restart_tachyon_after_successful_change();
|
||||||
|
clear_version_caches();
|
||||||
|
let current_version = provider_package_version(runtime_module);
|
||||||
|
if (current_version == "")
|
||||||
|
current_version = "unknown";
|
||||||
|
action_success(component, "install", label + " package has been installed", current_version, current_version, 1, "latest", TAILSCALE_PACKAGE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
function remove_optional_component(component, package_name, label, runtime_module) {
|
function remove_optional_component(component, package_name, label, runtime_module) {
|
||||||
if (!pkg_is_installed(package_name)) {
|
if (!pkg_is_installed(package_name)) {
|
||||||
if (provider_installed(runtime_module))
|
if (provider_installed(runtime_module))
|
||||||
@@ -2507,6 +2537,10 @@ function component_action(component, action) {
|
|||||||
install_byedpi(action);
|
install_byedpi(action);
|
||||||
else if (component == "byedpi" && action == "remove")
|
else if (component == "byedpi" && action == "remove")
|
||||||
remove_optional_component("byedpi", "byedpi", "ByeDPI", LIB_DIR + "/providers/byedpi/runtime.uc");
|
remove_optional_component("byedpi", "byedpi", "ByeDPI", LIB_DIR + "/providers/byedpi/runtime.uc");
|
||||||
|
else if (component == "tailscale" && (action == "check_update" || action == "install"))
|
||||||
|
install_tailscale(action);
|
||||||
|
else if (component == "tailscale" && action == "remove")
|
||||||
|
remove_optional_component("tailscale", "tailscale", "Tailscale", LIB_DIR + "/providers/tailscale/runtime.uc");
|
||||||
else
|
else
|
||||||
action_fail(component != "" ? component : "unknown", action != "" ? action : "unknown", "Unknown component action");
|
action_fail(component != "" ? component : "unknown", action != "" ? action : "unknown", "Unknown component action");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1370,7 +1370,7 @@ function normalize_component_name(component) {
|
|||||||
function valid_component_name(component) {
|
function valid_component_name(component) {
|
||||||
component = normalize_component_name(component);
|
component = normalize_component_name(component);
|
||||||
return component == "tachyon" || component == "sing_box" || component == "zapret" ||
|
return component == "tachyon" || component == "sing_box" || component == "zapret" ||
|
||||||
component == "zapret2" || component == "byedpi";
|
component == "zapret2" || component == "byedpi" || component == "tailscale";
|
||||||
}
|
}
|
||||||
|
|
||||||
function component_update_check_cache_path(component) {
|
function component_update_check_cache_path(component) {
|
||||||
|
|||||||
@@ -1772,6 +1772,25 @@ function validate_runtime_config(context) {
|
|||||||
let schedules = sections_by_type("schedule");
|
let schedules = sections_by_type("schedule");
|
||||||
|
|
||||||
validate_runtime_mark_ranges_context(context);
|
validate_runtime_mark_ranges_context(context);
|
||||||
|
|
||||||
|
// Native-mode Tailscale servers need the system tailscaled binary; the
|
||||||
|
// check lives here so every validation entry point (including fixtures)
|
||||||
|
// enforces it.
|
||||||
|
for (let ts_server in sections_by_type("server")) {
|
||||||
|
if (!server_enabled(ts_server))
|
||||||
|
continue;
|
||||||
|
if (option(ts_server, "protocol", "vless") != "tailscale")
|
||||||
|
continue;
|
||||||
|
if (option(ts_server, "tailscale_mode", "singbox") != "native")
|
||||||
|
continue;
|
||||||
|
// Without a key `tailscale up` falls back to interactive login and
|
||||||
|
// would block the runtime forever on a headless router.
|
||||||
|
if (trim(as_string(option(ts_server, "tailscale_auth_key", "") || "")) == "")
|
||||||
|
fail_requirement("Server '" + section_name(ts_server) + "' uses native Tailscale mode without an auth key. Generate one in the Tailscale admin console and fill in 'Tailscale auth key', or switch this server back to sing-box mode. Aborted.", "fatal");
|
||||||
|
if (!command_exists("tailscaled"))
|
||||||
|
fail_requirement("Server '" + section_name(ts_server) + "' uses native Tailscale mode, but tailscaled is not installed. Install the Tailscale component on the Updates tab, or switch this server back to sing-box mode. Aborted.", "fatal");
|
||||||
|
}
|
||||||
|
|
||||||
validate_dns_settings(settings, sections, context);
|
validate_dns_settings(settings, sections, context);
|
||||||
validate_list_update_settings(settings);
|
validate_list_update_settings(settings);
|
||||||
validate_http_url_option(option(settings, "latency_test_url", DEFAULT_LATENCY_TEST_URL) || DEFAULT_LATENCY_TEST_URL, "settings.latency_test_url");
|
validate_http_url_option(option(settings, "latency_test_url", DEFAULT_LATENCY_TEST_URL) || DEFAULT_LATENCY_TEST_URL, "settings.latency_test_url");
|
||||||
@@ -2029,8 +2048,11 @@ function validate_extended_server_features(ctx, sing_box_version, sing_box_versi
|
|||||||
if (protocol == "mtproto")
|
if (protocol == "mtproto")
|
||||||
fail_requirement("Server '" + name + "' uses MTProto proxy, but sing-box-extended is not installed. Install sing-box-extended or disable this server. Aborted.", "fatal");
|
fail_requirement("Server '" + name + "' uses MTProto proxy, but sing-box-extended is not installed. Install sing-box-extended or disable this server. Aborted.", "fatal");
|
||||||
|
|
||||||
if (protocol == "tailscale" && !sing_box_supports_tailscale(ctx, sing_box_version, sing_box_version_output))
|
// Native-mode Tailscale runs tailscaled outside sing-box and does
|
||||||
fail_requirement("Server '" + name + "' uses Tailscale, but the installed sing-box binary was built without Tailscale support. Install full sing-box or sing-box-extended, or disable this server. Aborted.", "fatal");
|
// not need the with_tailscale build.
|
||||||
|
if (protocol == "tailscale" && option(section, "tailscale_mode", "singbox") != "native" &&
|
||||||
|
!sing_box_supports_tailscale(ctx, sing_box_version, sing_box_version_output))
|
||||||
|
fail_requirement("Server '" + name + "' uses Tailscale, but the installed sing-box binary was built without Tailscale support. Install full sing-box or sing-box-extended, switch this server to native Tailscale mode, or disable this server. Aborted.", "fatal");
|
||||||
|
|
||||||
if (transport == "xhttp" && !sing_box_supports_xhttp(ctx, sing_box_version, sing_box_version_output))
|
if (transport == "xhttp" && !sing_box_supports_xhttp(ctx, sing_box_version, sing_box_version_output))
|
||||||
fail_requirement("Server '" + name + "' uses XHTTP transport, but the installed sing-box binary does not support it. Install sing-box-extended or a binary with XHTTP support, or change the transport. Aborted.", "fatal");
|
fail_requirement("Server '" + name + "' uses XHTTP transport, but the installed sing-box binary does not support it. Install sing-box-extended or a binary with XHTTP support, or change the transport. Aborted.", "fatal");
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const SINGBOX_RUNTIME_UC = LIB_DIR + "/singbox/runtime.uc";
|
|||||||
const ZAPRET_RUNTIME_UC = LIB_DIR + "/providers/zapret/runtime.uc";
|
const ZAPRET_RUNTIME_UC = LIB_DIR + "/providers/zapret/runtime.uc";
|
||||||
const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
|
const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
|
||||||
const BYEDPI_RUNTIME_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
|
const BYEDPI_RUNTIME_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
|
||||||
|
const TAILSCALE_RUNTIME_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
|
||||||
const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc";
|
const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc";
|
||||||
const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc";
|
const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc";
|
||||||
|
|
||||||
@@ -1146,6 +1147,8 @@ function build_system_info() {
|
|||||||
let zapret2_version = zapret2_installed ? provider_version(ZAPRET2_RUNTIME_UC) : "not installed";
|
let zapret2_version = zapret2_installed ? provider_version(ZAPRET2_RUNTIME_UC) : "not installed";
|
||||||
let byedpi_installed = provider_installed(BYEDPI_RUNTIME_UC) ? 1 : 0;
|
let byedpi_installed = provider_installed(BYEDPI_RUNTIME_UC) ? 1 : 0;
|
||||||
let byedpi_version = byedpi_installed ? provider_version(BYEDPI_RUNTIME_UC) : "not installed";
|
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 device_model = first_line_value("/tmp/sysinfo/model", "unknown");
|
let device_model = first_line_value("/tmp/sysinfo/model", "unknown");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1166,6 +1169,8 @@ function build_system_info() {
|
|||||||
zapret2_installed,
|
zapret2_installed,
|
||||||
byedpi_version,
|
byedpi_version,
|
||||||
byedpi_installed,
|
byedpi_installed,
|
||||||
|
tailscale_version,
|
||||||
|
tailscale_installed,
|
||||||
openwrt_version: openwrt_release(),
|
openwrt_version: openwrt_release(),
|
||||||
device_model,
|
device_model,
|
||||||
generated_at: int(clock()[0])
|
generated_at: int(clock()[0])
|
||||||
|
|||||||
@@ -41,6 +41,20 @@ function uci_settings() {
|
|||||||
return uci_section("settings");
|
return uci_section("settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True when at least one enabled server section runs Tailscale in native
|
||||||
|
// (tailscaled) mode; those need tailnet bypass rules in the mangle chain.
|
||||||
|
function native_tailscale_enabled() {
|
||||||
|
for (let section in uci_sections("server")) {
|
||||||
|
if (as_string(section["enabled"]) == "0" || as_string(section["enabled"]) == "false")
|
||||||
|
continue;
|
||||||
|
if (as_string(section["protocol"] || "") != "tailscale")
|
||||||
|
continue;
|
||||||
|
if (as_string(section["tailscale_mode"] || "singbox") == "native")
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function section_by_name(sections, section_name) {
|
function section_by_name(sections, section_name) {
|
||||||
section_name = as_string(section_name);
|
section_name = as_string(section_name);
|
||||||
for (let section in sections)
|
for (let section in sections)
|
||||||
@@ -1094,8 +1108,21 @@ function nft_create_runtime_base(table, localv4_set, common_set, port_set, ip_po
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!nft_add_rule(table, "mangle", [ "ct", "status", "dnat", "return" ]) ||
|
if (!nft_add_rule(table, "mangle", [ "ct", "status", "dnat", "return" ]) ||
|
||||||
!nft_add_rule(table, "mangle", [ "jump", "parental_control" ]) ||
|
!nft_add_rule(table, "mangle", [ "jump", "parental_control" ]))
|
||||||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip", "daddr", "@" + as_string(localv4_set), "return" ]) ||
|
return false;
|
||||||
|
|
||||||
|
// Native Tailscale: tailnet-bound traffic and anything already marked by
|
||||||
|
// the Tailscale runtime (mask 0x00ff0000, see config validator) must not
|
||||||
|
// be captured into tproxy — it is routed to tailscale0 instead.
|
||||||
|
if (native_tailscale_enabled()) {
|
||||||
|
if (!nft_add_rule(table, "mangle", [ "meta", "mark", "&", "0x00ff0000", "!=", "0", "return" ]) ||
|
||||||
|
!nft_add_rule(table, "mangle", [ "ip", "daddr", "100.64.0.0/10", "return" ]) ||
|
||||||
|
!nft_add_rule(table, "mangle", [ "ip6", "daddr", "fd7a:115c:a1e0::/48", "return" ]))
|
||||||
|
return false;
|
||||||
|
log_debug("Native Tailscale bypass rules added to mangle chain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip", "daddr", "@" + as_string(localv4_set), "return" ]) ||
|
||||||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip6", "daddr", "@" + as_string(localv6_set), "return" ]))
|
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip6", "daddr", "@" + as_string(localv6_set), "return" ]))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,526 @@
|
|||||||
|
#!/usr/bin/env ucode
|
||||||
|
|
||||||
|
let fs = require("fs");
|
||||||
|
let common = require("core.common");
|
||||||
|
let uci_core = require("core.uci");
|
||||||
|
|
||||||
|
// NOTE: ucode does not hoist function declarations - a callee must be
|
||||||
|
// textually defined above its first caller. Keep that order intact.
|
||||||
|
|
||||||
|
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 write_file = common.write_file;
|
||||||
|
let command_from_args = common.command_from_args;
|
||||||
|
let command_output = common.command_output;
|
||||||
|
let command_output_from_args = common.command_output_from_args;
|
||||||
|
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 parent_dir = common.parent_dir;
|
||||||
|
|
||||||
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||||
|
const TAILSCALE_BIN = getenv("TAILSCALE_BIN") || "/usr/sbin/tailscale";
|
||||||
|
const TAILSCALED_BIN = getenv("TAILSCALED_BIN") || "/usr/sbin/tailscaled";
|
||||||
|
const PERSISTENT_STATE_BASE = getenv("TAILSCALE_STATE_BASE") || "/etc/tachyon/tailscale";
|
||||||
|
const RUNTIME_DIR = getenv("TAILSCALE_RUNTIME_DIR") || "/var/run/tachyon/tailscale";
|
||||||
|
const TAILSCALED_PORT = getenv("TAILSCALED_PORT") || "41641";
|
||||||
|
const DEFAULT_CONTROL_URL = "https://controlplane.tailscale.com";
|
||||||
|
const MAGICDNS_DNSMASQ_FILE = getenv("TAILSCALE_DNSMASQ_FILE") || "/tmp/dnsmasq.d/tachyon-tailscale.conf";
|
||||||
|
const MAGICDNS_ADDRESS = getenv("TAILSCALE_MAGICDNS_ADDRESS") || "100.100.100.100";
|
||||||
|
|
||||||
|
// Marks live inside the mask reserved by the config validator (0x00ff0000):
|
||||||
|
// no other Tachyon engine may allocate bits there.
|
||||||
|
const TS_MARK_MASK = getenv("TAILSCALE_MARK_MASK") || "0x00ff0000";
|
||||||
|
const TS_MARK_EXIT_NODE = getenv("TAILSCALE_MARK_EXIT") || "0x00200000";
|
||||||
|
const TS_ROUTE_TABLE = getenv("TAILSCALE_ROUTE_TABLE") || "4247";
|
||||||
|
const TS_EXIT_TABLE = getenv("TAILSCALE_EXIT_TABLE") || "4248";
|
||||||
|
const TS_RULE_PRIORITY = getenv("TAILSCALE_RULE_PRIORITY") || "100";
|
||||||
|
const TS_EXIT_RULE_PRIORITY = getenv("TAILSCALE_EXIT_RULE_PRIORITY") || "90";
|
||||||
|
|
||||||
|
// Tailnet CGNAT ranges every peer address lives in.
|
||||||
|
const TAILNET_V4 = "100.64.0.0/10";
|
||||||
|
const TAILNET_V6 = "fd7a:115c:a1e0::/48";
|
||||||
|
// Destinations that must never be pushed through an exit node tunnel.
|
||||||
|
const EXIT_NODE_EXCLUDES_V4 = [
|
||||||
|
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16",
|
||||||
|
"100.64.0.0/10", "127.0.0.0/8", "224.0.0.0/4", "240.0.0.0/4"
|
||||||
|
];
|
||||||
|
const EXIT_NODE_EXCLUDES_V6 = [ "fc00::/7", "fe80::/10", "ff00::/8" ];
|
||||||
|
|
||||||
|
const NFT_TS_TABLE = "tachyon_tailscale";
|
||||||
|
|
||||||
|
function log_message(message, level) {
|
||||||
|
level = as_string(level || "info");
|
||||||
|
command_success_from_args([ "logger", "-t", "tachyon", "[tailscale] [" + level + "] " + as_string(message) ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bool_option(section, key, fallback) {
|
||||||
|
let value = object_or_empty(section)[key];
|
||||||
|
return value == null ? !!fallback : bool_value(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function list_option(section, key) {
|
||||||
|
let value = object_or_empty(section)[key];
|
||||||
|
if (value == null)
|
||||||
|
return [];
|
||||||
|
if (type(value) != "array")
|
||||||
|
value = [ value ];
|
||||||
|
let result = [];
|
||||||
|
for (let item in value) {
|
||||||
|
item = trim(as_string(item));
|
||||||
|
if (item != "")
|
||||||
|
push(result, item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function section_name(section) {
|
||||||
|
return as_string(object_or_empty(section)[".name"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uci_sections(type_name) {
|
||||||
|
return uci_core.section_objects(CONFIG_NAME, as_string(type_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_native_mode(section) {
|
||||||
|
return as_string(option(section, "tailscale_mode", "") || "") == "native";
|
||||||
|
}
|
||||||
|
|
||||||
|
function native_tailscale_sections() {
|
||||||
|
let result = [];
|
||||||
|
for (let section in uci_sections("server")) {
|
||||||
|
if (!bool_option(section, "enabled", true))
|
||||||
|
continue;
|
||||||
|
if (as_string(option(section, "protocol", "")) != "tailscale")
|
||||||
|
continue;
|
||||||
|
if (!is_native_mode(section))
|
||||||
|
continue;
|
||||||
|
push(result, section);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safe_filename(name) {
|
||||||
|
name = as_string(name);
|
||||||
|
name = replace(name, /[^a-zA-Z0-9_-]/g, "_");
|
||||||
|
return name == "" ? "section" : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function section_state_dir(section) {
|
||||||
|
return PERSISTENT_STATE_BASE + "/" + safe_filename(section_name(section)) + "/native";
|
||||||
|
}
|
||||||
|
|
||||||
|
function section_runtime_dir(section) {
|
||||||
|
return RUNTIME_DIR + "/" + safe_filename(section_name(section));
|
||||||
|
}
|
||||||
|
|
||||||
|
function section_pid_file(section) {
|
||||||
|
return section_runtime_dir(section) + "/tailscaled.pid";
|
||||||
|
}
|
||||||
|
|
||||||
|
function command_exists(name) {
|
||||||
|
return command_success_from_args([ "command", "-v", name ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function binary_available() {
|
||||||
|
return command_exists("tailscaled") && command_exists("tailscale");
|
||||||
|
}
|
||||||
|
|
||||||
|
function read_pid(path) {
|
||||||
|
let raw = trim(as_string(fs.readfile(path)) || "");
|
||||||
|
if (raw == "" || int(raw) <= 0)
|
||||||
|
return 0;
|
||||||
|
return int(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pid_alive(pid) {
|
||||||
|
if (pid <= 0)
|
||||||
|
return false;
|
||||||
|
return command_success_from_args([ "kill", "-0", as_string(pid) ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function running_pid(section) {
|
||||||
|
return pid_alive(read_pid(section_pid_file(section)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function package_installed() {
|
||||||
|
if (command_exists("apk") && command_success_from_args([ "apk", "info", "-e", "tailscale" ]))
|
||||||
|
return true;
|
||||||
|
if (!command_exists("opkg"))
|
||||||
|
return false;
|
||||||
|
for (let line in split(command_output_from_args([ "opkg", "list-installed" ]), "\n")) {
|
||||||
|
if (match(trim(as_string(line)), /^tailscale[ \t]+-/))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function daemon_version() {
|
||||||
|
if (!binary_available())
|
||||||
|
return "";
|
||||||
|
let out = trim(command_output_from_args([ "tailscale", "version" ]));
|
||||||
|
if (out == "")
|
||||||
|
return "";
|
||||||
|
return split(out, "\n")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensure_dirs(section) {
|
||||||
|
let state_dir = section_state_dir(section);
|
||||||
|
let runtime_dir = section_runtime_dir(section);
|
||||||
|
if (!command_success_from_args([ "mkdir", "-p", state_dir, runtime_dir ]))
|
||||||
|
return false;
|
||||||
|
// The daemon stores node identity here; it must survive reboots and resets.
|
||||||
|
fs.chmod(state_dir, 448);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_step(description, args) {
|
||||||
|
let status = command_status(command_from_args(args));
|
||||||
|
if (status != 0) {
|
||||||
|
log_message(description + " failed (exit " + as_string(status) + ")", "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log_message(description + ": ok", "debug");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tailscale_client_args(section) {
|
||||||
|
let runtime_dir = section_runtime_dir(section);
|
||||||
|
return [ TAILSCALE_BIN, "--socket", runtime_dir + "/tailscaled.sock" ];
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_magicdns_dns() {
|
||||||
|
fs.unlink(MAGICDNS_DNSMASQ_FILE);
|
||||||
|
command_status(command_from_args([ "/etc/init.d/dnsmasq", "restart" ]) + " >/dev/null 2>&1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_kernel_routing(keep_dns) {
|
||||||
|
command_status(command_from_args([ "ip", "rule", "del", "to", TAILNET_V4,
|
||||||
|
"priority", TS_RULE_PRIORITY, "table", TS_ROUTE_TABLE ]) + " 2>/dev/null");
|
||||||
|
command_status(command_from_args([ "ip", "-6", "rule", "del", "to", TAILNET_V6,
|
||||||
|
"priority", TS_RULE_PRIORITY, "table", TS_ROUTE_TABLE ]) + " 2>/dev/null");
|
||||||
|
command_status(command_from_args([ "ip", "route", "flush", "table", TS_ROUTE_TABLE ]) + " 2>/dev/null");
|
||||||
|
command_status(command_from_args([ "ip", "-6", "route", "flush", "table", TS_ROUTE_TABLE ]) + " 2>/dev/null");
|
||||||
|
command_status(command_from_args([ "ip", "rule", "del", "fwmark", TS_MARK_EXIT_NODE + "/" + TS_MARK_MASK,
|
||||||
|
"priority", TS_EXIT_RULE_PRIORITY, "table", TS_EXIT_TABLE ]) + " 2>/dev/null");
|
||||||
|
command_status(command_from_args([ "ip", "route", "flush", "table", TS_EXIT_TABLE ]) + " 2>/dev/null");
|
||||||
|
command_status(command_from_args([ "nft", "delete", "table", "inet", NFT_TS_TABLE ]) + " 2>/dev/null");
|
||||||
|
if (!keep_dns)
|
||||||
|
remove_magicdns_dns();
|
||||||
|
}
|
||||||
|
|
||||||
|
function install_exit_node_routing() {
|
||||||
|
// Opt-in: forward all non-local LAN traffic through the tailnet exit node.
|
||||||
|
// Marked packets are skipped by Tachyon's mangle chain (mask-based return),
|
||||||
|
// so they never enter tproxy, then follow the dedicated routing table.
|
||||||
|
command_status(command_from_args([ "ip", "rule", "del", "fwmark", TS_MARK_EXIT_NODE + "/" + TS_MARK_MASK,
|
||||||
|
"priority", TS_EXIT_RULE_PRIORITY, "table", TS_EXIT_TABLE ]) + " 2>/dev/null");
|
||||||
|
command_status(command_from_args([ "ip", "route", "flush", "table", TS_EXIT_TABLE ]) + " 2>/dev/null");
|
||||||
|
|
||||||
|
let nft_lines = [
|
||||||
|
"table inet " + NFT_TS_TABLE + " {",
|
||||||
|
" set exit_exclude_v4 { type ipv4_addr; flags interval; elements = { " + join(", ", EXIT_NODE_EXCLUDES_V4) + " } }",
|
||||||
|
" set exit_exclude_v6 { type ipv6_addr; flags interval; elements = { " + join(", ", EXIT_NODE_EXCLUDES_V6) + " } }",
|
||||||
|
" chain prerouting_mark {",
|
||||||
|
" type filter hook prerouting priority -150; policy accept;",
|
||||||
|
" meta mark & " + TS_MARK_MASK + " != 0 return",
|
||||||
|
" ip daddr @exit_exclude_v4 return",
|
||||||
|
" ip6 daddr @exit_exclude_v6 return",
|
||||||
|
" meta mark set " + TS_MARK_EXIT_NODE + " comment \"tachyon-tailscale exit-node\"",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
];
|
||||||
|
if (command_status("nft -f - <<'EOF'\n" + join("\n", nft_lines) + "\nEOF\n") != 0) {
|
||||||
|
log_message("Failed to install exit-node marking rules", "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let ok =
|
||||||
|
run_step("Adding exit-node route rule", [ "ip", "rule", "add", "fwmark", TS_MARK_EXIT_NODE + "/" + TS_MARK_MASK,
|
||||||
|
"priority", TS_EXIT_RULE_PRIORITY, "table", TS_EXIT_TABLE ]) &&
|
||||||
|
run_step("Adding exit-node default route", [ "ip", "route", "add", "default", "dev", "tailscale0", "table", TS_EXIT_TABLE ]);
|
||||||
|
if (!ok)
|
||||||
|
log_message("Exit-node routing setup failed", "warn");
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configure_firewall(section) {
|
||||||
|
// Masquerade LAN sources leaving through the tunnel unless the user opted
|
||||||
|
// out; without SNAT peers cannot answer addresses they don't know.
|
||||||
|
let masquerade = bool_option(section, "tailscale_masquerade", true);
|
||||||
|
let nft_lines = [
|
||||||
|
"table inet " + NFT_TS_TABLE + " {",
|
||||||
|
" chain postrouting_snat {",
|
||||||
|
" type nat hook postrouting priority 110; policy accept;",
|
||||||
|
" oifname \"tailscale0\" ip saddr " + TAILNET_V4 + " return",
|
||||||
|
" oifname \"tailscale0\" ip6 saddr " + TAILNET_V6 + " return"
|
||||||
|
];
|
||||||
|
if (masquerade)
|
||||||
|
push(nft_lines, " oifname \"tailscale0\" counter masquerade");
|
||||||
|
push(nft_lines, " }");
|
||||||
|
push(nft_lines, "}");
|
||||||
|
if (command_status("nft -f - <<'EOF'\n" + join("\n", nft_lines) + "\nEOF\n") != 0) {
|
||||||
|
log_message("Failed to install tailscale firewall rules", "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configure_magicdns_dns() {
|
||||||
|
// Hand *.ts.net to the tailnet resolver without letting tailscaled touch
|
||||||
|
// resolv.conf; dnsmasq picks the file up from its conf-dir.
|
||||||
|
if (!command_success_from_args([ "mkdir", "-p", parent_dir(MAGICDNS_DNSMASQ_FILE) ]))
|
||||||
|
return false;
|
||||||
|
let lines = [
|
||||||
|
"# Managed by Tachyon native Tailscale runtime",
|
||||||
|
"server=/ts.net/" + MAGICDNS_ADDRESS
|
||||||
|
];
|
||||||
|
if (!write_file(MAGICDNS_DNSMASQ_FILE, join("\n", lines) + "\n")) {
|
||||||
|
log_message("Failed to write MagicDNS dnsmasq config", "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
command_status(command_from_args([ "/etc/init.d/dnsmasq", "restart" ]) + " >/dev/null 2>&1");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function install_kernel_routing(section) {
|
||||||
|
// Steer tailnet-bound traffic into the tunnel before Tachyon's policy
|
||||||
|
// routing (fwmark rule at priority 105) can send it into tproxy.
|
||||||
|
// Start from a clean slate so restarts never stack duplicate rules.
|
||||||
|
remove_kernel_routing(true);
|
||||||
|
|
||||||
|
let installed =
|
||||||
|
run_step("Adding IPv4 tailnet route", [ "ip", "route", "add", TAILNET_V4, "dev", "tailscale0", "table", TS_ROUTE_TABLE ]) &&
|
||||||
|
run_step("Adding IPv6 tailnet route", [ "ip", "-6", "route", "add", TAILNET_V6, "dev", "tailscale0", "table", TS_ROUTE_TABLE ]) &&
|
||||||
|
run_step("Adding IPv4 route rule", [ "ip", "rule", "add", "to", TAILNET_V4, "priority", TS_RULE_PRIORITY, "table", TS_ROUTE_TABLE ]) &&
|
||||||
|
run_step("Adding IPv6 route rule", [ "ip", "-6", "rule", "add", "to", TAILNET_V6, "priority", TS_RULE_PRIORITY, "table", TS_ROUTE_TABLE ]);
|
||||||
|
if (!installed) {
|
||||||
|
log_message("Kernel routing setup failed for " + section_name(section), "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bool_option(section, "tailscale_use_exit_node", false))
|
||||||
|
install_exit_node_routing();
|
||||||
|
configure_firewall(section);
|
||||||
|
configure_magicdns_dns();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function start_daemon(section) {
|
||||||
|
if (running_pid(section)) {
|
||||||
|
log_message("tailscaled for " + section_name(section) + " already running", "debug");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let runtime_dir = section_runtime_dir(section);
|
||||||
|
let socket_path = runtime_dir + "/tailscaled.sock";
|
||||||
|
let log_file = runtime_dir + "/tailscaled.log";
|
||||||
|
let state_dir = section_state_dir(section);
|
||||||
|
|
||||||
|
let args = [
|
||||||
|
TAILSCALED_BIN,
|
||||||
|
"--tun=tailscale0",
|
||||||
|
"--statedir=" + state_dir,
|
||||||
|
"--socket=" + socket_path,
|
||||||
|
"--port=" + TAILSCALED_PORT,
|
||||||
|
"--netfilter-mode=on"
|
||||||
|
];
|
||||||
|
|
||||||
|
let cmdline = command_from_args(args) +
|
||||||
|
" >" + shell_quote(log_file) + " 2>&1 </dev/null & echo $!";
|
||||||
|
let output = trim(command_output(cmdline));
|
||||||
|
let pid = int(output);
|
||||||
|
if (pid <= 0) {
|
||||||
|
log_message("Failed to spawn tailscaled for " + section_name(section), "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
write_file(section_pid_file(section), as_string(pid));
|
||||||
|
|
||||||
|
// Wait until the client socket answers before running `up`.
|
||||||
|
for (let attempt = 0; attempt < 30; attempt++) {
|
||||||
|
if (command_success_from_args([ TAILSCALE_BIN, "--socket", socket_path, "status", "--peers=false" ]))
|
||||||
|
return true;
|
||||||
|
if (!pid_alive(pid))
|
||||||
|
break;
|
||||||
|
command_success_from_args([ "sleep", "1" ]);
|
||||||
|
}
|
||||||
|
log_message("tailscaled did not become ready for " + section_name(section), "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bring_up(section) {
|
||||||
|
let args = tailscale_client_args(section);
|
||||||
|
push(args, "up");
|
||||||
|
push(args, "--authkey=" + as_string(option(section, "tailscale_auth_key", "") || ""));
|
||||||
|
let hostname = as_string(option(section, "tailscale_hostname", "") || "");
|
||||||
|
if (hostname != "")
|
||||||
|
push(args, "--hostname=" + hostname);
|
||||||
|
let control_url = as_string(option(section, "tailscale_control_url", DEFAULT_CONTROL_URL) || DEFAULT_CONTROL_URL);
|
||||||
|
if (control_url != "" && control_url != DEFAULT_CONTROL_URL)
|
||||||
|
push(args, "--login-server=" + control_url);
|
||||||
|
if (bool_option(section, "tailscale_accept_routes", false))
|
||||||
|
push(args, "--accept-routes");
|
||||||
|
let advertise_routes = list_option(section, "tailscale_advertise_routes");
|
||||||
|
if (length(advertise_routes) > 0)
|
||||||
|
push(args, "--advertise-routes=" + join(",", advertise_routes));
|
||||||
|
else
|
||||||
|
push(args, "--advertise-routes=");
|
||||||
|
if (bool_option(section, "tailscale_advertise_exit_node", false))
|
||||||
|
push(args, "--advertise-exit-node");
|
||||||
|
else
|
||||||
|
push(args, "--advertise-exit-node=false");
|
||||||
|
// DNS stays under dnsmasq/Tachyon control; MagicDNS names are resolved
|
||||||
|
// through the dedicated dnsmasq forward instead of resolv.conf takeover.
|
||||||
|
push(args, "--accept-dns=false");
|
||||||
|
// Never block the runtime on interactive prompts or dead control planes.
|
||||||
|
push(args, "--timeout=120s");
|
||||||
|
push(args, "--reset");
|
||||||
|
|
||||||
|
if (!run_step("tailscale up (" + section_name(section) + ")", args))
|
||||||
|
return false;
|
||||||
|
return install_kernel_routing(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop_section(section) {
|
||||||
|
let down_args = tailscale_client_args(section);
|
||||||
|
push(down_args, "down");
|
||||||
|
command_status(command_from_args(down_args) + " 2>/dev/null");
|
||||||
|
let pid_file = section_pid_file(section);
|
||||||
|
let pid = read_pid(pid_file);
|
||||||
|
if (pid_alive(pid)) {
|
||||||
|
command_success_from_args([ "kill", as_string(pid) ]);
|
||||||
|
for (let attempt = 0; attempt < 10 && pid_alive(pid); attempt++)
|
||||||
|
command_success_from_args([ "sleep", "1" ]);
|
||||||
|
if (pid_alive(pid))
|
||||||
|
command_success_from_args([ "kill", "-9", as_string(pid) ]);
|
||||||
|
}
|
||||||
|
fs.unlink(pid_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function backend_state() {
|
||||||
|
if (!binary_available())
|
||||||
|
return "";
|
||||||
|
let out = trim(command_output_from_args([ TAILSCALE_BIN, "status", "--json", "--peers=false" ]));
|
||||||
|
if (out == "")
|
||||||
|
return "";
|
||||||
|
let parsed = json(out);
|
||||||
|
return as_string(object_or_empty(parsed)["BackendState"] || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function status_json() {
|
||||||
|
let sections = native_tailscale_sections();
|
||||||
|
let configured = length(sections) > 0;
|
||||||
|
let available = binary_available();
|
||||||
|
let pkg = package_installed();
|
||||||
|
let version = available ? (daemon_version() || "unknown") : "not installed";
|
||||||
|
|
||||||
|
let running = 0;
|
||||||
|
let states = {};
|
||||||
|
for (let section in sections) {
|
||||||
|
if (running_pid(section)) {
|
||||||
|
running++;
|
||||||
|
let state = backend_state();
|
||||||
|
if (state != "")
|
||||||
|
states[section_name(section)] = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = "tailscale provider status is normal";
|
||||||
|
if (configured && !available)
|
||||||
|
message = "native Tailscale is configured, but the tailscale package is missing; install it on the Updates tab";
|
||||||
|
else if (configured && running < length(sections))
|
||||||
|
message = "native Tailscale is configured, but tailscaled is not running for every section";
|
||||||
|
else if (!configured && available)
|
||||||
|
message = "Tailscale package is installed, but no server section uses native mode";
|
||||||
|
else if (!configured && !available)
|
||||||
|
message = "tailscale package is not installed; native Tailscale is unavailable";
|
||||||
|
|
||||||
|
write_json({
|
||||||
|
installed: available,
|
||||||
|
package_installed: pkg,
|
||||||
|
provider_available: available,
|
||||||
|
provider_path: TAILSCALED_BIN,
|
||||||
|
version,
|
||||||
|
configured,
|
||||||
|
expected_process_count: length(sections),
|
||||||
|
running_process_count: running,
|
||||||
|
backend_states: states,
|
||||||
|
ready: configured && available && running == length(sections) && length(sections) > 0,
|
||||||
|
conflict: running > length(sections),
|
||||||
|
status_message: message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_json() {
|
||||||
|
write_json({
|
||||||
|
tailscale_installed: binary_available(),
|
||||||
|
tailscale_package_installed: package_installed(),
|
||||||
|
tailscale_provider_path: TAILSCALED_BIN
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop_runtime() {
|
||||||
|
for (let section in native_tailscale_sections())
|
||||||
|
stop_section(section);
|
||||||
|
// Also clean leftovers for sections that disappeared from the config.
|
||||||
|
let entries = null;
|
||||||
|
try {
|
||||||
|
entries = fs.readdir(RUNTIME_DIR);
|
||||||
|
} catch (e) {
|
||||||
|
entries = null;
|
||||||
|
}
|
||||||
|
if (entries != null) {
|
||||||
|
for (let entry in entries) {
|
||||||
|
let pid_file = RUNTIME_DIR + "/" + as_string(entry) + "/tailscaled.pid";
|
||||||
|
let pid = read_pid(pid_file);
|
||||||
|
if (pid_alive(pid))
|
||||||
|
command_success_from_args([ "kill", as_string(pid) ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remove_kernel_routing(false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function start_runtime() {
|
||||||
|
let sections = native_tailscale_sections();
|
||||||
|
if (length(sections) == 0) {
|
||||||
|
// Nothing configured: make sure nothing is left over from a removed section.
|
||||||
|
stop_runtime();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!binary_available()) {
|
||||||
|
log_message("tailscale binaries are missing; install the Tailscale component first", "warn");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let failures = 0;
|
||||||
|
for (let section in sections) {
|
||||||
|
if (!ensure_dirs(section) || !start_daemon(section) || !bring_up(section))
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
return failures > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode = ARGV[0] || "";
|
||||||
|
|
||||||
|
if (mode == "start-runtime")
|
||||||
|
exit(start_runtime());
|
||||||
|
else if (mode == "stop-runtime")
|
||||||
|
exit(stop_runtime());
|
||||||
|
else if (mode == "status")
|
||||||
|
status_json();
|
||||||
|
else if (mode == "check")
|
||||||
|
check_json();
|
||||||
|
else if (mode == "installed" || mode == "provider-available")
|
||||||
|
exit(binary_available() ? 0 : 1);
|
||||||
|
else if (mode == "package-installed")
|
||||||
|
exit(package_installed() ? 0 : 1);
|
||||||
|
else if (mode == "package-version")
|
||||||
|
print(daemon_version(), "\n");
|
||||||
|
else if (mode == "native-section-count")
|
||||||
|
print(length(native_tailscale_sections()), "\n");
|
||||||
|
else {
|
||||||
|
warn("Usage: providers/tailscale/runtime.uc <start-runtime|stop-runtime|status|check|installed|package-installed|package-version|native-section-count>\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -119,6 +119,7 @@ const DIAGNOSTICS_UC = LIB_DIR + "/diagnostics/runtime.uc";
|
|||||||
const ZAPRET_UC = LIB_DIR + "/providers/zapret/runtime.uc";
|
const ZAPRET_UC = LIB_DIR + "/providers/zapret/runtime.uc";
|
||||||
const ZAPRET2_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
|
const ZAPRET2_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
|
||||||
const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
|
const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
|
||||||
|
const TAILSCALE_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
|
||||||
const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
|
const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
|
||||||
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
|
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
|
||||||
const TELEGRAM_UC = LIB_DIR + "/service/telegram.uc";
|
const TELEGRAM_UC = LIB_DIR + "/service/telegram.uc";
|
||||||
@@ -891,6 +892,8 @@ function stop_main() {
|
|||||||
log_message("Zapret2 stop failed (non-fatal)", "warn");
|
log_message("Zapret2 stop failed (non-fatal)", "warn");
|
||||||
if (!module_success(BYEDPI_UC, [ "stop-runtime" ]))
|
if (!module_success(BYEDPI_UC, [ "stop-runtime" ]))
|
||||||
log_message("ByeDPI stop failed (non-fatal)", "warn");
|
log_message("ByeDPI stop failed (non-fatal)", "warn");
|
||||||
|
if (!module_success(TAILSCALE_UC, [ "stop-runtime" ]))
|
||||||
|
log_message("Tailscale stop failed (non-fatal)", "warn");
|
||||||
|
|
||||||
if (command_success_from_args([ "nft", "list", "table", "inet", NFT_TABLE_NAME ])) {
|
if (command_success_from_args([ "nft", "list", "table", "inet", NFT_TABLE_NAME ])) {
|
||||||
if (!command_success_from_args([ "nft", "delete", "table", "inet", NFT_TABLE_NAME ]))
|
if (!command_success_from_args([ "nft", "delete", "table", "inet", NFT_TABLE_NAME ]))
|
||||||
@@ -1370,7 +1373,10 @@ function reload(reason) {
|
|||||||
if (plan.needs_zapret2_restart == 1)
|
if (plan.needs_zapret2_restart == 1)
|
||||||
module_success(ZAPRET2_UC, [ "start-runtime" ]);
|
module_success(ZAPRET2_UC, [ "start-runtime" ]);
|
||||||
if (plan.needs_byedpi_restart == 1)
|
if (plan.needs_byedpi_restart == 1)
|
||||||
module_success(BYEDPI_UC, [ "start-runtime" ]);
|
module_success(BYEDPI_UC, [ "start-runtime" ]);
|
||||||
|
// Native Tailscale must be up before sing-box so tailnet routes win over
|
||||||
|
// policy routing from the very first packet.
|
||||||
|
module_success(TAILSCALE_UC, [ "start-runtime" ]);
|
||||||
|
|
||||||
if (plan.needs_dnsmasq_configure == 1) {
|
if (plan.needs_dnsmasq_configure == 1) {
|
||||||
status = dnsmasq_configure(true);
|
status = dnsmasq_configure(true);
|
||||||
|
|||||||
@@ -1492,6 +1492,9 @@ function append_sing_box_server_signature_body(body, server) {
|
|||||||
[ "tailscale_auth_key", "" ],
|
[ "tailscale_auth_key", "" ],
|
||||||
[ "tailscale_control_url", "" ],
|
[ "tailscale_control_url", "" ],
|
||||||
[ "tailscale_hostname", "" ],
|
[ "tailscale_hostname", "" ],
|
||||||
|
[ "tailscale_mode", "singbox" ],
|
||||||
|
[ "tailscale_masquerade", "1" ],
|
||||||
|
[ "tailscale_use_exit_node", "0" ],
|
||||||
[ "tailscale_accept_routes", "" ],
|
[ "tailscale_accept_routes", "" ],
|
||||||
[ "tailscale_advertise_routes", "" ],
|
[ "tailscale_advertise_routes", "" ],
|
||||||
[ "tailscale_advertise_exit_node", "" ]
|
[ "tailscale_advertise_exit_node", "" ]
|
||||||
|
|||||||
@@ -1498,6 +1498,9 @@ function section_by_name(sections, name) {
|
|||||||
|
|
||||||
function add_server_routes(config, servers, sections) {
|
function add_server_routes(config, servers, sections) {
|
||||||
for (let server in servers) {
|
for (let server in servers) {
|
||||||
|
// Native-mode Tailscale runs outside sing-box: no inbound, no rules.
|
||||||
|
if (runtime_servers.is_native_tailscale(server))
|
||||||
|
continue;
|
||||||
runtime_servers.add_sniff_rule(config, server);
|
runtime_servers.add_sniff_rule(config, server);
|
||||||
|
|
||||||
let inbound = runtime_constants.server_inbound_tag(server[".name"]);
|
let inbound = runtime_constants.server_inbound_tag(server[".name"]);
|
||||||
|
|||||||
@@ -416,9 +416,18 @@ function add_tailscale_endpoint(config, section, tag_name) {
|
|||||||
push(config.endpoints, endpoint);
|
push(config.endpoints, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function is_native_tailscale(section) {
|
||||||
|
return option(section, "protocol", "vless") == "tailscale" &&
|
||||||
|
option(section, "tailscale_mode", "singbox") == "native";
|
||||||
|
}
|
||||||
|
|
||||||
function add_dns_bypass(config, section) {
|
function add_dns_bypass(config, section) {
|
||||||
if (option(section, "protocol", "vless") != "tailscale")
|
if (option(section, "protocol", "vless") != "tailscale")
|
||||||
return;
|
return;
|
||||||
|
// Native mode runs tailscaled outside sing-box; MagicDNS is served by
|
||||||
|
// dnsmasq instead of a sing-box DNS server.
|
||||||
|
if (is_native_tailscale(section))
|
||||||
|
return;
|
||||||
|
|
||||||
let section_name = section[".name"];
|
let section_name = section[".name"];
|
||||||
let inbound = runtime_constants.server_inbound_tag(section_name);
|
let inbound = runtime_constants.server_inbound_tag(section_name);
|
||||||
@@ -443,8 +452,11 @@ function add_server(config, section) {
|
|||||||
let protocol = option(section, "protocol", "vless");
|
let protocol = option(section, "protocol", "vless");
|
||||||
let tag_name = runtime_constants.server_inbound_tag(section_name);
|
let tag_name = runtime_constants.server_inbound_tag(section_name);
|
||||||
|
|
||||||
if (protocol == "tailscale")
|
if (protocol == "tailscale") {
|
||||||
|
if (is_native_tailscale(section))
|
||||||
|
return;
|
||||||
add_tailscale_endpoint(config, section, tag_name);
|
add_tailscale_endpoint(config, section, tag_name);
|
||||||
|
}
|
||||||
else if (protocol == "json_inbound")
|
else if (protocol == "json_inbound")
|
||||||
add_json_inbound(config, section, tag_name);
|
add_json_inbound(config, section, tag_name);
|
||||||
else
|
else
|
||||||
@@ -517,5 +529,6 @@ function clone_rules_for_inbound(config, source_inbound, target_inbound, skip_do
|
|||||||
return {
|
return {
|
||||||
add_server,
|
add_server,
|
||||||
add_sniff_rule,
|
add_sniff_rule,
|
||||||
clone_rules_for_inbound
|
clone_rules_for_inbound,
|
||||||
|
is_native_tailscale
|
||||||
};
|
};
|
||||||
File diff suppressed because one or more lines are too long
@@ -993,6 +993,12 @@ ws
|
|||||||
|
|
||||||
[server.srv1.tailscale_hostname]
|
[server.srv1.tailscale_hostname]
|
||||||
|
|
||||||
|
[server.srv1.tailscale_mode]
|
||||||
|
singbox
|
||||||
|
[server.srv1.tailscale_masquerade]
|
||||||
|
1
|
||||||
|
[server.srv1.tailscale_use_exit_node]
|
||||||
|
0
|
||||||
[server.srv1.tailscale_accept_routes]
|
[server.srv1.tailscale_accept_routes]
|
||||||
1
|
1
|
||||||
[server.srv1.tailscale_advertise_routes]
|
[server.srv1.tailscale_advertise_routes]
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||||
|
GENERATOR_UC="$TACHYON_LIB/singbox/generator.uc"
|
||||||
|
VALIDATOR_UC="$TACHYON_LIB/config/validator.uc"
|
||||||
|
RUNTIME_UC="$TACHYON_LIB/providers/tailscale/runtime.uc"
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_config() {
|
||||||
|
local fixture="$1"
|
||||||
|
local output="$2"
|
||||||
|
mkdir -p "${output}.section-cache"
|
||||||
|
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||||
|
"$fixture" "$output" "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
make_fixture() {
|
||||||
|
local mode="$1"
|
||||||
|
cat >"$WORK_DIR/ts_$mode.json" <<JSON
|
||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
".name": "settings",
|
||||||
|
".type": "settings",
|
||||||
|
"dns_server": [ "77.88.8.8" ],
|
||||||
|
"bootstrap_dns_server": [ "77.88.8.8" ]
|
||||||
|
},
|
||||||
|
"server": [
|
||||||
|
{
|
||||||
|
".name": "ts_native",
|
||||||
|
".type": "server",
|
||||||
|
"label": "Tailscale",
|
||||||
|
"enabled": "1",
|
||||||
|
"protocol": "tailscale",
|
||||||
|
"routing_mode": "rules",
|
||||||
|
"tailscale_mode": "$mode",
|
||||||
|
"tailscale_hostname": "tachyon-ts",
|
||||||
|
"tailscale_auth_key": "tskey-auth-secret"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"section": [
|
||||||
|
{
|
||||||
|
".name": "proxy",
|
||||||
|
".type": "section",
|
||||||
|
"enabled": "1",
|
||||||
|
"action": "connection",
|
||||||
|
"outbound_jsons": [
|
||||||
|
"{\"type\":\"vless\",\"tag\":\"Alpha\",\"server\":\"alpha.example\",\"server_port\":443,\"uuid\":\"00000000-0000-4000-8000-000000000001\",\"tls\":{\"enabled\":true}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
# Native mode: no sing-box endpoint and no MagicDNS DNS server must appear.
|
||||||
|
make_fixture native
|
||||||
|
generate_config "$WORK_DIR/ts_native.json" "$WORK_DIR/native.out"
|
||||||
|
grep -Fq '"type": "tailscale"' "$WORK_DIR/native.out" &&
|
||||||
|
fail "native mode must not emit a tailscale endpoint"
|
||||||
|
grep -Fq 'tailscale-dns' "$WORK_DIR/native.out" &&
|
||||||
|
fail "native mode must not emit a tailscale DNS server"
|
||||||
|
|
||||||
|
# Default (singbox) mode: endpoint present.
|
||||||
|
make_fixture singbox
|
||||||
|
generate_config "$WORK_DIR/ts_singbox.json" "$WORK_DIR/singbox.out"
|
||||||
|
grep -Fq '"type": "tailscale"' "$WORK_DIR/singbox.out" ||
|
||||||
|
fail "singbox mode must emit a tailscale endpoint"
|
||||||
|
|
||||||
|
# Validator: native mode without tailscaled binary fails with install hint,
|
||||||
|
# even when sing-box lacks Tailscale support. fail_requirement() reports
|
||||||
|
# through `logger`, so a recording stub is installed on PATH.
|
||||||
|
STUB_BIN="$WORK_DIR/bin"
|
||||||
|
LOG_FILE="$WORK_DIR/logger.log"
|
||||||
|
mkdir -p "$STUB_BIN"
|
||||||
|
cat >"$STUB_BIN/logger" <<SH
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
printf '%s\n' "\$*" >> '$LOG_FILE'
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
chmod +x "$STUB_BIN/logger"
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
: >"$LOG_FILE"
|
||||||
|
PATH="$STUB_BIN:$PATH" ucode -L "$TACHYON_LIB" "$VALIDATOR_UC" \
|
||||||
|
validate-runtime-fixture "$1" "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if validate "$WORK_DIR/ts_native.json"; then
|
||||||
|
fail "validator must reject native mode without tailscaled"
|
||||||
|
fi
|
||||||
|
grep -Fq "tailscaled is not installed" "$LOG_FILE" ||
|
||||||
|
fail "validator must require the Tailscale component for native mode"
|
||||||
|
|
||||||
|
# With tailscaled available the validator passes for native mode.
|
||||||
|
cat >"$STUB_BIN/tailscaled" <<'SH'
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
chmod +x "$STUB_BIN/tailscaled"
|
||||||
|
validate "$WORK_DIR/ts_native.json" ||
|
||||||
|
fail "validator must accept native mode with tailscaled available"
|
||||||
|
grep -Fq "tailscaled is not installed" "$LOG_FILE" &&
|
||||||
|
fail "validator rejected native mode even though tailscaled is available"
|
||||||
|
|
||||||
|
# Native mode without an auth key is rejected: `tailscale up` would fall back
|
||||||
|
# to an interactive login and hang the runtime on a headless router.
|
||||||
|
sed 's/"tailscale_auth_key": "tskey-auth-secret"//' \
|
||||||
|
"$WORK_DIR/ts_native.json" >"$WORK_DIR/ts_nokey.json"
|
||||||
|
if PATH="$STUB_BIN:$PATH" ucode -L "$TACHYON_LIB" "$VALIDATOR_UC" \
|
||||||
|
validate-runtime-fixture "$WORK_DIR/ts_nokey.json" "{}" >/dev/null 2>&1; then
|
||||||
|
fail "validator must reject native mode without an auth key"
|
||||||
|
fi
|
||||||
|
grep -Fq "without an auth key" "$LOG_FILE" ||
|
||||||
|
fail "validator message about missing auth key not reported"
|
||||||
|
|
||||||
|
# Runtime provider module: CLI surface responds without a config.
|
||||||
|
OUTPUT=$(ucode -L "$TACHYON_LIB" "$RUNTIME_UC" package-version)
|
||||||
|
[ "$OUTPUT" = "" ] || fail "package-version should be empty without binaries, got: $OUTPUT"
|
||||||
|
|
||||||
|
ucode -L "$TACHYON_LIB" "$RUNTIME_UC" status >/dev/null ||
|
||||||
|
fail "status mode must always produce JSON"
|
||||||
|
|
||||||
|
printf 'config tailscale native mode checks passed\n'
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke test for providers/tailscale/runtime.uc: runs the real start/stop
|
||||||
|
# runtime paths against stubbed tailscale/tailscaled binaries and verifies
|
||||||
|
# every kernel/nftables/dnsmasq side effect appears and is cleaned up.
|
||||||
|
# Requires a privileged OpenWrt container (ip rule, nft tables).
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||||
|
RUNTIME_UC="$TACHYON_LIB/providers/tailscale/runtime.uc"
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
printf 'skipped: requires root\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v nft >/dev/null || { printf 'skipped: nft not available\n'; exit 0; }
|
||||||
|
ip link add tailscale0 type dummy 2>/dev/null ||
|
||||||
|
{ printf 'skipped: cannot create dummy interface\n'; exit 0; }
|
||||||
|
ip link set tailscale0 up
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
TS_LOG="$WORK_DIR/ts.log"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
PATH="$STUB_BIN:$PATH" ucode -L "$TACHYON_LIB" "$RUNTIME_UC" stop-runtime >/dev/null 2>&1 || true
|
||||||
|
ip link del tailscale0 2>/dev/null || true
|
||||||
|
rm -rf "$WORK_DIR" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
STUB_BIN="$WORK_DIR/bin"
|
||||||
|
mkdir -p "$STUB_BIN"
|
||||||
|
ip link set tailscale0 up
|
||||||
|
|
||||||
|
cat >"$STUB_BIN/tailscaled" <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
printf 'scaled %s\n' "\$*" >> '$TS_LOG'
|
||||||
|
exec sleep 3600
|
||||||
|
EOF
|
||||||
|
cat >"$STUB_BIN/tailscale" <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
printf 'cli %s\n' "\$*" >> '$TS_LOG'
|
||||||
|
case "\$*" in
|
||||||
|
*version*) echo "1.80.3-smoke" ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
chmod +x "$STUB_BIN/tailscaled" "$STUB_BIN/tailscale"
|
||||||
|
|
||||||
|
STATE_FILE="$WORK_DIR/uci.state"
|
||||||
|
cat >"$STATE_FILE" <<'EOF'
|
||||||
|
tachyon.settings=settings
|
||||||
|
tachyon.settings.dns_server=77.88.8.8
|
||||||
|
tachyon.ts_native=server
|
||||||
|
tachyon.ts_native.label=Tailscale native
|
||||||
|
tachyon.ts_native.enabled=1
|
||||||
|
tachyon.ts_native.protocol=tailscale
|
||||||
|
tachyon.ts_native.tailscale_mode=native
|
||||||
|
tachyon.ts_native.routing_mode=rules
|
||||||
|
tachyon.ts_native.tailscale_auth_key=tskey-auth-smoke
|
||||||
|
tachyon.ts_native.tailscale_hostname=tachyon-smoke
|
||||||
|
EOF
|
||||||
|
|
||||||
|
export UCI_STATE="$STATE_FILE"
|
||||||
|
export TACHYON_CONFIG_NAME="tachyon"
|
||||||
|
export PATH="$STUB_BIN:$PATH"
|
||||||
|
export TAILSCALED_BIN="$STUB_BIN/tailscaled"
|
||||||
|
export TAILSCALE_BIN="$STUB_BIN/tailscale"
|
||||||
|
export TAILSCALE_STATE_BASE="$WORK_DIR/state"
|
||||||
|
export TAILSCALE_RUNTIME_DIR="$WORK_DIR/run"
|
||||||
|
export TAILSCALE_DNSMASQ_FILE="$WORK_DIR/dnsmasq.d/tachyon-tailscale.conf"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
ucode -L "$TACHYON_LIB" "$RUNTIME_UC" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- version reporting ------------------------------------------------------
|
||||||
|
[ "$(run package-version)" = "1.80.3-smoke" ] || fail "package-version output"
|
||||||
|
run installed || fail "installed must succeed with stubs present"
|
||||||
|
|
||||||
|
# --- start ------------------------------------------------------------------
|
||||||
|
run start-runtime || fail "start-runtime exited non-zero"
|
||||||
|
|
||||||
|
PID_FILE="$WORK_DIR/run/ts_native/tailscaled.pid"
|
||||||
|
[ -f "$PID_FILE" ] || fail "pid file was not written"
|
||||||
|
PID="$(cat "$PID_FILE")"
|
||||||
|
kill -0 "$PID" 2>/dev/null || fail "tailscaled pid is not alive"
|
||||||
|
|
||||||
|
grep -Fq "cli --socket $WORK_DIR/run/ts_native/tailscaled.sock up" "$TS_LOG" ||
|
||||||
|
fail "tailscale up was not invoked through the section socket"
|
||||||
|
grep -Fq -e "--authkey=tskey-auth-smoke" "$TS_LOG" ||
|
||||||
|
fail "auth key was not passed to tailscale up"
|
||||||
|
grep -Fq -e "--accept-dns=false" "$TS_LOG" ||
|
||||||
|
fail "up must disable tailscaled DNS takeover"
|
||||||
|
grep -Fq -e "--timeout=120s" "$TS_LOG" ||
|
||||||
|
fail "up must carry a hard timeout"
|
||||||
|
|
||||||
|
ip rule list | grep -Fq "100.64.0.0/10" || fail "tailnet ip rule missing"
|
||||||
|
ip rule list | grep -Fq "lookup 4247" || fail "route table rule missing"
|
||||||
|
[ "$(ip route show table 4247 | grep -cF 'dev tailscale0')" -ge 1 ] ||
|
||||||
|
fail "tailnet route missing from table 4247"
|
||||||
|
nft list table inet tachyon_tailscale >/dev/null 2>&1 ||
|
||||||
|
fail "nft table tachyon_tailscale was not created"
|
||||||
|
nft list table inet tachyon_tailscale | grep -Fq "masquerade" ||
|
||||||
|
fail "masquerade rule missing (default on)"
|
||||||
|
[ -f "$WORK_DIR/dnsmasq.d/tachyon-tailscale.conf" ] ||
|
||||||
|
fail "MagicDNS dnsmasq config was not written"
|
||||||
|
grep -Fq "server=/ts.net/100.100.100.100" \
|
||||||
|
"$WORK_DIR/dnsmasq.d/tachyon-tailscale.conf" ||
|
||||||
|
fail "MagicDNS forward entry missing"
|
||||||
|
|
||||||
|
STATUS="$(run status)"
|
||||||
|
printf '%s' "$STATUS" | grep -Fq '"configured": true' ||
|
||||||
|
fail "status must report configured=true, got: $STATUS"
|
||||||
|
printf '%s' "$STATUS" | grep -Fq '"ready": true' ||
|
||||||
|
fail "status must report ready=true, got: $STATUS"
|
||||||
|
|
||||||
|
# --- idempotent restart -----------------------------------------------------
|
||||||
|
run start-runtime || fail "second start-runtime failed"
|
||||||
|
RULE_COUNT=$(ip rule list | grep -cF "lookup 4247")
|
||||||
|
[ "$RULE_COUNT" = "1" ] || fail "restart duplicated route rules ($RULE_COUNT)"
|
||||||
|
|
||||||
|
# --- stop -------------------------------------------------------------------
|
||||||
|
run stop-runtime || fail "stop-runtime exited non-zero"
|
||||||
|
kill -0 "$PID" 2>/dev/null && fail "tailscaled pid survived stop-runtime"
|
||||||
|
ip rule list | grep -Fq "100.64.0.0/10" && fail "tailnet ip rule survived stop"
|
||||||
|
ip route show table 4247 | grep -q . && fail "route table 4247 survived stop"
|
||||||
|
nft list table inet tachyon_tailscale >/dev/null 2>&1 &&
|
||||||
|
fail "nft table survived stop"
|
||||||
|
[ -f "$WORK_DIR/dnsmasq.d/tachyon-tailscale.conf" ] &&
|
||||||
|
fail "dnsmasq config survived stop"
|
||||||
|
|
||||||
|
# --- exit node mode ---------------------------------------------------------
|
||||||
|
printf '%s\n' 'tachyon.ts_native.tailscale_use_exit_node=1' >>"$STATE_FILE"
|
||||||
|
run start-runtime || fail "start-runtime with exit node failed"
|
||||||
|
ip rule list | grep -Fq "lookup 4248" || fail "exit-node ip rule missing"
|
||||||
|
[ "$(ip route show table 4248 | grep -cF 'default dev tailscale0')" = "1" ] ||
|
||||||
|
fail "exit-node default route missing"
|
||||||
|
nft list chain inet tachyon_tailscale prerouting_mark >/dev/null 2>&1 ||
|
||||||
|
fail "exit-node marking chain missing"
|
||||||
|
run stop-runtime || fail "stop-runtime after exit node failed"
|
||||||
|
ip rule list | grep -Fq "lookup 4248" && fail "exit-node rule survived stop"
|
||||||
|
|
||||||
|
printf 'tailscale native runtime smoke checks passed\n'
|
||||||
Reference in New Issue
Block a user