feat: гармонизация секции AWG в дашборде и изоляция маршрутизации FPTN

This commit is contained in:
Dushnilin
2026-09-17 19:42:54 +03:00
parent 44e93b1f16
commit 623ea59797
11 changed files with 3313 additions and 3079 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 18:55+0300\n" "POT-Creation-Date: 2026-09-17 19:42+0300\n"
"PO-Revision-Date: 2026-09-17 18:55+0300\n" "PO-Revision-Date: 2026-09-17 19:42+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -47,6 +47,19 @@ interface IRenderSectionsProps {
testingOutboundCodes?: Record<string, boolean>; testingOutboundCodes?: Record<string, boolean>;
} }
const CONNECTION_ACTION_LABELS: Record<string, string> = {
awg: 'AmneziaWG',
warp: 'WARP',
anytls: 'AnyTLS',
snell: 'Snell',
mieru: 'Mieru',
sudoku: 'Sudoku',
masque: 'MASQUE',
openvpn: 'OpenVPN',
fptn: 'FPTN',
vpn: 'VPN',
};
function renderFailedState() { function renderFailedState() {
return E( return E(
'div', 'div',
@@ -529,89 +542,88 @@ function renderDefaultState({
.join(' '); .join(' ');
if (isConnectionNode) { if (isConnectionNode) {
const statusText = latencyFetching
? _('Checking...')
: outbound.latency === -1 || outbound.latency < 0
? _('Not responding')
: outbound.runtimeAvailable
? _('Connected')
: _('Not connected');
const statusColor = latencyFetching
? 'var(--warn-color-medium, orange)'
: outbound.latency === -1 || outbound.latency < 0
? 'var(--error-color-medium, red)'
: outbound.runtimeAvailable
? 'var(--success-color-medium, green)'
: 'var(--error-color-medium, red)';
const latencyDisplay =
outbound.latency && outbound.latency > 0
? `${outbound.latency} ms`
: '';
return E( return E(
'div', 'div',
{ {
class: className, class: className,
style: style:
'display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;', 'display: flex; flex-direction: column; justify-content: space-between; padding: 12px; min-width: 0;',
}, },
[ [
E( E(
'div', 'div',
{ {
style: class: 'tachyon_dashboard-page__outbound-grid__item__header',
'display: flex; align-items: center; gap: 12px; min-width: 0;', style: 'margin-bottom: 8px;',
}, },
[ [
E(
'b',
{
style:
'overflow-wrap: anywhere; word-break: break-all; min-width: 0;',
},
renderFlagEmojis(outbound.displayName),
),
outbound.prefix
? E(
'span',
{
class:
'tachyon_dashboard-page__outbound-grid__item__prefix-badge',
},
renderFlagEmojis(outbound.prefix),
)
: '',
E(
'span',
{
style:
'opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;',
},
[formatOutboundType(outbound.type, outbound.transport)].filter(
Boolean,
),
),
E( E(
'div', 'div',
{ {
class: getLatencyClass(), class:
style: 'white-space: nowrap; flex-shrink: 0;', 'tachyon_dashboard-page__outbound-grid__item__title-wrapper',
}, },
connectionStatusText, [
E(
'span',
{
style: `color: ${statusColor}; font-weight: 500; font-size: 13px;`,
},
statusText,
),
],
), ),
], ],
), ),
E( E(
'button', 'div',
{ {
type: 'button', class: 'tachyon_dashboard-page__outbound-grid__item__footer',
class: 'btn dashboard-sections-grid-item-test-latency', style:
style: 'padding: 4px 12px; height: 30px; flex-shrink: 0;', 'display: flex; align-items: center; justify-content: space-between; gap: 8px;',
disabled: latencyFetching ? true : undefined,
click: (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (latencyFetching) return;
testLatency();
},
}, },
latencyFetching [
? [ E(
renderLoaderCircleIcon24(), 'div',
E( {
'span', class: 'tachyon_dashboard-page__outbound-grid__item__type',
style:
'overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
},
renderFlagEmojis(outbound.displayName),
),
latencyDisplay
? E(
'div',
{ {
class: 'dashboard-sections-grid-item-test-latency__label', class: getLatencyClass(),
style: 'white-space: nowrap; flex-shrink: 0;',
}, },
_('Checking...'), latencyDisplay,
), )
] : '',
: E( ],
'span',
{ class: 'dashboard-sections-grid-item-test-latency__label' },
_('Check Connection'),
),
), ),
], ],
); );
@@ -817,26 +829,6 @@ function renderDefaultState({
); );
} }
if (isConnectionNode) {
return E(
'div',
{
class: 'tachyon_dashboard-page__outbound-section',
style: 'border: none; padding: 0;',
},
[
E(
'div',
{
style:
'display: flex; flex-direction: column; gap: 8px; padding: 0;',
},
[...section.outbounds.map((outbound) => renderOutbound(outbound))],
),
],
);
}
const metadataNodes = (section.subscriptionMetadata || []) const metadataNodes = (section.subscriptionMetadata || [])
.map((metadata) => renderSubscriptionMetadata(metadata)) .map((metadata) => renderSubscriptionMetadata(metadata))
.filter(Boolean) as HTMLElement[]; .filter(Boolean) as HTMLElement[];
@@ -907,6 +899,11 @@ function renderDefaultState({
'openvpn', 'openvpn',
].includes(section.action || ''); ].includes(section.action || '');
const connectionTitle =
(section.action &&
CONNECTION_ACTION_LABELS[section.action]) ||
selectedOutbound.displayName;
function getLatencyColor() { function getLatencyColor() {
if (isConnectionNode) { if (isConnectionNode) {
if (latencyFetching) if (latencyFetching)
@@ -974,7 +971,10 @@ function renderDefaultState({
E( E(
'span', 'span',
{ style: 'opacity: 0.7;' }, { style: 'opacity: 0.7;' },
selectedOutbound.displayName, isConnectionNode
? connectionTitle
: selectedOutbound.prefix ||
selectedOutbound.displayName,
), ),
latencyText latencyText
? E( ? E(
@@ -986,7 +986,32 @@ function renderDefaultState({
], ],
); );
})() })()
: '', : isConnectionNode
? (() => {
const selectedOutbound = section.outbounds.find(
(o) => o.selected,
);
if (!selectedOutbound) return '';
const connectionTitle =
(section.action &&
CONNECTION_ACTION_LABELS[section.action]) ||
selectedOutbound.displayName;
if (
section.displayName.toLowerCase().trim() ===
connectionTitle.toLowerCase().trim()
) {
return '';
}
return E(
'span',
{
style:
'font-size: 13px; font-weight: normal; margin-left: 8px; opacity: 0.7;',
},
connectionTitle,
);
})()
: '',
], ],
), ),
E( E(
@@ -527,4 +527,81 @@ describe('renderSections', () => {
expect(prefixBadge).not.toBeNull(); expect(prefixBadge).not.toBeNull();
expect(prefixBadge?.textContent).toBe('Geodema Network'); expect(prefixBadge?.textContent).toBe('Geodema Network');
}); });
it('renders connection node (AWG) with collapsible header and check connection action', () => {
const section: any = {
sectionName: 'AWG',
displayName: 'AMNEZIA',
action: 'awg',
outbounds: [
{
code: 'awg-out',
displayName: 'AmneziaWG 3.0',
latency: 155,
runtimeAvailable: true,
selected: true,
},
],
};
// 1. Collapsed state
const collapsedEl = renderSections({
loading: false,
failed: false,
section,
isCollapsed: true,
onTestLatency: vi.fn(),
onChooseOutbound: vi.fn(),
onCopyOutbound: vi.fn(),
onShowUrlTestInfo: vi.fn(),
onShowPriorityInfo: vi.fn(),
onUpdateSubscription: vi.fn(),
latencyFetching: false,
subscriptionUpdating: false,
});
const titleSec = collapsedEl.querySelector(
'.tachyon_dashboard-page__outbound-section__title-section',
);
expect(titleSec).not.toBeNull();
expect(titleSec?.textContent).toContain('AMNEZIA');
expect(titleSec?.textContent).toContain('AmneziaWG');
expect(titleSec?.textContent).toContain('155ms');
expect(titleSec?.textContent).toContain('Check Connection');
expect(
collapsedEl.querySelector('.tachyon_dashboard-page__outbound-grid'),
).toBeNull();
// 2. Expanded state
const expandedEl = renderSections({
loading: false,
failed: false,
section,
isCollapsed: false,
onTestLatency: vi.fn(),
onChooseOutbound: vi.fn(),
onCopyOutbound: vi.fn(),
onShowUrlTestInfo: vi.fn(),
onShowPriorityInfo: vi.fn(),
onUpdateSubscription: vi.fn(),
latencyFetching: false,
subscriptionUpdating: false,
});
const expandedTitleSec = expandedEl.querySelector(
'.tachyon_dashboard-page__outbound-section__title-section',
);
expect(expandedTitleSec).not.toBeNull();
expect(expandedTitleSec?.textContent).toContain('AMNEZIA');
expect(expandedTitleSec?.textContent).toContain('AmneziaWG');
expect(expandedTitleSec?.textContent).toContain('Check Connection');
const grid = expandedEl.querySelector(
'.tachyon_dashboard-page__outbound-grid',
);
expect(grid).not.toBeNull();
expect(grid?.textContent).toContain('Connected');
expect(grid?.textContent).toContain('AmneziaWG 3.0');
expect(grid?.textContent).toContain('155 ms');
});
}); });
@@ -1862,6 +1862,18 @@ function formatServiceStatusMessage(msg) {
} }
// src/tachyon/tabs/dashboard/partials/renderSections.ts // src/tachyon/tabs/dashboard/partials/renderSections.ts
var CONNECTION_ACTION_LABELS = {
awg: "AmneziaWG",
warp: "WARP",
anytls: "AnyTLS",
snell: "Snell",
mieru: "Mieru",
sudoku: "Sudoku",
masque: "MASQUE",
openvpn: "OpenVPN",
fptn: "FPTN",
vpn: "VPN"
};
function renderFailedState() { function renderFailedState() {
return E( return E(
"div", "div",
@@ -2209,80 +2221,64 @@ function renderDefaultState({
outboundSwitching ? "tachyon_dashboard-page__outbound-grid__item--switching" : "" outboundSwitching ? "tachyon_dashboard-page__outbound-grid__item--switching" : ""
].filter(Boolean).join(" "); ].filter(Boolean).join(" ");
if (isConnectionNode) { if (isConnectionNode) {
const statusText = latencyFetching ? _("Checking...") : outbound.latency === -1 || outbound.latency < 0 ? _("Not responding") : outbound.runtimeAvailable ? _("Connected") : _("Not connected");
const statusColor = latencyFetching ? "var(--warn-color-medium, orange)" : outbound.latency === -1 || outbound.latency < 0 ? "var(--error-color-medium, red)" : outbound.runtimeAvailable ? "var(--success-color-medium, green)" : "var(--error-color-medium, red)";
const latencyDisplay = outbound.latency && outbound.latency > 0 ? `${outbound.latency} ms` : "";
return E( return E(
"div", "div",
{ {
class: className, class: className,
style: "display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;" style: "display: flex; flex-direction: column; justify-content: space-between; padding: 12px; min-width: 0;"
}, },
[ [
E( E(
"div", "div",
{ {
style: "display: flex; align-items: center; gap: 12px; min-width: 0;" class: "tachyon_dashboard-page__outbound-grid__item__header",
style: "margin-bottom: 8px;"
}, },
[ [
E( E(
"b", "div",
{ {
style: "overflow-wrap: anywhere; word-break: break-all; min-width: 0;" class: "tachyon_dashboard-page__outbound-grid__item__title-wrapper"
},
[
E(
"span",
{
style: `color: ${statusColor}; font-weight: 500; font-size: 13px;`
},
statusText
)
]
)
]
),
E(
"div",
{
class: "tachyon_dashboard-page__outbound-grid__item__footer",
style: "display: flex; align-items: center; justify-content: space-between; gap: 8px;"
},
[
E(
"div",
{
class: "tachyon_dashboard-page__outbound-grid__item__type",
style: "overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
}, },
renderFlagEmojis(outbound.displayName) renderFlagEmojis(outbound.displayName)
), ),
outbound.prefix ? E( latencyDisplay ? E(
"span",
{
class: "tachyon_dashboard-page__outbound-grid__item__prefix-badge"
},
renderFlagEmojis(outbound.prefix)
) : "",
E(
"span",
{
style: "opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;"
},
[formatOutboundType(outbound.type, outbound.transport)].filter(
Boolean
)
),
E(
"div", "div",
{ {
class: getLatencyClass(), class: getLatencyClass(),
style: "white-space: nowrap; flex-shrink: 0;" style: "white-space: nowrap; flex-shrink: 0;"
}, },
connectionStatusText latencyDisplay
) ) : ""
] ]
),
E(
"button",
{
type: "button",
class: "btn dashboard-sections-grid-item-test-latency",
style: "padding: 4px 12px; height: 30px; flex-shrink: 0;",
disabled: latencyFetching ? true : void 0,
click: (event) => {
event.preventDefault();
event.stopPropagation();
if (latencyFetching) return;
testLatency();
}
},
latencyFetching ? [
renderLoaderCircleIcon24(),
E(
"span",
{
class: "dashboard-sections-grid-item-test-latency__label"
},
_("Checking...")
)
] : E(
"span",
{ class: "dashboard-sections-grid-item-test-latency__label" },
_("Check Connection")
)
) )
] ]
); );
@@ -2435,24 +2431,6 @@ function renderDefaultState({
] ]
); );
} }
if (isConnectionNode) {
return E(
"div",
{
class: "tachyon_dashboard-page__outbound-section",
style: "border: none; padding: 0;"
},
[
E(
"div",
{
style: "display: flex; flex-direction: column; gap: 8px; padding: 0;"
},
[...section.outbounds.map((outbound) => renderOutbound(outbound))]
)
]
);
}
const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean); const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean);
const subscriptionUpdateAction = renderSubscriptionUpdateAction( const subscriptionUpdateAction = renderSubscriptionUpdateAction(
section, section,
@@ -2512,6 +2490,7 @@ function renderDefaultState({
"masque", "masque",
"openvpn" "openvpn"
].includes(section.action || ""); ].includes(section.action || "");
const connectionTitle = section.action && CONNECTION_ACTION_LABELS[section.action] || selectedOutbound.displayName;
function getLatencyColor() { function getLatencyColor() {
if (isConnectionNode2) { if (isConnectionNode2) {
if (latencyFetching) if (latencyFetching)
@@ -2552,7 +2531,7 @@ function renderDefaultState({
E( E(
"span", "span",
{ style: "opacity: 0.7;" }, { style: "opacity: 0.7;" },
selectedOutbound.displayName isConnectionNode2 ? connectionTitle : selectedOutbound.prefix || selectedOutbound.displayName
), ),
latencyText ? E( latencyText ? E(
"span", "span",
@@ -2561,6 +2540,22 @@ function renderDefaultState({
) : "" ) : ""
] ]
); );
})() : isConnectionNode ? (() => {
const selectedOutbound = section.outbounds.find(
(o) => o.selected
);
if (!selectedOutbound) return "";
const connectionTitle = section.action && CONNECTION_ACTION_LABELS[section.action] || selectedOutbound.displayName;
if (section.displayName.toLowerCase().trim() === connectionTitle.toLowerCase().trim()) {
return "";
}
return E(
"span",
{
style: "font-size: 13px; font-weight: normal; margin-left: 8px; opacity: 0.7;"
},
connectionTitle
);
})() : "" })() : ""
] ]
), ),
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 18:55+0300\n" "POT-Creation-Date: 2026-09-17 19:42+0300\n"
"PO-Revision-Date: 2026-09-17 18:55+0300\n" "PO-Revision-Date: 2026-09-17 19:42+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,7 @@ function config(ctx) {
mark_mask: env_get("TACHYON_FPTN_MARK_MASK", "0x00ff0000"), mark_mask: env_get("TACHYON_FPTN_MARK_MASK", "0x00ff0000"),
rule_priority: env_get("TACHYON_FPTN_RULE_PRIORITY", "102"), rule_priority: env_get("TACHYON_FPTN_RULE_PRIORITY", "102"),
state_dir: state_dir, state_dir: state_dir,
shims_dir: state_dir + "/bin",
pid_file: state_dir + "/fptn.pid", pid_file: state_dir + "/fptn.pid",
log_file: state_dir + "/fptn.log", log_file: state_dir + "/fptn.log",
package_name: "fptn", package_name: "fptn",
+102 -9
View File
@@ -86,6 +86,87 @@ function package_version() {
return ""; return "";
} }
function install_shims() {
let shims_dir = cfg.shims_dir || (cfg.state_dir + "/bin");
command_status("mkdir -p " + shell_quote(shims_dir) + " 2>/dev/null");
let ip_shim = "#!/bin/sh\n" +
"REAL_IP=''\n" +
"for p in /sbin/ip /usr/sbin/ip /usr/bin/ip /bin/ip; do\n" +
" if [ -x \"$p\" ] && [ \"$p\" != \"$0\" ]; then\n" +
" REAL_IP=\"$p\"\n" +
" break\n" +
" fi\n" +
"done\n" +
"[ -z \"$REAL_IP\" ] && REAL_IP='ip'\n" +
"CMD_LINE=\"$*\"\n" +
"case \"$CMD_LINE\" in\n" +
" *route*default*|*default*route*)\n" +
" case \"$CMD_LINE\" in\n" +
" *replace*|*add*|*del*)\n" +
" exit 0\n" +
" ;;\n" +
" esac\n" +
" ;;\n" +
"esac\n" +
"exec \"$REAL_IP\" \"$@\"\n";
fs.writefile(shims_dir + "/ip", ip_shim);
command_status("chmod +x " + shell_quote(shims_dir + "/ip") + " 2>/dev/null");
let sed_shim = "#!/bin/sh\n" +
"REAL_SED=''\n" +
"for p in /bin/sed /usr/bin/sed /sbin/sed /usr/sbin/sed; do\n" +
" if [ -x \"$p\" ] && [ \"$p\" != \"$0\" ]; then\n" +
" REAL_SED=\"$p\"\n" +
" break\n" +
" fi\n" +
"done\n" +
"[ -z \"$REAL_SED\" ] && REAL_SED='sed'\n" +
"case \"$*\" in\n" +
" *resolv.conf*)\n" +
" exit 0\n" +
" ;;\n" +
"esac\n" +
"exec \"$REAL_SED\" \"$@\"\n";
fs.writefile(shims_dir + "/sed", sed_shim);
command_status("chmod +x " + shell_quote(shims_dir + "/sed") + " 2>/dev/null");
let dummy_names = ["iptables", "ip6tables", "resolvectl", "chattr", "systemctl"];
let dummy_shim = "#!/bin/sh\nexit 0\n";
for (let idx, name in dummy_names) {
fs.writefile(shims_dir + "/" + name, dummy_shim);
command_status("chmod +x " + shell_quote(shims_dir + "/" + name) + " 2>/dev/null");
}
}
function sanitize_system() {
command_status("chattr -i /etc/resolv.conf 2>/dev/null; true");
if (fs.stat("/etc/resolv.conf") == null) {
if (fs.stat("/tmp/resolv.conf") != null)
command_status("ln -sf /tmp/resolv.conf /etc/resolv.conf 2>/dev/null; true");
else if (fs.stat("/tmp/resolv.conf.d/resolv.conf.auto") != null)
command_status("ln -sf /tmp/resolv.conf.d/resolv.conf.auto /etc/resolv.conf 2>/dev/null; true");
}
command_status("sed -i '/nameserver 172\\.20\\./d' /etc/resolv.conf 2>/dev/null; true");
for (let i = 0; i < 4; i++) {
command_status("iptables -D OUTPUT -p udp --dport 53 -m comment --comment fptn -j DROP 2>/dev/null; true");
command_status("iptables -D OUTPUT -p tcp --dport 53 -m comment --comment fptn -j DROP 2>/dev/null; true");
command_status("iptables -D OUTPUT -p udp --dport 853 -m comment --comment fptn -j DROP 2>/dev/null; true");
command_status("iptables -D OUTPUT -p tcp --dport 853 -m comment --comment fptn -j DROP 2>/dev/null; true");
}
command_status("uci del_list dhcp.@dnsmasq[0].server='172.20.0.1' 2>/dev/null; uci commit dhcp 2>/dev/null; true");
let main_default = trim(command_output("ip -4 route show table main default 2>/dev/null | grep dev | grep " + shell_quote(cfg.tun_interface) + " || true"));
if (main_default != "") {
log_message("Sanitizing rogue default route dev " + cfg.tun_interface + " in table main", "warn");
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
}
}
function remove_kernel_routing() { function remove_kernel_routing() {
let p = cfg.rule_priority || "102"; let p = cfg.rule_priority || "102";
let mark_spec = cfg.fwmark + "/" + cfg.mark_mask; let mark_spec = cfg.fwmark + "/" + cfg.mark_mask;
@@ -99,6 +180,8 @@ function remove_kernel_routing() {
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true"); command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0) if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0)
command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " down 2>/dev/null; true"); command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " down 2>/dev/null; true");
sanitize_system();
} }
function install_kernel_routing() { function install_kernel_routing() {
@@ -119,12 +202,7 @@ function install_kernel_routing() {
return false; return false;
} }
// Ensure table main default route was not hijacked by fptn-client-cli sanitize_system();
let main_default_dev = trim(command_output("ip -4 route show table main default 2>/dev/null | awk '{print $5; exit}'"));
if (main_default_dev == cfg.tun_interface) {
log_message("FPTN client hijacked table main default route; restoring WAN route", "warn");
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
}
return true; return true;
} }
@@ -161,6 +239,7 @@ function launch_fptn_process(section) {
return false; return false;
command_status("mkdir -p " + shell_quote(cfg.state_dir) + " 2>/dev/null"); command_status("mkdir -p " + shell_quote(cfg.state_dir) + " 2>/dev/null");
install_shims();
let cmd_args = [ let cmd_args = [
cfg.binary, cfg.binary,
@@ -187,7 +266,9 @@ function launch_fptn_process(section) {
push(cmd_args, preferred_server); push(cmd_args, preferred_server);
} }
let cmd_str = command_from_args(cmd_args) + " >> " + shell_quote(cfg.log_file) + " 2>&1 & echo $! > " + shell_quote(cfg.pid_file); let shims_dir = cfg.shims_dir || (cfg.state_dir + "/bin");
let cmd_str = "PATH=" + shell_quote(shims_dir) + ":$PATH " +
command_from_args(cmd_args) + " >> " + shell_quote(cfg.log_file) + " 2>&1 & echo $! > " + shell_quote(cfg.pid_file);
log_message("Starting FPTN client on interface " + cfg.tun_interface, "info"); log_message("Starting FPTN client on interface " + cfg.tun_interface, "info");
system(cmd_str); system(cmd_str);
return true; return true;
@@ -246,8 +327,10 @@ function ensure_routing() {
if (p != null && tun_up) { if (p != null && tun_up) {
let route_installed = command_status("ip route show table " + cfg.route_table + " default dev " + shell_quote(cfg.tun_interface) + " 2>/dev/null | grep -q default") == 0; let route_installed = command_status("ip route show table " + cfg.route_table + " default dev " + shell_quote(cfg.tun_interface) + " 2>/dev/null | grep -q default") == 0;
let rule_installed = command_status("ip rule show 2>/dev/null | grep -q " + shell_quote(cfg.route_table)) == 0; let rule_installed = command_status("ip rule show 2>/dev/null | grep -q " + shell_quote(cfg.route_table)) == 0;
if (route_installed && rule_installed) if (route_installed && rule_installed) {
sanitize_system();
return true; return true;
}
return install_kernel_routing(); return install_kernel_routing();
} }
return start_runtime(); return start_runtime();
@@ -379,7 +462,9 @@ function module_exports() {
ensure_routing: ensure_routing, ensure_routing: ensure_routing,
supervise_runtime: supervise_runtime, supervise_runtime: supervise_runtime,
status_json: status_json, status_json: status_json,
check_json: check_json check_json: check_json,
install_shims: install_shims,
sanitize_system: sanitize_system
}; };
} }
@@ -399,6 +484,14 @@ else if (mode == "ensure-routing")
exit(ensure_routing() ? 0 : 1); exit(ensure_routing() ? 0 : 1);
else if (mode == "supervise-runtime") else if (mode == "supervise-runtime")
exit(supervise_runtime() ? 0 : 1); exit(supervise_runtime() ? 0 : 1);
else if (mode == "install-shims") {
install_shims();
exit(0);
}
else if (mode == "sanitize-system") {
sanitize_system();
exit(0);
}
else if (mode == "status") else if (mode == "status")
status_json(); status_json();
else if (mode == "check") else if (mode == "check")
+31
View File
@@ -154,5 +154,36 @@ TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$ROOT_DIR/tachyon/files/usr/
TG_MASK_OUT="$(ucode -L "$TACHYON_LIB" "$ROOT_DIR/tachyon/files/usr/lib/service/telegram.uc" mask-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Q7Fx")" TG_MASK_OUT="$(ucode -L "$TACHYON_LIB" "$ROOT_DIR/tachyon/files/usr/lib/service/telegram.uc" mask-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Q7Fx")"
assert_eq "••••••••Q7Fx" "$TG_MASK_OUT" "telegram token masking" assert_eq "••••••••Q7Fx" "$TG_MASK_OUT" "telegram token masking"
# 9. Test shims creation and isolation behavior
export TACHYON_FPTN_STATE_DIR="$WORK_DIR/fptn_state"
mkdir -p "$TACHYON_FPTN_STATE_DIR"
TACHYON_LIB_DIR="$TACHYON_LIB" ucode_run "$FPTN_RUNTIME_UC" install-shims
SHIMS_DIR="$TACHYON_FPTN_STATE_DIR/bin"
[ -x "$SHIMS_DIR/ip" ] || fail "ip shim not created or not executable"
[ -x "$SHIMS_DIR/sed" ] || fail "sed shim not created or not executable"
[ -x "$SHIMS_DIR/iptables" ] || fail "iptables shim not created or not executable"
[ -x "$SHIMS_DIR/chattr" ] || fail "chattr shim not created or not executable"
# Test ip shim blocks default route replace/del/add
IP_REPLACE_STATUS=0
"$SHIMS_DIR/ip" route replace default dev tun-fptn scope link || IP_REPLACE_STATUS=$?
assert_eq "0" "$IP_REPLACE_STATUS" "ip shim should return 0 for route replace default"
IP_DEL_STATUS=0
"$SHIMS_DIR/ip" route del default dev tun-fptn scope link || IP_DEL_STATUS=$?
assert_eq "0" "$IP_DEL_STATUS" "ip shim should return 0 for route del default"
# Test sed shim blocks modifying /etc/resolv.conf
SED_STATUS=0
"$SHIMS_DIR/sed" -i '1i nameserver 172.20.0.1' /etc/resolv.conf || SED_STATUS=$?
assert_eq "0" "$SED_STATUS" "sed shim should return 0 for resolv.conf"
# Test dummy shims exit 0
IPT_STATUS=0
"$SHIMS_DIR/iptables" -A OUTPUT -p udp --dport 53 -j DROP || IPT_STATUS=$?
assert_eq "0" "$IPT_STATUS" "iptables shim should return 0"
echo "fptn component tests passed" echo "fptn component tests passed"