mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +08:00
🗽 Sync 2026-09-29 04:49:38
This commit is contained in:
1 parent
1ba92ebff1
commit
cec9f6bff2
87 files changed
+25281
No files matched your search
@@ -0,0 +1,30 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for Tailscale Next
|
||||
LUCI_URL:=https://github.com/tokisaki-galaxy/luci-app-tailscale-community
|
||||
PKG_DESCRIPTION:=Provides a LuCI Web management interface for Tailscale, allowing viewing status, configuring nodes and daemons.
|
||||
PKG_MAINTAINER:=Tokisaki-Galaxy <moebest@outlook.jp>
|
||||
LUCI_DEPENDS:=+tailscale-next +ip-full +luci-base +ucode +ucode-mod-fs +ucode-mod-uci
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
define Package/luci-app-tailscale-next/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
rm -f /tmp/luci-indexcache.* /tmp/luci-modulecache/* 2>/dev/null
|
||||
chmod 0755 /etc/init.d/tailscale-settings /etc/hotplug.d/iface/99-tailscale-routes 2>/dev/null || true
|
||||
/etc/init.d/rpcd restart 2>/dev/null || true
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-tailscale-next/prerm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/etc/init.d/tailscale-settings clean_firewall 2>/dev/null || true
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,667 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
const callGetStatus = rpc.declare({ object: 'tailscale', method: 'get_status' });
|
||||
const callGetSettings = rpc.declare({ object: 'tailscale', method: 'get_settings' });
|
||||
const callDoLogin = rpc.declare({ object: 'tailscale', method: 'do_login', params: ['form_data'] });
|
||||
const callDoLogout = rpc.declare({ object: 'tailscale', method: 'do_logout' });
|
||||
const callGetSubroutes = rpc.declare({ object: 'tailscale', method: 'get_subroutes' });
|
||||
const callGetLogs = rpc.declare({ object: 'tailscale', method: 'get_logs' });
|
||||
const callReloadSettings = rpc.declare({ object: 'tailscale', method: 'reload_settings' });
|
||||
const callRestart = rpc.declare({ object: 'tailscale', method: 'restart' });
|
||||
let map;
|
||||
|
||||
const tailscaleSettingsConf = [
|
||||
[form.Flag, 'service_enabled', _('Enable Tailscale Service'), _('Enable or disable the Tailscale background service (/etc/init.d/tailscale).'), { rmempty: false }],
|
||||
[form.Flag, 'runwebclient', _('Enable Web Interface'), _('Expose a local web interface on port 5252 for managing this node over Tailscale (--webclient).'), { rmempty: false }],
|
||||
[form.Flag, 'tunnel_snat', _('Enable Tunnel Traffic SNAT'), _('Use OpenWrt system firewall to manage NAT for Tailscale tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network.'), { default: '0', rmempty: false }],
|
||||
[form.Flag, 'accept_routes', _('Accept Routes'), _('Allow accepting subnet routes announced by other nodes (--accept-routes).'), { rmempty: false }],
|
||||
[form.Flag, 'advertise_exit_node', _('Advertise Exit Node'), _('Declare this device as an exit node, allowing other nodes to route all traffic through it (--advertise-exit-node).'), { rmempty: false }],
|
||||
[form.Flag, 'exit_node_allow_lan_access', _('Allow LAN Access'), _('When using or advertising the exit node, allow access to the local LAN (--exit-node-allow-lan-access).'), { rmempty: false, depends: { 'advertise_exit_node': '1' } }],
|
||||
[form.Flag, 'ssh', _('Enable Tailscale SSH'), _('Allow connecting to this device through the native SSH function of Tailscale (--ssh).'), { rmempty: false }],
|
||||
[form.Flag, 'shields_up', _('Shields Up'), _('When enabled, blocks all inbound connections from the Tailscale network (--shields-up).'), { rmempty: false }],
|
||||
[form.ListValue, 'dns_mode', _('DNS Mode'), _('Controls how Tailscale DNS is handled (--accept-dns).') + '<br>' + _('Disabled: system DNS only.') + '<br>' + _('MagicDNS: Tailscale overrides resolv.conf.') + '<br>' + _('OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)'), { values: [['disabled', _('Disabled')], ['magicdns', 'MagicDNS'], ['openwrt_forward', _('OpenWrt Forward')]], rmempty: false }],
|
||||
[form.Flag, 'enable_relay', _('Enable Peer Relay'), _('Enable this device as a Peer Relay server (--peer-relay). Requires a public IP and an UDP port open on the router.'), { rmempty: false }]
|
||||
];
|
||||
|
||||
const daemonConf = [
|
||||
[form.Flag, 'daemon_reduce_memory', _('(Experimental) Reduce Memory Usage'), _('Enabling this option can reduce memory usage, but it may sacrifice some performance (set GOGC=10).'), { rmempty: false }]
|
||||
];
|
||||
|
||||
// this function copy from luci-app-frpc. thx
|
||||
function setParams(o, params) {
|
||||
if (!params) return;
|
||||
|
||||
for (const [key, val] of Object.entries(params)) {
|
||||
if (key === 'values') {
|
||||
[].concat(val).forEach(v =>
|
||||
o.value.apply(o, Array.isArray(v) ? v : [v])
|
||||
);
|
||||
} else if (key === 'depends') {
|
||||
const arr = Array.isArray(val) ? val : [val];
|
||||
o.deps = arr.map(dep => Object.assign({}, ...o.deps, dep));
|
||||
} else {
|
||||
o[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.datatype === 'bool')
|
||||
Object.assign(o, { enabled: 'true', disabled: 'false' });
|
||||
}
|
||||
|
||||
// this function copy from luci-app-frpc. thx
|
||||
function defTabOpts(s, t, opts, params) {
|
||||
for (let i = 0; i < opts.length; i++) {
|
||||
const opt = opts[i];
|
||||
const o = s.taboption(t, opt[0], opt[1], opt[2], opt[3]);
|
||||
setParams(o, opt[4]);
|
||||
setParams(o, params);
|
||||
}
|
||||
}
|
||||
|
||||
function getRunningStatus() {
|
||||
return L.resolveDefault(callGetStatus(), { running: false }).then(function (res) {
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
const bytes_num = parseInt(bytes, 10);
|
||||
if (isNaN(bytes_num) || bytes_num === 0) return '-';
|
||||
const k = 1000;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes_num) / Math.log(k));
|
||||
return parseFloat((bytes_num / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatLastSeen(d) {
|
||||
if (!d) return _('N/A');
|
||||
if (d === '0001-01-01T00:00:00Z') return _('Now');
|
||||
const t = new Date(d);
|
||||
if (isNaN(t)) return _('Invalid Date');
|
||||
const diff = (Date.now() - t) / 1000;
|
||||
if (diff < 0) return t.toLocaleString();
|
||||
if (diff < 60) return _('Just now');
|
||||
|
||||
const mins = diff / 60, hrs = mins / 60, days = hrs / 24;
|
||||
const fmt = (n, s, p) => `${Math.floor(n)} ${Math.floor(n) === 1 ? _(s) : _(p)} ${_('ago')}`;
|
||||
|
||||
if (mins < 60) return fmt(mins, 'minute', 'minutes');
|
||||
if (hrs < 24) return fmt(hrs, 'hour', 'hours');
|
||||
if (days < 30) return fmt(days, 'day', 'days');
|
||||
|
||||
return t.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
const defaultDerpRegions = {
|
||||
"1": "NYC", "nyc": "NYC",
|
||||
"2": "SFO", "sfo": "SFO",
|
||||
"3": "SIN", "sin": "SIN",
|
||||
"4": "FRA", "fra": "FRA",
|
||||
"5": "SYD", "syd": "SYD",
|
||||
"6": "BLR", "blr": "BLR",
|
||||
"7": "NRT", "nrt": "NRT",
|
||||
"8": "LHR", "lhr": "LHR",
|
||||
"9": "DFW", "dfw": "DFW",
|
||||
"10": "SEA", "sea": "SEA",
|
||||
"11": "MIA", "mia": "MIA",
|
||||
"12": "ORD", "ord": "ORD",
|
||||
"13": "MAD", "mad": "MAD",
|
||||
"14": "AMS", "ams": "AMS",
|
||||
"15": "WAW", "waw": "WAW",
|
||||
"16": "YUL", "yul": "YUL",
|
||||
"17": "LAX", "lax": "LAX",
|
||||
"18": "SCL", "scl": "SCL",
|
||||
"19": "DXB", "dxb": "DXB",
|
||||
"20": "DEN", "den": "DEN",
|
||||
"21": "JNB", "jnb": "JNB",
|
||||
"22": "GRU", "gru": "GRU",
|
||||
"23": "DUB", "dub": "DUB",
|
||||
"24": "MAN", "man": "MAN",
|
||||
"25": "HND", "hnd": "HND",
|
||||
"26": "YYZ", "yyz": "YYZ"
|
||||
};
|
||||
let regionCodeMap = Object.assign({}, defaultDerpRegions);
|
||||
|
||||
function getRegionName(idOrCode) {
|
||||
if (!idOrCode) return '';
|
||||
return regionCodeMap[idOrCode] || regionCodeMap[String(idOrCode).toLowerCase()] || '';
|
||||
}
|
||||
|
||||
function formatConnectionInfo(info) {
|
||||
if (!info) { return '-'; }
|
||||
if (typeof info === 'string' && info.length === 3) {
|
||||
const lowerCaseInfo = info.toLowerCase();
|
||||
return regionCodeMap[lowerCaseInfo] || info;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
const customServerInput = document.getElementById('widget.cbid.tailscale.settings.custom_login_url');
|
||||
const customServer = (customServerInput ? customServerInput.value : '') || uci.get('tailscale', 'settings', 'custom_login_url') || '';
|
||||
const customServerAuthInput = document.getElementById('widget.cbid.tailscale.settings.custom_login_AuthKey');
|
||||
const customServerAuth = (customServerAuthInput ? customServerAuthInput.value : '') || uci.get('tailscale', 'settings', 'custom_login_AuthKey') || '';
|
||||
|
||||
const loginWindow = window.open('', '_blank');
|
||||
if (!loginWindow) {
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Could not open a new tab. Please check if your browser or an extension blocked the pop-up.')) ], 10000, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = loginWindow.document;
|
||||
doc.body.innerHTML =
|
||||
'<h2>' + _('Tailscale Login') + '</h2>' +
|
||||
'<p>' + _('Requesting Tailscale login URL... Please wait.') + '</p>' +
|
||||
'<p>' + _('This can take up to 30 seconds.') + '</p>';
|
||||
|
||||
ui.showModal(_('Requesting Login URL...'), E('em', {}, _('Please wait.')));
|
||||
const payload = {
|
||||
loginserver: customServer,
|
||||
loginserver_authkey: customServerAuth
|
||||
};
|
||||
|
||||
return callDoLogin(payload).then(function(res) {
|
||||
ui.hideModal();
|
||||
if (res && res.url) {
|
||||
loginWindow.location.href = res.url;
|
||||
} else {
|
||||
doc.body.innerHTML =
|
||||
'<h2>' + _('Error') + '</h2>' +
|
||||
'<p>' + _('Failed to get login URL. You may close this tab.') + '</p>';
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Failed to get login URL: Invalid response from server.')) ], 7000, 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Failed to get login URL: %s').format(err.message || _('Unknown error'))) ], 7000, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
const confirmationContent = E([
|
||||
E('p', {}, _('Are you sure you want to log out?') + '<br>' + _('This will disconnect this device from your Tailnet and require you to re-authenticate.')),
|
||||
E('div', { 'style': 'text-align: right; margin-top: 1em;' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative',
|
||||
'click': function() {
|
||||
ui.hideModal();
|
||||
ui.showModal(_('Logging out...'), E('em', {}, _('Please wait.')));
|
||||
|
||||
return callDoLogout().then(function(res) {
|
||||
ui.hideModal();
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Successfully logged out.')) ], 3000, 'info');
|
||||
window.location.reload();
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Logout failed: %s').format(err.message || _('Unknown error'))) ], 7000, 'error');
|
||||
});
|
||||
}
|
||||
}, _('Logout'))
|
||||
])
|
||||
]);
|
||||
ui.showModal(_('Confirm Logout'), confirmationContent);
|
||||
}
|
||||
|
||||
function handleRestart() {
|
||||
ui.showModal(_('Restart Service'), [
|
||||
E('p', {}, _('Are you sure you want to restart Tailscale service?')),
|
||||
E('div', { 'class': 'right', 'style': 'margin-top: 15px;' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': function(ev) {
|
||||
ui.showModal(_('Restarting Tailscale...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Please wait while the service is restarting...'))
|
||||
]);
|
||||
return L.resolveDefault(callRestart(), {}).then(function() {
|
||||
return new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}).then(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', {}, _('Failed to restart: %s').format(e.message || e)), 'error');
|
||||
ui.hideModal();
|
||||
});
|
||||
}
|
||||
}, _('Restart'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
function renderStatus(status) {
|
||||
// If status object is not yet available, show a loading message.
|
||||
if (!status || !status.hasOwnProperty('status')) {
|
||||
return E('em', {}, _('Collecting data ...'));
|
||||
}
|
||||
|
||||
const customServerUrl = uci.get('tailscale', 'settings', 'custom_login_url');
|
||||
let serverDisplayText = _('Official Server (Tailscale SaaS)');
|
||||
if (customServerUrl && customServerUrl !== '') {
|
||||
serverDisplayText = _('Custom Server (%s)').format(customServerUrl.replace(/^https?:\/\//, ''));
|
||||
}
|
||||
|
||||
let statusBadge;
|
||||
let actionButtons = [];
|
||||
|
||||
if (status.status === 'not_installed') {
|
||||
statusBadge = E('span', { 'style': 'color:red;' }, E('strong', {}, _('NOT INSTALLED')));
|
||||
} else if (status.status === 'logout') {
|
||||
statusBadge = E('span', { 'style': 'color:red;' }, E('strong', {}, _('NOT LOGGED IN')));
|
||||
actionButtons.push(E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'style': 'margin-left: 10px; padding: 2px 10px;',
|
||||
'click': ui.createHandlerFn(this, handleLogin)
|
||||
}, _('Login')));
|
||||
} else if (status.status === 'running') {
|
||||
statusBadge = E('span', { 'style': 'color:green;' }, E('strong', {}, _('RUNNING')));
|
||||
actionButtons.push(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'style': 'margin-left: 10px; padding: 2px 10px;',
|
||||
'click': ui.createHandlerFn(this, handleRestart)
|
||||
}, _('Restart')));
|
||||
actionButtons.push(E('button', {
|
||||
'class': 'cbi-button cbi-button-remove',
|
||||
'style': 'margin-left: 8px; padding: 2px 10px;',
|
||||
'click': ui.createHandlerFn(this, handleLogout)
|
||||
}, _('Logout')));
|
||||
} else {
|
||||
// stopped / not running
|
||||
statusBadge = E('span', { 'style': 'color:orange;' }, E('strong', {}, _('NOT RUNNING')));
|
||||
}
|
||||
|
||||
const statusData = [
|
||||
{ label: _('Service Status'), value: E('span', {}, [statusBadge, ...actionButtons]) },
|
||||
{ label: _('Control Server'), value: serverDisplayText },
|
||||
{ label: _('Version'), value: status.version || 'N/A' },
|
||||
{ label: _('TUN Mode'), value: status.TUNMode ? _('Enabled') : _('Disabled') },
|
||||
{ label: _('Tailscale IPv4'), value: status.ipv4 || '-' },
|
||||
{ label: _('Tailscale IPv6'), value: status.ipv6 || '-' },
|
||||
{ label: _('Tailnet Account'), value: status.account || status.domain_name || '-' }
|
||||
];
|
||||
|
||||
const statusTable = E('table', { 'style': 'width: 100%; border-spacing: 0 5px;' }, [
|
||||
E('tr', {}, statusData.map(item => E('td', { 'style': 'padding-right: 20px;' }, E('strong', {}, item.label)))),
|
||||
E('tr', {}, statusData.map(item => E('td', { 'style': 'padding-right: 20px;' }, item.value)))
|
||||
]);
|
||||
|
||||
let connSection = null;
|
||||
if (status.connectivity) {
|
||||
const conn = status.connectivity;
|
||||
const badges = [
|
||||
{ name: 'Varies', val: conn.varies, ok: (conn.varies === 'No') },
|
||||
{ name: 'IPv4', val: conn.ipv4, ok: (conn.ipv4 === 'Yes') },
|
||||
{ name: 'IPv6', val: conn.ipv6, ok: (conn.ipv6 === 'Yes') },
|
||||
{ name: 'UDP', val: conn.udp, ok: (conn.udp === 'Yes') },
|
||||
{ name: 'UPnP', val: conn.upnp, ok: (conn.upnp === 'Yes') },
|
||||
{ name: 'PCP', val: conn.pcp, ok: (conn.pcp === 'Yes') },
|
||||
{ name: 'NAT-PMP', val: conn.pmp, ok: (conn.pmp === 'Yes') },
|
||||
{ name: 'Hairpinning', val: conn.hairpinning, ok: (conn.hairpinning === 'Yes') }
|
||||
];
|
||||
|
||||
const badgeElements = badges.map(b => {
|
||||
const color = b.ok ? '#28a745' : '#6c757d';
|
||||
return E('span', {
|
||||
'style': 'display: inline-block; margin-right: 8px; margin-bottom: 6px; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; background: #e9ecef; color: ' + color + '; border: 1px solid #ced4da;'
|
||||
}, `${b.name}: ${b.val || 'No'}`);
|
||||
});
|
||||
|
||||
let nearestDerpText = '-';
|
||||
if (conn.preferred_derp) {
|
||||
const regionName = getRegionName(conn.preferred_derp) || `DERP-${conn.preferred_derp}`;
|
||||
nearestDerpText = regionName + (conn.derp_latency ? ` (${conn.derp_latency})` : '');
|
||||
}
|
||||
|
||||
let endpointsText = (conn.endpoints && conn.endpoints.length > 0) ? conn.endpoints.join(', ') : '-';
|
||||
|
||||
connSection = E('div', { 'style': 'margin-top: 12px; padding-top: 10px; border-top: 1px dashed #ccc;' }, [
|
||||
E('div', { 'style': 'font-weight: bold; margin-bottom: 8px; font-size: 14px;' }, [
|
||||
_('Client Connectivity')
|
||||
]),
|
||||
E('div', { 'style': 'margin-bottom: 8px; font-size: 13px;' }, [
|
||||
E('strong', {}, _('Nearest Relay') + ': '),
|
||||
E('span', { 'style': 'color: #007bff; font-weight: bold;' }, nearestDerpText),
|
||||
E('span', { 'style': 'margin-left: 20px;' }, [
|
||||
E('strong', {}, _('Public Endpoints') + ': '),
|
||||
E('span', {}, endpointsText)
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'margin-top: 4px;' }, badgeElements)
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', {}, [statusTable, connSection].filter(Boolean));
|
||||
}
|
||||
|
||||
function renderLogs(logs_data) {
|
||||
if (!logs_data || !logs_data.logs || logs_data.logs.length === 0) {
|
||||
return E('em', {}, _('No tailscale-related logs found.'));
|
||||
}
|
||||
|
||||
const lines = logs_data.logs.map(function(line) {
|
||||
return E('div', { 'style': 'white-space: pre; font-family: monospace; font-size: 13px; line-height: 1.5;' }, line);
|
||||
});
|
||||
|
||||
return E('div', {
|
||||
'style': 'max-height: 500px; overflow-y: auto; background: #f5f5f5; border: 1px solid #ccc; padding: 8px; border-radius: 3px;'
|
||||
}, lines);
|
||||
}
|
||||
|
||||
function renderDevices(status) {
|
||||
if (!status || !status.hasOwnProperty('status')) {
|
||||
return E('em', {}, _('Collecting data ...'));
|
||||
}
|
||||
|
||||
if (status.status === 'not_installed') {
|
||||
return E('em', {}, _('Tailscale is not installed.'));
|
||||
}
|
||||
|
||||
if (status.status === 'logout') {
|
||||
return E('em', {}, _('Tailscale is not logged in. Please log in to view devices.'));
|
||||
}
|
||||
|
||||
if (status.status !== 'running') {
|
||||
return E('em', {}, _('Tailscale is not running.'));
|
||||
}
|
||||
|
||||
const peers = status.peers;
|
||||
if (!peers || Object.keys(peers).length === 0) {
|
||||
return E('p', {}, _('No peer devices found.'));
|
||||
}
|
||||
|
||||
const peerTableHeaders = [
|
||||
{ text: _('Status'), style: 'width: 80px;' },
|
||||
{ text: _('Hostname') },
|
||||
{ text: _('Tailscale IP') },
|
||||
{ text: _('OS') },
|
||||
{ text: _('Connection Info') },
|
||||
{ text: _('RX') },
|
||||
{ text: _('TX') },
|
||||
{ text: _('Last Seen') }
|
||||
];
|
||||
|
||||
return E('table', { 'class': 'cbi-table' }, [
|
||||
E('tr', { 'class': 'cbi-table-header' }, peerTableHeaders.map(header => {
|
||||
let th_style = 'padding-right: 20px; text-align: left;';
|
||||
if (header.style) {
|
||||
th_style += header.style;
|
||||
}
|
||||
return E('th', { 'class': 'cbi-table-cell', 'style': th_style }, header.text);
|
||||
})),
|
||||
|
||||
...Object.entries(peers).map(([peerid, peer]) => {
|
||||
const td_style = 'padding-right: 20px;';
|
||||
|
||||
return E('tr', { 'class': 'cbi-row', 'id': `tailscale-peer-${peerid}` }, [
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style },
|
||||
E('span', {
|
||||
'class': peer.online ? 'badge-status-positive' : 'badge-status-neutral',
|
||||
'style': `color: ${peer.online ? 'green' : 'grey'}; font-size: 1.2em;`,
|
||||
'title': (peer.exit_node ? _('Exit Node') + ' ' : '') + (peer.online ? _('Online') : _('Offline'))
|
||||
}, peer.online ? '●' : '○')
|
||||
),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, E('strong', {}, peer.hostname + (peer.exit_node_option ? ' (ExNode)' : ''))),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ip || 'N/A'),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ostype || 'N/A'),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatConnectionInfo(peer.linkadress || '-')),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.rx)),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.tx)),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatLastSeen(peer.lastseen))
|
||||
]);
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callGetStatus(), { running: '', peers: [] }),
|
||||
L.resolveDefault(callGetSettings(), { accept_routes: false }),
|
||||
L.resolveDefault(callGetSubroutes(), { routes: [] })
|
||||
])
|
||||
.then(function([status, settings_from_rpc, subroutes]) {
|
||||
return uci.load('tailscale').then(function() {
|
||||
if (uci.get('tailscale', 'settings') === null) {
|
||||
// No existing settings found; initialize UCI with RPC settings
|
||||
uci.add('tailscale', 'settings', 'settings');
|
||||
uci.set('tailscale', 'settings', 'service_enabled', '0');
|
||||
uci.set('tailscale', 'settings', 'accept_routes', (settings_from_rpc.accept_routes ? '1' : '0'));
|
||||
uci.set('tailscale', 'settings', 'advertise_exit_node', ((settings_from_rpc.advertise_exit_node || false) ? '1' : '0'));
|
||||
uci.set('tailscale', 'settings', 'advertise_routes', (settings_from_rpc.advertise_routes || []).join(', '));
|
||||
uci.set('tailscale', 'settings', 'exit_node', settings_from_rpc.exit_node || '');
|
||||
uci.set('tailscale', 'settings', 'exit_node_allow_lan_access', ((settings_from_rpc.exit_node_allow_lan_access || false) ? '1' : '0'));
|
||||
uci.set('tailscale', 'settings', 'ssh', ((settings_from_rpc.ssh || false) ? '1' : '0'));
|
||||
uci.set('tailscale', 'settings', 'shields_up', ((settings_from_rpc.shields_up || false) ? '1' : '0'));
|
||||
uci.set('tailscale', 'settings', 'runwebclient', ((settings_from_rpc.runwebclient || false) ? '1' : '0'));
|
||||
uci.set('tailscale', 'settings', 'tunnel_snat', (settings_from_rpc.tunnel_snat || '0'));
|
||||
uci.set('tailscale', 'settings', 'dns_mode', 'disabled');
|
||||
|
||||
uci.set('tailscale', 'settings', 'daemon_reduce_memory', '0');
|
||||
uci.set('tailscale', 'settings', 'daemon_mtu', '');
|
||||
return uci.save();
|
||||
}
|
||||
}).then(function() {
|
||||
var dirty = false;
|
||||
// Migrate from old disable_magic_dns to dns_mode if needed
|
||||
if (uci.get('tailscale', 'settings', 'dns_mode') === null) {
|
||||
var oldMagicDns = uci.get('tailscale', 'settings', 'disable_magic_dns');
|
||||
uci.set('tailscale', 'settings', 'dns_mode', oldMagicDns === '1' ? 'disabled' : 'magicdns');
|
||||
uci.unset('tailscale', 'settings', 'disable_magic_dns');
|
||||
dirty = true;
|
||||
}
|
||||
// Migrate and clean up legacy nosnat and outbound_masq options
|
||||
if (uci.get('tailscale', 'settings', 'tunnel_snat') === null) {
|
||||
var oldNosnat = uci.get('tailscale', 'settings', 'nosnat');
|
||||
var oldMasq = uci.get('tailscale', 'settings', 'outbound_masq');
|
||||
if (oldNosnat !== null) {
|
||||
uci.set('tailscale', 'settings', 'tunnel_snat', oldNosnat === '1' ? '0' : '1');
|
||||
} else if (oldMasq !== null) {
|
||||
uci.set('tailscale', 'settings', 'tunnel_snat', oldMasq === '1' ? '1' : '0');
|
||||
} else {
|
||||
uci.set('tailscale', 'settings', 'tunnel_snat', '0');
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
if (uci.get('tailscale', 'settings', 'nosnat') !== null) {
|
||||
uci.unset('tailscale', 'settings', 'nosnat');
|
||||
dirty = true;
|
||||
}
|
||||
if (uci.get('tailscale', 'settings', 'outbound_masq') !== null) {
|
||||
uci.unset('tailscale', 'settings', 'outbound_masq');
|
||||
dirty = true;
|
||||
}
|
||||
if (dirty) {
|
||||
return uci.save();
|
||||
}
|
||||
}).then(function() {
|
||||
return [status, settings_from_rpc, subroutes];
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render ([status = {}, settings = {}, subroutes_obj]) {
|
||||
const subroutes = (subroutes_obj && subroutes_obj.routes) ? subroutes_obj.routes : [];
|
||||
|
||||
let s;
|
||||
map = new form.Map('tailscale', _('Tailscale'), _('Tailscale is a mesh VPN solution that makes it easy to connect your devices securely. This configuration page allows you to manage Tailscale settings on your OpenWrt device.'));
|
||||
|
||||
let lastStatus = null;
|
||||
s = map.section(form.NamedSection, '_status');
|
||||
s.anonymous = true;
|
||||
s.render = function (section_id) {
|
||||
L.Poll.add(
|
||||
function () {
|
||||
return getRunningStatus().then(function (res) {
|
||||
if (lastStatus === 'logout' && (res.status === 'running' || res.status === 'stopped')) {
|
||||
L.resolveDefault(callReloadSettings(), {}).then(function() {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
if (res.status === 'logout' || res.status === 'running' || res.status === 'stopped') {
|
||||
lastStatus = res.status;
|
||||
}
|
||||
|
||||
const view = document.getElementById("service_status_display");
|
||||
if (view) {
|
||||
const content = renderStatus(res);
|
||||
view.replaceChildren(content);
|
||||
}
|
||||
|
||||
const devicesView = document.getElementById("tailscale_devices_display");
|
||||
if (devicesView) {
|
||||
devicesView.replaceChildren(renderDevices(res));
|
||||
}
|
||||
});
|
||||
}, 5);
|
||||
|
||||
return E('div', {}, [
|
||||
E('hr', { 'style': 'margin: 5px 0 15px 0; border: 0; border-top: 1px solid #e5e5e5;' }),
|
||||
E('div', { 'id': 'service_status_display', 'class': 'cbi-value' },
|
||||
_('Collecting data ...')
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
// Bind settings to the 'settings' section of uci
|
||||
s = map.section(form.NamedSection, 'settings', 'settings', null);
|
||||
s.dynamic = true;
|
||||
|
||||
// Tab 1: General Settings
|
||||
s.tab('general', _('General Settings'));
|
||||
|
||||
defTabOpts(s, 'general', tailscaleSettingsConf, { optional: false });
|
||||
|
||||
const relayPort = s.taboption('general', form.Value, 'relay_server_port', _('Peer Relay Port'),
|
||||
_('UDP port for the Peer Relay service (--peer-relay-port). Open this port on your router firewall/NAT.')
|
||||
);
|
||||
relayPort.datatype = 'port';
|
||||
relayPort.placeholder = '40000';
|
||||
relayPort.rmempty = false;
|
||||
relayPort.depends('enable_relay', '1');
|
||||
|
||||
const en = s.taboption('general', form.ListValue, 'exit_node', _('Exit Node'), _('Select an exit node from the list (--exit-node). If enabled, Allow LAN Access is enabled implicitly.'));
|
||||
en.value('', _('None'));
|
||||
if (status.peers) {
|
||||
Object.values(status.peers).forEach(function(peer) {
|
||||
if (peer.exit_node_option) {
|
||||
const primaryIp = peer.ip.split('<br>')[0];
|
||||
const label = peer.hostname ? `${peer.hostname} (${primaryIp})` : primaryIp;
|
||||
en.value(primaryIp, label);
|
||||
}
|
||||
});
|
||||
}
|
||||
en.rmempty = true;
|
||||
en.cfgvalue = function(section_id) {
|
||||
if (status && status.status === 'running' && status.peers) {
|
||||
for (const id in status.peers) {
|
||||
if (status.peers[id].exit_node) {
|
||||
return status.peers[id].ip.split('<br>')[0];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return uci.get('tailscale', 'settings', 'exit_node') || '';
|
||||
};
|
||||
|
||||
const o = s.taboption('general', form.DynamicList, 'advertise_routes', _('Advertise Subnet Routes'),
|
||||
_('Announce subnet routes behind this device (--advertise-routes). Select from the detected subnets below or enter custom routes (comma-separated).')
|
||||
);
|
||||
if (subroutes.length > 0) {
|
||||
subroutes.forEach(function(subnet) {
|
||||
o.value(subnet, subnet);
|
||||
});
|
||||
}
|
||||
o.rmempty = true;
|
||||
|
||||
const customLoginUrl = s.taboption('general', form.Value, 'custom_login_url',
|
||||
_('Custom Control Server'),
|
||||
_('Optional: Specify a custom control server URL (e.g., a Headscale instance, --login-server). Leave blank for default Tailscale control plane.')
|
||||
);
|
||||
customLoginUrl.placeholder = '';
|
||||
customLoginUrl.rmempty = true;
|
||||
|
||||
const customLoginAuthKey = s.taboption('general', form.Value, 'custom_login_AuthKey',
|
||||
_('Custom Server Auth Key'),
|
||||
_('Optional: Specify an authentication key for the custom control server (--auth-key). Leave blank if not required.')
|
||||
);
|
||||
customLoginAuthKey.placeholder = '';
|
||||
customLoginAuthKey.rmempty = true;
|
||||
|
||||
defTabOpts(s, 'general', daemonConf, { optional: false });
|
||||
|
||||
// Tab 2: Devices List
|
||||
s.tab('devices', _('Devices List'));
|
||||
|
||||
const refreshDevicesBtn = s.taboption('devices', form.Button, '_refresh_devices', _('Refresh'));
|
||||
refreshDevicesBtn.inputstyle = 'action';
|
||||
refreshDevicesBtn.onclick = function() {
|
||||
const display = document.getElementById('tailscale_devices_display');
|
||||
if (display) {
|
||||
display.replaceChildren(E('em', {}, _('Collecting data ...')));
|
||||
}
|
||||
return getRunningStatus().then(function(res) {
|
||||
if (display) {
|
||||
display.replaceChildren(renderDevices(res));
|
||||
}
|
||||
}).catch(function(err) {
|
||||
if (display) {
|
||||
display.replaceChildren(E('em', {}, _('Failed to load devices: %s').format(err.message || _('Unknown error'))));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const devicesSection = s.taboption('devices', form.DummyValue, '_devices');
|
||||
devicesSection.render = function () {
|
||||
return E('div', { 'id': 'tailscale_devices_display', 'class': 'cbi-value' }, renderDevices(status));
|
||||
};
|
||||
|
||||
// Tab 3: Logs
|
||||
s.tab('logs', _('Logs'));
|
||||
const logsSection = s.taboption('logs', form.DummyValue, '_logs');
|
||||
logsSection.render = function () {
|
||||
const container = E('div', { 'id': 'tailscale_logs_display', 'class': 'cbi-value' },
|
||||
_('No tailscale-related logs found.')
|
||||
);
|
||||
return container;
|
||||
};
|
||||
|
||||
const refreshLogsBtn = s.taboption('logs', form.Button, '_refresh_logs', _('Refresh'));
|
||||
refreshLogsBtn.inputstyle = 'action';
|
||||
refreshLogsBtn.onclick = function() {
|
||||
const display = document.getElementById('tailscale_logs_display');
|
||||
if (display) {
|
||||
display.replaceChildren(E('em', {}, _('Collecting data ...')));
|
||||
}
|
||||
return callGetLogs().then(function(res) {
|
||||
if (display) {
|
||||
display.replaceChildren(renderLogs(res));
|
||||
}
|
||||
}).catch(function(err) {
|
||||
if (display) {
|
||||
display.replaceChildren(E('em', {}, _('Failed to load logs: %s').format(err.message || _('Unknown error'))));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return map.render();
|
||||
},
|
||||
|
||||
handleSaveApply(ev, mode) {
|
||||
return map.save().then(function () {
|
||||
return ui.changes.apply(mode == '0');
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,335 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "(Experimental) Reduce Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Accept Routes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Advertise Exit Node"
|
||||
msgstr ""
|
||||
|
||||
msgid "Advertise Subnet Routes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Allow LAN Access"
|
||||
msgstr ""
|
||||
|
||||
msgid "Allow accepting subnet routes announced by other nodes (--accept-routes)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Allow connecting to this device through the native SSH function of Tailscale (--ssh)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Allow user access to tailscale"
|
||||
msgstr ""
|
||||
|
||||
msgid "Announce subnet routes behind this device (--advertise-routes). Select from the detected subnets below or enter custom routes (comma-separated)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to log out?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to restart Tailscale service?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Client Connectivity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collecting data ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm Logout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Connection Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Server"
|
||||
msgstr ""
|
||||
|
||||
msgid "Controls how Tailscale DNS is handled (--accept-dns)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not open a new tab. Please check if your browser or an extension blocked the pop-up."
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom Control Server"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom Server (%s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom Server Auth Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "DNS Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Declare this device as an exit node, allowing other nodes to route all traffic through it (--advertise-exit-node)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Devices List"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled: system DNS only."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Peer Relay"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Tailscale SSH"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Tailscale Service"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Tunnel Traffic SNAT"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Web Interface"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable or disable the Tailscale background service (/etc/init.d/tailscale)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable this device as a Peer Relay server (--peer-relay). Requires a public IP and an UDP port open on the router."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabling this option can reduce memory usage, but it may sacrifice some performance (set GOGC=10)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit Node"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expose a local web interface on port 5252 for managing this node over Tailscale (--webclient)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to get login URL. You may close this tab."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to get login URL: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to get login URL: Invalid response from server."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load devices: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load logs: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to restart: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "General Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hostname"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid Date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Just now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last Seen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Logging out..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Logout failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
msgid "MagicDNS: Tailscale overrides resolv.conf."
|
||||
msgstr ""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
msgid "NOT INSTALLED"
|
||||
msgstr ""
|
||||
|
||||
msgid "NOT LOGGED IN"
|
||||
msgstr ""
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nearest Relay"
|
||||
msgstr ""
|
||||
|
||||
msgid "No peer devices found."
|
||||
msgstr ""
|
||||
|
||||
msgid "No tailscale-related logs found."
|
||||
msgstr ""
|
||||
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
msgid "Now"
|
||||
msgstr ""
|
||||
|
||||
msgid "OS"
|
||||
msgstr ""
|
||||
|
||||
msgid "Official Server (Tailscale SaaS)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Offline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Online"
|
||||
msgstr ""
|
||||
|
||||
msgid "OpenWrt Forward"
|
||||
msgstr ""
|
||||
|
||||
msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Optional: Specify a custom control server URL (e.g., a Headscale instance, --login-server). Leave blank for default Tailscale control plane."
|
||||
msgstr ""
|
||||
|
||||
msgid "Optional: Specify an authentication key for the custom control server (--auth-key). Leave blank if not required."
|
||||
msgstr ""
|
||||
|
||||
msgid "Peer Relay Port"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please wait while the service is restarting..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please wait."
|
||||
msgstr ""
|
||||
|
||||
msgid "Public Endpoints"
|
||||
msgstr ""
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr ""
|
||||
|
||||
msgid "RX"
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
msgid "Requesting Login URL..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Requesting Tailscale login URL... Please wait."
|
||||
msgstr ""
|
||||
|
||||
msgid "Restart"
|
||||
msgstr ""
|
||||
|
||||
msgid "Restart Service"
|
||||
msgstr ""
|
||||
|
||||
msgid "Restarting Tailscale..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select an exit node from the list (--exit-node). If enabled, Allow LAN Access is enabled implicitly."
|
||||
msgstr ""
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shields Up"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Successfully logged out."
|
||||
msgstr ""
|
||||
|
||||
msgid "TUN Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "TX"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailnet Account"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale IP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale IPv4"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale IPv6"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale Login"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale is a mesh VPN solution that makes it easy to connect your devices securely. This configuration page allows you to manage Tailscale settings on your OpenWrt device."
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale is not installed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale is not logged in. Please log in to view devices."
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale is not running."
|
||||
msgstr ""
|
||||
|
||||
msgid "This can take up to 30 seconds."
|
||||
msgstr ""
|
||||
|
||||
msgid "This will disconnect this device from your Tailnet and require you to re-authenticate."
|
||||
msgstr ""
|
||||
|
||||
msgid "UDP port for the Peer Relay service (--peer-relay-port). Open this port on your router firewall/NAT."
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use OpenWrt system firewall to manage NAT for Tailscale tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network."
|
||||
msgstr ""
|
||||
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "When enabled, blocks all inbound connections from the Tailscale network (--shields-up)."
|
||||
msgstr ""
|
||||
|
||||
msgid "When using or advertising the exit node, allow access to the local LAN (--exit-node-allow-lan-access)."
|
||||
msgstr ""
|
||||
|
||||
msgid "ago"
|
||||
msgstr ""
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
@@ -0,0 +1,335 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "(Experimental) Reduce Memory Usage"
|
||||
msgstr "(实验性)减少内存占用"
|
||||
|
||||
msgid "Accept Routes"
|
||||
msgstr "接受子网路由"
|
||||
|
||||
msgid "Advertise Exit Node"
|
||||
msgstr "宣告出口节点"
|
||||
|
||||
msgid "Advertise Subnet Routes"
|
||||
msgstr "通告子网路由"
|
||||
|
||||
msgid "Allow LAN Access"
|
||||
msgstr "允许访问局域网"
|
||||
|
||||
msgid "Allow accepting subnet routes announced by other nodes (--accept-routes)."
|
||||
msgstr "允许接收并应用其他节点通告的子网路由 (--accept-routes)。"
|
||||
|
||||
msgid "Allow connecting to this device through the native SSH function of Tailscale (--ssh)."
|
||||
msgstr "允许通过 Tailscale 原生 SSH 功能连接到此设备 (--ssh)。"
|
||||
|
||||
msgid "Allow user access to tailscale"
|
||||
msgstr "允许访问 Tailscale"
|
||||
|
||||
msgid "Announce subnet routes behind this device (--advertise-routes). Select from the detected subnets below or enter custom routes (comma-separated)."
|
||||
msgstr "向 Tailnet 通告本设备后方的子网路由 (--advertise-routes)。从下方检测到的子网中选择或手动输入自定义路由(逗号分隔)。"
|
||||
|
||||
msgid "Are you sure you want to log out?"
|
||||
msgstr "确定要退出登录吗?"
|
||||
|
||||
msgid "Are you sure you want to restart Tailscale service?"
|
||||
msgstr "确定要重启 Tailscale 服务吗?"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Client Connectivity"
|
||||
msgstr "客户端连通性"
|
||||
|
||||
msgid "Collecting data ..."
|
||||
msgstr "正在收集数据..."
|
||||
|
||||
msgid "Confirm Logout"
|
||||
msgstr "确认退出登录"
|
||||
|
||||
msgid "Connection Info"
|
||||
msgstr "连接信息"
|
||||
|
||||
msgid "Control Server"
|
||||
msgstr "控制服务器"
|
||||
|
||||
msgid "Controls how Tailscale DNS is handled (--accept-dns)."
|
||||
msgstr "控制 Tailscale DNS 的处理方式 (--accept-dns)。"
|
||||
|
||||
msgid "Could not open a new tab. Please check if your browser or an extension blocked the pop-up."
|
||||
msgstr "无法打开新标签页,请检查浏览器或扩展程序是否拦截了弹出窗口。"
|
||||
|
||||
msgid "Custom Control Server"
|
||||
msgstr "自定义控制服务器"
|
||||
|
||||
msgid "Custom Server (%s)"
|
||||
msgstr "自定义服务器 (%s)"
|
||||
|
||||
msgid "Custom Server Auth Key"
|
||||
msgstr "自定义服务器预认证密钥"
|
||||
|
||||
msgid "DNS Mode"
|
||||
msgstr "DNS 模式"
|
||||
|
||||
msgid "Declare this device as an exit node, allowing other nodes to route all traffic through it (--advertise-exit-node)."
|
||||
msgstr "将此设备声明为出口节点,允许其他节点通过它路由所有流量 (--advertise-exit-node)。"
|
||||
|
||||
msgid "Devices List"
|
||||
msgstr "设备列表"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已禁用"
|
||||
|
||||
msgid "Disabled: system DNS only."
|
||||
msgstr "禁用:仅使用系统 DNS。"
|
||||
|
||||
msgid "Enable Peer Relay"
|
||||
msgstr "启用节点中继"
|
||||
|
||||
msgid "Enable Tailscale SSH"
|
||||
msgstr "启用 Tailscale SSH"
|
||||
|
||||
msgid "Enable Tailscale Service"
|
||||
msgstr "启用 Tailscale 服务"
|
||||
|
||||
msgid "Enable Tunnel Traffic SNAT"
|
||||
msgstr "开启隧道流量 SNAT"
|
||||
|
||||
msgid "Enable Web Interface"
|
||||
msgstr "启用 Web 管理界面"
|
||||
|
||||
msgid "Enable or disable the Tailscale background service (/etc/init.d/tailscale)."
|
||||
msgstr "启用或禁用 Tailscale 后台守护进程 (/etc/init.d/tailscale)。"
|
||||
|
||||
msgid "Enable this device as a Peer Relay server (--peer-relay). Requires a public IP and an UDP port open on the router."
|
||||
msgstr "将此设备作为节点中继服务器 (--peer-relay)。需要公网 IP 并在路由器上开放 UDP 端口。"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "已启用"
|
||||
|
||||
msgid "Enabling this option can reduce memory usage, but it may sacrifice some performance (set GOGC=10)."
|
||||
msgstr "启用此选项可以减少内存占用,但可能会牺牲部分性能(设置 GOGC=10)。"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "错误"
|
||||
|
||||
msgid "Exit Node"
|
||||
msgstr "出口节点"
|
||||
|
||||
msgid "Expose a local web interface on port 5252 for managing this node over Tailscale (--webclient)."
|
||||
msgstr "在 5252 端口开放本地 Web 管理界面,以便通过 Tailscale 管理此节点 (--webclient)。"
|
||||
|
||||
msgid "Failed to get login URL. You may close this tab."
|
||||
msgstr "获取登录 URL 失败,您可以关闭此标签页。"
|
||||
|
||||
msgid "Failed to get login URL: %s"
|
||||
msgstr "获取登录 URL 失败:%s"
|
||||
|
||||
msgid "Failed to get login URL: Invalid response from server."
|
||||
msgstr "获取登录 URL 失败:服务器响应无效。"
|
||||
|
||||
msgid "Failed to load devices: %s"
|
||||
msgstr "加载设备列表失败:%s"
|
||||
|
||||
msgid "Failed to load logs: %s"
|
||||
msgstr "加载日志失败:%s"
|
||||
|
||||
msgid "Failed to restart: %s"
|
||||
msgstr "重启失败:%s"
|
||||
|
||||
msgid "General Settings"
|
||||
msgstr "常规设置"
|
||||
|
||||
msgid "Hostname"
|
||||
msgstr "主机名"
|
||||
|
||||
msgid "Invalid Date"
|
||||
msgstr "无效日期"
|
||||
|
||||
msgid "Just now"
|
||||
msgstr "刚刚"
|
||||
|
||||
msgid "Last Seen"
|
||||
msgstr "最后活跃"
|
||||
|
||||
msgid "Logging out..."
|
||||
msgstr "正在退出登录..."
|
||||
|
||||
msgid "Login"
|
||||
msgstr "登录"
|
||||
|
||||
msgid "Logout"
|
||||
msgstr "退出登录"
|
||||
|
||||
msgid "Logout failed: %s"
|
||||
msgstr "退出登录失败:%s"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "运行日志"
|
||||
|
||||
msgid "MagicDNS: Tailscale overrides resolv.conf."
|
||||
msgstr "MagicDNS:Tailscale 将根据设置覆写系统 resolv.conf 配置。"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不适用"
|
||||
|
||||
msgid "NOT INSTALLED"
|
||||
msgstr "未安装"
|
||||
|
||||
msgid "NOT LOGGED IN"
|
||||
msgstr "未登录"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Nearest Relay"
|
||||
msgstr "最近中继节点"
|
||||
|
||||
msgid "No peer devices found."
|
||||
msgstr "未发现对等设备。"
|
||||
|
||||
msgid "No tailscale-related logs found."
|
||||
msgstr "未找到 Tailscale 相关日志。"
|
||||
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
|
||||
msgid "Now"
|
||||
msgstr "当前在线"
|
||||
|
||||
msgid "OS"
|
||||
msgstr "操作系统"
|
||||
|
||||
msgid "Official Server (Tailscale SaaS)"
|
||||
msgstr "官方服务器 (Tailscale SaaS)"
|
||||
|
||||
msgid "Offline"
|
||||
msgstr "离线"
|
||||
|
||||
msgid "Online"
|
||||
msgstr "在线"
|
||||
|
||||
msgid "OpenWrt Forward"
|
||||
msgstr "OpenWrt 转发"
|
||||
|
||||
msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
|
||||
msgstr "OpenWrt 转发:通过 dnsmasq 转发 MagicDNS(仅支持 ts.net 域名)。"
|
||||
|
||||
msgid "Optional: Specify a custom control server URL (e.g., a Headscale instance, --login-server). Leave blank for default Tailscale control plane."
|
||||
msgstr "可选:指定自定义控制服务器地址(例如 Headscale 实例,--login-server)。留空则使用 Tailscale 官方服务器。"
|
||||
|
||||
msgid "Optional: Specify an authentication key for the custom control server (--auth-key). Leave blank if not required."
|
||||
msgstr "可选:指定自定义控制服务器的预认证密钥(--auth-key)。留空则无需认证密钥。"
|
||||
|
||||
msgid "Peer Relay Port"
|
||||
msgstr "节点中继端口"
|
||||
|
||||
msgid "Please wait while the service is restarting..."
|
||||
msgstr "服务正在重启,请稍候..."
|
||||
|
||||
msgid "Please wait."
|
||||
msgstr "请稍候。"
|
||||
|
||||
msgid "Public Endpoints"
|
||||
msgstr "公网端点"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "RX"
|
||||
msgstr "接收流量"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "刷新"
|
||||
|
||||
msgid "Requesting Login URL..."
|
||||
msgstr "正在获取登录链接..."
|
||||
|
||||
msgid "Requesting Tailscale login URL... Please wait."
|
||||
msgstr "正在请求 Tailscale 登录链接,请稍候..."
|
||||
|
||||
msgid "Restart"
|
||||
msgstr "重启"
|
||||
|
||||
msgid "Restart Service"
|
||||
msgstr "重启服务"
|
||||
|
||||
msgid "Restarting Tailscale..."
|
||||
msgstr "正在重启 Tailscale..."
|
||||
|
||||
msgid "Select an exit node from the list (--exit-node). If enabled, Allow LAN Access is enabled implicitly."
|
||||
msgstr "从列表中选择出口节点 (--exit-node)。如果启用,将隐式允许访问本地局域网。"
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
msgid "Shields Up"
|
||||
msgstr "防护模式"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态概览"
|
||||
|
||||
msgid "Successfully logged out."
|
||||
msgstr "已成功退出登录。"
|
||||
|
||||
msgid "TUN Mode"
|
||||
msgstr "TUN 模式"
|
||||
|
||||
msgid "TX"
|
||||
msgstr "发送流量"
|
||||
|
||||
msgid "Tailnet Account"
|
||||
msgstr "Tailnet 账户"
|
||||
|
||||
msgid "Tailscale"
|
||||
msgstr "Tailscale"
|
||||
|
||||
msgid "Tailscale IP"
|
||||
msgstr "Tailscale IP"
|
||||
|
||||
msgid "Tailscale IPv4"
|
||||
msgstr "Tailscale IPv4"
|
||||
|
||||
msgid "Tailscale IPv6"
|
||||
msgstr "Tailscale IPv6"
|
||||
|
||||
msgid "Tailscale Login"
|
||||
msgstr "Tailscale 登录认证"
|
||||
|
||||
msgid "Tailscale is a mesh VPN solution that makes it easy to connect your devices securely. This configuration page allows you to manage Tailscale settings on your OpenWrt device."
|
||||
msgstr "Tailscale 是一种网状 Mesh VPN 解决方案,可让您安全轻松地连接各类设备。此页面用于管理 OpenWrt 上的 Tailscale 服务配置。"
|
||||
|
||||
msgid "Tailscale is not installed."
|
||||
msgstr "Tailscale 未安装。"
|
||||
|
||||
msgid "Tailscale is not logged in. Please log in to view devices."
|
||||
msgstr "Tailscale 未登录。请先登录以查看设备列表。"
|
||||
|
||||
msgid "Tailscale is not running."
|
||||
msgstr "Tailscale 未运行。"
|
||||
|
||||
msgid "This can take up to 30 seconds."
|
||||
msgstr "此过程最长可能需要 30 秒。"
|
||||
|
||||
msgid "This will disconnect this device from your Tailnet and require you to re-authenticate."
|
||||
msgstr "这将断开本设备与 Tailnet 的连接,并且需要重新进行身份验证。"
|
||||
|
||||
msgid "UDP port for the Peer Relay service (--peer-relay-port). Open this port on your router firewall/NAT."
|
||||
msgstr "节点中继服务的 UDP 端口 (--peer-relay-port)。请确保在路由器防火墙中放行此端口。"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "未知错误"
|
||||
|
||||
msgid "Use OpenWrt system firewall to manage NAT for Tailscale tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network."
|
||||
msgstr "由 OpenWrt 系统防火墙统一接管 Tailscale 隧道及对端子网流量的地址转换。禁用此项将禁用源地址伪装,在局域网中保留对端节点和子网的真实源 IP。"
|
||||
|
||||
msgid "Version"
|
||||
msgstr "版本信息"
|
||||
|
||||
msgid "When enabled, blocks all inbound connections from the Tailscale network (--shields-up)."
|
||||
msgstr "启用后,阻止所有来自 Tailscale 网络的主动入站连接 (--shields-up)。"
|
||||
|
||||
msgid "When using or advertising the exit node, allow access to the local LAN (--exit-node-allow-lan-access)."
|
||||
msgstr "在使用或宣告出口节点时,允许访问本地局域网设备 (--exit-node-allow-lan-access)。"
|
||||
|
||||
msgid "ago"
|
||||
msgstr "前"
|
||||
@@ -0,0 +1,335 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "(Experimental) Reduce Memory Usage"
|
||||
msgstr "(實驗性)減少記憶體佔用"
|
||||
|
||||
msgid "Accept Routes"
|
||||
msgstr "接受子網路由"
|
||||
|
||||
msgid "Advertise Exit Node"
|
||||
msgstr "宣告出口節點"
|
||||
|
||||
msgid "Advertise Subnet Routes"
|
||||
msgstr "通告子網路由"
|
||||
|
||||
msgid "Allow LAN Access"
|
||||
msgstr "允許存取區域網路"
|
||||
|
||||
msgid "Allow accepting subnet routes announced by other nodes (--accept-routes)."
|
||||
msgstr "允許接收並套用其他節點通告的子網路由 (--accept-routes)。"
|
||||
|
||||
msgid "Allow connecting to this device through the native SSH function of Tailscale (--ssh)."
|
||||
msgstr "允許透過 Tailscale 原生 SSH 功能連線到此裝置 (--ssh)。"
|
||||
|
||||
msgid "Allow user access to tailscale"
|
||||
msgstr "允許使用者存取 Tailscale"
|
||||
|
||||
msgid "Announce subnet routes behind this device (--advertise-routes). Select from the detected subnets below or enter custom routes (comma-separated)."
|
||||
msgstr "向 Tailnet 通告本裝置後方的子網路由 (--advertise-routes)。從下方偵測到的子網中選擇或手動輸入自訂路由(逗號分隔)。"
|
||||
|
||||
msgid "Are you sure you want to log out?"
|
||||
msgstr "確定要登出嗎?"
|
||||
|
||||
msgid "Are you sure you want to restart Tailscale service?"
|
||||
msgstr "確定要重啟 Tailscale 服務嗎?"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Client Connectivity"
|
||||
msgstr "用戶端連通性"
|
||||
|
||||
msgid "Collecting data ..."
|
||||
msgstr "正在收集資料..."
|
||||
|
||||
msgid "Confirm Logout"
|
||||
msgstr "確認登出"
|
||||
|
||||
msgid "Connection Info"
|
||||
msgstr "連線資訊"
|
||||
|
||||
msgid "Control Server"
|
||||
msgstr "控制伺服器"
|
||||
|
||||
msgid "Controls how Tailscale DNS is handled (--accept-dns)."
|
||||
msgstr "控制 Tailscale DNS 的處理方式 (--accept-dns)。"
|
||||
|
||||
msgid "Could not open a new tab. Please check if your browser or an extension blocked the pop-up."
|
||||
msgstr "無法開啟新分頁,請檢查瀏覽器或擴充功能是否封鎖了彈出視窗。"
|
||||
|
||||
msgid "Custom Control Server"
|
||||
msgstr "自訂控制伺服器"
|
||||
|
||||
msgid "Custom Server (%s)"
|
||||
msgstr "自訂伺服器 (%s)"
|
||||
|
||||
msgid "Custom Server Auth Key"
|
||||
msgstr "自訂伺服器預認證密鑰"
|
||||
|
||||
msgid "DNS Mode"
|
||||
msgstr "DNS 模式"
|
||||
|
||||
msgid "Declare this device as an exit node, allowing other nodes to route all traffic through it (--advertise-exit-node)."
|
||||
msgstr "將此裝置宣告為出口節點,允許其他節點透過它路由所有流量 (--advertise-exit-node)。"
|
||||
|
||||
msgid "Devices List"
|
||||
msgstr "裝置列表"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已停用"
|
||||
|
||||
msgid "Disabled: system DNS only."
|
||||
msgstr "停用:僅使用系統 DNS。"
|
||||
|
||||
msgid "Enable Peer Relay"
|
||||
msgstr "啟用節點中繼"
|
||||
|
||||
msgid "Enable Tailscale SSH"
|
||||
msgstr "啟用 Tailscale SSH"
|
||||
|
||||
msgid "Enable Tailscale Service"
|
||||
msgstr "啟用 Tailscale 服務"
|
||||
|
||||
msgid "Enable Tunnel Traffic SNAT"
|
||||
msgstr "開啟隧道流量 SNAT"
|
||||
|
||||
msgid "Enable Web Interface"
|
||||
msgstr "啟用 Web 管理介面"
|
||||
|
||||
msgid "Enable or disable the Tailscale background service (/etc/init.d/tailscale)."
|
||||
msgstr "啟用或停用 Tailscale 背景守護程序 (/etc/init.d/tailscale)。"
|
||||
|
||||
msgid "Enable this device as a Peer Relay server (--peer-relay). Requires a public IP and an UDP port open on the router."
|
||||
msgstr "將此裝置作為節點中繼伺服器 (--peer-relay)。需要公網 IP 並在路由器上開放 UDP 連接埠。"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "已啟用"
|
||||
|
||||
msgid "Enabling this option can reduce memory usage, but it may sacrifice some performance (set GOGC=10)."
|
||||
msgstr "啟用此選項可以減少記憶體佔用,但可能會犧牲部分效能(設定 GOGC=10)。"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "錯誤"
|
||||
|
||||
msgid "Exit Node"
|
||||
msgstr "出口節點"
|
||||
|
||||
msgid "Expose a local web interface on port 5252 for managing this node over Tailscale (--webclient)."
|
||||
msgstr "在 5252 連接埠開放本地 Web 管理介面,以便透過 Tailscale 管理此節點 (--webclient)。"
|
||||
|
||||
msgid "Failed to get login URL. You may close this tab."
|
||||
msgstr "取得登入 URL 失敗,您可以關閉此分頁。"
|
||||
|
||||
msgid "Failed to get login URL: %s"
|
||||
msgstr "取得登入 URL 失敗:%s"
|
||||
|
||||
msgid "Failed to get login URL: Invalid response from server."
|
||||
msgstr "取得登入 URL 失敗:伺服器回應無效。"
|
||||
|
||||
msgid "Failed to load devices: %s"
|
||||
msgstr "載入裝置列表失敗:%s"
|
||||
|
||||
msgid "Failed to load logs: %s"
|
||||
msgstr "載入日誌失敗:%s"
|
||||
|
||||
msgid "Failed to restart: %s"
|
||||
msgstr "重啟失敗:%s"
|
||||
|
||||
msgid "General Settings"
|
||||
msgstr "一般設定"
|
||||
|
||||
msgid "Hostname"
|
||||
msgstr "主機名稱"
|
||||
|
||||
msgid "Invalid Date"
|
||||
msgstr "無效日期"
|
||||
|
||||
msgid "Just now"
|
||||
msgstr "剛剛"
|
||||
|
||||
msgid "Last Seen"
|
||||
msgstr "最後活躍"
|
||||
|
||||
msgid "Logging out..."
|
||||
msgstr "正在登出..."
|
||||
|
||||
msgid "Login"
|
||||
msgstr "登入"
|
||||
|
||||
msgid "Logout"
|
||||
msgstr "登出"
|
||||
|
||||
msgid "Logout failed: %s"
|
||||
msgstr "登出失敗:%s"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "執行日誌"
|
||||
|
||||
msgid "MagicDNS: Tailscale overrides resolv.conf."
|
||||
msgstr "MagicDNS:Tailscale 將根據設定覆寫系統 resolv.conf 配置。"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不適用"
|
||||
|
||||
msgid "NOT INSTALLED"
|
||||
msgstr "未安裝"
|
||||
|
||||
msgid "NOT LOGGED IN"
|
||||
msgstr "未登入"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未執行"
|
||||
|
||||
msgid "Nearest Relay"
|
||||
msgstr "最近中繼節點"
|
||||
|
||||
msgid "No peer devices found."
|
||||
msgstr "未發現對等裝置。"
|
||||
|
||||
msgid "No tailscale-related logs found."
|
||||
msgstr "未找到 Tailscale 相關日誌。"
|
||||
|
||||
msgid "None"
|
||||
msgstr "無"
|
||||
|
||||
msgid "Now"
|
||||
msgstr "目前在線"
|
||||
|
||||
msgid "OS"
|
||||
msgstr "作業系統"
|
||||
|
||||
msgid "Official Server (Tailscale SaaS)"
|
||||
msgstr "官方伺服器 (Tailscale SaaS)"
|
||||
|
||||
msgid "Offline"
|
||||
msgstr "離線"
|
||||
|
||||
msgid "Online"
|
||||
msgstr "在線"
|
||||
|
||||
msgid "OpenWrt Forward"
|
||||
msgstr "OpenWrt 轉發"
|
||||
|
||||
msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
|
||||
msgstr "OpenWrt 轉發:透過 dnsmasq 轉發 MagicDNS(僅支援 ts.net 網域名稱)。"
|
||||
|
||||
msgid "Optional: Specify a custom control server URL (e.g., a Headscale instance, --login-server). Leave blank for default Tailscale control plane."
|
||||
msgstr "可選:指定自訂控制伺服器位址(例如 Headscale 實例,--login-server)。留空則使用 Tailscale 官方伺服器。"
|
||||
|
||||
msgid "Optional: Specify an authentication key for the custom control server (--auth-key). Leave blank if not required."
|
||||
msgstr "可選:指定自訂控制伺服器的預認證密鑰(--auth-key)。留空則無需認證密鑰。"
|
||||
|
||||
msgid "Peer Relay Port"
|
||||
msgstr "節點中繼連接埠"
|
||||
|
||||
msgid "Please wait while the service is restarting..."
|
||||
msgstr "服務正在重啟,請稍候..."
|
||||
|
||||
msgid "Please wait."
|
||||
msgstr "請稍候。"
|
||||
|
||||
msgid "Public Endpoints"
|
||||
msgstr "公網端點"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "執行中"
|
||||
|
||||
msgid "RX"
|
||||
msgstr "接收流量"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "重新整理"
|
||||
|
||||
msgid "Requesting Login URL..."
|
||||
msgstr "正在取得登入連結..."
|
||||
|
||||
msgid "Requesting Tailscale login URL... Please wait."
|
||||
msgstr "正在請求 Tailscale 登入連結,請稍候..."
|
||||
|
||||
msgid "Restart"
|
||||
msgstr "重啟"
|
||||
|
||||
msgid "Restart Service"
|
||||
msgstr "重啟服務"
|
||||
|
||||
msgid "Restarting Tailscale..."
|
||||
msgstr "正在重啟 Tailscale..."
|
||||
|
||||
msgid "Select an exit node from the list (--exit-node). If enabled, Allow LAN Access is enabled implicitly."
|
||||
msgstr "從列表中選擇出口節點 (--exit-node)。啟用後將隱式開啟允許存取區域網路。"
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr "服務狀態"
|
||||
|
||||
msgid "Shields Up"
|
||||
msgstr "防護模式"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "狀態概覽"
|
||||
|
||||
msgid "Successfully logged out."
|
||||
msgstr "已成功登出。"
|
||||
|
||||
msgid "TUN Mode"
|
||||
msgstr "TUN 模式"
|
||||
|
||||
msgid "TX"
|
||||
msgstr "發送流量"
|
||||
|
||||
msgid "Tailnet Account"
|
||||
msgstr "Tailnet 帳戶"
|
||||
|
||||
msgid "Tailscale"
|
||||
msgstr "Tailscale"
|
||||
|
||||
msgid "Tailscale IP"
|
||||
msgstr "Tailscale IP"
|
||||
|
||||
msgid "Tailscale IPv4"
|
||||
msgstr "Tailscale IPv4"
|
||||
|
||||
msgid "Tailscale IPv6"
|
||||
msgstr "Tailscale IPv6"
|
||||
|
||||
msgid "Tailscale Login"
|
||||
msgstr "Tailscale 登入認證"
|
||||
|
||||
msgid "Tailscale is a mesh VPN solution that makes it easy to connect your devices securely. This configuration page allows you to manage Tailscale settings on your OpenWrt device."
|
||||
msgstr "Tailscale 是一種網狀 Mesh VPN 解決方案,可讓您安全輕鬆地連接各類裝置。此頁面用於管理 OpenWrt 上的 Tailscale 服務設定。"
|
||||
|
||||
msgid "Tailscale is not installed."
|
||||
msgstr "Tailscale 未安裝。"
|
||||
|
||||
msgid "Tailscale is not logged in. Please log in to view devices."
|
||||
msgstr "Tailscale 未登入。請先登入以檢視裝置列表。"
|
||||
|
||||
msgid "Tailscale is not running."
|
||||
msgstr "Tailscale 未執行。"
|
||||
|
||||
msgid "This can take up to 30 seconds."
|
||||
msgstr "此過程最長可能需要 30 秒。"
|
||||
|
||||
msgid "This will disconnect this device from your Tailnet and require you to re-authenticate."
|
||||
msgstr "這將中斷本裝置與 Tailnet 的連線,並且需要重新進行身分驗證。"
|
||||
|
||||
msgid "UDP port for the Peer Relay service (--peer-relay-port). Open this port on your router firewall/NAT."
|
||||
msgstr "節點中繼服務的 UDP 連接埠 (--peer-relay-port)。請確保在路由器防火牆中放行此連接埠。"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "未知錯誤"
|
||||
|
||||
msgid "Use OpenWrt system firewall to manage NAT for Tailscale tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network."
|
||||
msgstr "由 OpenWrt 系統防火牆統一接管 Tailscale 隧道及對端子網流量的位址轉換。停用此項將停用來源位址偽裝,在區域網路中保留對端節點和子網的真實來源 IP。"
|
||||
|
||||
msgid "Version"
|
||||
msgstr "版本資訊"
|
||||
|
||||
msgid "When enabled, blocks all inbound connections from the Tailscale network (--shields-up)."
|
||||
msgstr "啟用後,阻止所有來自 Tailscale 網路的主動入站連線 (--shields-up)。"
|
||||
|
||||
msgid "When using or advertising the exit node, allow access to the local LAN (--exit-node-allow-lan-access)."
|
||||
msgstr "在使用或宣告出口節點時,允許存取本地區域網路裝置 (--exit-node-allow-lan-access)。"
|
||||
|
||||
msgid "ago"
|
||||
msgstr "前"
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Tailscale table 52 route recovery via tailscaled restart.
|
||||
#
|
||||
# Trigger: netifd brings tailscale0 back up (ifup) after an external
|
||||
# interface rebuild (e.g. network reload, firewall reload).
|
||||
#
|
||||
# tailscaled maintains an in-memory route state that diverges from the
|
||||
# kernel after netifd flushes tailscale0 routes. The only reliable
|
||||
# fix is a full tailscaled restart, which re-initialises from a clean
|
||||
# state and re-applies all table 52 routes from the current NetworkMap.
|
||||
#
|
||||
# Atomicity: a timestamp lock file (30 s TTL) prevents the recursive
|
||||
# loop that would otherwise arise because the restart itself causes
|
||||
# tailscale0 to be torn down and rebuilt, firing another ifup event.
|
||||
|
||||
[ "$DEVICE" = "tailscale0" ] || exit 0
|
||||
[ "$ACTION" = "ifup" ] || exit 0
|
||||
|
||||
# Check if Tailscale service is enabled
|
||||
service_enabled=$(uci -q get tailscale.settings.service_enabled 2>/dev/null || echo 0)
|
||||
[ "$service_enabled" = "1" ] || exit 0
|
||||
|
||||
LOCK=/tmp/tailscale_hotplug.lock
|
||||
|
||||
# Timestamp-based lock: bail out if another restart fired within 30 s.
|
||||
if [ -f "$LOCK" ]; then
|
||||
ts=$(cat "$LOCK" 2>/dev/null)
|
||||
[ -n "$ts" ] && [ $(( $(date +%s) - ts )) -lt 30 ] && exit 0
|
||||
fi
|
||||
|
||||
# Skip if tailscaled is not yet ready (normal first ifup during boot).
|
||||
tailscale status >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Record lock timestamp only after confirming service readiness
|
||||
date +%s > "$LOCK"
|
||||
|
||||
logger -t tailscale-hotplug \
|
||||
"tailscale0 ifup: restarting tailscaled to restore table 52 routes"
|
||||
|
||||
/etc/init.d/tailscale restart
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
NAME=tailscale-settings
|
||||
EXTRA_COMMANDS="clean_firewall clean_firewall_rules"
|
||||
|
||||
# Compare version strings: return 0 (true) if $1 >= $2
|
||||
version_gte() {
|
||||
awk -v a="${1%%-*}" -v b="${2%%-*}" 'BEGIN {
|
||||
split(a, av, ".")
|
||||
split(b, bv, ".")
|
||||
for (i=1; i<=3; i++) {
|
||||
ai = int(av[i]+0); bi = int(bv[i]+0)
|
||||
if (ai > bi) { exit 0 }
|
||||
if (ai < bi) { exit 1 }
|
||||
}
|
||||
exit 0
|
||||
}'
|
||||
return $?
|
||||
}
|
||||
|
||||
# Add/remove DNS forward entry in dnsmasq for MagicDNS via OpenWrt
|
||||
setup_tailscale_dns_forward() {
|
||||
if [ ! -x /usr/sbin/tailscale ] && [ ! -x /usr/bin/tailscale ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local entry="/.ts.net/100.100.100.100"
|
||||
local count
|
||||
count=$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null | tr ' ' '\n' | grep -Fxc "$entry" 2>/dev/null)
|
||||
|
||||
if [ "$count" -gt 1 ]; then
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="$entry"
|
||||
uci add_list dhcp.@dnsmasq[0].server="$entry"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null
|
||||
logger -t "$NAME" "Collapsed duplicate DNS forward entries"
|
||||
elif [ "$count" -eq 0 ]; then
|
||||
uci add_list dhcp.@dnsmasq[0].server="$entry"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null
|
||||
logger -t "$NAME" "Added DNS forward: $entry"
|
||||
fi
|
||||
}
|
||||
|
||||
remove_tailscale_dns_forward() {
|
||||
local entry="/.ts.net/100.100.100.100"
|
||||
local idx=0
|
||||
local removed=0
|
||||
while true; do
|
||||
local val
|
||||
val=$(uci -q get dhcp.@dnsmasq[0].server."${idx}" 2>/dev/null)
|
||||
[ -z "$val" ] && break
|
||||
if [ "$val" = "$entry" ]; then
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="$entry"
|
||||
removed=1
|
||||
break
|
||||
fi
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
if [ "$removed" = "1" ]; then
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null
|
||||
logger -t "$NAME" "Removed Tailscale DNS forward entry: $entry"
|
||||
fi
|
||||
}
|
||||
|
||||
check_zone() {
|
||||
local cfg="$1"
|
||||
local name
|
||||
config_get name "$cfg" name
|
||||
if [ "$name" = "tailscale" ]; then
|
||||
FOUND_TS_ZONE="$cfg"
|
||||
fi
|
||||
}
|
||||
|
||||
check_forwarding() {
|
||||
local cfg="$1"
|
||||
local src dst
|
||||
config_get src "$cfg" src
|
||||
config_get dst "$cfg" dest
|
||||
[ "$src" = "lan" ] && [ "$dst" = "tailscale" ] && FWD_LAN_TS=1
|
||||
[ "$src" = "tailscale" ] && [ "$dst" = "lan" ] && FWD_TS_LAN=1
|
||||
[ "$src" = "tailscale" ] && [ "$dst" = "wan" ] && FWD_TS_WAN=1
|
||||
}
|
||||
|
||||
setup_firewall_rules() {
|
||||
local changed_net=0
|
||||
local changed_fw=0
|
||||
|
||||
# 1. Interface (set both device and ifname for broad OpenWrt version compatibility)
|
||||
local dev ifname
|
||||
dev=$(uci -q get network.tailscale.device)
|
||||
ifname=$(uci -q get network.tailscale.device)
|
||||
|
||||
if [ -z "$dev" ] && [ -z "$ifname" ]; then
|
||||
uci -q set network.tailscale=interface
|
||||
uci -q set network.tailscale.proto='none'
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
changed_net=1
|
||||
else
|
||||
if [ -z "$dev" ]; then
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
changed_net=1
|
||||
fi
|
||||
if [ -z "$ifname" ]; then
|
||||
uci -q set network.tailscale.device='tailscale0'
|
||||
changed_net=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Zone
|
||||
local tunnel_snat target_masq="0"
|
||||
tunnel_snat=$(uci -q get tailscale.settings.tunnel_snat)
|
||||
[ "$tunnel_snat" = "1" ] && target_masq="1"
|
||||
|
||||
config_load firewall
|
||||
FOUND_TS_ZONE=""
|
||||
config_foreach check_zone zone
|
||||
|
||||
if [ -z "$FOUND_TS_ZONE" ]; then
|
||||
local zid
|
||||
zid=$(uci -q add firewall zone)
|
||||
uci -q set firewall."$zid".name='tailscale'
|
||||
uci -q set firewall."$zid".input='ACCEPT'
|
||||
uci -q set firewall."$zid".output='ACCEPT'
|
||||
uci -q set firewall."$zid".forward='ACCEPT'
|
||||
uci -q set firewall."$zid".masq="$target_masq"
|
||||
uci -q set firewall."$zid".mtu_fix='1'
|
||||
uci -q add_list firewall."$zid".network='tailscale'
|
||||
changed_fw=1
|
||||
else
|
||||
local cur_masq
|
||||
cur_masq=$(uci -q get firewall."$FOUND_TS_ZONE".masq)
|
||||
if [ "$cur_masq" != "$target_masq" ]; then
|
||||
uci -q set firewall."$FOUND_TS_ZONE".masq="$target_masq"
|
||||
changed_fw=1
|
||||
fi
|
||||
|
||||
local has_net=0
|
||||
local net
|
||||
for net in $(uci -q get firewall."$FOUND_TS_ZONE".network); do
|
||||
[ "$net" = "tailscale" ] && has_net=1
|
||||
done
|
||||
if [ "$has_net" = "0" ]; then
|
||||
uci -q add_list firewall."$FOUND_TS_ZONE".network='tailscale'
|
||||
changed_fw=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Forwardings
|
||||
FWD_LAN_TS=0
|
||||
FWD_TS_LAN=0
|
||||
FWD_TS_WAN=0
|
||||
config_foreach check_forwarding forwarding
|
||||
|
||||
if [ "$FWD_LAN_TS" = "0" ]; then
|
||||
local fid
|
||||
fid=$(uci -q add firewall forwarding)
|
||||
uci -q set firewall."$fid".src='lan'
|
||||
uci -q set firewall."$fid".dest='tailscale'
|
||||
changed_fw=1
|
||||
fi
|
||||
if [ "$FWD_TS_LAN" = "0" ]; then
|
||||
local fid=$(uci -q add firewall forwarding)
|
||||
uci -q set firewall."$fid".src='tailscale'
|
||||
uci -q set firewall."$fid".dest='lan'
|
||||
changed_fw=1
|
||||
fi
|
||||
if [ "$FWD_TS_WAN" = "0" ]; then
|
||||
local fid=$(uci -q add firewall forwarding)
|
||||
uci -q set firewall."$fid".src='tailscale'
|
||||
uci -q set firewall."$fid".dest='wan'
|
||||
changed_fw=1
|
||||
fi
|
||||
|
||||
if [ "$changed_net" = "1" ]; then
|
||||
uci -q commit network
|
||||
logger -t "$NAME" "Auto configured tailscale network interface (tailscale0)"
|
||||
fi
|
||||
|
||||
if [ "$changed_fw" = "1" ]; then
|
||||
uci -q commit firewall
|
||||
/etc/init.d/firewall reload 2>/dev/null
|
||||
logger -t "$NAME" "Auto configured tailscale firewall zone and forwarding rules"
|
||||
fi
|
||||
}
|
||||
|
||||
clean_firewall_rules() {
|
||||
local changed_net=0
|
||||
local changed_fw=0
|
||||
|
||||
# 1. Remove forwarding rules referencing tailscale
|
||||
config_load firewall
|
||||
local fwds_to_del=""
|
||||
delete_ts_fwd() {
|
||||
local cfg="$1"
|
||||
local src dst
|
||||
config_get src "$cfg" src
|
||||
config_get dst "$cfg" dest
|
||||
if [ "$src" = "tailscale" ] || [ "$dst" = "tailscale" ]; then
|
||||
fwds_to_del="$fwds_to_del $cfg"
|
||||
fi
|
||||
}
|
||||
config_foreach delete_ts_fwd forwarding
|
||||
|
||||
for fwd in $fwds_to_del; do
|
||||
uci -q delete firewall."$fwd"
|
||||
changed_fw=1
|
||||
done
|
||||
|
||||
# 2. Remove tailscale zone
|
||||
local zones_to_del=""
|
||||
find_ts_zone() {
|
||||
local cfg="$1"
|
||||
local name
|
||||
config_get name "$cfg" name
|
||||
if [ "$name" = "tailscale" ]; then
|
||||
zones_to_del="$zones_to_del $cfg"
|
||||
fi
|
||||
}
|
||||
config_foreach find_ts_zone zone
|
||||
|
||||
for z in $zones_to_del; do
|
||||
uci -q delete firewall."$z"
|
||||
changed_fw=1
|
||||
done
|
||||
|
||||
# 3. Remove network interface
|
||||
if uci -q get network.tailscale >/dev/null; then
|
||||
uci -q delete network.tailscale
|
||||
changed_net=1
|
||||
fi
|
||||
|
||||
# 4. Remove DNS forward entry in dnsmasq
|
||||
remove_tailscale_dns_forward
|
||||
|
||||
if [ "$changed_net" = "1" ]; then
|
||||
uci -q commit network
|
||||
logger -t "$NAME" "Cleaned tailscale network interface"
|
||||
fi
|
||||
|
||||
if [ "$changed_fw" = "1" ]; then
|
||||
uci -q commit firewall
|
||||
/etc/init.d/firewall reload 2>/dev/null
|
||||
logger -t "$NAME" "Cleaned tailscale firewall zone and forwarding rules"
|
||||
fi
|
||||
}
|
||||
|
||||
clean_firewall() {
|
||||
clean_firewall_rules
|
||||
}
|
||||
|
||||
apply_settings() {
|
||||
setup_firewall_rules
|
||||
|
||||
local ts_bin
|
||||
if [ -x /usr/sbin/tailscale ]; then
|
||||
ts_bin=/usr/sbin/tailscale
|
||||
elif [ -x /usr/bin/tailscale ]; then
|
||||
ts_bin=/usr/bin/tailscale
|
||||
else
|
||||
logger -t "$NAME" "tailscale binary not found, skipping settings apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
config_load tailscale
|
||||
|
||||
local accept_routes advertise_exit_node exit_node exit_node_allow_lan_access
|
||||
local ssh shields_up runwebclient tunnel_snat hostname dns_mode
|
||||
local enable_relay relay_server_port
|
||||
|
||||
config_get accept_routes settings accept_routes '0'
|
||||
config_get advertise_exit_node settings advertise_exit_node '0'
|
||||
config_get exit_node settings exit_node ''
|
||||
config_get exit_node_allow_lan_access settings exit_node_allow_lan_access '0'
|
||||
config_get ssh settings ssh '0'
|
||||
config_get dns_mode settings dns_mode 'disabled'
|
||||
config_get shields_up settings shields_up '0'
|
||||
config_get runwebclient settings runwebclient '0'
|
||||
config_get tunnel_snat settings tunnel_snat '0'
|
||||
config_get hostname settings hostname ''
|
||||
config_get enable_relay settings enable_relay '0'
|
||||
config_get relay_server_port settings relay_server_port '40000'
|
||||
|
||||
# Collect advertise_routes UCI list into comma-separated string
|
||||
local routes=""
|
||||
append_route() {
|
||||
if [ -z "$routes" ]; then
|
||||
routes="$1"
|
||||
else
|
||||
routes="$routes,$1"
|
||||
fi
|
||||
}
|
||||
config_list_foreach settings advertise_routes append_route
|
||||
|
||||
# Build argument list for 'tailscale set'
|
||||
set -- set
|
||||
|
||||
# --accept-routes
|
||||
[ "$accept_routes" = "1" ] \
|
||||
&& set -- "$@" --accept-routes=true \
|
||||
|| set -- "$@" --accept-routes=false
|
||||
|
||||
# --advertise-exit-node (only when no exit node is selected)
|
||||
[ "$advertise_exit_node" = "1" ] && [ -z "$exit_node" ] \
|
||||
&& set -- "$@" --advertise-exit-node=true \
|
||||
|| set -- "$@" --advertise-exit-node=false
|
||||
|
||||
# --exit-node-allow-lan-access (only when no exit node is selected)
|
||||
if [ -z "$exit_node" ]; then
|
||||
[ "$exit_node_allow_lan_access" = "1" ] \
|
||||
&& set -- "$@" --exit-node-allow-lan-access=true \
|
||||
|| set -- "$@" --exit-node-allow-lan-access=false
|
||||
fi
|
||||
|
||||
# --ssh
|
||||
[ "$ssh" = "1" ] \
|
||||
&& set -- "$@" --ssh=true \
|
||||
|| set -- "$@" --ssh=false
|
||||
|
||||
# --accept-dns based on dns_mode
|
||||
case "$dns_mode" in
|
||||
magicdns)
|
||||
set -- "$@" --accept-dns=true
|
||||
remove_tailscale_dns_forward
|
||||
;;
|
||||
openwrt_forward)
|
||||
set -- "$@" --accept-dns=false
|
||||
setup_tailscale_dns_forward
|
||||
;;
|
||||
*)
|
||||
# disabled (default)
|
||||
set -- "$@" --accept-dns=false
|
||||
remove_tailscale_dns_forward
|
||||
;;
|
||||
esac
|
||||
|
||||
# --shields-up
|
||||
[ "$shields_up" = "1" ] \
|
||||
&& set -- "$@" --shields-up=true \
|
||||
|| set -- "$@" --shields-up=false
|
||||
|
||||
# --webclient
|
||||
[ "$runwebclient" = "1" ] \
|
||||
&& set -- "$@" --webclient=true \
|
||||
|| set -- "$@" --webclient=false
|
||||
|
||||
# --snat-subnet-routes (aligned with tunnel_snat)
|
||||
[ "$tunnel_snat" = "1" ] \
|
||||
&& set -- "$@" --snat-subnet-routes=true \
|
||||
|| set -- "$@" --snat-subnet-routes=false
|
||||
|
||||
# --advertise-routes (comma-separated, empty clears routes)
|
||||
set -- "$@" "--advertise-routes=$routes"
|
||||
|
||||
# --exit-node (empty clears the exit node)
|
||||
set -- "$@" "--exit-node=$exit_node"
|
||||
|
||||
# when an exit node is configured, always allow LAN access
|
||||
[ -n "$exit_node" ] && set -- "$@" --exit-node-allow-lan-access=true
|
||||
|
||||
# --hostname (only set if non-empty)
|
||||
[ -n "$hostname" ] && set -- "$@" "--hostname=$hostname"
|
||||
|
||||
# --relay-server-port (requires tailscale >= 1.90.5)
|
||||
local ts_ver
|
||||
ts_ver=$("$ts_bin" version 2>/dev/null | head -n1)
|
||||
if version_gte "$ts_ver" "1.90.5"; then
|
||||
if [ "$enable_relay" = "1" ]; then
|
||||
set -- "$@" "--relay-server-port=${relay_server_port:-40000}"
|
||||
else
|
||||
set -- "$@" "--relay-server-port="
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always enforce netfilter-mode=off so OpenWrt manages all firewall rules cleanly
|
||||
set -- "$@" --netfilter-mode=off
|
||||
|
||||
# Wait for tailscaled daemon to be ready (up to 5 seconds)
|
||||
local i=0
|
||||
local daemon_ready=0
|
||||
while [ $i -lt 5 ]; do
|
||||
if "$ts_bin" status >/dev/null 2>&1; then
|
||||
daemon_ready=1
|
||||
break
|
||||
fi
|
||||
if ! "$ts_bin" status 2>&1 | grep -q "failed to connect to local tailscaled"; then
|
||||
daemon_ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
if [ "$daemon_ready" = "0" ]; then
|
||||
logger -t "$NAME" "Tailscale daemon is not running or socket is unreachable, skipping settings apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check logged-in status. Skip applying settings if logged out to prevent command error.
|
||||
local status_out
|
||||
status_out=$("$ts_bin" status 2>&1)
|
||||
if echo "$status_out" | grep -qE "Logged out|NeedsLogin|NeedsCard"; then
|
||||
logger -t "$NAME" "Tailscale is logged out, saving settings locally but skipping immediate apply"
|
||||
return 0
|
||||
fi
|
||||
|
||||
logger -t "$NAME" "Applying settings via tailscale set"
|
||||
"$ts_bin" "$@"
|
||||
local ret=$?
|
||||
if [ $ret -eq 0 ]; then
|
||||
logger -t "$NAME" "Settings applied successfully"
|
||||
else
|
||||
logger -t "$NAME" "Failed to apply settings (exit code: $ret)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
handle_service_state() {
|
||||
config_load tailscale
|
||||
local service_enabled
|
||||
config_get service_enabled settings service_enabled '0'
|
||||
|
||||
if [ "$service_enabled" = "0" ]; then
|
||||
logger -t "$NAME" "Service is disabled, stopping tailscale and cleaning up"
|
||||
/etc/init.d/tailscale stop 2>/dev/null
|
||||
/etc/init.d/tailscale disable 2>/dev/null
|
||||
clean_firewall_rules
|
||||
killall -9 tailscaled 2>/dev/null || true
|
||||
else
|
||||
logger -t "$NAME" "Service is enabled, ensuring tailscale is running"
|
||||
/etc/init.d/tailscale enable 2>/dev/null
|
||||
if ! /usr/sbin/tailscale status >/dev/null 2>&1 && ! /usr/bin/tailscale status >/dev/null 2>&1; then
|
||||
/etc/init.d/tailscale start 2>/dev/null
|
||||
fi
|
||||
apply_settings
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
handle_service_state
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "tailscale"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
handle_service_state
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/vpn/tailscale": {
|
||||
"title": "Tailscale",
|
||||
"order": 90,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "tailscale"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-tailscale" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"luci-app-tailscale": {
|
||||
"description": "Allow user access to tailscale",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"tailscale": [
|
||||
"get_status",
|
||||
"get_settings",
|
||||
"get_subroutes",
|
||||
"get_logs"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"tailscale": [
|
||||
"do_login",
|
||||
"do_logout",
|
||||
"restart",
|
||||
"setup_firewall",
|
||||
"reload_settings"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { access, popen, readfile, writefile, unlink, stat } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const uci = cursor();
|
||||
|
||||
function exec(command) {
|
||||
let stdout_content = '';
|
||||
let p = popen(command, 'r');
|
||||
if (p == null) {
|
||||
return { code: -1, stdout: '', stderr: `Failed to execute: ${command}` };
|
||||
}
|
||||
for (let line = p.read('line'); length(line); line = p.read('line')) {
|
||||
stdout_content = stdout_content+line;
|
||||
}
|
||||
stdout_content = rtrim(stdout_content);
|
||||
stdout_content = split(stdout_content, '\n');
|
||||
|
||||
let exit_code = p.close();
|
||||
let stderr_content = '';
|
||||
if (exit_code != 0) {
|
||||
stderr_content = stdout_content;
|
||||
}
|
||||
return { code: exit_code, stdout: stdout_content, stderr: stderr_content };
|
||||
}
|
||||
|
||||
function shell_quote(s) {
|
||||
if (s == null || s == '') return "''";
|
||||
return "'" + replace(s, "'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
function get_connectivity_data() {
|
||||
let ts_cmd = access('/usr/sbin/tailscale') ? '/usr/sbin/tailscale' : '/usr/bin/tailscale';
|
||||
if (!access(ts_cmd)) return null;
|
||||
|
||||
const cache_file = '/tmp/tailscale_netcheck.json';
|
||||
const lock_file = '/tmp/tailscale_netcheck.lock';
|
||||
|
||||
let cached_data = null;
|
||||
if (access(cache_file)) {
|
||||
try {
|
||||
let content = readfile(cache_file);
|
||||
if (content != null && content != '') {
|
||||
cached_data = json(content);
|
||||
}
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
let cache_st = access(cache_file) ? stat(cache_file) : null;
|
||||
let now = time();
|
||||
if (!cache_st || (now - cache_st.mtime) > 60) {
|
||||
let lock_st = access(lock_file) ? stat(lock_file) : null;
|
||||
if (!lock_st || (now - lock_st.mtime) > 30) {
|
||||
writefile(lock_file, '' + now);
|
||||
system('(' + ts_cmd + ' netcheck --format=json > /tmp/tailscale_netcheck.tmp 2>/dev/null && mv /tmp/tailscale_netcheck.tmp /tmp/tailscale_netcheck.json; rm -f ' + lock_file + ') >/dev/null 2>&1 &');
|
||||
}
|
||||
}
|
||||
|
||||
return cached_data;
|
||||
}
|
||||
|
||||
function get_cached_version() {
|
||||
const cache_file = '/tmp/tailscale_version.cache';
|
||||
const ts_bin = access('/usr/sbin/tailscale') ? '/usr/sbin/tailscale' : '/usr/bin/tailscale';
|
||||
if (!access(ts_bin)) return 'Unknown';
|
||||
|
||||
let bin_st = stat(ts_bin);
|
||||
let cache_st = access(cache_file) ? stat(cache_file) : null;
|
||||
|
||||
if (cache_st && cache_st.size > 0 && bin_st && cache_st.mtime >= bin_st.mtime) {
|
||||
let cached = trim(readfile(cache_file) || '');
|
||||
if (cached != '') return cached;
|
||||
}
|
||||
|
||||
let ver_res = exec(ts_bin + ' version 2>/dev/null');
|
||||
if (ver_res.code == 0 && length(ver_res.stdout) > 0) {
|
||||
let ver_line = trim(ver_res.stdout[0]);
|
||||
if (ver_line != '') {
|
||||
writefile(cache_file, ver_line + '\n');
|
||||
return ver_line;
|
||||
}
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const methods = {};
|
||||
|
||||
methods.get_status = {
|
||||
call: function() {
|
||||
let data = {
|
||||
status: '',
|
||||
version: '',
|
||||
TUNMode: '',
|
||||
health: '',
|
||||
ipv4: null,
|
||||
ipv6: null,
|
||||
domain_name: '',
|
||||
peers: [],
|
||||
connectivity: null
|
||||
};
|
||||
|
||||
if (!access('/usr/sbin/tailscale') && !access('/usr/bin/tailscale')) {
|
||||
data.status = 'not_installed';
|
||||
return data;
|
||||
}
|
||||
|
||||
data.version = get_cached_version();
|
||||
|
||||
let status_out = exec('tailscale status --json 2>/dev/null');
|
||||
let peer_map = {};
|
||||
|
||||
if (status_out.code == 0 && length(status_out.stdout) > 0) {
|
||||
try {
|
||||
let status_data = json(join('', status_out.stdout));
|
||||
if (status_data?.Version) {
|
||||
data.version = status_data.Version;
|
||||
}
|
||||
data.health = status_data?.Health || '';
|
||||
data.TUNMode = (status_data?.TUN == false ? 'false' : 'true');
|
||||
if (status_data?.BackendState == 'NeedsLogin' || status_data?.AuthURL || status_data?.BackendState == 'NoState' || (status_data?.HaveNodeKey == false && status_data?.BackendState != 'Running')) {
|
||||
data.status = 'logout';
|
||||
} else if (status_data?.BackendState == 'Running' && status_data?.HaveNodeKey == true) {
|
||||
data.status = 'running';
|
||||
} else if (status_data?.HaveNodeKey == true) {
|
||||
data.status = 'stopped';
|
||||
} else {
|
||||
data.status = (status_data?.BackendState == 'Stopped') ? 'stopped' : 'logout';
|
||||
}
|
||||
|
||||
if (status_data?.Self?.TailscaleIPs) {
|
||||
data.ipv4 = status_data.Self.TailscaleIPs[0] || null;
|
||||
data.ipv6 = status_data.Self.TailscaleIPs[1] || null;
|
||||
}
|
||||
|
||||
let account_str = '';
|
||||
if (status_data?.CurrentTailnet?.Name) {
|
||||
account_str = status_data.CurrentTailnet.Name;
|
||||
}
|
||||
if (status_data?.Self?.UserID && status_data?.User) {
|
||||
let uid = '' + status_data.Self.UserID;
|
||||
let uinfo = status_data.User[uid];
|
||||
if (uinfo) {
|
||||
let login_name = uinfo?.LoginName || uinfo?.Username || '';
|
||||
let display_name = uinfo?.DisplayName || '';
|
||||
if (login_name != '' && display_name != '' && login_name != display_name) {
|
||||
account_str = login_name + ' (' + display_name + ')';
|
||||
} else if (login_name != '') {
|
||||
account_str = login_name;
|
||||
} else if (display_name != '') {
|
||||
account_str = display_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (account_str == '' && status_data?.Self?.DNSName) {
|
||||
account_str = status_data.Self.DNSName;
|
||||
}
|
||||
data.account = account_str;
|
||||
data.domain_name = account_str;
|
||||
|
||||
if (status_data?.Peer) {
|
||||
for (let id, p in status_data.Peer) {
|
||||
let ips = '';
|
||||
if (p?.TailscaleIPs) {
|
||||
ips = join('<br>', p.TailscaleIPs);
|
||||
}
|
||||
let hostname = p?.DNSName || '';
|
||||
if (hostname != '') {
|
||||
let parts = split(hostname, '.');
|
||||
hostname = parts[0];
|
||||
}
|
||||
|
||||
peer_map[id] = {
|
||||
ip: ips,
|
||||
hostname: hostname,
|
||||
ostype: p?.OS,
|
||||
online: p?.Online || false,
|
||||
linkadress: (!p?.CurAddr || p?.CurAddr == '') ? p?.Relay : p?.CurAddr,
|
||||
lastseen: p?.LastSeen,
|
||||
exit_node: !(!p?.ExitNode),
|
||||
exit_node_option: !(!p?.ExitNodeOption),
|
||||
tx: p?.TxBytes || '',
|
||||
rx: p?.RxBytes || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.status == 'running') {
|
||||
let netcheck = get_connectivity_data();
|
||||
let derp_lat_str = '';
|
||||
let pref_derp = netcheck?.PreferredDERP || status_data?.Self?.Relay || 0;
|
||||
if (pref_derp != 0 && pref_derp != '' && netcheck?.DERPLatencies) {
|
||||
let lat = netcheck.DERPLatencies['' + pref_derp];
|
||||
if (lat) {
|
||||
derp_lat_str = sprintf('%d ms', lat * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
let ep_list = [];
|
||||
if (status_data?.Self?.Addrs && length(status_data.Self.Addrs) > 0) {
|
||||
ep_list = status_data.Self.Addrs;
|
||||
} else if (status_data?.Self?.Endpoints && length(status_data.Self.Endpoints) > 0) {
|
||||
ep_list = status_data.Self.Endpoints;
|
||||
} else if (netcheck) {
|
||||
if (netcheck.GlobalV4) push(ep_list, netcheck.GlobalV4);
|
||||
if (netcheck.GlobalV6) push(ep_list, netcheck.GlobalV6);
|
||||
if (netcheck.GlobalAddrs && length(netcheck.GlobalAddrs) > 0) {
|
||||
for (let a in netcheck.GlobalAddrs) push(ep_list, a);
|
||||
}
|
||||
}
|
||||
|
||||
data.connectivity = {
|
||||
varies: (netcheck?.MappingVariesByDestIP == true) ? 'Yes' : 'No',
|
||||
ipv4: (netcheck?.IPv4 == true || (ep_list && length(ep_list) > 0)) ? 'Yes' : 'No',
|
||||
ipv6: (netcheck?.IPv6 == true) ? 'Yes' : 'No',
|
||||
udp: (netcheck?.UDP == true || (ep_list && length(ep_list) > 0)) ? 'Yes' : 'No',
|
||||
upnp: (netcheck?.UPnP == true) ? 'Yes' : 'No',
|
||||
pcp: (netcheck?.PCP == true) ? 'Yes' : 'No',
|
||||
pmp: (netcheck?.PMP == true) ? 'Yes' : 'No',
|
||||
hairpinning: (netcheck?.HairPinning == true) ? 'Yes' : 'No',
|
||||
preferred_derp: pref_derp,
|
||||
derp_latency: derp_lat_str,
|
||||
endpoints: ep_list
|
||||
};
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
uci.load('tailscale');
|
||||
let state_file_path = uci.get('tailscale', 'settings', 'state_file') || "/etc/tailscale/tailscaled.state";
|
||||
let has_state = false;
|
||||
if (access(state_file_path) && stat(state_file_path)?.size > 0) {
|
||||
has_state = true;
|
||||
} else if (access('/var/lib/tailscale/tailscaled.state') && stat('/var/lib/tailscale/tailscaled.state')?.size > 0) {
|
||||
has_state = true;
|
||||
}
|
||||
data.status = has_state ? 'stopped' : 'logout';
|
||||
}
|
||||
|
||||
data.peers = peer_map;
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_settings = {
|
||||
call: function() {
|
||||
let settings = {};
|
||||
uci.load('tailscale');
|
||||
let state_file_path = uci.get('tailscale', 'settings', 'state_file') || "/etc/tailscale/tailscaled.state";
|
||||
if (access(state_file_path)) {
|
||||
try {
|
||||
let state_content = readfile(state_file_path);
|
||||
if (state_content != null) {
|
||||
let state_data = json(state_content);
|
||||
let profiles_b64 = state_data?._profiles;
|
||||
if (!profiles_b64) return settings;
|
||||
|
||||
let profiles_data = json(b64dec(profiles_b64));
|
||||
let profiles_key = null;
|
||||
for (let key in profiles_data) {
|
||||
profiles_key = key;
|
||||
break;
|
||||
}
|
||||
profiles_key = 'profile-'+profiles_key;
|
||||
|
||||
let status_data = json(b64dec(state_data?.[profiles_key]));
|
||||
if (status_data != null) {
|
||||
settings.accept_routes = status_data?.RouteAll || false;
|
||||
settings.advertise_exit_node = status_data?.AdvertiseExitNode || false;
|
||||
settings.advertise_routes = status_data?.AdvertiseRoutes || [];
|
||||
settings.exit_node = status_data?.ExitNodeID || "";
|
||||
settings.exit_node_allow_lan_access = status_data?.ExitNodeAllowLANAccess || false;
|
||||
settings.shields_up = status_data?.ShieldsUp || false;
|
||||
settings.ssh = status_data?.RunSSH || false;
|
||||
settings.runwebclient = status_data?.RunWebClient || false;
|
||||
settings.tunnel_snat = uci.get('tailscale', 'settings', 'tunnel_snat') || '0';
|
||||
settings.dns_mode = uci.get('tailscale', 'settings', 'dns_mode') || 'disabled';
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
methods.do_login = {
|
||||
args: { form_data: {} },
|
||||
call: function(request) {
|
||||
const form_data = request.args.form_data;
|
||||
if (form_data == null || length(form_data) == 0) {
|
||||
return { error: 'Missing or invalid form_data parameter. Please provide login data.' };
|
||||
}
|
||||
|
||||
let status = methods.get_status.call();
|
||||
if (status.status == 'running') {
|
||||
return { error: 'Tailscale is already logged in and running.' };
|
||||
}
|
||||
|
||||
let status_test = exec('tailscale status >/dev/null 2>&1');
|
||||
if (status_test.code != 0) {
|
||||
exec('/etc/init.d/tailscale start >/dev/null 2>&1');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
sleep(1000);
|
||||
let res = exec('tailscale status >/dev/null 2>&1');
|
||||
if (res.code == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 1. Prepare and Run Login Command (Once) ---
|
||||
let loginargs = [];
|
||||
const loginserver = trim(form_data.loginserver) || '';
|
||||
const loginserver_authkey = trim(form_data.loginserver_authkey) || '';
|
||||
|
||||
if (loginserver != '') {
|
||||
push(loginargs, '--login-server ' + shell_quote(loginserver));
|
||||
if (loginserver_authkey != '') {
|
||||
push(loginargs, '--auth-key ' + shell_quote(loginserver_authkey));
|
||||
}
|
||||
}
|
||||
|
||||
// Run the command in the background using /bin/sh -c to handle the '&' correctly
|
||||
let login_cmd = 'tailscale login ' + join(' ', loginargs);
|
||||
popen('/bin/sh -c ' + shell_quote(login_cmd + ' &'), 'r');
|
||||
|
||||
// --- 2. Loop to Check Status for URL ---
|
||||
let max_attempts = 15;
|
||||
let interval = 2000;
|
||||
|
||||
for (let i = 0; i < max_attempts; i++) {
|
||||
let tresult = exec('tailscale status 2>&1');
|
||||
for (let line in tresult.stdout) {
|
||||
let trline = trim(line);
|
||||
if (index(trline, 'http') != -1) {
|
||||
let parts = split(trline, ' ');
|
||||
for (let part in parts) {
|
||||
if (index(part, 'http') != -1) {
|
||||
return { url: part };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sleep(interval);
|
||||
}
|
||||
return { error: 'Could not retrieve login URL from tailscale command after 30 seconds.' };
|
||||
}
|
||||
};
|
||||
|
||||
methods.do_logout = {
|
||||
call: function() {
|
||||
let status = methods.get_status.call();
|
||||
if (status.status == 'not_installed') {
|
||||
return { error: 'Tailscale is not installed.' };
|
||||
}
|
||||
if (status.status == 'logout') {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
uci.load('tailscale');
|
||||
let state_file_path = uci.get('tailscale', 'settings', 'state_file') || "/etc/tailscale/tailscaled.state";
|
||||
|
||||
if (status.status == 'running') {
|
||||
exec('tailscale logout 2>/dev/null');
|
||||
} else {
|
||||
exec('/etc/init.d/tailscale start >/dev/null 2>&1');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
sleep(1000);
|
||||
let res = exec('tailscale logout 2>/dev/null');
|
||||
if (res.code == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
exec('/etc/init.d/tailscale stop >/dev/null 2>&1');
|
||||
|
||||
if (access(state_file_path)) {
|
||||
unlink(state_file_path);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_subroutes = {
|
||||
call: function() {
|
||||
try {
|
||||
let cmd = 'ip -j route';
|
||||
let result = exec(cmd);
|
||||
let subnets = [];
|
||||
|
||||
if (result.code == 0 && length(result.stdout) > 0) {
|
||||
let routes_json = json(join('',result.stdout));
|
||||
|
||||
for (let route in routes_json) {
|
||||
// We need to filter out local subnets
|
||||
// 1. 'dst' (target address) is not' default' (default gateway)
|
||||
// 2. 'scope' is' link' (indicating directly connected network)
|
||||
// 3. It is an IPv4 address (simple judgment: including'.')
|
||||
if (route?.dst && route.dst != 'default' && route?.scope == 'link' && index(route.dst,'.') != -1) {
|
||||
push(subnets,route.dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { routes: subnets };
|
||||
}
|
||||
catch(e) {
|
||||
return { routes: [] };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
methods.setup_firewall = {
|
||||
call: function() {
|
||||
try {
|
||||
uci.load('tailscale');
|
||||
|
||||
uci.load('network');
|
||||
uci.load('firewall');
|
||||
|
||||
let changed_network = false;
|
||||
let changed_firewall = false;
|
||||
|
||||
// 1. config Network Interface
|
||||
let net_ts = uci.get('network', 'tailscale');
|
||||
if (net_ts == null) {
|
||||
uci.set('network', 'tailscale', 'interface');
|
||||
uci.set('network', 'tailscale', 'proto', 'none');
|
||||
uci.set('network', 'tailscale', 'device', 'tailscale0');
|
||||
changed_network = true;
|
||||
} else {
|
||||
let current_dev = uci.get('network', 'tailscale', 'device');
|
||||
if (current_dev != 'tailscale0') {
|
||||
uci.set('network', 'tailscale', 'device', 'tailscale0');
|
||||
changed_network = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. config Firewall Zone
|
||||
let ts_zone_section = null;
|
||||
let fwd_lan_to_ts = false;
|
||||
let fwd_ts_to_lan = false;
|
||||
|
||||
uci.foreach('firewall', 'zone', function(s) {
|
||||
if (s['name'] == 'tailscale')
|
||||
ts_zone_section = s['.name'];
|
||||
});
|
||||
uci.foreach('firewall', 'forwarding', function(s) {
|
||||
if (s['src'] == 'lan' && s['dest'] == 'tailscale') fwd_lan_to_ts = true;
|
||||
if (s['src'] == 'tailscale' && s['dest'] == 'lan') fwd_ts_to_lan = true;
|
||||
});
|
||||
|
||||
let tunnel_snat = uci.get('tailscale', 'settings', 'tunnel_snat') || '0';
|
||||
let target_masq = (tunnel_snat == '1') ? '1' : '0';
|
||||
|
||||
if (ts_zone_section == null) {
|
||||
let zid = uci.add('firewall', 'zone');
|
||||
uci.set('firewall', zid, 'name', 'tailscale');
|
||||
uci.set('firewall', zid, 'input', 'ACCEPT');
|
||||
uci.set('firewall', zid, 'output', 'ACCEPT');
|
||||
uci.set('firewall', zid, 'forward', 'ACCEPT');
|
||||
uci.set('firewall', zid, 'masq', target_masq);
|
||||
uci.set('firewall', zid, 'mtu_fix', '1');
|
||||
uci.set('firewall', zid, 'network', ['tailscale']);
|
||||
changed_firewall = true;
|
||||
} else {
|
||||
let cur_masq = uci.get('firewall', ts_zone_section, 'masq');
|
||||
if (cur_masq != target_masq) {
|
||||
uci.set('firewall', ts_zone_section, 'masq', target_masq);
|
||||
changed_firewall = true;
|
||||
}
|
||||
let nets = uci.get('firewall', ts_zone_section, 'network');
|
||||
let net_list = [];
|
||||
let has_ts_net = false;
|
||||
|
||||
if (type(nets) == 'array') {
|
||||
net_list = nets;
|
||||
} else if (type(nets) == 'string') {
|
||||
net_list = [nets];
|
||||
}
|
||||
|
||||
// check if 'tailscale' is already in the list
|
||||
for (let n in net_list) {
|
||||
if (n == 'tailscale') {
|
||||
has_ts_net = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_ts_net) {
|
||||
push(net_list, 'tailscale');
|
||||
uci.set('firewall', ts_zone_section, 'network', net_list);
|
||||
changed_firewall = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. config Forwarding
|
||||
if (!fwd_lan_to_ts) {
|
||||
let fid = uci.add('firewall', 'forwarding');
|
||||
uci.set('firewall', fid, 'src', 'lan');
|
||||
uci.set('firewall', fid, 'dest', 'tailscale');
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
if (!fwd_ts_to_lan) {
|
||||
let fid = uci.add('firewall', 'forwarding');
|
||||
uci.set('firewall', fid, 'src', 'tailscale');
|
||||
uci.set('firewall', fid, 'dest', 'lan');
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
// Exit node requires WAN <- tailscale forwarding
|
||||
let fwd_ts_to_wan = false;
|
||||
uci.foreach('firewall', 'forwarding', function(s) {
|
||||
if (s['src'] == 'tailscale' && s['dest'] == 'wan') fwd_ts_to_wan = true;
|
||||
});
|
||||
|
||||
if (!fwd_ts_to_wan) {
|
||||
let fid = uci.add('firewall', 'forwarding');
|
||||
uci.set('firewall', fid, 'src', 'tailscale');
|
||||
uci.set('firewall', fid, 'dest', 'wan');
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
// 4. save
|
||||
if (changed_network) {
|
||||
uci.save('network');
|
||||
uci.commit('network');
|
||||
}
|
||||
|
||||
if (changed_firewall) {
|
||||
uci.save('firewall');
|
||||
uci.commit('firewall');
|
||||
exec('/etc/init.d/firewall reload');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
changed_network: changed_network,
|
||||
changed_firewall: changed_firewall,
|
||||
message: (changed_network || changed_firewall) ? 'Tailscale firewall/interface configuration applied.' : 'Tailscale firewall/interface already configured.'
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
return { error: 'Exception in setup_firewall: ' + e + '\nStack: ' + (e.stacktrace || '') };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_logs = {
|
||||
args: { lines: 200 },
|
||||
call: function(request) {
|
||||
let lines = int(request?.args?.lines) || 200;
|
||||
let cmd = 'logread -l ' + lines + ' 2>/dev/null | grep -i -E "tailscale" || true';
|
||||
let result = exec(cmd);
|
||||
if (result.code == 0) {
|
||||
return { logs: result.stdout };
|
||||
}
|
||||
return { logs: [], error: join(' ', result.stderr) };
|
||||
}
|
||||
};
|
||||
|
||||
methods.reload_settings = {
|
||||
call: function() {
|
||||
exec('/etc/init.d/tailscale-settings reload');
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
methods.restart = {
|
||||
call: function() {
|
||||
writefile('/tmp/tailscale_hotplug.lock', '' + time() + '\n');
|
||||
exec('/etc/init.d/tailscale restart');
|
||||
exec('/etc/init.d/tailscale-settings reload');
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
return { 'tailscale': methods };
|
||||
Reference in new issue
Block a user