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,422 @@
1
+ """Prepare private VLESS Reality profiles for the pinned Xray implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import ipaddress
8
+ import json
9
+ import re
10
+ import secrets
11
+ import uuid
12
+ from dataclasses import asdict, dataclass, field
13
+ from importlib.resources import files
14
+ from urllib.parse import quote, urlencode
15
+
16
+ from flayer.core.contracts import ContractError, ValidateName
17
+ from flayer.profiles._amnezia_export import BuildAmneziaArtifacts
18
+ from flayer.profiles.artifacts import Artifact
19
+ from flayer.profiles.vpn import (
20
+ BuildVpnDeviceBundle,
21
+ BuildVpnServerBundle,
22
+ PreparedVpnTransport,
23
+ ValidateVpnTransport,
24
+ VpnCapabilities,
25
+ VpnEndpoint,
26
+ VpnProfile,
27
+ )
28
+
29
+ VLESS_TRANSPORT = "vless-reality"
30
+ VLESS_FLOW = "xtls-rprx-vision"
31
+ VLESS_CAPABILITIES = VpnCapabilities(VLESS_TRANSPORT, "application-proxy", ("full", "split"))
32
+ XRAY_VERSION = "26.9.30"
33
+ _HOST = re.compile(r"[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?\Z")
34
+
35
+
36
+ class VlessError(ContractError):
37
+ """VLESS intent or credentials cannot safely produce a supported configuration."""
38
+
39
+
40
+ def _Host(value: object) -> str:
41
+ """Require an explicit canonical DNS hostname without transport or shell syntax."""
42
+
43
+ if not isinstance(value, str) or _HOST.fullmatch(value) is None or "." not in value:
44
+ raise VlessError("Reality target and server name require a canonical DNS hostname")
45
+
46
+ if any(not part or len(part) > 63 or part.startswith("-") or part.endswith("-")
47
+ for part in value.split(".")):
48
+ raise VlessError("Reality hostname contains an invalid DNS label")
49
+
50
+ try:
51
+ ipaddress.ip_address(value)
52
+
53
+ except ValueError:
54
+ return value
55
+
56
+ raise VlessError("Reality target and server name require DNS names, not IP addresses")
57
+
58
+
59
+ def _Port(value: object) -> int:
60
+ """Reject booleans and require a real TCP port."""
61
+
62
+ if type(value) is not int or not 1 <= value <= 65535:
63
+ raise VlessError("VLESS ports must be integers within 1..65535")
64
+
65
+ return value
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class VlessRealitySettings:
70
+ """Public Reality handshake target and local application-proxy listener settings."""
71
+
72
+ target_host: str
73
+ server_name: str
74
+ target_port: int = 443
75
+ fingerprint: str = "chrome"
76
+ socks_port: int = 10808
77
+
78
+ def __post_init__(self) -> None:
79
+ """Require explicit TLS identities and a supported stable browser fingerprint."""
80
+
81
+ _Host(self.target_host)
82
+ _Host(self.server_name)
83
+ _Port(self.target_port)
84
+ _Port(self.socks_port)
85
+
86
+ if not isinstance(self.fingerprint, str) or self.fingerprint not in {
87
+ "chrome", "firefox", "safari", "ios", "android", "edge",
88
+ }:
89
+ raise VlessError("Reality fingerprint is not supported by this profile")
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class VlessDeviceCredential:
94
+ """One revocable per-device VLESS UUID, excluded from object representations."""
95
+
96
+ device_id: str
97
+ user_id: str = field(repr=False)
98
+
99
+ def __post_init__(self) -> None:
100
+ """Accept only canonical nonzero randomly generated UUID credentials."""
101
+
102
+ ValidateName(self.device_id, "device_id")
103
+
104
+ try:
105
+ value = uuid.UUID(self.user_id)
106
+
107
+ except (ValueError, TypeError, AttributeError):
108
+ raise VlessError("VLESS device credential must be a canonical UUID version 4") from None
109
+
110
+ if str(value) != self.user_id or value.version != 4:
111
+ raise VlessError("VLESS device credential must be a canonical UUID version 4")
112
+
113
+
114
+ @dataclass(frozen=True, slots=True)
115
+ class VlessMaterial:
116
+ """Explicit secret material retained in private bundles, never generic lifecycle state."""
117
+
118
+ server_private_key: bytes = field(repr=False)
119
+ server_public_key: bytes = field(repr=False)
120
+ short_id: str = field(repr=False)
121
+ devices: tuple[VlessDeviceCredential, ...] = field(repr=False)
122
+
123
+ def __post_init__(self) -> None:
124
+ """Validate material structure without including values in error messages."""
125
+
126
+ if any(not isinstance(value, bytes) or len(value) != 32 or value == bytes(32)
127
+ for value in (self.server_private_key, self.server_public_key)):
128
+ raise VlessError("Reality keys must contain 32 nonzero raw bytes")
129
+
130
+ if not isinstance(self.short_id, str) or re.fullmatch(r"[0-9a-f]{16}", self.short_id) is None:
131
+ raise VlessError("Reality short ID must contain 16 lowercase hexadecimal characters")
132
+
133
+ if not isinstance(self.devices, tuple) or not 1 <= len(self.devices) <= 16 or any(
134
+ not isinstance(item, VlessDeviceCredential) for item in self.devices
135
+ ):
136
+ raise VlessError("VLESS requires 1..16 immutable per-device credentials")
137
+
138
+ if len({item.device_id for item in self.devices}) != len(self.devices) or len({
139
+ item.user_id for item in self.devices
140
+ }) != len(self.devices):
141
+ raise VlessError("VLESS device identities and UUID credentials must be unique")
142
+
143
+
144
+ def _PublicKey(private_key: bytes) -> bytes:
145
+ """Derive a raw X25519 public key through the optional audited crypto dependency."""
146
+
147
+ try:
148
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
149
+
150
+ except ImportError:
151
+ raise VlessError("Install the f-layer[vpn] extra before preparing VPN credentials") from None
152
+
153
+ return X25519PrivateKey.from_private_bytes(private_key).public_key().public_bytes_raw()
154
+
155
+
156
+ def GenerateVlessMaterial(profile: VpnProfile) -> VlessMaterial:
157
+ """Generate fresh server and device credentials only for an explicitly validated profile."""
158
+
159
+ ValidateVpnTransport(profile, VLESS_CAPABILITIES)
160
+ private_key = secrets.token_bytes(32)
161
+
162
+ return VlessMaterial(
163
+ private_key, _PublicKey(private_key), secrets.token_hex(8),
164
+ tuple(VlessDeviceCredential(device.device_id, str(uuid.uuid4())) for device in profile.devices),
165
+ )
166
+
167
+
168
+
169
+ def EncodeVlessMaterial(material: VlessMaterial) -> bytes:
170
+ """Serialize credentials only for a caller-owned sensitive artifact, never general state."""
171
+
172
+ if not isinstance(material, VlessMaterial):
173
+ raise VlessError("Expected validated VLESS material")
174
+
175
+ return _Json({
176
+ "schema_version": 1,
177
+ "server_private_key": _Encode(material.server_private_key),
178
+ "server_public_key": _Encode(material.server_public_key),
179
+ "short_id": material.short_id,
180
+ "devices": [asdict(item) for item in material.devices],
181
+ })
182
+
183
+
184
+ def DecodeVlessMaterial(content: bytes) -> VlessMaterial:
185
+ """Read an explicitly supplied bounded private snapshot without consulting local defaults."""
186
+
187
+ try:
188
+ if not isinstance(content, bytes) or not 1 <= len(content) <= 16384:
189
+ raise ValueError("Invalid input size")
190
+
191
+ data = json.loads(content)
192
+ expected = {"schema_version", "server_private_key", "server_public_key", "short_id", "devices"}
193
+
194
+ if not isinstance(data, dict) or set(data) != expected or type(data["schema_version"]) is not int:
195
+ raise ValueError("Invalid schema")
196
+
197
+ if data["schema_version"] != 1 or not isinstance(data["devices"], list):
198
+ raise ValueError("Invalid schema version")
199
+
200
+ keys = []
201
+
202
+ for name in ("server_private_key", "server_public_key"):
203
+ value = data[name]
204
+
205
+ if not isinstance(value, str) or re.fullmatch(r"[a-zA-Z0-9_-]{43}", value) is None:
206
+ raise ValueError("Invalid key encoding")
207
+
208
+ raw = base64.urlsafe_b64decode(value + "=")
209
+
210
+ if _Encode(raw) != value:
211
+ raise ValueError("Noncanonical key encoding")
212
+
213
+ keys.append(raw)
214
+
215
+ devices = []
216
+
217
+ for device in data["devices"]:
218
+ if not isinstance(device, dict) or set(device) != {"device_id", "user_id"}:
219
+ raise ValueError("Invalid device credential")
220
+
221
+ devices.append(VlessDeviceCredential(device["device_id"], device["user_id"]))
222
+
223
+ material = VlessMaterial(keys[0], keys[1], data["short_id"], tuple(devices))
224
+
225
+ if not secrets.compare_digest(_PublicKey(material.server_private_key), material.server_public_key):
226
+ raise ValueError("Inconsistent keys")
227
+
228
+ return material
229
+
230
+ except (ValueError, TypeError, KeyError, RecursionError):
231
+ raise VlessError("Private VLESS material does not satisfy its schema or key contract") from None
232
+
233
+
234
+ def _Validate(profile: VpnProfile, settings: VlessRealitySettings, material: VlessMaterial) -> int:
235
+ """Bind each credential to one declared device and reject inconsistent key pairs."""
236
+
237
+ listener = ValidateVpnTransport(profile, VLESS_CAPABILITIES)
238
+
239
+ if listener.protocol != "tcp" or not isinstance(settings, VlessRealitySettings):
240
+ raise VlessError("VLESS Reality requires one TCP listener and explicit Reality settings")
241
+
242
+ if not isinstance(material, VlessMaterial):
243
+ raise VlessError("VLESS requires separately supplied private material")
244
+
245
+ if {item.device_id for item in material.devices} != {
246
+ device.device_id for device in profile.devices
247
+ }:
248
+ raise VlessError("VLESS credentials must match the declared devices exactly")
249
+
250
+ if not secrets.compare_digest(_PublicKey(material.server_private_key), material.server_public_key):
251
+ raise VlessError("Reality server private and public keys do not form a pair")
252
+
253
+ return listener.port
254
+
255
+
256
+ def _Encode(value: bytes) -> str:
257
+ """Encode an X25519 key using Xray's unpadded URL-safe representation."""
258
+
259
+ return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
260
+
261
+
262
+ def _Json(value: object) -> bytes:
263
+ """Render deterministic UTF-8 JSON with a trailing newline for private artifacts."""
264
+
265
+ return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
266
+
267
+
268
+ def RenderVlessServer(
269
+ profile: VpnProfile, settings: VlessRealitySettings, material: VlessMaterial,
270
+ ) -> bytes:
271
+ """Build a Reality-protected IPv4 proxy without device traffic or credential logging."""
272
+
273
+ port = _Validate(profile, settings, material)
274
+ blocked = [
275
+ "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16",
276
+ "172.16.0.0/12", "192.168.0.0/16", "224.0.0.0/4", "240.0.0.0/4", "::/0",
277
+ ]
278
+
279
+ return _Json({
280
+ "log": {"loglevel": "none", "access": "none", "error": "none"},
281
+ "dns": {"servers": list(profile.routes.dns_servers), "queryStrategy": "UseIPv4"},
282
+ "inbounds": [{
283
+ "tag": "vless-reality", "listen": "0.0.0.0", "port": port, "protocol": "vless",
284
+ "settings": {
285
+ "clients": [{"id": item.user_id, "flow": VLESS_FLOW} for item in material.devices],
286
+ "decryption": "none",
287
+ },
288
+ "streamSettings": {
289
+ "network": "tcp", "security": "reality",
290
+ "realitySettings": {
291
+ "show": False, "target": f"{settings.target_host}:{settings.target_port}",
292
+ "xver": 0, "serverNames": [settings.server_name],
293
+ "privateKey": _Encode(material.server_private_key), "shortIds": [material.short_id],
294
+ },
295
+ },
296
+ }],
297
+ "outbounds": [
298
+ {"tag": "internet", "protocol": "freedom", "settings": {"domainStrategy": "UseIPv4"}},
299
+ {"tag": "blocked", "protocol": "blackhole"},
300
+ ],
301
+ "routing": {
302
+ "domainStrategy": "IPOnDemand",
303
+ "rules": [{"type": "field", "ip": blocked, "outboundTag": "blocked"}],
304
+ },
305
+ })
306
+
307
+
308
+ def _Credential(material: VlessMaterial, device_id: str) -> VlessDeviceCredential:
309
+ """Select one device without exposing other clients' authentication credentials."""
310
+
311
+ for item in material.devices:
312
+ if item.device_id == device_id:
313
+ return item
314
+
315
+ raise VlessError("Requested device has no VLESS credential")
316
+
317
+
318
+ def RenderVlessClient(
319
+ profile: VpnProfile, endpoint: VpnEndpoint, settings: VlessRealitySettings,
320
+ material: VlessMaterial, device_id: str,
321
+ ) -> bytes:
322
+ """Render a loopback SOCKS client; split destinations outside the allowlist are rejected."""
323
+
324
+ port = _Validate(profile, settings, material)
325
+
326
+ if not isinstance(endpoint, VpnEndpoint) or endpoint.port != port:
327
+ raise VlessError("VLESS endpoint must match the declared TCP listener port")
328
+
329
+ credential = _Credential(material, device_id)
330
+ routing_rules: list[dict[str, object]] = [
331
+ {"type": "field", "ip": ["::/0"], "outboundTag": "blocked"},
332
+ {"type": "field", "ip": list(profile.routes.dns_servers), "port": "53",
333
+ "outboundTag": "vpn"},
334
+ {"type": "field", "ip": list(profile.routes.ipv4_cidrs), "outboundTag": "vpn"},
335
+ {"type": "field", "network": "tcp,udp", "outboundTag": "blocked"},
336
+ ]
337
+
338
+ return _Json({
339
+ "log": {"loglevel": "none", "access": "none", "error": "none"},
340
+ "dns": {"servers": list(profile.routes.dns_servers), "queryStrategy": "UseIPv4"},
341
+ "inbounds": [{
342
+ "tag": "local-socks", "listen": "127.0.0.1", "port": settings.socks_port,
343
+ "protocol": "socks", "settings": {"auth": "noauth", "udp": True, "ip": "127.0.0.1"},
344
+ }],
345
+ "outbounds": [{
346
+ "tag": "vpn", "protocol": "vless",
347
+ "settings": {"vnext": [{
348
+ "address": endpoint.host, "port": endpoint.port,
349
+ "users": [{"id": credential.user_id, "encryption": "none", "flow": VLESS_FLOW}],
350
+ }]},
351
+ "streamSettings": {
352
+ "network": "tcp", "security": "reality",
353
+ "realitySettings": {
354
+ "fingerprint": settings.fingerprint, "serverName": settings.server_name,
355
+ "publicKey": _Encode(material.server_public_key), "shortId": material.short_id,
356
+ "spiderX": "/",
357
+ },
358
+ },
359
+ }, {"tag": "blocked", "protocol": "blackhole"}],
360
+ "routing": {"domainStrategy": "IPOnDemand", "rules": routing_rules},
361
+ })
362
+
363
+
364
+ def BuildVlessUri(
365
+ profile: VpnProfile, endpoint: VpnEndpoint, settings: VlessRealitySettings,
366
+ material: VlessMaterial, device_id: str,
367
+ ) -> str:
368
+ """Produce a transport import URI; client route and DNS policy need separate configuration."""
369
+
370
+ RenderVlessClient(profile, endpoint, settings, material, device_id)
371
+ credential = _Credential(material, device_id)
372
+ parameters = urlencode({
373
+ "encryption": "none", "flow": VLESS_FLOW, "security": "reality",
374
+ "sni": settings.server_name, "fp": settings.fingerprint,
375
+ "pbk": _Encode(material.server_public_key), "sid": material.short_id,
376
+ "type": "tcp", "headerType": "none", "spx": "/",
377
+ })
378
+ label = quote(f"{profile.identity.stack}-{device_id}", safe="")
379
+
380
+ return f"vless://{credential.user_id}@{endpoint.host}:{endpoint.port}?{parameters}#{label}"
381
+
382
+
383
+
384
+ def VlessServiceName(profile: VpnProfile) -> str:
385
+ """Return the deterministic systemd unit for this exact stack ownership identity."""
386
+
387
+ ValidateVpnTransport(profile, VLESS_CAPABILITIES)
388
+ owner = hashlib.sha256(_Json(asdict(profile.identity))).hexdigest()
389
+
390
+ return f"flayer-vless-{owner[:24]}.service"
391
+
392
+
393
+ def PrepareVless(
394
+ profile: VpnProfile, endpoint: VpnEndpoint, settings: VlessRealitySettings,
395
+ material: VlessMaterial,
396
+ ) -> PreparedVpnTransport:
397
+ """Prepare an owned installer and one private import bundle per declared device."""
398
+
399
+ server = RenderVlessServer(profile, settings, material)
400
+ identity = asdict(profile.identity)
401
+ owner = hashlib.sha256(_Json(identity)).hexdigest()
402
+ deployment = _Json({
403
+ "schema_version": 1, "owner": owner, "config_sha256": hashlib.sha256(server).hexdigest(),
404
+ "port": endpoint.port, "xray_version": XRAY_VERSION,
405
+ })
406
+ installer = files("flayer.profiles").joinpath("_vless_guest.py").read_bytes().replace(b"\r\n", b"\n")
407
+ server_bundle = BuildVpnServerBundle(profile, VLESS_TRANSPORT, (
408
+ Artifact("server.json", server), Artifact("install.py", installer),
409
+ Artifact("deployment.json", deployment),
410
+ ))
411
+ device_bundles = tuple(BuildVpnDeviceBundle(profile, VLESS_TRANSPORT, device.device_id, (
412
+ Artifact("client.json", RenderVlessClient(profile, endpoint, settings, material, device.device_id)),
413
+ Artifact("import.txt", (BuildVlessUri(
414
+ profile, endpoint, settings, material, device.device_id,
415
+ ) + "\n").encode("utf-8")),
416
+ *BuildAmneziaArtifacts(
417
+ RenderVlessClient(profile, endpoint, settings, material, device.device_id),
418
+ endpoint.host, device.device_id, profile.routes.dns_servers,
419
+ ),
420
+ )) for device in profile.devices)
421
+
422
+ return PreparedVpnTransport(profile, VLESS_CAPABILITIES, server_bundle, device_bundles)