From: Tokisaki Galaxy Date: Sat, 15 Nov 2025 16:05:13 +0000 (+0800) Subject: luci-app-tailscale-community: add new application X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=471ac6b59cff98a68593b5b2d336beb495e2e388;p=openwrt-luci.git luci-app-tailscale-community: add new application This commit adds a new LuCI application for managing Tailscale on OpenWrt. The application provides a web interface to view service status, list network peers, and configure various Tailscale settings, such as exit nodes, advertised routes, and daemon options. Co-authored-by: Sandro Signed-off-by: Tokisaki Galaxy --- diff --git a/applications/luci-app-tailscale-community/Makefile b/applications/luci-app-tailscale-community/Makefile new file mode 100644 index 0000000000..d448315a1a --- /dev/null +++ b/applications/luci-app-tailscale-community/Makefile @@ -0,0 +1,12 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for Tailscale +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 +LUCI_DEPENDS:=+tailscale +ip +luci-base +LUCI_PKGARCH:=all + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js b/applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js new file mode 100644 index 0000000000..f6140d7c28 --- /dev/null +++ b/applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js @@ -0,0 +1,574 @@ +'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 callSetSettings = rpc.declare({ object: 'tailscale', method: 'set_settings', params: ['form_data'] }); +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 callSetupFirewall = rpc.declare({ object: 'tailscale', method: 'setup_firewall' }); +let map; + +const tailscaleSettingsConf = [ + [form.ListValue, 'fw_mode', _('Firewall Mode'), _('Select the firewall backend for Tailscale to use. Requires service restart to take effect.'), {values: ['nftables','iptables'],rmempty: false}], + [form.Flag, 'accept_routes', _('Accept Routes'), _('Allow accepting routes announced by other nodes.'), { rmempty: false }], + [form.Flag, 'advertise_exit_node', _('Advertise Exit Node'), _('Declare this device as an Exit Node.'), { rmempty: false }], + [form.Flag, 'exit_node_allow_lan_access', _('Allow LAN Access'), _('When using the exit node, access to the local LAN is allowed.'), { rmempty: false }], + [form.Flag, 'runwebclient', _('Enable Web Interface'), _('Expose a web interface on port 5252 for managing this node over Tailscale.'), { rmempty: false }], + [form.Flag, 'nosnat', _('Disable SNAT'), _('Disable Source NAT (SNAT) for traffic to advertised routes. Most users should leave this unchecked.'), { rmempty: false }], + [form.Flag, 'shields_up', _('Shields Up'), _('When enabled, blocks all inbound connections from the Tailscale network.'), { rmempty: false }], + [form.Flag, 'ssh', _('Enable Tailscale SSH'), _('Allow connecting to this device through the SSH function of Tailscale.'), { rmempty: false }], + [form.Flag, 'disable_magic_dns', _('Disable MagicDNS'), _('Use system DNS instead of MagicDNS.'), { rmempty: false }] +]; + +const accountConf = []; // dynamic created in render function + +const daemonConf = [ + //[form.Value, 'daemon_mtu', _('Daemon MTU'), _('Set a custom MTU for the Tailscale daemon. Leave blank to use the default value.'), { datatype: 'uinteger', placeholder: '1280' }, { rmempty: false }], + [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 }] +]; + +const derpMapUrl = 'https://controlplane.tailscale.com/derpmap/default'; +let regionCodeMap = {}; + +// 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); +} + +async function initializeRegionMap() { + const cacheKey = 'tailscale_derp_map_cache'; + const ttl = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds + + try { + const cachedItem = localStorage.getItem(cacheKey); + if (cachedItem) { + const cached = JSON.parse(cachedItem); + // Check if the cached data is still valid (not expired) + if (Date.now() - cached.timestamp < ttl) { + regionCodeMap = cached.data; + return; + } + } + } catch (e) { + ui.addTimeLimitedNotification(null, [ E('p', _('Error reading cached DERP region map: %s').format(e.message || _('Unknown error'))) ], 7000, 'error'); + } + + // If no valid cache, fetch from the network + try { + const response = await fetch(derpMapUrl); + if (!response.ok) { + return; + } + const data = await response.json(); + const newRegionMap = {}; + for (const regionId in data.Regions) { + const region = data.Regions[regionId]; + const code = (region.RegionCode || '').toLowerCase(); + const name = region.RegionName || region.RegionCode || `Region ${regionId}`; + newRegionMap[code] = name; + } + regionCodeMap = newRegionMap; + + // Save the newly fetched data to the cache + try { + const itemToCache = { + timestamp: Date.now(), + data: regionCodeMap + }; + localStorage.setItem(cacheKey, JSON.stringify(itemToCache)); + } catch (e) { + ui.addTimeLimitedNotification(null, [ E('p', _('Error caching DERP region map: %s').format(e.message || _('Unknown error'))) ], 7000, 'error'); + } + } catch (error) { + ui.addTimeLimitedNotification(null, [ E('p', _('Error fetching DERP region map: %s').format(error.message || _('Unknown error'))) ], 7000, 'error'); + } +} + +function formatConnectionInfo(info) { + if (!info) { return '-'; } + if (typeof info === 'string' && info.length === 3) { + const lowerCaseInfo = info.toLowerCase(); + return regionCodeMap[lowerCaseInfo] || info; + } + return info; +} + +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 notificationId = 'tailscale_health_notification'; + let notificationElement = document.getElementById(notificationId); + if (status.health != '') { + const message = _('Tailscale Health Check: %s').format(status.health); + if (notificationElement) { + notificationElement.textContent = message; + } + else { + let newNotificationContent = E('p', { 'id': notificationId }, message); + ui.addNotification(null, newNotificationContent, 'info'); + } + }else{ + try{ + notificationElement.parentNode.parentNode.remove(); + }catch(e){} + } + + if (Object.keys(regionCodeMap).length === 0) { + initializeRegionMap(); + } + + // --- Part 1: Handle non-running states --- + + // State: Tailscale binary not found. + if (status.status == 'not_installed') { + return E('dl', { 'class': 'cbi-value' }, [ + E('dt', {}, _('Service Status')), + E('dd', {}, E('span', { 'style': 'color:red;' }, E('strong', {}, _('TAILSCALE NOT FOUND')))) + ]); + } + + // State: Logged out, requires user action. + if (status.status == 'logout') { + return E('dl', { 'class': 'cbi-value' }, [ + E('dt', {}, _('Service Status')), + E('dd', {}, [ + E('span', { 'style': 'color:orange;' }, E('strong', {}, _('LOGGED OUT'))), + E('br'), + E('span', {}, _('Please use the login button in the settings below to authenticate.')) + ]) + ]); + } + + // State: Service is installed but not running. + if (status.status != 'running') { + return E('dl', { 'class': 'cbi-value' }, [ + E('dt', {}, _('Service Status')), + E('dd', {}, E('span', { 'style': 'color:red;' }, E('strong', {}, _('NOT RUNNING')))) + ]); + } + + // --- Part 2: Render the full status display for a running service --- + + // A helper array to define the data for the main status table. + const statusData = [ + { label: _('Service Status'), value: E('span', { 'style': 'color:green;' }, E('strong', {}, _('RUNNING'))) }, + { label: _('Version'), value: status.version || 'N/A' }, + { label: _('TUN Mode'), value: status.TUNMode ? _('Enabled') : _('Disabled') }, + { label: _('Tailscale IPv4'), value: status.ipv4 || 'N/A' }, + { label: _('Tailscale IPv6'), value: status.ipv6 || 'N/A' }, + { label: _('Tailnet Name'), value: status.domain_name || 'N/A' } + ]; + + // Build the horizontal status table using the data array. + 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))) + ]); + + // --- Part 3: Render the Peers/Network Devices table --- + + const peers = status.peers; + let peersContent; + + if (!peers || Object.keys(peers).length === 0) { + // Display a message if no peers are found. + peersContent = E('p', {}, _('No peer devices found.')); + } else { + // Define headers for the peers table. + const peerTableHeaders = [ + { text: _('Status'), style: 'width: 80px;' }, + { text: _('Hostname') }, + { text: _('Tailscale IP') }, + { text: _('OS') }, + { text: _('Connection Info') }, + { text: _('RX') }, + { text: _('TX') }, + { text: _('Last Seen') } + ]; + + // Build the peers table. + peersContent = E('table', { 'class': 'cbi-table' }, [ + // Table Header Row + 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); + })), + + // Table Body Rows (one for each peer) + ...Object.entries(peers).map(([peerid, peer]) => { + const td_style = 'padding-right: 20px;'; + + return E('tr', { 'class': 'cbi-rowstyle-1' }, [ + E('td', { 'class': 'cbi-value-field', 'style': td_style }, + E('span', { + 'style': `color:${peer.exit_node ? 'blue' : (peer.online ? 'green' : 'gray')};`, + '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)) + ]); + }) + ]); + } + + // Combine all parts into a single DocumentFragment. + // Using E() without a tag name creates a fragment, which is perfect for grouping elements. + return E([ + statusTable, + E('div', { 'style': 'margin-top: 25px;' }, [ + E('h4', {}, _('Network Devices')), + peersContent + ]) + ]); +} + +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', 'fw_mode', 'nftables'); + 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', 'nosnat', ((settings_from_rpc.nosnat || false) ? '1' : '0')); + uci.set('tailscale', 'settings', 'disable_magic_dns', ((settings_from_rpc.disable_magic_dns || false) ? '1' : '0')); + + uci.set('tailscale', 'settings', 'daemon_reduce_memory', '0'); + uci.set('tailscale', 'settings', 'daemon_mtu', ''); + 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.')); + + s = map.section(form.NamedSection, '_status'); + s.anonymous = true; + s.render = function (section_id) { + L.Poll.add( + function () { + return getRunningStatus().then(function (res) { + const view = document.getElementById("service_status_display"); + if (view) { + const content = renderStatus(res); + view.replaceChildren(content); + } + + // login button only available when logged out + const login_btn=document.getElementsByClassName('cbi-button cbi-button-apply')[0]; + if(login_btn) { login_btn.disabled=(res.status != 'logout'); } + }); + }, 10); + + return 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', _('Settings')); + s.dynamic = true; + + // Create the "General Settings" tab and apply tailscaleSettingsConf + s.tab('general', _('General Settings')); + + defTabOpts(s, 'general', tailscaleSettingsConf, { optional: false }); + + const en = s.taboption('general', form.ListValue, 'exit_node', _('Exit Node'), _('Select an exit node from the list. 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('
')[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('
')[0]; + } + } + return ''; + } + return uci.get('tailscale', 'settings', 'exit_node') || ''; + }; + + const o = s.taboption('general', form.DynamicList, 'advertise_routes', _('Advertise Routes'),_('Advertise subnet routes behind this device. 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 fwBtn = s.taboption('general', form.Button, '_setup_firewall', _('Auto Configure Firewall')); + fwBtn.description = _('Experimental: applies minimal firewall and interface setup for Tailscale. It will create/patch network.tailscale (proto none, device tailscale0), add a firewall zone "tailscale" with ACCEPT/ACCEPT/ACCEPT, masq, mtu_fix, and ensure forwarding tailscale<->lan. It reloads network/firewall only if changes are made.'); + fwBtn.inputstyle = 'action'; + fwBtn.onclick = function() { + const btn = this; + btn.disabled = true; + return callSetupFirewall().then(function(res) { + const msg = res?.message || _('Firewall configuration applied.'); + ui.addNotification(null, E('p', {}, msg), 'info'); + }).catch(function(err) { + ui.addNotification(null, E('p', {}, _('Failed to configure firewall: %s').format(err?.message || err || 'Unknown error')), 'error'); + }).finally(function() { + btn.disabled = false; + }); + }; + + // Create the account settings + s.tab('account', _('Account Settings')); + defTabOpts(s, 'account', accountConf, { optional: false }); + + const loginBtn = s.taboption('account', form.Button, '_login', _('Login'), + _('Click to get a login URL for this device.') + +'
'+_('If the timeout is displayed, you can refresh the page and click Login again.')); + loginBtn.inputstyle = 'apply'; + + const customLoginUrl = s.taboption('account', form.Value, 'custom_login_url', + _('Custom Login Server'), + _('Optional: Specify a custom control server URL (e.g., a Headscale instance, https://example.com).') + +'
'+_('Leave blank for default Tailscale control plane.') + ); + customLoginUrl.placeholder = ''; + customLoginUrl.rmempty = true; + + const customLoginAuthKey = s.taboption('account', form.Value, 'custom_login_AuthKey', + _('Custom Login Server Auth Key'), + _('Optional: Specify an authentication key for the custom control server. Leave blank if not required.') + +'
'+_('If you are using custom login server but not providing an Auth Key, will redirect to the login page without pre-filling the key.') + ); + customLoginAuthKey.placeholder = ''; + customLoginAuthKey.rmempty = true; + + const logoutBtn = s.taboption('account', form.Button, '_logout', _('Logout'), + _('Click to Log out account on this device.') + +'
'+_('Disconnect from Tailscale and expire current node key.')); + logoutBtn.inputstyle = 'apply'; + logoutBtn.id = 'tailscale_logout_btn'; + + loginBtn.onclick = function() { + const customServerInput = document.getElementById('widget.cbid.tailscale.settings.custom_login_url'); + const customServer = customServerInput ? customServerInput.value : ''; + const customserverAuthInput = document.getElementById('widget.cbid.tailscale.settings.custom_login_AuthKey'); + const customServerAuth = customserverAuthInput ? customserverAuthInput.value : ''; + 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; + } + // Display a prompt message in the new window + const doc = loginWindow.document; + doc.body.innerHTML = + '

' + _('Tailscale Login') + '

' + + '

' + _('Requesting Tailscale login URL... Please wait.') + '

' + + '

' + _('This can take up to 30 seconds.') + '

'; + + ui.showModal(_('Requesting Login URL...'), E('em', {}, _('Please wait.'))); + const payload = { + loginserver: customServer || '', + loginserver_authkey: customServerAuth || '' + }; + // Show a "loading" modal and execute the asynchronous RPC call + ui.showModal(_('Requesting Login URL...'), E('em', {}, _('Please wait.'))); + return callDoLogin(payload).then(function(res) { + ui.hideModal(); + if (res && res.url) { + // After successfully obtaining the URL, redirect the previously opened tab + loginWindow.location.href = res.url; + } else { + // If it fails, inform the user and they can close the new tab + doc.body.innerHTML = + '

' + _('Error') + '

' + + '

' + _('Failed to get login URL. You may close this tab.') + '

'; + 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'); + }); + }; + + logoutBtn.onclick = function() { + const confirmationContent = E([ + E('p', {}, _('Are you sure you want to log out?') + +'
'+_('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.')) ], 5000, 'info'); + }).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); + }; + + // Create the "Daemon Settings" tab and apply daemonConf + //s.tab('daemon', _('Daemon Settings')); + //defTabOpts(s, 'daemon', daemonConf, { optional: false }); + + return map.render(); + }, + + // The handleSaveApply function is executed after clicking "Save & Apply" + handleSaveApply(ev) { + return map.save().then(function () { + const data = map.data.get('tailscale', 'settings'); + + // fix empty value issue + if(!data.advertise_exit_node) data.advertise_exit_node = ''; + if(!data.advertise_routes) data.advertise_routes = ''; + if(!data.exit_node) data.exit_node = ''; + if(!data.custom_login_url) data.custom_login_url = ''; + if(!data.custom_login_AuthKey) data.custom_login_AuthKey = ''; + + ui.showModal(_('Applying changes...'), E('em', {}, _('Please wait.'))); + + return callSetSettings(data).then(function (response) { + if (response.success) { + ui.hideModal(); + setTimeout(function() { + ui.addTimeLimitedNotification(null, [ E('p', _('Tailscale settings applied successfully.')) ], 5000, 'info'); + }, 1000); + try { + L.ui.changes.revert(); + } catch (error) { + ui.addTimeLimitedNotification(null, [ E('p', _('Error saving settings: %s').format(error || _('Unknown error'))) ], 7000, 'error'); + } + } else { + ui.hideModal(); + ui.addTimeLimitedNotification(null, [ E('p', _('Error applying settings: %s').format(response.error || _('Unknown error'))) ], 7000, 'error'); + } + }); + }).catch(function(err) { + ui.hideModal(); + //console.error('Save failed:', err); + ui.addTimeLimitedNotification(null, [ E('p', _('Failed to save settings: %s').format(err.message)) ], 7000, 'error'); + }); + }, + + handleSave: null, + handleReset: null +}); diff --git a/applications/luci-app-tailscale-community/po/templates/community.pot b/applications/luci-app-tailscale-community/po/templates/community.pot new file mode 100644 index 0000000000..54f3165beb --- /dev/null +++ b/applications/luci-app-tailscale-community/po/templates/community.pot @@ -0,0 +1,470 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:34 +msgid "(Experimental) Reduce Memory Usage" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 +msgid "Accept Routes" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:425 +msgid "Account Settings" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 +msgid "Advertise Exit Node" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:400 +msgid "Advertise Routes" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:400 +msgid "" +"Advertise subnet routes behind this device. Select from the detected subnets " +"below or enter custom routes (comma-separated)." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 +msgid "Allow LAN Access" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 +msgid "Allow accepting routes announced by other nodes." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 +msgid "Allow connecting to this device through the SSH function of Tailscale." +msgstr "" + +#: applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:3 +msgid "Allow user access to tailscale" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:547 +msgid "Applying changes..." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:499 +msgid "Are you sure you want to log out?" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:408 +msgid "Auto Configure Firewall" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:506 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:450 +msgid "Click to Log out account on this device." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:429 +msgid "Click to get a login URL for this device." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:166 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:363 +msgid "Collecting data ..." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 +msgid "Confirm Logout" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:253 +msgid "Connection Info" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:462 +msgid "" +"Could not open a new tab. Please check if your browser or an extension " +"blocked the pop-up." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:434 +msgid "Custom Login Server" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:442 +msgid "Custom Login Server Auth Key" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 +msgid "Declare this device as an Exit Node." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 +msgid "Disable MagicDNS" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 +msgid "Disable SNAT" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 +msgid "" +"Disable Source NAT (SNAT) for traffic to advertised routes. Most users " +"should leave this unchecked." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 +msgid "Disabled" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:451 +msgid "Disconnect from Tailscale and expire current node key." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 +msgid "Enable Tailscale SSH" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 +msgid "Enable Web Interface" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:34 +msgid "" +"Enabling this option can reduce memory usage, but it may sacrifice some " +"performance (set GOGC=10)." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487 +msgid "Error" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562 +msgid "Error applying settings: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:147 +msgid "Error caching DERP region map: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 +msgid "Error fetching DERP region map: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:120 +msgid "Error reading cached DERP region map: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:558 +msgid "Error saving settings: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 +msgid "Exit Node" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:409 +msgid "" +"Experimental: applies minimal firewall and interface setup for Tailscale. It " +"will create/patch network.tailscale (proto none, device tailscale0), add a " +"firewall zone \"tailscale\" with ACCEPT/ACCEPT/ACCEPT, masq, mtu_fix, and " +"ensure forwarding tailscale<->lan. It reloads network/firewall only if " +"changes are made." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 +msgid "" +"Expose a web interface on port 5252 for managing this node over Tailscale." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:418 +msgid "Failed to configure firewall: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488 +msgid "Failed to get login URL. You may close this tab." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:493 +msgid "Failed to get login URL: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:489 +msgid "Failed to get login URL: Invalid response from server." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:568 +msgid "Failed to save settings: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 +msgid "Firewall Mode" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:415 +msgid "Firewall configuration applied." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:372 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:250 +msgid "Hostname" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:430 +msgid "" +"If the timeout is displayed, you can refresh the page and click Login again." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:444 +msgid "" +"If you are using custom login server but not providing an Auth Key, will " +"redirect to the login page without pre-filling the key." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:90 +msgid "Invalid Date" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:93 +msgid "Just now" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:205 +msgid "LOGGED OUT" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:256 +msgid "Last Seen" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:436 +msgid "Leave blank for default Tailscale control plane." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:512 +msgid "Logging out..." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:428 +msgid "Login" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:449 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522 +msgid "Logout" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:519 +msgid "Logout failed: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:87 +msgid "N/A" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:216 +msgid "NOT RUNNING" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:298 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245 +msgid "No peer devices found." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:377 +msgid "None" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:88 +msgid "Now" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:252 +msgid "OS" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278 +msgid "Offline" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278 +msgid "Online" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:435 +msgid "" +"Optional: Specify a custom control server URL (e.g., a Headscale instance, " +"https://example.com)." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:443 +msgid "" +"Optional: Specify an authentication key for the custom control server. Leave " +"blank if not required." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:207 +msgid "Please use the login button in the settings below to authenticate." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:478 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:512 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:547 +msgid "Please wait." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:224 +msgid "RUNNING" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:254 +msgid "RX" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:478 +msgid "Requesting Login URL..." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469 +msgid "Requesting Tailscale login URL... Please wait." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 +msgid "" +"Select an exit node from the list. If enabled, Allow LAN Access is enabled " +"implicitly." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 +msgid "" +"Select the firewall backend for Tailscale to use. Requires service restart " +"to take effect." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:195 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:203 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:215 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:224 +msgid "Service Status" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368 +msgid "Settings" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 +msgid "Shields Up" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:249 +msgid "Status" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:516 +msgid "Successfully logged out." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:196 +msgid "TAILSCALE NOT FOUND" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 +msgid "TUN Mode" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:255 +msgid "TX" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229 +msgid "Tailnet Name" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:342 +#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3 +msgid "Tailscale" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:172 +msgid "Tailscale Health Check: %s" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:251 +msgid "Tailscale IP" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 +msgid "Tailscale IPv4" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:228 +msgid "Tailscale IPv6" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:468 +msgid "Tailscale Login" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:342 +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 "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:553 +msgid "Tailscale settings applied successfully." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:470 +msgid "This can take up to 30 seconds." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:500 +msgid "" +"This will disconnect this device from your Tailnet and require you to re-" +"authenticate." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:120 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:147 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:493 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:519 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:558 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562 +msgid "Unknown error" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 +msgid "Use system DNS instead of MagicDNS." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:225 +msgid "Version" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 +msgid "" +"When enabled, blocks all inbound connections from the Tailscale network." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 +msgid "When using the exit node, access to the local LAN is allowed." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:96 +msgid "ago" +msgstr "" diff --git a/applications/luci-app-tailscale-community/po/zh_Hans/community.po b/applications/luci-app-tailscale-community/po/zh_Hans/community.po new file mode 100644 index 0000000000..f6936ded04 --- /dev/null +++ b/applications/luci-app-tailscale-community/po/zh_Hans/community.po @@ -0,0 +1,484 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:34 +msgid "(Experimental) Reduce Memory Usage" +msgstr "(实验性) 减少内存使用" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 +msgid "Accept Routes" +msgstr "接受路由" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:425 +msgid "Account Settings" +msgstr "账户设置" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 +msgid "Advertise Exit Node" +msgstr "通告出口节点" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:400 +msgid "Advertise Routes" +msgstr "通告路由" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:400 +msgid "" +"Advertise subnet routes behind this device. Select from the detected subnets " +"below or enter custom routes (comma-separated)." +msgstr "" +"通告此设备后的子网路由。从下面的子网中选择,或输入自定义路由 (逗号分隔)。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 +msgid "Allow LAN Access" +msgstr "允许局域网访问" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 +msgid "Allow accepting routes announced by other nodes." +msgstr "允许接受由其他节点通告的路由。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 +msgid "Allow connecting to this device through the SSH function of Tailscale." +msgstr "允许通过 Tailscale 的 SSH 功能连接到此设备。" + +#: applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:3 +msgid "Allow user access to tailscale" +msgstr "允许用户访问 Tailscale" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:547 +msgid "Applying changes..." +msgstr "正在应用更改..." + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:499 +msgid "Are you sure you want to log out?" +msgstr "您确定要登出吗?" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:408 +msgid "Auto Configure Firewall" +msgstr "自动配置防火墙" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:506 +msgid "Cancel" +msgstr "取消" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:450 +msgid "Click to Log out account on this device." +msgstr "点击以登出此设备上的账户。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:429 +msgid "Click to get a login URL for this device." +msgstr "点击获取此设备的登录 URL。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:166 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:363 +msgid "Collecting data ..." +msgstr "正在收集数据..." + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 +msgid "Confirm Logout" +msgstr "确认登出" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:253 +msgid "Connection Info" +msgstr "连接信息" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:462 +msgid "" +"Could not open a new tab. Please check if your browser or an extension " +"blocked the pop-up." +msgstr "无法打开新标签页。请检查您的浏览器或扩展程序是否阻止了弹出窗口。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:434 +msgid "Custom Login Server" +msgstr "自定义登录服务器" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:442 +msgid "Custom Login Server Auth Key" +msgstr "自定义登录服务器认证密钥" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 +msgid "Declare this device as an Exit Node." +msgstr "将此设备声明为出口节点。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 +msgid "Disable MagicDNS" +msgstr "禁用 MagicDNS" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 +msgid "Disable SNAT" +msgstr "禁用 SNAT" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 +msgid "" +"Disable Source NAT (SNAT) for traffic to advertised routes. Most users " +"should leave this unchecked." +msgstr "为通告路由的流量禁用源地址转换 (SNAT)。大多数用户应保持此项不勾选。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 +msgid "Disabled" +msgstr "已禁用" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:451 +msgid "Disconnect from Tailscale and expire current node key." +msgstr "从 Tailscale 断开连接并使当前节点密钥过期。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 +msgid "Enable Tailscale SSH" +msgstr "启用 Tailscale SSH" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 +msgid "Enable Web Interface" +msgstr "启用 Web 界面" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 +msgid "Enabled" +msgstr "已启用" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:34 +msgid "" +"Enabling this option can reduce memory usage, but it may sacrifice some " +"performance (set GOGC=10)." +msgstr "启用此选项可以减少内存使用,但可能会牺牲一些性能 (设置 GOGC=10)。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487 +msgid "Error" +msgstr "错误" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562 +msgid "Error applying settings: %s" +msgstr "应用设置时出错: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:147 +msgid "Error caching DERP region map: %s" +msgstr "缓存 DERP 区域地图时出错: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 +msgid "Error fetching DERP region map: %s" +msgstr "获取 DERP 区域地图时出错: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:120 +msgid "Error reading cached DERP region map: %s" +msgstr "读取缓存的 DERP 区域地图时出错: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:558 +msgid "Error saving settings: %s" +msgstr "保存设置时出错: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 +msgid "Exit Node" +msgstr "出口节点" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:409 +msgid "" +"Experimental: applies minimal firewall and interface setup for Tailscale. It " +"will create/patch network.tailscale (proto none, device tailscale0), add a " +"firewall zone \"tailscale\" with ACCEPT/ACCEPT/ACCEPT, masq, mtu_fix, and " +"ensure forwarding tailscale<->lan. It reloads network/firewall only if " +"changes are made." +msgstr "" +"实验性功能:为Tailscale应用所必须最小的防火墙设置。它将创建/修补network." +"tailscale (proto none,device tailscale0),添加ACCEPT/ACCEPT/ACCEPT、masq、" +"mtu_fix的防火墙区域“tailscale”,并转发tailscale<->lan。反正总之如果你不知道这" +"个是干什么的,而且你tailscale网络又有问题,说明你需要点这个。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 +msgid "" +"Expose a web interface on port 5252 for managing this node over Tailscale." +msgstr "在端口 5252 上暴露一个 Web 界面,用于通过 Tailscale 管理此节点。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:418 +msgid "Failed to configure firewall: %s" +msgstr "获取防火墙设置失败: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488 +msgid "Failed to get login URL. You may close this tab." +msgstr "获取登录 URL 失败。您可以关闭此标签页。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:493 +msgid "Failed to get login URL: %s" +msgstr "获取登录 URL 失败: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:489 +msgid "Failed to get login URL: Invalid response from server." +msgstr "获取登录 URL 失败: 服务器响应无效。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:568 +msgid "Failed to save settings: %s" +msgstr "保存设置失败: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 +msgid "Firewall Mode" +msgstr "防火墙模式" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:415 +msgid "Firewall configuration applied." +msgstr "已应用防火墙配置" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:372 +msgid "General Settings" +msgstr "常规设置" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:250 +msgid "Hostname" +msgstr "主机名" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:430 +msgid "" +"If the timeout is displayed, you can refresh the page and click Login again." +msgstr "如果显示超时,您可以刷新页面并再次点击登录。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:444 +msgid "" +"If you are using custom login server but not providing an Auth Key, will " +"redirect to the login page without pre-filling the key." +msgstr "" +"如果您使用自定义登录服务器但未提供认证密钥,将重定向到登录页面而不会预先填充" +"密钥。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:90 +msgid "Invalid Date" +msgstr "无效日期" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:93 +msgid "Just now" +msgstr "刚才" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:205 +msgid "LOGGED OUT" +msgstr "已登出" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:256 +msgid "Last Seen" +msgstr "上次在线" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:436 +msgid "Leave blank for default Tailscale control plane." +msgstr "留空以使用默认的 Tailscale 控制平面。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:512 +msgid "Logging out..." +msgstr "正在登出..." + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:428 +msgid "Login" +msgstr "登录" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:449 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522 +msgid "Logout" +msgstr "登出" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:519 +msgid "Logout failed: %s" +msgstr "登出失败: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:87 +msgid "N/A" +msgstr "N/A" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:216 +msgid "NOT RUNNING" +msgstr "未运行" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:298 +msgid "Network Devices" +msgstr "网络设备" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245 +msgid "No peer devices found." +msgstr "未找到对等设备。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:377 +msgid "None" +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:88 +msgid "Now" +msgstr "现在" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:252 +msgid "OS" +msgstr "操作系统" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278 +msgid "Offline" +msgstr "离线" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278 +msgid "Online" +msgstr "在线" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:435 +msgid "" +"Optional: Specify a custom control server URL (e.g., a Headscale instance, " +"https://example.com)." +msgstr "" +"可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例,https://" +"example.com)。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:443 +msgid "" +"Optional: Specify an authentication key for the custom control server. Leave " +"blank if not required." +msgstr "可选:为自定义控制服务器指定一个认证密钥。如果不需要,请留空。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:207 +msgid "Please use the login button in the settings below to authenticate." +msgstr "请使用下方设置中的登录按钮进行认证。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:478 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:512 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:547 +msgid "Please wait." +msgstr "请稍候。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:224 +msgid "RUNNING" +msgstr "正在运行" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:254 +msgid "RX" +msgstr "接收" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:478 +msgid "Requesting Login URL..." +msgstr "正在请求登录 URL..." + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469 +msgid "Requesting Tailscale login URL... Please wait." +msgstr "正在请求 Tailscale 登录 URL... 请稍候。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 +msgid "" +"Select an exit node from the list. If enabled, Allow LAN Access is enabled " +"implicitly." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 +msgid "" +"Select the firewall backend for Tailscale to use. Requires service restart " +"to take effect." +msgstr "选择 Tailscale 使用的防火墙后端。需要重启服务才能生效。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:195 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:203 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:215 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:224 +msgid "Service Status" +msgstr "服务状态" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368 +msgid "Settings" +msgstr "设置" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 +msgid "Shields Up" +msgstr "开启防护 (Shields Up)" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:249 +msgid "Status" +msgstr "状态" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:516 +msgid "Successfully logged out." +msgstr "登出成功。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:196 +msgid "TAILSCALE NOT FOUND" +msgstr "未找到 TAILSCALE" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 +msgid "TUN Mode" +msgstr "TUN 模式" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:255 +msgid "TX" +msgstr "发送" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229 +msgid "Tailnet Name" +msgstr "Tailnet 名称" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:342 +#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3 +msgid "Tailscale" +msgstr "Tailscale" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:172 +msgid "Tailscale Health Check: %s" +msgstr "Tailscale 健康检查: %s" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:251 +msgid "Tailscale IP" +msgstr "Tailscale IP" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 +msgid "Tailscale IPv4" +msgstr "Tailscale IPv4" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:228 +msgid "Tailscale IPv6" +msgstr "Tailscale IPv6" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:468 +msgid "Tailscale Login" +msgstr "Tailscale 登录" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:342 +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 是一个网状 VPN 解决方案,可以轻松地安全连接您的设备。此配置页面允许" +"您在 OpenWrt 设备上管理 Tailscale 设置。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:553 +msgid "Tailscale settings applied successfully." +msgstr "Tailscale 设置已成功应用。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:470 +msgid "This can take up to 30 seconds." +msgstr "此过程最多可能需要 30 秒。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:500 +msgid "" +"This will disconnect this device from your Tailnet and require you to re-" +"authenticate." +msgstr "这将使此设备从您的 Tailnet 断开连接,并需要您重新进行身份验证。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:120 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:147 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:493 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:519 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:558 +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562 +msgid "Unknown error" +msgstr "未知错误" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 +msgid "Use system DNS instead of MagicDNS." +msgstr "" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:225 +msgid "Version" +msgstr "版本" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 +msgid "" +"When enabled, blocks all inbound connections from the Tailscale network." +msgstr "启用后,将阻止来自 Tailscale 网络的所有入站连接。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 +msgid "When using the exit node, access to the local LAN is allowed." +msgstr "使用出口节点时,允许访问本地局域网。" + +#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:96 +msgid "ago" +msgstr "前" + +#~ msgid "Specify an exit node. Leave it blank and it will not be used." +#~ msgstr "指定一个出口节点。留空则不使用。" diff --git a/applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json b/applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json new file mode 100644 index 0000000000..70c90695cf --- /dev/null +++ b/applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json @@ -0,0 +1,10 @@ +{ + "admin/services/tailscale": { + "title": "Tailscale", + "order": 90, + "action": { + "type": "view", + "path": "tailscale" + } + } +} diff --git a/applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json b/applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json new file mode 100644 index 0000000000..f80185ff12 --- /dev/null +++ b/applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json @@ -0,0 +1,26 @@ +{ + "luci-app-tailscale-community": { + "description": "Allow user access to tailscale", + "read": { + "ubus": { + "tailscale": [ + "get_status", + "get_settings", + "get_subroutes" + ] + }, + "uci": [ "tailscale" ] + }, + "write": { + "ubus": { + "tailscale": [ + "do_login", + "do_logout", + "setup_firewall", + "set_settings" + ] + }, + "uci": [ "tailscale" ] + } + } +} diff --git a/applications/luci-app-tailscale-community/root/usr/share/rpcd/ucode/tailscale.uc b/applications/luci-app-tailscale-community/root/usr/share/rpcd/ucode/tailscale.uc new file mode 100644 index 0000000000..792f7c346a --- /dev/null +++ b/applications/luci-app-tailscale-community/root/usr/share/rpcd/ucode/tailscale.uc @@ -0,0 +1,393 @@ +#!/usr/bin/env ucode + +'use strict'; + +import { access, popen, readfile, writefile, unlink } from 'fs'; +import { cursor } from 'uci'; + +const uci = cursor(); + +function exec(command) { + let stdout_content = ''; + let p = popen(command, 'r'); + sleep(100); + 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, "'", "'\\''") + "'"; +} + +const methods = {}; + +methods.get_status = { + call: function() { + let data = { + status: '', + version: '', + TUNMode: '', + health: '', + ipv4: "Not running", + ipv6: null, + domain_name: '', + peers: [] + }; + if (access('/usr/sbin/tailscale')==true || access('/usr/bin/tailscale')==true){ }else{ + data.status = 'not_installed'; + return data; + } + + let status_json_output = exec('tailscale status --json'); + let peer_map = {}; + if (status_json_output.code == 0 && length(status_json_output.stdout) > 0) { + try { + let status_data = json(join('',status_json_output.stdout)); + data.version = status_data?.Version || 'Unknown'; + data.health = status_data?.Health || ''; + data.TUNMode = status_data?.TUN || 'true'; + if (status_data?.BackendState == 'Running') { data.status = 'running'; } + if (status_data?.BackendState == 'NeedsLogin') { data.status = 'logout'; } + + data.ipv4 = status_data?.Self?.TailscaleIPs?.[0] || 'No IP assigned'; + data.ipv6 = status_data?.Self?.TailscaleIPs?.[1] || null; + data.domain_name = status_data?.CurrentTailnet?.Name || ''; + + // peers list + for (let p in status_data?.Peer) { + p = status_data.Peer[p]; + peer_map[p.ID] = { + ip: join('
', p?.TailscaleIPs) || '', + hostname: split(p?.DNSName || '','.')[0] || '', + ostype: p?.OS, + online: p?.Online, + linkadress: (!p?.CurAddr) ? p?.Relay : p?.CurAddr, + lastseen: p?.LastSeen, + exit_node: !!p?.ExitNode, + exit_node_option: !!p?.ExitNodeOption, + tx: p?.TxBytes || '', + rx: p?.RxBytes || '' + }; + } + } catch (e) { /* ignore */ } + } + + 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') || "/var/lib/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.nosnat = status_data?.NoSNAT || false; + settings.disable_magic_dns = !status_data?.CorpDNS || false; + settings.fw_mode = split(uci.get('tailscale', 'settings', 'fw_mode'),' ')[0] || 'nftables'; + } + } + } catch (e) { /* ignore */ } + } + return settings; + } +}; + +methods.set_settings = { + 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 settings data.' }; + } + let args = ['set']; + + push(args,'--accept-routes=' + (form_data.accept_routes == '1')); + push(args,'--advertise-exit-node=' + (form_data.advertise_exit_node == '1')); + push(args,'--exit-node-allow-lan-access=' + (form_data.exit_node_allow_lan_access == '1')); + push(args,'--ssh=' + (form_data.ssh == '1')); + push(args,'--accept-dns=' + (form_data.disable_magic_dns != '1')); + push(args,'--shields-up=' + (form_data.shields_up == '1')); + push(args,'--webclient=' + (form_data.runwebclient == '1')); + push(args,'--snat-subnet-routes=' + (form_data.nosnat != '1')); + push(args,'--advertise-routes ' + (shell_quote(join(',',form_data.advertise_routes)) || '\"\"')); + push(args,'--exit-node=' + (shell_quote(form_data.exit_node) || '\"\"')); + if (form_data.exit_node != "") push(args,' --exit-node-allow-lan-access'); + push(args,'--hostname ' + (shell_quote(form_data.hostname) || '\"\"')); + + let cmd_array = 'tailscale '+join(' ', args); + let set_result = exec(cmd_array); + if (set_result.code != 0) { + return { error: 'Failed to apply node settings: ' + set_result.stderr }; + } + + uci.load('tailscale'); + for (let key in form_data) { + uci.set('tailscale', 'settings', key, form_data[key]); + } + uci.save('tailscale'); + uci.commit('tailscale'); + + // process reduce memory https://github.com/GuNanOvO/openwrt-tailscale + // some new versions of Tailscale may not work well with this method + //if (form_data.daemon_mtu != "" || form_data.daemon_reduce_memory != "") { + // popen('/bin/sh -c ". ' + env_script_path + ' && /etc/init.d/tailscale restart" &'); + //} + return { success: true }; + } +}; + +methods.do_login = { + args: { form_data: {} }, + call: function(request) { + const form_data = request.args.form_data; + let loginargs = []; + 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 != 'logout') { + return { error: 'Tailscale is already logged in and running.' }; + } + + // --- 1. Prepare and Run Login Command (Once) --- + 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 "' + 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'); + 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 != 'running') { + return { error: 'Tailscale is not running. Cannot perform logout.' }; + } + + let logout_result = exec('tailscale logout'); + if (logout_result.code != 0) { + return { error: 'Failed to logout: ' + logout_result.stderr }; + } + 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('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 fw_all = uci.get_all('firewall'); + let ts_zone_section = null; + let fwd_lan_to_ts = false; + let fwd_ts_to_lan = false; + + for (let sec_key in fw_all) { + let s = fw_all[sec_key]; + if (s['.type'] == 'zone' && s['name'] == 'tailscale') { + ts_zone_section = sec_key; + } + if (s['.type'] == 'forwarding') { + if (s.src == 'lan' && s.dest == 'tailscale') fwd_lan_to_ts = true; + if (s.src == 'tailscale' && s.dest == 'lan') fwd_ts_to_lan = true; + } + } + + 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', '1'); + uci.set('firewall', zid, 'mtu_fix', '1'); + uci.set('firewall', zid, 'network', ['tailscale']); + changed_firewall = true; + } else { + 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 (net_list[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; + } + + // 4. save + if (changed_network) { + uci.save('network'); + uci.commit('network'); + exec('/etc/init.d/network reload'); + } + + 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 || '') }; + } + } +}; + +return { 'tailscale': methods };