mirror of
https://github.com/openwrt/luci.git
synced 2026-05-31 10:31:55 +08:00
b1a20c42bd
add switch DNSMode add fw now donot duplicate fw remove WAN-> TAILSCALE FORWARD CHAIN Signed-off-by: tokisaki galaxy <moebest@outlook.jp>
361 lines
10 KiB
Ucode
Executable File
361 lines
10 KiB
Ucode
Executable File
#!/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('<br>', 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') || "/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.nosnat = status_data?.NoSNAT || false;
|
|
settings.dns_mode = uci.get('tailscale', 'settings', 'dns_mode') || 'disabled';
|
|
settings.fw_mode = split(uci.get('tailscale', 'settings', 'fw_mode'),' ')[0] || 'nftables';
|
|
}
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
return settings;
|
|
}
|
|
};
|
|
|
|
|
|
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 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;
|
|
});
|
|
|
|
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 (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');
|
|
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 };
|