feat(tailscale): watchdog нативного режима и панель пиров тейлнета
- watchdog: проверка native-секции в slow-тире, авторестарт демона после 2 сбоев, TG-алерт раз в час - AI Doctor: узел Tailscale (готов/не установлен/упал) только при настроенном native - провайдер: режим peers — парсинг status --json через per-section socket - RPC get_tailscale_status/get_tailscale_peers/tailscale_restart - дашборд: карточка Tailscale (состояние, IP роутера, до 5 пиров с онлайн-статусом)
This commit is contained in:
@@ -260,6 +260,13 @@ export const TachyonShellMethods = {
|
||||
'/usr/bin/tachyon',
|
||||
{ allowNonZeroWithStdout: true },
|
||||
),
|
||||
getTailscalePeers: async () =>
|
||||
callBaseMethod<Tachyon.GetTailscalePeers>(
|
||||
Tachyon.AvailableMethods.GET_TAILSCALE_PEERS,
|
||||
[],
|
||||
'/usr/bin/tachyon',
|
||||
{ allowNonZeroWithStdout: true },
|
||||
),
|
||||
getZapretStatus: async () =>
|
||||
callBaseMethod<Tachyon.GetZapretStatus>(
|
||||
Tachyon.AvailableMethods.GET_ZAPRET_STATUS,
|
||||
|
||||
@@ -169,6 +169,11 @@ export interface StoreType {
|
||||
watchdogRunning: number;
|
||||
};
|
||||
};
|
||||
tailscaleWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
data: Tachyon.GetTailscalePeers | null;
|
||||
};
|
||||
sectionsWidget: {
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
@@ -290,6 +295,11 @@ const initialStore: StoreType = {
|
||||
watchdogRunning: 0,
|
||||
},
|
||||
},
|
||||
tailscaleWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: null,
|
||||
},
|
||||
sectionsWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
|
||||
@@ -337,6 +337,7 @@ async function completeSubscriptionUpdateJob(
|
||||
}
|
||||
void fetchDashboardSections({ force: true });
|
||||
void fetchServicesInfo();
|
||||
void fetchTailscalePeers();
|
||||
}
|
||||
|
||||
async function followSubscriptionUpdateState(
|
||||
@@ -1754,7 +1755,8 @@ function renderStoreWidget(
|
||||
| 'bandwidthWidget'
|
||||
| 'trafficTotalWidget'
|
||||
| 'systemInfoWidget'
|
||||
| 'servicesInfoWidget',
|
||||
| 'servicesInfoWidget'
|
||||
| 'tailscaleWidget',
|
||||
title: string,
|
||||
getItems: (data: any) => Array<{
|
||||
key: string;
|
||||
@@ -1907,6 +1909,89 @@ async function renderServicesInfoWidget() {
|
||||
);
|
||||
}
|
||||
|
||||
let latestTailscalePeersRequestId = 0;
|
||||
|
||||
async function fetchTailscalePeers() {
|
||||
const requestId = ++latestTailscalePeersRequestId;
|
||||
try {
|
||||
const response = await TachyonShellMethods.getTailscalePeers();
|
||||
if (requestId !== latestTailscalePeersRequestId) return;
|
||||
store.set({
|
||||
tailscaleWidget: {
|
||||
loading: false,
|
||||
failed: !response.success,
|
||||
data: response.success ? response.data : null,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
if (requestId !== latestTailscalePeersRequestId) return;
|
||||
store.set({
|
||||
tailscaleWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: store.get().tailscaleWidget.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function peerRow(name: string, online: boolean) {
|
||||
return {
|
||||
key: name,
|
||||
value: online ? _('✔ Online') : _('✘ Offline'),
|
||||
attributes: {
|
||||
class: online
|
||||
? 'tachyon_dashboard-page__widgets-section__item__row--success'
|
||||
: 'tachyon_dashboard-page__widgets-section__item__row--error',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function renderTailscaleWidget() {
|
||||
renderStoreWidget(
|
||||
'dashboard-widget-tailscale',
|
||||
'tailscaleWidget',
|
||||
_('Tailscale'),
|
||||
(data) => {
|
||||
type WidgetItem = {
|
||||
key: string;
|
||||
value: string;
|
||||
attributes?: Record<string, string>;
|
||||
};
|
||||
if (!data || !data.configured) return [] as WidgetItem[];
|
||||
const items: WidgetItem[] = [
|
||||
{
|
||||
key: _('State'),
|
||||
value: data.backend_state || '—',
|
||||
attributes: {
|
||||
class:
|
||||
data.backend_state === 'Running'
|
||||
? 'tachyon_dashboard-page__widgets-section__item__row--success'
|
||||
: 'tachyon_dashboard-page__widgets-section__item__row--error',
|
||||
},
|
||||
},
|
||||
];
|
||||
if (data.self && data.self.ips.length) {
|
||||
items.push({ key: _('This router'), value: data.self.ips[0] });
|
||||
}
|
||||
for (const peer of data.peers.slice(0, 5)) {
|
||||
if (!peer.online && !peer.name) continue;
|
||||
items.push(
|
||||
peerRow(peer.name || peer.dns_name || peer.ips[0], peer.online),
|
||||
);
|
||||
}
|
||||
if (data.peers.length > 5) {
|
||||
items.push({
|
||||
key: _('Other peers'),
|
||||
value: `+${data.peers.length - 5}`,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
'renderTailscaleWidget',
|
||||
);
|
||||
}
|
||||
|
||||
async function onStoreUpdate(
|
||||
next: StoreType,
|
||||
prev: StoreType,
|
||||
@@ -1940,6 +2025,10 @@ async function onStoreUpdate(
|
||||
syncDashboardServiceAvailability();
|
||||
renderServicesInfoWidget();
|
||||
}
|
||||
|
||||
if (diff.tailscaleWidget) {
|
||||
renderTailscaleWidget();
|
||||
}
|
||||
}
|
||||
|
||||
async function onPageMount() {
|
||||
@@ -1959,6 +2048,7 @@ async function onPageMount() {
|
||||
|
||||
if (!uiState) {
|
||||
void fetchServicesInfo();
|
||||
void fetchTailscalePeers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1970,7 +2060,9 @@ async function onPageMount() {
|
||||
void renderTrafficTotalWidget();
|
||||
void renderSystemInfoWidget();
|
||||
void renderServicesInfoWidget();
|
||||
void renderTailscaleWidget();
|
||||
syncDashboardServiceAvailability();
|
||||
void fetchTailscalePeers();
|
||||
|
||||
if (hasRuntimeSnapshot) {
|
||||
void refreshRuntimeUiState({ force: true });
|
||||
|
||||
@@ -60,6 +60,16 @@ export function render() {
|
||||
items: [],
|
||||
}),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{ id: 'dashboard-widget-tailscale' },
|
||||
renderWidget({
|
||||
loading: true,
|
||||
failed: false,
|
||||
title: '',
|
||||
items: [],
|
||||
}),
|
||||
),
|
||||
]),
|
||||
E('div', { id: 'dashboard-connections-grid' }, []),
|
||||
E(
|
||||
|
||||
@@ -66,6 +66,7 @@ export namespace Tachyon {
|
||||
CHECK_INBOUNDS = 'check_inbounds',
|
||||
GET_SING_BOX_STATUS = 'get_sing_box_status',
|
||||
GET_ZAPRET_STATUS = 'get_zapret_status',
|
||||
GET_TAILSCALE_PEERS = 'get_tailscale_peers',
|
||||
GET_ZAPRET2_STATUS = 'get_zapret2_status',
|
||||
GET_BYEDPI_STATUS = 'get_byedpi_status',
|
||||
CLASH_API = 'clash_api',
|
||||
@@ -694,6 +695,20 @@ export namespace Tachyon {
|
||||
updated_at?: number | null;
|
||||
}
|
||||
|
||||
export interface TailscalePeer {
|
||||
name: string;
|
||||
dns_name: string;
|
||||
ips: string[];
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export interface GetTailscalePeers {
|
||||
configured: boolean;
|
||||
backend_state: string;
|
||||
self: TailscalePeer | null;
|
||||
peers: TailscalePeer[];
|
||||
}
|
||||
|
||||
export interface GetZapretStatus {
|
||||
installed: 0 | 1;
|
||||
package_installed: 0 | 1;
|
||||
|
||||
@@ -2906,6 +2906,16 @@ function render() {
|
||||
title: "",
|
||||
items: []
|
||||
})
|
||||
),
|
||||
E(
|
||||
"div",
|
||||
{ id: "dashboard-widget-tailscale" },
|
||||
renderWidget({
|
||||
loading: true,
|
||||
failed: false,
|
||||
title: "",
|
||||
items: []
|
||||
})
|
||||
)
|
||||
]),
|
||||
E("div", { id: "dashboard-connections-grid" }, []),
|
||||
@@ -3072,6 +3082,7 @@ var Tachyon;
|
||||
AvailableMethods2["CHECK_INBOUNDS"] = "check_inbounds";
|
||||
AvailableMethods2["GET_SING_BOX_STATUS"] = "get_sing_box_status";
|
||||
AvailableMethods2["GET_ZAPRET_STATUS"] = "get_zapret_status";
|
||||
AvailableMethods2["GET_TAILSCALE_PEERS"] = "get_tailscale_peers";
|
||||
AvailableMethods2["GET_ZAPRET2_STATUS"] = "get_zapret2_status";
|
||||
AvailableMethods2["GET_BYEDPI_STATUS"] = "get_byedpi_status";
|
||||
AvailableMethods2["CLASH_API"] = "clash_api";
|
||||
@@ -3324,6 +3335,12 @@ var TachyonShellMethods = {
|
||||
"/usr/bin/tachyon",
|
||||
{ allowNonZeroWithStdout: true }
|
||||
),
|
||||
getTailscalePeers: async () => callBaseMethod(
|
||||
Tachyon.AvailableMethods.GET_TAILSCALE_PEERS,
|
||||
[],
|
||||
"/usr/bin/tachyon",
|
||||
{ allowNonZeroWithStdout: true }
|
||||
),
|
||||
getZapretStatus: async () => callBaseMethod(
|
||||
Tachyon.AvailableMethods.GET_ZAPRET_STATUS,
|
||||
[],
|
||||
@@ -5453,6 +5470,11 @@ var initialStore = {
|
||||
watchdogRunning: 0
|
||||
}
|
||||
},
|
||||
tailscaleWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
data: null
|
||||
},
|
||||
sectionsWidget: {
|
||||
loading: true,
|
||||
failed: false,
|
||||
@@ -6786,6 +6808,7 @@ async function completeSubscriptionUpdateJob(jobId, sectionName, response) {
|
||||
}
|
||||
void fetchDashboardSections({ force: true });
|
||||
void fetchServicesInfo();
|
||||
void fetchTailscalePeers();
|
||||
}
|
||||
async function followSubscriptionUpdateState(state) {
|
||||
const jobId = state.job_id;
|
||||
@@ -8008,6 +8031,75 @@ async function renderServicesInfoWidget() {
|
||||
"renderServicesInfoWidget"
|
||||
);
|
||||
}
|
||||
var latestTailscalePeersRequestId = 0;
|
||||
async function fetchTailscalePeers() {
|
||||
const requestId = ++latestTailscalePeersRequestId;
|
||||
try {
|
||||
const response = await TachyonShellMethods.getTailscalePeers();
|
||||
if (requestId !== latestTailscalePeersRequestId) return;
|
||||
store.set({
|
||||
tailscaleWidget: {
|
||||
loading: false,
|
||||
failed: !response.success,
|
||||
data: response.success ? response.data : null
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
if (requestId !== latestTailscalePeersRequestId) return;
|
||||
store.set({
|
||||
tailscaleWidget: {
|
||||
loading: false,
|
||||
failed: true,
|
||||
data: store.get().tailscaleWidget.data
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function peerRow(name, online) {
|
||||
return {
|
||||
key: name,
|
||||
value: online ? _("\u2714 Online") : _("\u2718 Offline"),
|
||||
attributes: {
|
||||
class: online ? "tachyon_dashboard-page__widgets-section__item__row--success" : "tachyon_dashboard-page__widgets-section__item__row--error"
|
||||
}
|
||||
};
|
||||
}
|
||||
async function renderTailscaleWidget() {
|
||||
renderStoreWidget(
|
||||
"dashboard-widget-tailscale",
|
||||
"tailscaleWidget",
|
||||
_("Tailscale"),
|
||||
(data) => {
|
||||
if (!data || !data.configured) return [];
|
||||
const items = [
|
||||
{
|
||||
key: _("State"),
|
||||
value: data.backend_state || "\u2014",
|
||||
attributes: {
|
||||
class: data.backend_state === "Running" ? "tachyon_dashboard-page__widgets-section__item__row--success" : "tachyon_dashboard-page__widgets-section__item__row--error"
|
||||
}
|
||||
}
|
||||
];
|
||||
if (data.self && data.self.ips.length) {
|
||||
items.push({ key: _("This router"), value: data.self.ips[0] });
|
||||
}
|
||||
for (const peer of data.peers.slice(0, 5)) {
|
||||
if (!peer.online && !peer.name) continue;
|
||||
items.push(
|
||||
peerRow(peer.name || peer.dns_name || peer.ips[0], peer.online)
|
||||
);
|
||||
}
|
||||
if (data.peers.length > 5) {
|
||||
items.push({
|
||||
key: _("Other peers"),
|
||||
value: `+${data.peers.length - 5}`
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
"renderTailscaleWidget"
|
||||
);
|
||||
}
|
||||
async function onStoreUpdate(next, prev, diff) {
|
||||
if (diff.sectionsWidget) {
|
||||
const inlineUpdated = canUpdateLatencyProgressInline(
|
||||
@@ -8031,6 +8123,9 @@ async function onStoreUpdate(next, prev, diff) {
|
||||
syncDashboardServiceAvailability();
|
||||
renderServicesInfoWidget();
|
||||
}
|
||||
if (diff.tailscaleWidget) {
|
||||
renderTailscaleWidget();
|
||||
}
|
||||
}
|
||||
async function onPageMount() {
|
||||
onPageUnmount();
|
||||
@@ -8045,6 +8140,7 @@ async function onPageMount() {
|
||||
}
|
||||
if (!uiState) {
|
||||
void fetchServicesInfo();
|
||||
void fetchTailscalePeers();
|
||||
}
|
||||
}
|
||||
store.subscribe(onStoreUpdate);
|
||||
@@ -8055,7 +8151,9 @@ async function onPageMount() {
|
||||
void renderTrafficTotalWidget();
|
||||
void renderSystemInfoWidget();
|
||||
void renderServicesInfoWidget();
|
||||
void renderTailscaleWidget();
|
||||
syncDashboardServiceAvailability();
|
||||
void fetchTailscalePeers();
|
||||
if (hasRuntimeSnapshot) {
|
||||
void refreshRuntimeUiState({ force: true });
|
||||
}
|
||||
|
||||
@@ -208,6 +208,9 @@ function command_spec(command) {
|
||||
get_zapret_status: [ "diagnostics/runtime.uc", "get-zapret-status", 0 ],
|
||||
get_zapret2_status: [ "diagnostics/runtime.uc", "get-zapret2-status", 0 ],
|
||||
get_byedpi_status: [ "diagnostics/runtime.uc", "get-byedpi-status", 0 ],
|
||||
get_tailscale_status: [ "diagnostics/runtime.uc", "get-tailscale-status", 0 ],
|
||||
get_tailscale_peers: [ "diagnostics/runtime.uc", "get-tailscale-peers", 0 ],
|
||||
tailscale_restart: [ "providers/tailscale/runtime.uc", "start-runtime", 0 ],
|
||||
get_system_info: [ "diagnostics/runtime.uc", "get-system-info", 0 ],
|
||||
get_server_capabilities: [ "diagnostics/runtime.uc", "get-server-capabilities", 0 ],
|
||||
get_ui_capabilities: [ "service/ui.uc", "get-ui-capabilities", 0 ],
|
||||
|
||||
@@ -2871,6 +2871,24 @@ function run_doctor_checks_impl(repair) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tailscale native runtime check (skipped entirely without native sections
|
||||
// so sing-box-mode users never see the node).
|
||||
let ts_status_raw = trim(module_output(TAILSCALE_RUNTIME_UC, [ "status" ]));
|
||||
if (ts_status_raw != "") {
|
||||
let ts = object_or_empty(json(ts_status_raw));
|
||||
if (ts.configured == true) {
|
||||
if (ts.installed != true) {
|
||||
issues++;
|
||||
doc_check("⚠️", "Tailscale", "package missing", "→ install the Tailscale component on the Updates tab");
|
||||
} else if (ts.ready == true) {
|
||||
doc_check("✅", "Tailscale", "running (" + as_string(ts.version || "") + ")", "");
|
||||
} else {
|
||||
issues++;
|
||||
doc_check("❌", "Tailscale", as_string(ts.status_message || "not ready"), "→ run: tachyon tailscale_restart");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 11. MSS Clamping Check
|
||||
if (routing_mode == "nftables") {
|
||||
let out_clamping = command_capture("nft list table inet " + NFT_TABLE_NAME + " | grep maxseg").output;
|
||||
@@ -4856,6 +4874,10 @@ else if (mode == "get-zapret2-status")
|
||||
exit(module_passthrough(ZAPRET2_RUNTIME_UC, [ "status" ]));
|
||||
else if (mode == "get-byedpi-status")
|
||||
exit(module_passthrough(BYEDPI_RUNTIME_UC, [ "status" ]));
|
||||
else if (mode == "get-tailscale-status")
|
||||
exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "status" ]));
|
||||
else if (mode == "get-tailscale-peers")
|
||||
exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "peers" ]));
|
||||
else if (mode == "get-system-info")
|
||||
exit(get_system_info());
|
||||
else if (mode == "get-server-capabilities")
|
||||
|
||||
@@ -18,6 +18,8 @@ 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 object_key_count = common.object_key_count;
|
||||
let array_or_empty = common.array_or_empty;
|
||||
let option = common.option;
|
||||
let parent_dir = common.parent_dir;
|
||||
|
||||
@@ -461,6 +463,55 @@ function check_json() {
|
||||
});
|
||||
}
|
||||
|
||||
function peer_entry(peer) {
|
||||
peer = object_or_empty(peer);
|
||||
return {
|
||||
name: as_string(peer["HostName"] || ""),
|
||||
dns_name: as_string(peer["DNSName"] || ""),
|
||||
ips: array_or_empty(peer["TailscaleIPs"]),
|
||||
online: peer["Online"] == true
|
||||
};
|
||||
}
|
||||
|
||||
// Tailnet peers of the first native section, via its per-section socket.
|
||||
// All Tachyon-managed sections are separate nodes; the dashboard shows the
|
||||
// first one (users run a single native section in practice).
|
||||
function peers_json() {
|
||||
let sections = native_tailscale_sections();
|
||||
if (!binary_available() || length(sections) == 0) {
|
||||
write_json({ configured: false, backend_state: "", self: null, peers: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
let args = tailscale_client_args(sections[0]);
|
||||
push(args, "status");
|
||||
push(args, "--json");
|
||||
let out = trim(command_output_from_args(args));
|
||||
|
||||
let backend_state = "";
|
||||
let self_entry = null;
|
||||
let peers = [];
|
||||
if (out != "") {
|
||||
let parsed = object_or_empty(json(out));
|
||||
backend_state = as_string(parsed["BackendState"] || "");
|
||||
let self = object_or_empty(parsed["Self"]);
|
||||
if (object_key_count(self) > 0) {
|
||||
self_entry = peer_entry(self);
|
||||
self_entry.online = true;
|
||||
}
|
||||
let peer_map = object_or_empty(parsed["Peer"]);
|
||||
for (let key, peer in peer_map)
|
||||
push(peers, peer_entry(peer));
|
||||
}
|
||||
|
||||
write_json({
|
||||
configured: true,
|
||||
backend_state,
|
||||
self: self_entry,
|
||||
peers
|
||||
});
|
||||
}
|
||||
|
||||
function stop_runtime() {
|
||||
for (let section in native_tailscale_sections())
|
||||
stop_section(section);
|
||||
@@ -512,6 +563,8 @@ else if (mode == "status")
|
||||
status_json();
|
||||
else if (mode == "check")
|
||||
check_json();
|
||||
else if (mode == "peers")
|
||||
peers_json();
|
||||
else if (mode == "installed" || mode == "provider-available")
|
||||
exit(binary_available() ? 0 : 1);
|
||||
else if (mode == "package-installed")
|
||||
|
||||
@@ -24,6 +24,8 @@ let proxy_restart_window_start = time();
|
||||
const PROXY_RESTART_LOCK = "/var/run/tachyon_proxy_restart.lock";
|
||||
let telegram_msg_count = 0;
|
||||
let telegram_msg_window = time();
|
||||
let tailscale_fail_streak = 0;
|
||||
let tailscale_last_alert = 0;
|
||||
|
||||
let command_from_args = common.command_from_args;
|
||||
let command_status = common.command_status;
|
||||
@@ -1875,11 +1877,42 @@ function check_telegram_worker() {
|
||||
telegram_worker_restart();
|
||||
}
|
||||
|
||||
// Native Tailscale sections run tailscaled outside sing-box; nobody else
|
||||
// watches that daemon. Two consecutive unhealthy statuses trigger an
|
||||
// idempotent provider restart (start-runtime), alerts throttled to 1/hour.
|
||||
function check_tailscale_worker() {
|
||||
let raw = trim(command_output_from_args([ "/usr/bin/tachyon", "get_tailscale_status" ]) || "");
|
||||
if (raw == "")
|
||||
return;
|
||||
let st = common.object_or_empty(json(raw));
|
||||
if (st.configured != true)
|
||||
return;
|
||||
if (st.ready == true) {
|
||||
tailscale_fail_streak = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
tailscale_fail_streak++;
|
||||
if (tailscale_fail_streak < 2)
|
||||
return;
|
||||
tailscale_fail_streak = 0;
|
||||
|
||||
log_message("Native Tailscale runtime is unhealthy (" + as_string(st.status_message || "") + "); restarting", "warn");
|
||||
command_status("/usr/bin/tachyon tailscale_restart >/dev/null 2>&1");
|
||||
|
||||
let now = time();
|
||||
if (now - tailscale_last_alert >= 3600) {
|
||||
tailscale_last_alert = now;
|
||||
send_telegram_notification("🛰 *Watchdog:* Native Tailscale was down, restarting the service.");
|
||||
}
|
||||
}
|
||||
|
||||
function perform_slow_checks() {
|
||||
controller.probe_slow();
|
||||
safe_call(ai_heal_dns_loop, "ai_heal_dns_loop");
|
||||
safe_call(check_mixed_proxy_port, "check_mixed_proxy_port");
|
||||
safe_call(check_telegram_worker, "check_telegram_worker");
|
||||
safe_call(check_tailscale_worker, "check_tailscale_worker");
|
||||
}
|
||||
if (uloop) {
|
||||
let tick;
|
||||
|
||||
@@ -43,9 +43,13 @@ cat >"$STUB_BIN/tailscale" <<EOF
|
||||
printf 'cli %s\n' "\$*" >> '$TS_LOG'
|
||||
case "\$*" in
|
||||
*version*) echo "1.80.3-smoke" ;;
|
||||
*--json*) cat '$WORK_DIR/ts-status.json' ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
EOF
|
||||
cat >"$WORK_DIR/ts-status.json" <<'JSON'
|
||||
{"BackendState":"Running","Self":{"HostName":"router","DNSName":"router.tailnet.ts.net.","TailscaleIPs":["100.64.0.1"]},"Peer":{"k1":{"HostName":"phone","DNSName":"phone.tailnet.ts.net.","TailscaleIPs":["100.64.0.2"],"Online":true},"k2":{"HostName":"laptop","DNSName":"laptop.tailnet.ts.net.","TailscaleIPs":["100.64.0.3"],"Online":false}}}
|
||||
JSON
|
||||
chmod +x "$STUB_BIN/tailscaled" "$STUB_BIN/tailscale"
|
||||
|
||||
STATE_FILE="$WORK_DIR/uci.state"
|
||||
@@ -121,6 +125,14 @@ printf '%s' "$STATUS" | grep -Fq '"configured": true' ||
|
||||
printf '%s' "$STATUS" | grep -Fq '"ready": true' ||
|
||||
fail "status must report ready=true, got: $STATUS"
|
||||
|
||||
PEERS="$(run peers)"
|
||||
printf '%s' "$PEERS" | grep -Fq '"backend_state": "Running"' ||
|
||||
fail "peers must relay backend state, got: $PEERS"
|
||||
printf '%s' "$PEERS" | grep -Fq '"dns_name": "phone.tailnet.ts.net."' ||
|
||||
fail "peers must include tailnet peers, got: $PEERS"
|
||||
printf '%s' "$PEERS" | grep -Fq '"online": false' ||
|
||||
fail "peers must report offline flag, got: $PEERS"
|
||||
|
||||
# --- idempotent restart -----------------------------------------------------
|
||||
run start-runtime || fail "second start-runtime failed"
|
||||
RULE_COUNT=$(ip rule list | grep -cF "lookup 4247")
|
||||
|
||||
Reference in New Issue
Block a user