f-layer 2.0.0__py3-none-any.whl

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.
Files changed (45) hide show
  1. f_layer-2.0.0.dist-info/METADATA +232 -0
  2. f_layer-2.0.0.dist-info/RECORD +45 -0
  3. f_layer-2.0.0.dist-info/WHEEL +4 -0
  4. f_layer-2.0.0.dist-info/entry_points.txt +2 -0
  5. f_layer-2.0.0.dist-info/licenses/LICENSE +201 -0
  6. flayer/__init__.py +5 -0
  7. flayer/__main__.py +247 -0
  8. flayer/_vpn_guest.py +130 -0
  9. flayer/core/__init__.py +1 -0
  10. flayer/core/config.py +198 -0
  11. flayer/core/contracts.py +114 -0
  12. flayer/core/lifecycle.py +767 -0
  13. flayer/core/state.py +351 -0
  14. flayer/diagnostics/__init__.py +20 -0
  15. flayer/diagnostics/benchmark.py +272 -0
  16. flayer/diagnostics/contracts.py +167 -0
  17. flayer/diagnostics/health.py +154 -0
  18. flayer/diagnostics/reporting.py +24 -0
  19. flayer/extensions/__init__.py +25 -0
  20. flayer/extensions/builtins.py +165 -0
  21. flayer/extensions/contracts.py +196 -0
  22. flayer/extensions/discovery.py +152 -0
  23. flayer/extensions/registry.py +347 -0
  24. flayer/guest.py +270 -0
  25. flayer/integrations/__init__.py +23 -0
  26. flayer/integrations/adapters.py +92 -0
  27. flayer/integrations/contracts.py +241 -0
  28. flayer/integrations/wire.py +273 -0
  29. flayer/profiles/__init__.py +33 -0
  30. flayer/profiles/_amnezia_export.py +176 -0
  31. flayer/profiles/_vless_guest.py +432 -0
  32. flayer/profiles/amneziawg.py +555 -0
  33. flayer/profiles/artifacts.py +609 -0
  34. flayer/profiles/gateway.py +729 -0
  35. flayer/profiles/static_site.py +596 -0
  36. flayer/profiles/vless.py +422 -0
  37. flayer/profiles/vpn.py +524 -0
  38. flayer/providers/__init__.py +1 -0
  39. flayer/providers/contracts.py +207 -0
  40. flayer/providers/lifecycle.py +177 -0
  41. flayer/providers/yandex.py +387 -0
  42. flayer/providers/yandex_lifecycle.py +781 -0
  43. flayer/py.typed +0 -0
  44. flayer/vpn_export.py +72 -0
  45. flayer/vpn_project.py +828 -0
@@ -0,0 +1,432 @@
1
+ """Install, check, or remove one private F-Layer Xray deployment on Ubuntu 24.04."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import fcntl
7
+ import hashlib
8
+ import io
9
+ import json
10
+ import os
11
+ import platform
12
+ import re
13
+ import socket
14
+ import ssl
15
+ import stat
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import time
20
+ import urllib.request
21
+ import zipfile
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ XRAY_VERSION = "26.9.30"
26
+ ARCHIVES = {
27
+ "x86_64": ("Xray-linux-64.zip", "f851110beaff16e78d643f0ccfd9524b4a44dfd59bae3e34bb52bba378f7690e"),
28
+ "aarch64": ("Xray-linux-arm64-v8a.zip", "9886f077f9fd8e6713b84c377c1c7db4e53b9bfa8c276a5bd12561139522b473"),
29
+ }
30
+ _ROOT_UID = 0
31
+ BASE = Path("/opt/flayer/vless")
32
+ SYSTEMD = Path("/etc/systemd/system")
33
+ MAX_BINARY_BYTES = 96 * 1024 * 1024
34
+ MAX_ARCHIVE_BYTES = 32 * 1024 * 1024
35
+
36
+
37
+ class GuestError(ValueError):
38
+ """A bounded installation check failed without exposing private payloads."""
39
+
40
+
41
+ def _Digest(value: bytes) -> str:
42
+ """Hash exact managed bytes for drift detection and ownership receipts."""
43
+
44
+ return hashlib.sha256(value).hexdigest()
45
+
46
+
47
+ def _Read(path: Path, limit: int, *, root_owned: bool = False) -> bytes:
48
+ """Read a bounded regular non-linked file, keeping filesystem errors private."""
49
+
50
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
51
+
52
+ try:
53
+ info = os.fstat(descriptor)
54
+
55
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > limit:
56
+ raise GuestError("Managed input must be a bounded regular file without hardlinks")
57
+
58
+ if path.name == "server.json" and stat.S_IMODE(info.st_mode) != 0o600:
59
+ raise GuestError("Server credential files must use private mode 0600")
60
+
61
+ if root_owned and (info.st_uid != _ROOT_UID or info.st_mode & 0o022):
62
+ raise GuestError("Managed files must be root-owned and not writable by other users")
63
+
64
+ with os.fdopen(descriptor, "rb", closefd=False) as stream:
65
+ content = stream.read(limit + 1)
66
+
67
+ if len(content) > limit:
68
+ raise GuestError("Managed input exceeds its size bound")
69
+
70
+ return content
71
+
72
+ finally:
73
+ os.close(descriptor)
74
+
75
+
76
+ def _Directory(path: Path, *, create: bool = False) -> None:
77
+ """Reject symlink ancestry and require a root-owned non-writable managed directory."""
78
+
79
+ if any(item.is_symlink() for item in (path, *path.parents)):
80
+ raise GuestError("Managed directory paths must not contain symbolic links")
81
+
82
+ if create:
83
+ path.mkdir(mode=0o755, exist_ok=True)
84
+
85
+ info = path.stat()
86
+
87
+ if not stat.S_ISDIR(info.st_mode) or info.st_uid != _ROOT_UID or info.st_mode & 0o022:
88
+ raise GuestError("Managed directories must be root-owned and not writable by other users")
89
+
90
+
91
+ def _Atomic(path: Path, content: bytes, mode: int) -> None:
92
+ """Replace one bounded owned file atomically without following its destination."""
93
+
94
+ descriptor, temporary = tempfile.mkstemp(prefix=".flayer-", dir=path.parent)
95
+
96
+ try:
97
+ with os.fdopen(descriptor, "wb") as stream:
98
+ stream.write(content)
99
+ stream.flush()
100
+ os.fchmod(stream.fileno(), mode)
101
+ os.fsync(stream.fileno())
102
+
103
+ os.replace(temporary, path)
104
+
105
+ finally:
106
+ if os.path.exists(temporary):
107
+ os.unlink(temporary)
108
+
109
+
110
+ def _Command(arguments: list[str], *, timeout: int = 30) -> None:
111
+ """Run fixed commands with bounded time and suppress credential-bearing daemon errors."""
112
+
113
+ try:
114
+ result = subprocess.run(
115
+ arguments, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
116
+ stderr=subprocess.DEVNULL, timeout=timeout, check=False,
117
+ )
118
+
119
+ except (OSError, subprocess.TimeoutExpired):
120
+ raise GuestError("Required guest command failed or timed out") from None
121
+
122
+ if result.returncode != 0:
123
+ raise GuestError("Required guest command returned an unsuccessful status")
124
+
125
+
126
+ def _DownloadBinary() -> bytes:
127
+ """Download only a pinned official asset and extract one verified regular ELF entry."""
128
+
129
+ architecture = platform.machine()
130
+
131
+ if architecture not in ARCHIVES:
132
+ raise GuestError("Xray deployment supports only amd64 and arm64 guests")
133
+
134
+ filename, expected_hash = ARCHIVES[architecture]
135
+ url = f"https://github.com/XTLS/Xray-core/releases/download/v{XRAY_VERSION}/{filename}"
136
+
137
+ with urllib.request.urlopen(url, timeout=60) as response:
138
+ content = response.read(MAX_ARCHIVE_BYTES + 1)
139
+
140
+ if len(content) > MAX_ARCHIVE_BYTES or _Digest(content) != expected_hash:
141
+ raise GuestError("Xray archive does not match the pinned release checksum")
142
+
143
+ with zipfile.ZipFile(io.BytesIO(content)) as archive:
144
+ entries = archive.infolist()
145
+
146
+ if len(entries) > 16 or len({item.filename for item in entries}) != len(entries):
147
+ raise GuestError("Xray archive contains unexpected or duplicate entries")
148
+
149
+ for entry in entries:
150
+ if (
151
+ entry.filename not in {"xray", "geoip.dat", "geosite.dat", "LICENSE", "README.md"}
152
+ or entry.file_size > MAX_BINARY_BYTES
153
+ or stat.S_IFMT(entry.external_attr >> 16) not in {0, stat.S_IFREG}
154
+ ):
155
+ raise GuestError("Xray archive contains an unsafe path or entry")
156
+
157
+ binary = archive.read("xray")
158
+
159
+ if not 4 <= len(binary) <= MAX_BINARY_BYTES or not binary.startswith(b"\x7fELF"):
160
+ raise GuestError("Xray archive must contain a bounded Linux executable")
161
+
162
+ return binary
163
+
164
+
165
+ def _Request(directory: Path) -> tuple[dict[str, Any], bytes]:
166
+ """Bind private configuration bytes to a strict nonsecret deployment descriptor."""
167
+
168
+ deployment = json.loads(_Read(directory / "deployment.json", 4096))
169
+ expected = {"schema_version", "owner", "config_sha256", "port", "xray_version"}
170
+
171
+ if not isinstance(deployment, dict) or set(deployment) != expected:
172
+ raise GuestError("Deployment descriptor has an unsupported schema")
173
+
174
+ if (type(deployment["schema_version"]) is not int or deployment["schema_version"] != 1
175
+ or deployment["xray_version"] != XRAY_VERSION):
176
+ raise GuestError("Deployment descriptor has an unsupported version")
177
+
178
+ if any(not isinstance(deployment[key], str) or re.fullmatch(r"[0-9a-f]{64}", deployment[key])
179
+ is None for key in ("owner", "config_sha256")):
180
+ raise GuestError("Deployment descriptor requires exact ownership and content hashes")
181
+
182
+ if type(deployment["port"]) is not int or not 1 <= deployment["port"] <= 65535:
183
+ raise GuestError("Deployment descriptor requires one TCP port")
184
+
185
+ config = _Read(directory / "server.json", 1048576)
186
+
187
+ if _Digest(config) != deployment["config_sha256"]:
188
+ raise GuestError("Server configuration differs from the prepared private bundle")
189
+
190
+ return deployment, config
191
+
192
+
193
+ def _Unit(root: Path) -> bytes:
194
+ """Run the daemon under a transient unprivileged identity with a private config credential."""
195
+
196
+ return ("\n".join((
197
+ "[Unit]", "Description=F-Layer VLESS Reality gateway", "After=network-online.target",
198
+ "Wants=network-online.target", "", "[Service]", "Type=simple", "DynamicUser=yes",
199
+ f"LoadCredential=server.json:{root}/server.json",
200
+ f"ExecStart={root}/xray run -config %d/server.json", "Restart=on-failure", "RestartSec=3",
201
+ "NoNewPrivileges=yes", "ProtectSystem=strict", "ProtectHome=yes", "PrivateTmp=yes",
202
+ "PrivateDevices=yes", "ProtectKernelTunables=yes", "ProtectKernelModules=yes",
203
+ "ProtectControlGroups=yes", "RestrictSUIDSGID=yes", "LockPersonality=yes",
204
+ "RestrictAddressFamilies=AF_INET AF_UNIX", "CapabilityBoundingSet=CAP_NET_BIND_SERVICE",
205
+ "AmbientCapabilities=CAP_NET_BIND_SERVICE", "StandardOutput=null", "StandardError=null",
206
+ "", "[Install]", "WantedBy=multi-user.target", "",
207
+ ))).encode("utf-8")
208
+
209
+
210
+ def _Existing(root: Path, service: Path, owner: str) -> dict[str, Any] | None:
211
+ """Refuse foreign, drifted, or incomplete installations before any managed mutation."""
212
+
213
+ if not root.exists() and not root.is_symlink():
214
+ if service.exists() or service.is_symlink():
215
+ raise GuestError("An unowned systemd service already occupies this deployment name")
216
+
217
+ return None
218
+
219
+ _Directory(root)
220
+ receipt = json.loads(_Read(root / "owner.json", 4096, root_owned=True))
221
+
222
+ if not isinstance(receipt, dict) or set(receipt) != {"owner", "files", "xray_version"} or receipt["owner"] != owner:
223
+ raise GuestError("Existing deployment does not belong to the requested stack")
224
+
225
+ if receipt["xray_version"] != XRAY_VERSION:
226
+ raise GuestError("Existing deployment uses a different pinned Xray version")
227
+
228
+ hashes = receipt["files"]
229
+
230
+ if not isinstance(hashes, dict) or set(hashes) != {"xray", "server.json", "service"}:
231
+ raise GuestError("Existing deployment has an invalid ownership receipt")
232
+
233
+ if {item.name for item in root.iterdir()} != {"xray", "server.json", "owner.json"}:
234
+ raise GuestError("Existing deployment contains untracked or interrupted files")
235
+
236
+ for name, path in (("xray", root / "xray"), ("server.json", root / "server.json"), ("service", service)):
237
+ if _Digest(_Read(path, MAX_BINARY_BYTES, root_owned=True)) != hashes[name]:
238
+ raise GuestError("Existing deployment changed outside F-Layer; refusing replacement")
239
+
240
+ if stat.S_IMODE((root / "server.json").stat().st_mode) != 0o600:
241
+ raise GuestError("Managed server credentials must retain private permissions")
242
+
243
+ return receipt
244
+
245
+
246
+
247
+ def _TargetHealth(config: bytes) -> None:
248
+ """Require the configured Reality target to offer certificate-valid TLS 1.3 and HTTP/2."""
249
+
250
+ try:
251
+ settings = json.loads(config)["inbounds"][0]["streamSettings"]["realitySettings"]
252
+ host, port = settings["target"].rsplit(":", 1)
253
+ server_name = settings["serverNames"][0]
254
+ context = ssl.create_default_context()
255
+ context.minimum_version = ssl.TLSVersion.TLSv1_3
256
+ context.set_alpn_protocols(["h2"])
257
+
258
+ with socket.create_connection((host, int(port)), timeout=10) as connection:
259
+ with context.wrap_socket(connection, server_hostname=server_name) as channel:
260
+ if channel.version() != "TLSv1.3" or channel.selected_alpn_protocol() != "h2":
261
+ raise GuestError("Reality target must support TLS 1.3 and HTTP/2")
262
+
263
+ except (OSError, ValueError, KeyError, IndexError, TypeError):
264
+ raise GuestError(
265
+ "Reality target preflight failed: verify its TLS 1.3/HTTP2 support, certificate name, and guest reachability"
266
+ ) from None
267
+
268
+
269
+ def _Health(root: Path, service: Path, port: int) -> None:
270
+ """Check daemon syntax, systemd state, and a loopback listener without claiming tunnel health."""
271
+
272
+ _TargetHealth(_Read(root / "server.json", 1048576, root_owned=True))
273
+ _Command([str(root / "xray"), "run", "-test", "-config", str(root / "server.json")])
274
+ _Command(["systemctl", "is-active", "--quiet", service.name])
275
+
276
+ deadline = time.monotonic() + 5
277
+
278
+ while True:
279
+ try:
280
+ with socket.create_connection(("127.0.0.1", port), timeout=1):
281
+ return
282
+
283
+ except OSError:
284
+ if time.monotonic() >= deadline:
285
+ raise GuestError("VLESS process has no reachable local TCP listener") from None
286
+
287
+ time.sleep(0.1)
288
+
289
+
290
+ def _Install(root: Path, service: Path, request: dict[str, Any], config: bytes) -> None:
291
+ """Install or retry exact owned bytes; reject implicit credential replacement."""
292
+
293
+ previous = _Existing(root, service, request["owner"])
294
+ unit = _Unit(root)
295
+
296
+ if previous is not None:
297
+ if previous["files"]["server.json"] != _Digest(config):
298
+ raise GuestError("VLESS credential replacement requires an explicit expected-old authorization")
299
+
300
+ if previous["files"]["service"] != _Digest(unit):
301
+ raise GuestError("Owned service layout differs from this installer version")
302
+
303
+ _TargetHealth(config)
304
+ _Command(["systemctl", "enable", "--now", service.name])
305
+ _Health(root, service, request["port"])
306
+
307
+ return
308
+
309
+ _TargetHealth(config)
310
+ binary = _DownloadBinary()
311
+
312
+ with tempfile.TemporaryDirectory(prefix=".verify-", dir=BASE) as temporary:
313
+ staged = Path(temporary)
314
+ _Atomic(staged / "xray", binary, 0o700)
315
+ _Atomic(staged / "server.json", config, 0o600)
316
+ _Command([str(staged / "xray"), "run", "-test", "-config", str(staged / "server.json")])
317
+
318
+ _Directory(root, create=True)
319
+ receipt = {"owner": request["owner"], "xray_version": XRAY_VERSION, "files": {
320
+ "xray": _Digest(binary), "server.json": _Digest(config), "service": _Digest(unit),
321
+ }}
322
+
323
+ try:
324
+ _Atomic(root / "xray", binary, 0o755)
325
+ _Atomic(root / "server.json", config, 0o600)
326
+ _Atomic(service, unit, 0o644)
327
+ _Command(["systemctl", "daemon-reload"])
328
+ _Command(["systemctl", "enable", service.name])
329
+ _Command(["systemctl", "restart", service.name])
330
+ _Health(root, service, request["port"])
331
+ _Atomic(root / "owner.json", (json.dumps(receipt, sort_keys=True) + "\n").encode(), 0o600)
332
+
333
+ except (GuestError, OSError):
334
+ _Command(["systemctl", "disable", "--now", service.name])
335
+
336
+ for path in (service, root / "xray", root / "server.json", root / "owner.json"):
337
+ path.unlink(missing_ok=True)
338
+
339
+ root.rmdir()
340
+ _Command(["systemctl", "daemon-reload"])
341
+
342
+ raise GuestError("VLESS installation failed; this attempt's owned files were removed") from None
343
+
344
+
345
+ def _Remove(root: Path, service: Path, owner: str) -> None:
346
+ """Delete only an unchanged owned daemon and its exact three tracked local files."""
347
+
348
+ if _Existing(root, service, owner) is None:
349
+ return
350
+
351
+ _Command(["systemctl", "disable", "--now", service.name])
352
+
353
+ for path in (service, root / "xray", root / "server.json", root / "owner.json"):
354
+ path.unlink()
355
+
356
+ root.rmdir()
357
+ _Command(["systemctl", "daemon-reload"])
358
+
359
+
360
+ def Main(argv: list[str] | None = None) -> int:
361
+ """Accept only install/check/remove for the descriptor next to this private installer."""
362
+
363
+ parser = argparse.ArgumentParser(description="Manage an owned F-Layer VLESS Reality service")
364
+ action = parser.add_mutually_exclusive_group()
365
+ action.add_argument("--check", action="store_true")
366
+ action.add_argument("--remove", action="store_true")
367
+ arguments = parser.parse_args(argv)
368
+
369
+ try:
370
+ if os.geteuid() != 0:
371
+ raise GuestError("VLESS guest installation requires sudo on the selected guest")
372
+
373
+ release = platform.freedesktop_os_release()
374
+
375
+ if release.get("ID") != "ubuntu" or release.get("VERSION_ID") != "24.04":
376
+ raise GuestError("VLESS guest installation supports only Ubuntu 24.04")
377
+
378
+ request, config = _Request(Path(__file__).absolute().parent)
379
+
380
+ for directory in (BASE.parent.parent, BASE.parent, BASE):
381
+ _Directory(directory, create=True)
382
+
383
+ _Directory(SYSTEMD)
384
+ root = BASE / request["owner"][:24]
385
+ service = SYSTEMD / f"flayer-vless-{request['owner'][:24]}.service"
386
+
387
+ descriptor = os.open(
388
+ BASE / f".{request['owner'][:24]}.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600,
389
+ )
390
+
391
+ try:
392
+ info = os.fstat(descriptor)
393
+
394
+ if not stat.S_ISREG(info.st_mode) or info.st_uid != _ROOT_UID or info.st_nlink != 1:
395
+ raise GuestError("VLESS deployment lock must be an owned regular file")
396
+
397
+ fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
398
+
399
+ if arguments.remove:
400
+ _Remove(root, service, request["owner"])
401
+
402
+ elif arguments.check:
403
+ receipt = _Existing(root, service, request["owner"])
404
+
405
+ if receipt is None or receipt["files"]["server.json"] != request["config_sha256"]:
406
+ raise GuestError("Requested VLESS configuration is not installed")
407
+
408
+ _Health(root, service, request["port"])
409
+
410
+ else:
411
+ _Install(root, service, request, config)
412
+
413
+ finally:
414
+ os.close(descriptor)
415
+
416
+ except GuestError as error:
417
+ print(f"VLESS guest operation failed: {error}", file=sys.stderr)
418
+
419
+ return 1
420
+
421
+ except (OSError, ValueError, KeyError, RecursionError, zipfile.BadZipFile):
422
+ print("VLESS guest operation failed; check ownership, pinned download, guest prerequisites, and service state.", file=sys.stderr)
423
+
424
+ return 1
425
+
426
+ print("VLESS guest operation completed; end-to-end client connectivity was not tested.")
427
+
428
+ return 0
429
+
430
+
431
+ if __name__ == "__main__":
432
+ raise SystemExit(Main())