localghost 4.2.0__tar.gz → 4.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {localghost-4.2.0 → localghost-4.2.1}/PKG-INFO +1 -1
- {localghost-4.2.0 → localghost-4.2.1}/pyproject.toml +1 -1
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/cli.py +43 -20
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/feedback.py +21 -6
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale.py +34 -13
- {localghost-4.2.0 → localghost-4.2.1}/LICENSE +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/README.md +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/.dockerignore +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/Dockerfile +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/__init__.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/compose.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/config.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/detached.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/forwarder.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/generator.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/init.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/paths.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/picker.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/ports.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/proxy_compose.yaml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/proxy_compose_https.yaml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/proxy_compose_tailscale.yaml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/proxy_compose_tailscale_https.yaml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/registry.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/roots.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/routes.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/runner.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/sessions.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/statusbar.py +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/Dockerfile +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/go.mod +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/go.sum +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/identity.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/identity_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/main.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/main_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/tailscale_gateway/proxy_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/.traefik.yml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/ca.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/ca_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/cmd/bootstrap/main.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/docker.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/docker_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/go.mod +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/go.sum +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/provider.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/provider_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/storage.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/LICENSE +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/config.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/http_config.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/marshaler.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/middlewares.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/plugins.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tcp_config.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tcp_middlewares.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tls/certificate.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/tls/tls.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/types/domains.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/types/tls.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/github.com/traefik/genconf/dynamic/udp_config.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca/vendor/modules.txt +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/.traefik.yml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/client.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/client_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/go.mod +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/identity.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-client/identity_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/.traefik.yml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/fallback.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/fallback_test.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/go.mod +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-fallback/logo.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-tailnet-ca/.traefik.yml +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-tailnet-ca/go.mod +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-tailnet-ca/provider.go +0 -0
- {localghost-4.2.0 → localghost-4.2.1}/src/localghost/trust.py +0 -0
|
@@ -31,6 +31,8 @@ from .config import (
|
|
|
31
31
|
write_run_config,
|
|
32
32
|
)
|
|
33
33
|
from .feedback import (
|
|
34
|
+
LIME,
|
|
35
|
+
MINT,
|
|
34
36
|
action,
|
|
35
37
|
choices,
|
|
36
38
|
compose_dry_run,
|
|
@@ -41,6 +43,7 @@ from .feedback import (
|
|
|
41
43
|
routes,
|
|
42
44
|
run_plan,
|
|
43
45
|
sign_off,
|
|
46
|
+
styled,
|
|
44
47
|
success,
|
|
45
48
|
title,
|
|
46
49
|
warning,
|
|
@@ -90,11 +93,14 @@ from .tailscale import (
|
|
|
90
93
|
API as TailscaleAPI,
|
|
91
94
|
)
|
|
92
95
|
from .tailscale import (
|
|
96
|
+
DNS_CONSOLE_URL,
|
|
97
|
+
MACHINES_CONSOLE_URL,
|
|
93
98
|
OAUTH_CONSOLE_URL,
|
|
94
99
|
POLICY_CONSOLE_URL,
|
|
95
100
|
TailnetHub,
|
|
96
101
|
TailscaleError,
|
|
97
102
|
TailscaleState,
|
|
103
|
+
tag_in_use,
|
|
98
104
|
tailnet_hubs,
|
|
99
105
|
)
|
|
100
106
|
from .tailscale import (
|
|
@@ -1252,7 +1258,7 @@ def _refuse_active_mapping(
|
|
|
1252
1258
|
f"split DNS for .{suffix} already points at "
|
|
1253
1259
|
f"{', '.join(active)} — another machine may be hosting this "
|
|
1254
1260
|
"suffix. Choose a different --suffix, or pass --takeover to "
|
|
1255
|
-
"replace the mapping."
|
|
1261
|
+
f"replace the mapping. Split DNS is listed at {DNS_CONSOLE_URL}."
|
|
1256
1262
|
)
|
|
1257
1263
|
|
|
1258
1264
|
|
|
@@ -1310,31 +1316,41 @@ def _resolve_tailscale_credential(
|
|
|
1310
1316
|
info("Using the OAuth credential stored in the system keyring.")
|
|
1311
1317
|
return stored.client_id, stored.client_secret
|
|
1312
1318
|
if tag is not None:
|
|
1313
|
-
|
|
1314
|
-
|
|
1319
|
+
client = "Add OAuth credential"
|
|
1320
|
+
if tag_in_use(tag):
|
|
1321
|
+
# A device already carries the tag, so only the client is left.
|
|
1322
|
+
success(f"The {tag} device tag is already allowed on the tailnet.")
|
|
1323
|
+
steps = [(client, OAUTH_CONSOLE_URL)]
|
|
1324
|
+
indent = ""
|
|
1325
|
+
else:
|
|
1326
|
+
steps = [
|
|
1315
1327
|
("1. Allow the device tag", POLICY_CONSOLE_URL),
|
|
1316
1328
|
(
|
|
1317
1329
|
" Add to the policy file",
|
|
1318
1330
|
f'"tagOwners": {{"{tag}": ["autogroup:admin"]}}',
|
|
1319
1331
|
),
|
|
1320
|
-
("2.
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
"
|
|
1328
|
-
"
|
|
1329
|
-
|
|
1332
|
+
(f"2. {client}", OAUTH_CONSOLE_URL),
|
|
1333
|
+
]
|
|
1334
|
+
indent = " "
|
|
1335
|
+
details(steps)
|
|
1336
|
+
styled(
|
|
1337
|
+
[
|
|
1338
|
+
(f"{indent}with scopes ", "default"),
|
|
1339
|
+
("DNS: Write", MINT),
|
|
1340
|
+
(", ", "default"),
|
|
1341
|
+
("Auth Keys: Write", MINT),
|
|
1342
|
+
(" (", "default"),
|
|
1343
|
+
(tag, MINT),
|
|
1344
|
+
(")", "default"),
|
|
1345
|
+
]
|
|
1330
1346
|
)
|
|
1331
1347
|
else:
|
|
1332
1348
|
details(
|
|
1333
|
-
[("OAuth
|
|
1349
|
+
[("OAuth credential with DNS: Write", OAUTH_CONSOLE_URL)],
|
|
1334
1350
|
title="No OAuth credential is stored in the system keyring",
|
|
1335
1351
|
)
|
|
1336
1352
|
if not client_id:
|
|
1337
|
-
client_id = click.prompt("Client
|
|
1353
|
+
client_id = click.prompt("Client ID")
|
|
1338
1354
|
if not client_secret:
|
|
1339
1355
|
client_secret = click.prompt("Client secret", hide_input=True)
|
|
1340
1356
|
return client_id, client_secret
|
|
@@ -1399,14 +1415,18 @@ def tailscale_unshare(
|
|
|
1399
1415
|
"`localghost trust remove` on each to revoke."
|
|
1400
1416
|
)
|
|
1401
1417
|
info(
|
|
1402
|
-
f"Remove the offline tagged localghost-{state.node_label} device
|
|
1403
|
-
"
|
|
1418
|
+
f"Remove the offline tagged localghost-{state.node_label} device at "
|
|
1419
|
+
f"{MACHINES_CONSOLE_URL}."
|
|
1404
1420
|
)
|
|
1405
1421
|
|
|
1406
1422
|
|
|
1407
1423
|
def _forget_tailscale_credential() -> None:
|
|
1408
1424
|
if delete_tailscale_credential():
|
|
1409
1425
|
info("The OAuth credential was removed from the system keyring.")
|
|
1426
|
+
info(
|
|
1427
|
+
"The OAuth client itself still exists; revoke it on the Trust "
|
|
1428
|
+
f"credentials page ({OAUTH_CONSOLE_URL}) if no other machine uses it."
|
|
1429
|
+
)
|
|
1410
1430
|
else:
|
|
1411
1431
|
info("No OAuth credential was stored in the system keyring.")
|
|
1412
1432
|
|
|
@@ -1515,14 +1535,17 @@ def _trust_tailnet_hub(target: str | None) -> None:
|
|
|
1515
1535
|
def _share_trust(suffix: str, certificate: PublicCertificate) -> None:
|
|
1516
1536
|
"""Show another tailnet machine how to trust this hub's root."""
|
|
1517
1537
|
action(
|
|
1518
|
-
f"
|
|
1538
|
+
f"To trust https://*.{suffix} on other machines",
|
|
1519
1539
|
f"uvx localghost tailscale trust {suffix}",
|
|
1520
1540
|
)
|
|
1521
1541
|
# The fingerprint is for clients that cannot ask Tailscale for the hub;
|
|
1522
1542
|
# the plain command above is the way forward for everyone else.
|
|
1523
1543
|
info(
|
|
1524
|
-
|
|
1525
|
-
|
|
1544
|
+
[
|
|
1545
|
+
("Without the tailscale CLI, add ", "default"),
|
|
1546
|
+
(f"--fingerprint {certificate.fingerprint}", LIME),
|
|
1547
|
+
(" to pin the root.", "default"),
|
|
1548
|
+
]
|
|
1526
1549
|
)
|
|
1527
1550
|
|
|
1528
1551
|
|
|
@@ -106,7 +106,7 @@ def details(
|
|
|
106
106
|
console.print(Text(title, style="bold"))
|
|
107
107
|
table = Table.grid(padding=(0, 1))
|
|
108
108
|
table.add_column(style="bold")
|
|
109
|
-
table.add_column(style=MINT)
|
|
109
|
+
table.add_column(style=MINT, overflow="fold")
|
|
110
110
|
for label, value in entries:
|
|
111
111
|
table.add_row(label, value)
|
|
112
112
|
console.print(table)
|
|
@@ -117,6 +117,16 @@ def details(
|
|
|
117
117
|
console.print("\n".join(lines), soft_wrap=True)
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def styled(parts: Iterable[tuple[str, str]], *, err: bool = False) -> None:
|
|
121
|
+
"""Show one line of (text, style) parts; plain text off a terminal."""
|
|
122
|
+
entries = list(parts)
|
|
123
|
+
console = _console(err)
|
|
124
|
+
if _rich_terminal(err):
|
|
125
|
+
console.print(Text.assemble(*entries))
|
|
126
|
+
return
|
|
127
|
+
console.print("".join(text for text, _ in entries), soft_wrap=True)
|
|
128
|
+
|
|
129
|
+
|
|
120
130
|
def choices(title: str, items: Iterable[tuple[str, str, bool]]) -> None:
|
|
121
131
|
"""Show interactive choices with the same terminal hierarchy as other output."""
|
|
122
132
|
entries = list(items)
|
|
@@ -141,7 +151,7 @@ def choices(title: str, items: Iterable[tuple[str, str, bool]]) -> None:
|
|
|
141
151
|
console.print("\n".join(lines), soft_wrap=True)
|
|
142
152
|
|
|
143
153
|
|
|
144
|
-
def info(message: str, *, err: bool = False) -> None:
|
|
154
|
+
def info(message: str | list[tuple[str, str]], *, err: bool = False) -> None:
|
|
145
155
|
_message(message, MINT, err=err)
|
|
146
156
|
|
|
147
157
|
|
|
@@ -171,7 +181,7 @@ def _labeled_block(rows: list[tuple[str, str]], *, heading: str, err: bool) -> N
|
|
|
171
181
|
console = _console(err)
|
|
172
182
|
console.print(Text.assemble(("▌ ", MINT), (heading, "bold")))
|
|
173
183
|
table = Table.grid(padding=(0, 1))
|
|
174
|
-
table.add_column(style=MINT)
|
|
184
|
+
table.add_column(style=MINT, overflow="fold")
|
|
175
185
|
table.add_column(style="bold")
|
|
176
186
|
table.add_column()
|
|
177
187
|
for label, value in rows:
|
|
@@ -269,12 +279,17 @@ def interrupt_break() -> None:
|
|
|
269
279
|
_console(False).print()
|
|
270
280
|
|
|
271
281
|
|
|
272
|
-
def _message(
|
|
282
|
+
def _message(
|
|
283
|
+
message: str | list[tuple[str, str]], color: str, *, err: bool, symbol: str = "•"
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Show a message; one given as (text, style) parts can highlight a command."""
|
|
286
|
+
parts = [message] if isinstance(message, str) else message
|
|
273
287
|
console = _console(err)
|
|
274
288
|
if _rich_terminal(err):
|
|
275
|
-
console.print(Text.assemble((f"{symbol} ", color),
|
|
289
|
+
console.print(Text.assemble((f"{symbol} ", color), *parts))
|
|
276
290
|
else:
|
|
277
|
-
|
|
291
|
+
plain = "".join(part if isinstance(part, str) else part[0] for part in parts)
|
|
292
|
+
console.print(plain, soft_wrap=True)
|
|
278
293
|
|
|
279
294
|
|
|
280
295
|
def _console(err: bool) -> Console:
|
|
@@ -25,8 +25,10 @@ from .paths import state_directory
|
|
|
25
25
|
|
|
26
26
|
API_ROOT = "https://api.tailscale.com/api/v2"
|
|
27
27
|
TOKEN_URL = f"{API_ROOT}/oauth/token"
|
|
28
|
-
OAUTH_CONSOLE_URL = "https://
|
|
29
|
-
POLICY_CONSOLE_URL = "https://
|
|
28
|
+
OAUTH_CONSOLE_URL = "https://console.tailscale.com/admin/settings/trust-credentials"
|
|
29
|
+
POLICY_CONSOLE_URL = "https://console.tailscale.com/admin/acls/file"
|
|
30
|
+
MACHINES_CONSOLE_URL = "https://console.tailscale.com/admin/machines"
|
|
31
|
+
DNS_CONSOLE_URL = "https://console.tailscale.com/admin/dns"
|
|
30
32
|
KEYRING_SERVICE = "localghost-tailscale"
|
|
31
33
|
_SUFFIX = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
|
|
32
34
|
|
|
@@ -329,7 +331,8 @@ class API:
|
|
|
329
331
|
if exc.code == 403:
|
|
330
332
|
raise TailscaleError(
|
|
331
333
|
f"the OAuth client may not create auth keys: it needs the "
|
|
332
|
-
f"
|
|
334
|
+
f"Auth Keys: Write scope with {tag} selected (Trust "
|
|
335
|
+
f"credentials page, {OAUTH_CONSOLE_URL})"
|
|
333
336
|
) from exc
|
|
334
337
|
raise
|
|
335
338
|
try:
|
|
@@ -341,8 +344,8 @@ class API:
|
|
|
341
344
|
def _dns_scope_guidance(exc: APIError) -> TailscaleError:
|
|
342
345
|
if exc.code == 403:
|
|
343
346
|
return TailscaleError(
|
|
344
|
-
"the OAuth client lacks the
|
|
345
|
-
f"{OAUTH_CONSOLE_URL}"
|
|
347
|
+
"the OAuth client lacks the DNS: Write scope; recreate it on the "
|
|
348
|
+
f"Trust credentials page ({OAUTH_CONSOLE_URL})"
|
|
346
349
|
)
|
|
347
350
|
return exc
|
|
348
351
|
|
|
@@ -435,13 +438,8 @@ class TailnetHub:
|
|
|
435
438
|
) # fmt: skip
|
|
436
439
|
|
|
437
440
|
|
|
438
|
-
def
|
|
439
|
-
"""The
|
|
440
|
-
|
|
441
|
-
The names and addresses come from the coordination server through the
|
|
442
|
-
local Tailscale client, not from DNS, so split DNS cannot redirect them.
|
|
443
|
-
None means there is no tailscale CLI to ask.
|
|
444
|
-
"""
|
|
441
|
+
def _network_map_peers() -> dict | None:
|
|
442
|
+
"""The peers in this client's network map; None without a tailscale CLI."""
|
|
445
443
|
try:
|
|
446
444
|
result = subprocess.run(
|
|
447
445
|
["tailscale", "status", "--json"],
|
|
@@ -454,9 +452,32 @@ def tailnet_hubs(tag: str = "tag:localghost") -> list[TailnetHub] | None:
|
|
|
454
452
|
if result.returncode != 0:
|
|
455
453
|
return None
|
|
456
454
|
try:
|
|
457
|
-
|
|
455
|
+
return json.loads(result.stdout).get("Peer") or {}
|
|
458
456
|
except (json.JSONDecodeError, AttributeError):
|
|
459
457
|
return None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def tag_in_use(tag: str) -> bool:
|
|
461
|
+
"""Whether a device in this client's network map, online or not, has tag.
|
|
462
|
+
|
|
463
|
+
A device can only carry a tag its owner allowed, so this proves the
|
|
464
|
+
policy file once allowed it. False proves nothing: the policy may hide
|
|
465
|
+
tagged devices from this client.
|
|
466
|
+
"""
|
|
467
|
+
peers = _network_map_peers() or {}
|
|
468
|
+
return any(tag in (peer.get("Tags") or ()) for peer in peers.values())
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def tailnet_hubs(tag: str = "tag:localghost") -> list[TailnetHub] | None:
|
|
472
|
+
"""The online localghost gateways in this client's network map.
|
|
473
|
+
|
|
474
|
+
The names and addresses come from the coordination server through the
|
|
475
|
+
local Tailscale client, not from DNS, so split DNS cannot redirect them.
|
|
476
|
+
None means there is no tailscale CLI to ask.
|
|
477
|
+
"""
|
|
478
|
+
peers = _network_map_peers()
|
|
479
|
+
if peers is None:
|
|
480
|
+
return None
|
|
460
481
|
hubs = [
|
|
461
482
|
TailnetHub(
|
|
462
483
|
str(peer["DNSName"]).lower().removesuffix("."),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|