Skip to content
This repository was archived by the owner on Jul 31, 2025. It is now read-only.

Commit d4fd0ef

Browse files
authored
feat: use LocalAPI for Tailscale config changes instead of exec (#33)
* feat: add initial local-api support * fix: authURL requires POST * feat: move config page to LocalAPI * feat: use localAPI for lock * feat: use localAPI for watcher
1 parent 9c96b79 commit d4fd0ef

File tree

8 files changed

+171
-45
lines changed

8 files changed

+171
-45
lines changed
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#!/usr/bin/php -q
22
<?php
33

4+
namespace Tailscale;
5+
46
require_once "include/common.php";
57

8+
$localAPI = new LocalAPI();
9+
610
foreach (array_slice($argv, 1) as $key => $value) {
7-
Tailscale\Utils::logmsg("Tailnet lock: signing {$value}");
8-
exec("tailscale lock sign {$value}");
11+
Utils::logmsg("Tailnet lock: signing {$value}");
12+
$localAPI->postTkaSign($value);
913
}

src/usr/local/emhttp/plugins/tailscale/include/Pages/Tailscale.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ function showTailscaleConfig() {
5454
var res = await $.post('/plugins/tailscale/include/data/Config.php',{action: 'set-feature', feature: feature, enable: enable});
5555
showTailscaleConfig();
5656
}
57+
async function setAdvertiseExitNode(enable) {
58+
$('div.spinner.fixed').show('fast');
59+
tailscaleControlsDisabled(true);
60+
var res = await $.post('/plugins/tailscale/include/data/Config.php',{action: 'set-advertise-exit-node', enable: enable});
61+
showTailscaleConfig();
62+
}
5763
async function tailscaleUp() {
5864
$('div.spinner.fixed').show('fast');
5965
tailscaleControlsDisabled(true);
@@ -70,7 +76,6 @@ function showTailscaleConfig() {
7076
var res = await $.post('/plugins/tailscale/include/data/Config.php',{action: 'exit-node', node: $('#exitNodeSelect').val()});
7177
showTailscaleConfig();
7278
}
73-
7479
async function removeTailscaleRoute(route) {
7580
$('div.spinner.fixed').show('fast');
7681
tailscaleControlsDisabled(true);

src/usr/local/emhttp/plugins/tailscale/include/Tailscale/Info.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Info
77
private string $useNetbios;
88
private string $smbEnabled;
99
private Translator $tr;
10+
private LocalAPI $localAPI;
1011
private \stdClass $status;
1112
private \stdClass $prefs;
1213
private \stdClass $lock;
@@ -16,30 +17,29 @@ public function __construct(Translator $tr)
1617
$share_config = parse_ini_file("/boot/config/share.cfg") ?: array();
1718
$ident_config = parse_ini_file("/boot/config/ident.cfg") ?: array();
1819

20+
$this->localAPI = new LocalAPI();
21+
1922
$this->tr = $tr;
2023
$this->smbEnabled = $share_config['shareSMBEnabled'] ?? "";
2124
$this->useNetbios = $ident_config['USE_NETBIOS'] ?? "";
22-
$this->status = self::getStatus();
23-
$this->prefs = self::getPrefs();
24-
$this->lock = self::getLock();
25+
$this->status = $this->localAPI->getStatus();
26+
$this->prefs = $this->localAPI->getPrefs();
27+
$this->lock = $this->localAPI->getTkaStatus();
2528
}
2629

27-
public static function getStatus(): \stdClass
30+
public function getStatus(): \stdClass
2831
{
29-
exec("tailscale status --json", $out_status);
30-
return (object) json_decode(implode($out_status));
32+
return $this->status;
3133
}
3234

33-
public static function getPrefs(): \stdClass
35+
public function getPrefs(): \stdClass
3436
{
35-
exec("tailscale debug prefs", $out_prefs);
36-
return (object) json_decode(implode($out_prefs));
37+
return $this->prefs;
3738
}
3839

39-
public static function getLock(): \stdClass
40+
public function getLock(): \stdClass
4041
{
41-
exec("tailscale lock status -json=true", $out_status);
42-
return (object) json_decode(implode($out_status));
42+
return $this->lock;
4343
}
4444

4545
private function tr(string $message): string
@@ -381,7 +381,7 @@ public function getExitNodes(): array
381381
if (isset($status->Location->City)) {
382382
$nodeName .= " (" . $status->Location->City . ")";
383383
}
384-
$exitNodes[$status->DNSName] = $nodeName;
384+
$exitNodes[$status->ID] = $nodeName;
385385
}
386386
}
387387

@@ -392,7 +392,7 @@ public function getCurrentExitNode(): string
392392
{
393393
foreach ($this->status->Peer as $node => $status) {
394394
if ($status->ExitNode ?? false) {
395-
return $status->DNSName;
395+
return $status->ID;
396396
}
397397
}
398398

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Tailscale;
4+
5+
enum APIMethods
6+
{
7+
case GET;
8+
case POST;
9+
case PATCH;
10+
}
11+
12+
class LocalAPI
13+
{
14+
private string $tailscaleSocket = '/var/run/tailscale/tailscaled.sock';
15+
16+
private function tailscaleLocalAPI(string $url, APIMethods $method = APIMethods::GET, object $body = new \stdClass()): string
17+
{
18+
$ch = curl_init();
19+
20+
$headers = [];
21+
22+
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
23+
curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, $this->tailscaleSocket);
24+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
25+
curl_setopt($ch, CURLOPT_URL, "http://local-tailscaled.sock/localapi/{$url}");
26+
27+
if ($method == APIMethods::POST) {
28+
curl_setopt($ch, CURLOPT_POST, true);
29+
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
30+
Utils::logmsg("Tailscale Local API: {$url} POST " . json_encode($body));
31+
$headers[] = "Content-Type: application/json";
32+
}
33+
34+
if ($method == APIMethods::PATCH) {
35+
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
36+
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
37+
Utils::logmsg("Tailscale Local API: {$url} PATCH " . json_encode($body));
38+
$headers[] = "Content-Type: application/json";
39+
}
40+
41+
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
42+
43+
$out = curl_exec($ch) ?: false;
44+
curl_close($ch);
45+
return strval($out);
46+
}
47+
48+
public function getStatus(): \stdClass
49+
{
50+
return (object) json_decode($this->tailscaleLocalAPI('v0/status'));
51+
}
52+
53+
public function getPrefs(): \stdClass
54+
{
55+
return (object) json_decode($this->tailscaleLocalAPI('v0/prefs'));
56+
}
57+
58+
public function getTkaStatus(): \stdClass
59+
{
60+
return (object) json_decode($this->tailscaleLocalAPI('v0/tka/status'));
61+
}
62+
63+
public function postLoginInteractive(): void
64+
{
65+
$this->tailscaleLocalAPI('v0/login-interactive', APIMethods::POST);
66+
}
67+
68+
public function patchPref(string $key, mixed $value): void
69+
{
70+
$body = [];
71+
$body[$key] = $value;
72+
$body["{$key}Set"] = true;
73+
74+
$this->tailscaleLocalAPI('v0/prefs', APIMethods::PATCH, (object) $body);
75+
}
76+
77+
public function postTkaSign(string $key): void
78+
{
79+
$body = ["NodeKey" => $key];
80+
$this->tailscaleLocalAPI("v0/tka/sign", APIMethods::POST, (object) $body);
81+
}
82+
}

src/usr/local/emhttp/plugins/tailscale/include/Tailscale/System.php

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ public static function applyGRO(): void
137137

138138
public static function notifyOnKeyExpiration(): void
139139
{
140-
$status = Info::getStatus();
140+
$localAPI = new LocalAPI();
141+
$status = $localAPI->getStatus();
141142

142143
if (isset($status->Self->KeyExpiry)) {
143144
$expiryTime = new \DateTime($status->Self->KeyExpiry);
@@ -173,7 +174,8 @@ public static function notifyOnKeyExpiration(): void
173174

174175
public static function refreshWebGuiCert(bool $restartIfChanged = true): void
175176
{
176-
$status = Info::getStatus();
177+
$localAPI = new LocalAPI();
178+
$status = $localAPI->getStatus();
177179

178180
$certDomains = $status->CertDomains;
179181

@@ -262,21 +264,23 @@ public static function setExtraInterface(Config $config): void
262264
}
263265
}
264266

265-
private static function disableTailscaleFeature(bool $allow, string $flag): void
267+
private static function disableTailscaleFeature(LocalAPI $localAPI, bool $allow, string $flag): void
266268
{
267269
if ($allow) {
268270
Utils::logmsg("Ignoring {$flag}");
269271
} else {
270-
Utils::run_command("/usr/local/sbin/tailscale set {$flag}=false");
272+
$localAPI->patchPref($flag, false);
271273
}
272274
}
273275

274276
public static function applyTailscaleConfig(Config $config): void
275277
{
276-
self::disableTailscaleFeature($config->AllowRoutes, '--accept-routes');
277-
self::disableTailscaleFeature($config->AllowDNS, '--accept-dns');
278+
$localAPI = new LocalAPI();
278279

279-
self::disableTailscaleFeature(false, '--stateful-filtering');
280+
self::disableTailscaleFeature($localAPI, $config->AllowRoutes, 'RouteAll');
281+
self::disableTailscaleFeature($localAPI, $config->AllowDNS, 'CorpDNS');
282+
283+
$localAPI->patchPref('NoStatefulFiltering', true);
280284
}
281285

282286
public static function createTailscaledParamsFile(Config $config): void

src/usr/local/emhttp/plugins/tailscale/include/Tailscale/Utils.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public static function sendUsageData(Config $config): void
7676

7777
$tailscaleInfo = new Info(new Translator());
7878

79-
$prefs = Info::getPrefs();
79+
$prefs = $tailscaleInfo->getPrefs();
8080
if (isset($prefs->LoggedOut) ? ($prefs->LoggedOut ? true : false) : true) {
8181
Utils::logmsg("Skipping usage data collection; not logged in.");
8282
return;
@@ -251,4 +251,12 @@ public static function validateCidr(string $cidr): bool
251251

252252
return false;
253253
}
254+
255+
/**
256+
* @return array<string>
257+
*/
258+
public static function getExitRoutes(): array
259+
{
260+
return ["0.0.0.0/0", "::/0"];
261+
}
254262
}

src/usr/local/emhttp/plugins/tailscale/include/Tailscale/Watcher.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ public function run(): void
4242
Utils::logmsg("Tailscale IP detected, applying configuration");
4343
$need_ip = false;
4444

45-
$status = Info::getStatus();
46-
$tsName = $status->Self->DNSName;
45+
$localAPI = new LocalAPI();
46+
$status = $localAPI->getStatus();
47+
$tsName = $status->Self->DNSName;
4748

4849
Utils::run_task('Tailscale\System::applyTailscaleConfig', array($this->config));
4950
Utils::run_task('Tailscale\System::applyGRO');
@@ -55,7 +56,8 @@ public function run(): void
5556

5657
// Watch for changes to the DNS name (e.g., if someone changes the tailnet name or the Tailscale name of the server via the admin console)
5758
// If a change happens, refresh the Tailscale WebGUI certificate
58-
$status = Info::getStatus();
59+
$localAPI = new LocalAPI();
60+
$status = $localAPI->getStatus();
5961
$newTsName = $status->Self->DNSName;
6062

6163
if ($newTsName != $tsName) {

0 commit comments

Comments
 (0)