nullforge 0.1.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.
- nullforge/__init__.py +1 -0
- nullforge/foundry/README.md +0 -0
- nullforge/foundry/__init__.py +7 -0
- nullforge/foundry/full_cast.py +22 -0
- nullforge/inventories/README.md +0 -0
- nullforge/inventories/example.py +87 -0
- nullforge/models/__init__.py +1 -0
- nullforge/models/containers.py +60 -0
- nullforge/models/dns.py +222 -0
- nullforge/models/monitoring/__init__.py +29 -0
- nullforge/models/monitoring/base.py +19 -0
- nullforge/models/monitoring/nezha.py +13 -0
- nullforge/models/netsec.py +43 -0
- nullforge/models/profiles.py +33 -0
- nullforge/models/system.py +15 -0
- nullforge/models/users.py +9 -0
- nullforge/models/warp.py +65 -0
- nullforge/models/zerotrust.py +9 -0
- nullforge/molds/__init__.py +38 -0
- nullforge/molds/base_mold.py +59 -0
- nullforge/molds/containers.py +26 -0
- nullforge/molds/defaults.py +10 -0
- nullforge/molds/dns.py +70 -0
- nullforge/molds/features.py +75 -0
- nullforge/molds/haproxy.py +23 -0
- nullforge/molds/monitoring/__init__.py +45 -0
- nullforge/molds/monitoring/nezha.py +118 -0
- nullforge/molds/netsec.py +237 -0
- nullforge/molds/profiles.py +30 -0
- nullforge/molds/system.py +140 -0
- nullforge/molds/telemt.py +140 -0
- nullforge/molds/tor.py +28 -0
- nullforge/molds/user.py +77 -0
- nullforge/molds/utils.py +115 -0
- nullforge/molds/warp.py +54 -0
- nullforge/molds/xray.py +16 -0
- nullforge/molds/zerotrust.py +59 -0
- nullforge/runes/__init__.py +46 -0
- nullforge/runes/base.py +327 -0
- nullforge/runes/containers.py +212 -0
- nullforge/runes/dns.py +256 -0
- nullforge/runes/haproxy.py +135 -0
- nullforge/runes/monitoring.py +21 -0
- nullforge/runes/netsec.py +547 -0
- nullforge/runes/prepare.py +30 -0
- nullforge/runes/profiles.py +583 -0
- nullforge/runes/telemt.py +252 -0
- nullforge/runes/tor.py +57 -0
- nullforge/runes/users.py +159 -0
- nullforge/runes/warp.py +273 -0
- nullforge/runes/xray.py +81 -0
- nullforge/runes/zerotrust.py +91 -0
- nullforge/smithy/__init__.py +1 -0
- nullforge/smithy/admin.py +40 -0
- nullforge/smithy/arch.py +36 -0
- nullforge/smithy/blocky.py +37 -0
- nullforge/smithy/cloudflare.py +32 -0
- nullforge/smithy/github.py +256 -0
- nullforge/smithy/http.py +85 -0
- nullforge/smithy/install.py +149 -0
- nullforge/smithy/monitoring/__init__.py +1 -0
- nullforge/smithy/monitoring/nezha/__init__.py +0 -0
- nullforge/smithy/monitoring/nezha/agent.py +54 -0
- nullforge/smithy/monitoring/nezha/dashboard.py +137 -0
- nullforge/smithy/monitoring/nezha/deploy.py +81 -0
- nullforge/smithy/network.py +95 -0
- nullforge/smithy/packages.py +176 -0
- nullforge/smithy/service.py +37 -0
- nullforge/smithy/sni.py +358 -0
- nullforge/smithy/swap.py +132 -0
- nullforge/smithy/system.py +86 -0
- nullforge/smithy/versions.py +282 -0
- nullforge/templates/__init__.py +32 -0
- nullforge/templates/cloudflared/tunnel.yml.j2 +6 -0
- nullforge/templates/dns/blocky.yaml.j2 +11 -0
- nullforge/templates/dns/dns.yaml.j2 +6 -0
- nullforge/templates/dns/resolv.conf.j2 +1 -0
- nullforge/templates/dns/resolved.conf.j2 +13 -0
- nullforge/templates/etc/default/zramswap.j2 +13 -0
- nullforge/templates/nvim/nvim_patch.lua.j2 +5 -0
- nullforge/templates/profiles/direnv.toml +2 -0
- nullforge/templates/profiles/starship.toml +186 -0
- nullforge/templates/profiles/tmux.conf +32 -0
- nullforge/templates/profiles/zshrc.j2 +46 -0
- nullforge/templates/scripts/telemt-synfix.sh +89 -0
- nullforge/templates/scripts/teleproxy-warp.sh +127 -0
- nullforge/templates/scripts/warp-v6-policy.sh +118 -0
- nullforge/templates/scripts/zt-tunnel-warp.sh +28 -0
- nullforge/templates/systemd/blocky.service.j2 +19 -0
- nullforge/templates/systemd/cloudflare-tunnel.service.j2 +27 -0
- nullforge/templates/systemd/cloudflare-warp.service.j2 +21 -0
- nullforge/templates/systemd/dns-internal.service.j2 +15 -0
- nullforge/templates/systemd/telemt.service.j2 +36 -0
- nullforge/templates/telemt/telemt.toml.j2 +49 -0
- nullforge/templates/tor/torrc.j2 +5 -0
- nullforge-0.1.0.dist-info/METADATA +39 -0
- nullforge-0.1.0.dist-info/RECORD +99 -0
- nullforge-0.1.0.dist-info/WHEEL +4 -0
- nullforge-0.1.0.dist-info/licenses/LICENSE +7 -0
nullforge/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0" # x-release-please-version
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pyinfra import local
|
|
2
|
+
from pyinfra.context import host
|
|
3
|
+
|
|
4
|
+
from nullforge.molds.features import iter_runes
|
|
5
|
+
from nullforge.molds.utils import ensure_features, ensure_system
|
|
6
|
+
from nullforge.runes import rune_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cast_full() -> None:
|
|
10
|
+
host.data.features = ensure_features(getattr(host.data, "features", None))
|
|
11
|
+
host.data.system = ensure_system(getattr(host.data, "system", None))
|
|
12
|
+
|
|
13
|
+
local.include(str(rune_path("prepare")))
|
|
14
|
+
|
|
15
|
+
local.include(str(rune_path("base")))
|
|
16
|
+
|
|
17
|
+
for name, active in host.loop(iter_runes(host.data.features)):
|
|
18
|
+
if active:
|
|
19
|
+
local.include(str(rune_path(name)))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
cast_full()
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from nullforge.models.dns import DnsMode
|
|
2
|
+
from nullforge.models.users import Shell
|
|
3
|
+
from nullforge.molds import DnsMold, MonitoringMold, TelemtMold, UserMold, WarpMold
|
|
4
|
+
from nullforge.molds.defaults import BASE_FEATURES, BASE_SYSTEM
|
|
5
|
+
from nullforge.molds.monitoring import NezhaBackend
|
|
6
|
+
from nullforge.molds.utils import merge_features, merge_system
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
users = UserMold(
|
|
10
|
+
manage=True,
|
|
11
|
+
name="example",
|
|
12
|
+
shell=Shell.ZSH,
|
|
13
|
+
)
|
|
14
|
+
"""User configuration preset
|
|
15
|
+
with user management enabled and the user "example".
|
|
16
|
+
with shell set to ZSH (default behavior).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
warp = WarpMold(
|
|
20
|
+
install=True,
|
|
21
|
+
iface="warp-example",
|
|
22
|
+
)
|
|
23
|
+
"""WARP configuration preset
|
|
24
|
+
setup Cloudflare WARP
|
|
25
|
+
with default MASQUE engine and interface "warp-example".
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
dns = DnsMold(
|
|
29
|
+
mode=DnsMode.BLOCKY,
|
|
30
|
+
)
|
|
31
|
+
"""DNS configuration preset
|
|
32
|
+
with DNS over HTTPS Blocky mode.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
monitoring = MonitoringMold(
|
|
36
|
+
install=True,
|
|
37
|
+
backend=NezhaBackend(
|
|
38
|
+
server="agent.status.example.com:443",
|
|
39
|
+
client_secret="REPLACE_WITH_NZ_CLIENT_SECRET", # noqa: S106 - example placeholder
|
|
40
|
+
dashboard_url="https://dash.example.com",
|
|
41
|
+
api_token="REPLACE_WITH_DASHBOARD_PAT", # noqa: S106 - example placeholder
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
"""Monitoring configuration preset
|
|
45
|
+
installing Nezha agent and renaming dashboard entry to host hostname.
|
|
46
|
+
The dashboard_url/api_token are used on control node for rename API call.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
telemt = TelemtMold(
|
|
50
|
+
install=True,
|
|
51
|
+
tls_domain="example.com",
|
|
52
|
+
users={
|
|
53
|
+
"example": "bf777cca8384a074a671460d51e4e31f",
|
|
54
|
+
},
|
|
55
|
+
route_via_warp=True,
|
|
56
|
+
synfix=True,
|
|
57
|
+
)
|
|
58
|
+
"""Telemt MTProto proxy preset with Fake-TLS masking.
|
|
59
|
+
Telegram-bound egress is routed through WARP (enabled above) via per-uid policy routing,
|
|
60
|
+
and the MEKO SYN rate-limiting fix is applied.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
overrides = (
|
|
64
|
+
users,
|
|
65
|
+
warp,
|
|
66
|
+
dns,
|
|
67
|
+
monitoring,
|
|
68
|
+
telemt,
|
|
69
|
+
)
|
|
70
|
+
"""Wrappers for the features to be merged with the base features."""
|
|
71
|
+
|
|
72
|
+
hosts = [
|
|
73
|
+
(
|
|
74
|
+
"203.0.113.10",
|
|
75
|
+
{
|
|
76
|
+
"system": merge_system(BASE_SYSTEM, {"hostname": "example-node1.local"}),
|
|
77
|
+
"features": merge_features(BASE_FEATURES, *overrides),
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
(
|
|
81
|
+
"203.0.113.20",
|
|
82
|
+
{
|
|
83
|
+
"system": merge_system(BASE_SYSTEM, {"hostname": "example-node2.local"}),
|
|
84
|
+
"features": merge_features(BASE_FEATURES, *overrides),
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal domain types for NullForge - pure value types."""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Containers configuration models."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Annotated, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContainersBackendType(StrEnum):
|
|
10
|
+
DOCKER = "docker"
|
|
11
|
+
PODMAN = "podman"
|
|
12
|
+
CRIO = "crio"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContainersRuntimeType(StrEnum):
|
|
16
|
+
DEFAULT = "default"
|
|
17
|
+
CRUN = "crun"
|
|
18
|
+
GVISOR = "gvisor"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _ContainersBackendBase(BaseModel):
|
|
22
|
+
"""Base for a containers backend."""
|
|
23
|
+
|
|
24
|
+
type: ContainersBackendType = Field(description="The type of containers backend")
|
|
25
|
+
runtime: ContainersRuntimeType = Field(description="The type of containers runtime")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DockerContainersBackend(_ContainersBackendBase):
|
|
29
|
+
type: Literal[ContainersBackendType.DOCKER] = ContainersBackendType.DOCKER
|
|
30
|
+
runtime: Literal[ContainersRuntimeType.GVISOR] = ContainersRuntimeType.GVISOR
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PodmanContainersBackend(_ContainersBackendBase):
|
|
34
|
+
type: Literal[ContainersBackendType.PODMAN] = ContainersBackendType.PODMAN
|
|
35
|
+
runtime: Literal[ContainersRuntimeType.CRUN] = ContainersRuntimeType.CRUN
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CrioContainersBackend(_ContainersBackendBase):
|
|
39
|
+
type: Literal[ContainersBackendType.CRIO] = ContainersBackendType.CRIO
|
|
40
|
+
runtime: Literal[ContainersRuntimeType.DEFAULT] = ContainersRuntimeType.DEFAULT
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
ContainersBackend = Annotated[
|
|
44
|
+
DockerContainersBackend | PodmanContainersBackend | CrioContainersBackend,
|
|
45
|
+
Field(discriminator="type"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def containers_backend_factory(type: ContainersBackendType) -> ContainersBackend:
|
|
50
|
+
"""Factory function for containers backends."""
|
|
51
|
+
|
|
52
|
+
match type:
|
|
53
|
+
case ContainersBackendType.DOCKER:
|
|
54
|
+
return DockerContainersBackend()
|
|
55
|
+
case ContainersBackendType.PODMAN:
|
|
56
|
+
return PodmanContainersBackend()
|
|
57
|
+
case ContainersBackendType.CRIO:
|
|
58
|
+
return CrioContainersBackend()
|
|
59
|
+
case _:
|
|
60
|
+
raise ValueError(f"Unknown ContainersBackendType: {type}")
|
nullforge/models/dns.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""DNS configuration models."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Annotated, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, IPvAnyAddress, field_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DnsProtocol(StrEnum):
|
|
10
|
+
DOU = "dou"
|
|
11
|
+
DOT = "dot"
|
|
12
|
+
DOH = "doh"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DnsMode(StrEnum):
|
|
16
|
+
DOU = "dou"
|
|
17
|
+
DOT_RESOLVED = "dot_resolved"
|
|
18
|
+
BLOCKY = "blocky"
|
|
19
|
+
NONE = "none"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DnsProvider(StrEnum):
|
|
23
|
+
CLOUDFLARE = "cloudflare"
|
|
24
|
+
GOOGLE = "google"
|
|
25
|
+
QUAD9 = "quad9"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _DnsServerBase(BaseModel):
|
|
29
|
+
"""Base for a DNS server."""
|
|
30
|
+
|
|
31
|
+
protocol: DnsProtocol
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DnsServerDoU(_DnsServerBase):
|
|
35
|
+
protocol: Literal[DnsProtocol.DOU] = DnsProtocol.DOU
|
|
36
|
+
host: IPvAnyAddress | str = Field(description="Resolver hostname or IP")
|
|
37
|
+
port: int = Field(default=53, ge=1, le=65535, description="UDP port")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DnsServerDoH(_DnsServerBase):
|
|
41
|
+
protocol: Literal[DnsProtocol.DOH] = DnsProtocol.DOH
|
|
42
|
+
url: str = Field(description="HTTPS endpoint for DNS over HTTPS (RFC 8484).")
|
|
43
|
+
|
|
44
|
+
@field_validator("url")
|
|
45
|
+
@classmethod
|
|
46
|
+
def _require_https(cls, v: str) -> str:
|
|
47
|
+
"""Enforce HTTPS."""
|
|
48
|
+
|
|
49
|
+
if not v.startswith("https://"):
|
|
50
|
+
raise ValueError("DoH endpoint must use HTTPS")
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DnsServerDoT(_DnsServerBase):
|
|
55
|
+
protocol: Literal[DnsProtocol.DOT] = DnsProtocol.DOT
|
|
56
|
+
host: IPvAnyAddress | str = Field(description="Resolver hostname or IP")
|
|
57
|
+
port: int = Field(default=853, ge=1, le=65535, description="TLS port")
|
|
58
|
+
sni: str | None = Field(default=None, description="Optional SNI/hostname for TLS verification")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
DnsServer = Annotated[
|
|
62
|
+
DnsServerDoH | DnsServerDoT | DnsServerDoU,
|
|
63
|
+
Field(discriminator="protocol"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DnsProviders:
|
|
68
|
+
"""DNS providers."""
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _cloudflare_doh(ipv6: bool) -> list[DnsServer]:
|
|
72
|
+
ups: list[DnsServer] = [
|
|
73
|
+
DnsServerDoH(url="https://1.1.1.1/dns-query"),
|
|
74
|
+
DnsServerDoH(url="https://1.0.0.1/dns-query"),
|
|
75
|
+
]
|
|
76
|
+
if ipv6:
|
|
77
|
+
ups.extend(
|
|
78
|
+
[
|
|
79
|
+
DnsServerDoH(url="https://[2606:4700:4700::1111]/dns-query"),
|
|
80
|
+
DnsServerDoH(url="https://[2606:4700:4700::1001]/dns-query"),
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
return ups
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _cloudflare_dot(ipv6: bool) -> list[DnsServer]:
|
|
87
|
+
ups: list[DnsServer] = [
|
|
88
|
+
DnsServerDoT(host="1.1.1.1", sni="cloudflare-dns.com"),
|
|
89
|
+
DnsServerDoT(host="1.0.0.1", sni="cloudflare-dns.com"),
|
|
90
|
+
]
|
|
91
|
+
if ipv6:
|
|
92
|
+
ups.extend(
|
|
93
|
+
[
|
|
94
|
+
DnsServerDoT(host="2606:4700:4700::1111", sni="cloudflare-dns.com"),
|
|
95
|
+
DnsServerDoT(host="2606:4700:4700::1001", sni="cloudflare-dns.com"),
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
return ups
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _google_doh(ipv6: bool) -> list[DnsServer]:
|
|
102
|
+
ups: list[DnsServer] = [
|
|
103
|
+
DnsServerDoH(url="https://8.8.8.8/dns-query"),
|
|
104
|
+
DnsServerDoH(url="https://8.8.4.4/dns-query"),
|
|
105
|
+
]
|
|
106
|
+
if ipv6:
|
|
107
|
+
ups.extend(
|
|
108
|
+
[
|
|
109
|
+
DnsServerDoH(url="https://[2001:4860:4860::8888]/dns-query"),
|
|
110
|
+
DnsServerDoH(url="https://[2001:4860:4860::8844]/dns-query"),
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
return ups
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _google_dot(ipv6: bool) -> list[DnsServer]:
|
|
117
|
+
ups: list[DnsServer] = [
|
|
118
|
+
DnsServerDoT(host="8.8.8.8", sni="dns.google"),
|
|
119
|
+
DnsServerDoT(host="8.8.4.4", sni="dns.google"),
|
|
120
|
+
]
|
|
121
|
+
if ipv6:
|
|
122
|
+
ups.extend(
|
|
123
|
+
[
|
|
124
|
+
DnsServerDoT(host="2001:4860:4860::8888", sni="dns.google"),
|
|
125
|
+
DnsServerDoT(host="2001:4860:4860::8844", sni="dns.google"),
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
return ups
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _quad9_doh(ipv6: bool, ecs: bool) -> list[DnsServer]:
|
|
132
|
+
if ecs:
|
|
133
|
+
primary_ipv4 = "https://9.9.9.12/dns-query"
|
|
134
|
+
secondary_ipv4 = "https://149.112.112.12/dns-query"
|
|
135
|
+
primary_ipv6 = "https://[2620:fe::12]/dns-query"
|
|
136
|
+
secondary_ipv6 = "https://[2620:fe::fe:12]/dns-query"
|
|
137
|
+
else:
|
|
138
|
+
primary_ipv4 = "https://9.9.9.10/dns-query"
|
|
139
|
+
secondary_ipv4 = "https://149.112.112.10/dns-query"
|
|
140
|
+
primary_ipv6 = "https://[2620:fe::10]/dns-query"
|
|
141
|
+
secondary_ipv6 = "https://[2620:fe::fe:10]/dns-query"
|
|
142
|
+
|
|
143
|
+
ups: list[DnsServer] = [
|
|
144
|
+
DnsServerDoH(url=primary_ipv4),
|
|
145
|
+
DnsServerDoH(url=secondary_ipv4),
|
|
146
|
+
]
|
|
147
|
+
if ipv6:
|
|
148
|
+
ups.extend(
|
|
149
|
+
[
|
|
150
|
+
DnsServerDoH(url=primary_ipv6),
|
|
151
|
+
DnsServerDoH(url=secondary_ipv6),
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
return ups
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _quad9_dot(ipv6: bool, ecs: bool) -> list[DnsServer]:
|
|
158
|
+
if ecs:
|
|
159
|
+
sni = "dns11.quad9.net"
|
|
160
|
+
primary_ipv4 = "9.9.9.12"
|
|
161
|
+
secondary_ipv4 = "149.112.112.12"
|
|
162
|
+
primary_ipv6 = "2620:fe::12"
|
|
163
|
+
secondary_ipv6 = "2620:fe::fe:12"
|
|
164
|
+
else:
|
|
165
|
+
sni = "dns10.quad9.net"
|
|
166
|
+
primary_ipv4 = "9.9.9.10"
|
|
167
|
+
secondary_ipv4 = "149.112.112.10"
|
|
168
|
+
primary_ipv6 = "2620:fe::10"
|
|
169
|
+
secondary_ipv6 = "2620:fe::fe:10"
|
|
170
|
+
|
|
171
|
+
ups: list[DnsServer] = [
|
|
172
|
+
DnsServerDoT(host=primary_ipv4, sni=sni),
|
|
173
|
+
DnsServerDoT(host=secondary_ipv4, sni=sni),
|
|
174
|
+
]
|
|
175
|
+
if ipv6:
|
|
176
|
+
ups.extend(
|
|
177
|
+
[
|
|
178
|
+
DnsServerDoT(host=primary_ipv6, sni=sni),
|
|
179
|
+
DnsServerDoT(host=secondary_ipv6, sni=sni),
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
return ups
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def get_upstreams(
|
|
186
|
+
provider: DnsProvider,
|
|
187
|
+
protocol: DnsProtocol,
|
|
188
|
+
ipv6: bool,
|
|
189
|
+
ecs: bool = False,
|
|
190
|
+
) -> list[DnsServer]:
|
|
191
|
+
"""Get upstream DNS servers for a provider and protocol."""
|
|
192
|
+
|
|
193
|
+
if ecs and provider != DnsProvider.QUAD9:
|
|
194
|
+
raise ValueError(f"ECS is supported only with the Quad9 provider, not {provider}")
|
|
195
|
+
|
|
196
|
+
match provider:
|
|
197
|
+
case DnsProvider.CLOUDFLARE:
|
|
198
|
+
if protocol == DnsProtocol.DOH:
|
|
199
|
+
return DnsProviders._cloudflare_doh(ipv6)
|
|
200
|
+
elif protocol == DnsProtocol.DOT:
|
|
201
|
+
return DnsProviders._cloudflare_dot(ipv6)
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError(f"Unsupported protocol {protocol} for Cloudflare")
|
|
204
|
+
case DnsProvider.GOOGLE:
|
|
205
|
+
if protocol == DnsProtocol.DOH:
|
|
206
|
+
return DnsProviders._google_doh(ipv6)
|
|
207
|
+
elif protocol == DnsProtocol.DOT:
|
|
208
|
+
return DnsProviders._google_dot(ipv6)
|
|
209
|
+
else:
|
|
210
|
+
raise ValueError(f"Unsupported protocol {protocol} for Google")
|
|
211
|
+
case DnsProvider.QUAD9:
|
|
212
|
+
if protocol == DnsProtocol.DOH:
|
|
213
|
+
return DnsProviders._quad9_doh(ipv6, ecs)
|
|
214
|
+
elif protocol == DnsProtocol.DOT:
|
|
215
|
+
return DnsProviders._quad9_dot(ipv6, ecs)
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(f"Unsupported protocol {protocol} for Quad9")
|
|
218
|
+
case _:
|
|
219
|
+
raise ValueError(f"Unknown provider: {provider}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
dns_providers = DnsProviders()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Monitoring configuration models."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from .base import MonitoringBackendType, _MonitoringLayoutBase
|
|
8
|
+
from .nezha import NezhaLayout
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MonitoringLayout = Annotated[NezhaLayout, Field(discriminator="type")]
|
|
12
|
+
"""Union-of-one for now. When second monitoring backend is added, extend union."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def monitoring_layout_factory(type: MonitoringBackendType) -> MonitoringLayout:
|
|
16
|
+
match type:
|
|
17
|
+
case MonitoringBackendType.NEZHA:
|
|
18
|
+
return NezhaLayout()
|
|
19
|
+
case _:
|
|
20
|
+
raise ValueError(f"Unknown MonitoringBackendType: {type}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"MonitoringBackendType",
|
|
25
|
+
"MonitoringLayout",
|
|
26
|
+
"NezhaLayout",
|
|
27
|
+
"_MonitoringLayoutBase",
|
|
28
|
+
"monitoring_layout_factory",
|
|
29
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared monitoring backend value types."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MonitoringBackendType(StrEnum):
|
|
9
|
+
NEZHA = "nezha"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _MonitoringLayoutBase(BaseModel):
|
|
13
|
+
"""Static on-disk/service layout for monitoring agent backend."""
|
|
14
|
+
|
|
15
|
+
type: MonitoringBackendType = Field(description="The monitoring backend type")
|
|
16
|
+
base_dir: str | None = Field(..., description="The base/installation directory")
|
|
17
|
+
binary_path: str | None = Field(..., description="The path to the binary")
|
|
18
|
+
config_path: str | None = Field(..., description="The path to the configuration file")
|
|
19
|
+
systemd_service_name: str | None = Field(..., description="The systemd service name")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Nezha monitoring backend layout."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from .base import MonitoringBackendType, _MonitoringLayoutBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NezhaLayout(_MonitoringLayoutBase):
|
|
9
|
+
type: Literal[MonitoringBackendType.NEZHA] = MonitoringBackendType.NEZHA
|
|
10
|
+
base_dir: Literal["/opt/nezha/agent"] = "/opt/nezha/agent"
|
|
11
|
+
binary_path: Literal["/opt/nezha/agent/nezha-agent"] = "/opt/nezha/agent/nezha-agent"
|
|
12
|
+
config_path: Literal["/opt/nezha/agent/config.yml"] = "/opt/nezha/agent/config.yml"
|
|
13
|
+
systemd_service_name: Literal["nezha-agent"] = "nezha-agent"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Network security domain types."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SshHostKeyType(StrEnum):
|
|
7
|
+
"""Host key type sshd offers."""
|
|
8
|
+
|
|
9
|
+
ED25519 = "ed25519"
|
|
10
|
+
ECDSA = "ecdsa"
|
|
11
|
+
RSA = "rsa"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
PQ_KEX_ALGORITHMS = (
|
|
15
|
+
"mlkem768x25519-sha256",
|
|
16
|
+
"sntrup761x25519-sha512@openssh.com",
|
|
17
|
+
)
|
|
18
|
+
"""Post-quantum hybrid key exchange algorithms, most preferred first."""
|
|
19
|
+
|
|
20
|
+
CLASSICAL_KEX_ALGORITHMS = (
|
|
21
|
+
"curve25519-sha256",
|
|
22
|
+
"curve25519-sha256@libssh.org",
|
|
23
|
+
"diffie-hellman-group-exchange-sha256",
|
|
24
|
+
"diffie-hellman-group16-sha512",
|
|
25
|
+
"diffie-hellman-group18-sha512",
|
|
26
|
+
)
|
|
27
|
+
"""Strong classical key exchange in default-preference order, offered after the PQ hybrids."""
|
|
28
|
+
|
|
29
|
+
WEAK_KEX_PATTERNS = "ecdh-sha2-*,diffie-hellman-group14-*,diffie-hellman-group-exchange-sha1"
|
|
30
|
+
"""Key exchanges removed from default set."""
|
|
31
|
+
|
|
32
|
+
WEAK_MAC_PATTERNS = "hmac-sha1*,umac-64*,hmac-sha2-256,hmac-sha2-512,umac-128@openssh.com"
|
|
33
|
+
"""MACs removed from default set."""
|
|
34
|
+
|
|
35
|
+
MIN_DH_MODULUS_SIZE = 3071
|
|
36
|
+
"""Minimum size kept in /etc/ssh/moduli; the file's size column is bits-1, so 3071 keeps >=3072-bit groups."""
|
|
37
|
+
|
|
38
|
+
HOST_KEY_ALGORITHMS: dict[SshHostKeyType, tuple[str, ...]] = {
|
|
39
|
+
SshHostKeyType.ED25519: ("ssh-ed25519",),
|
|
40
|
+
SshHostKeyType.ECDSA: ("ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521"),
|
|
41
|
+
SshHostKeyType.RSA: ("rsa-sha2-512", "rsa-sha2-256"),
|
|
42
|
+
}
|
|
43
|
+
"""Signature algorithms sshd may use per host key type."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Profiles domain models."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NerdFont(StrEnum):
|
|
7
|
+
"""Nerd Font family, valued as its base asset name in a `ryanoasis/nerd-fonts` release.
|
|
8
|
+
|
|
9
|
+
A curated subset of the ~70 published families; add a member to offer another one.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
CASCADIA_CODE = "CascadiaCode"
|
|
13
|
+
COMMIT_MONO = "CommitMono"
|
|
14
|
+
DEJAVU_SANS_MONO = "DejaVuSansMono"
|
|
15
|
+
FIRA_CODE = "FiraCode"
|
|
16
|
+
FIRA_MONO = "FiraMono"
|
|
17
|
+
GEIST_MONO = "GeistMono"
|
|
18
|
+
HACK = "Hack"
|
|
19
|
+
IBM_PLEX_MONO = "IBMPlexMono"
|
|
20
|
+
INCONSOLATA = "Inconsolata"
|
|
21
|
+
INTEL_ONE_MONO = "IntelOneMono"
|
|
22
|
+
IOSEVKA = "Iosevka"
|
|
23
|
+
JETBRAINS_MONO = "JetBrainsMono"
|
|
24
|
+
MESLO = "Meslo"
|
|
25
|
+
MONASPACE = "Monaspace"
|
|
26
|
+
MONONOKI = "Mononoki"
|
|
27
|
+
ROBOTO_MONO = "RobotoMono"
|
|
28
|
+
SOURCE_CODE_PRO = "SourceCodePro"
|
|
29
|
+
SPACE_MONO = "SpaceMono"
|
|
30
|
+
SYMBOLS_ONLY = "NerdFontsSymbolsOnly"
|
|
31
|
+
UBUNTU_MONO = "UbuntuMono"
|
|
32
|
+
VICTOR_MONO = "VictorMono"
|
|
33
|
+
ZED_MONO = "ZedMono"
|
nullforge/models/warp.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""WARP configuration models."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Annotated, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WarpEngineType(StrEnum):
|
|
10
|
+
WIREGUARD = "wireguard"
|
|
11
|
+
MASQUE = "masque"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _WarpEngineBase(BaseModel):
|
|
15
|
+
"""Base for a WARP engine."""
|
|
16
|
+
|
|
17
|
+
type: WarpEngineType = Field(description="The type of WARP engine")
|
|
18
|
+
binary_path: str = Field(description="The path to the WARP binary")
|
|
19
|
+
config_dir: str = Field(description="The path to the WARP configuration directory")
|
|
20
|
+
systemd_service_name: str = Field(description="The name of the systemd service")
|
|
21
|
+
policy_script: str = Field(default="", description="The path to the WARP policy script")
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def config_path(self) -> str:
|
|
25
|
+
return f"{self.config_dir}/config.json"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def account_path(self) -> str:
|
|
29
|
+
return f"{self.config_dir}/wgcf-account.toml"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def profile_path(self) -> str:
|
|
33
|
+
return f"{self.config_dir}/warp.conf"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MasqueWarpEngine(_WarpEngineBase):
|
|
37
|
+
type: Literal[WarpEngineType.MASQUE] = WarpEngineType.MASQUE
|
|
38
|
+
binary_path: Literal["/usr/local/bin/usque"] = "/usr/local/bin/usque"
|
|
39
|
+
config_dir: Literal["/etc/usque"] = "/etc/usque"
|
|
40
|
+
systemd_service_name: Literal["cloudflare-warp"] = "cloudflare-warp"
|
|
41
|
+
policy_script: Literal["/etc/usque/warp-v6-policy.sh"] = "/etc/usque/warp-v6-policy.sh"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class WireguardWarpEngine(_WarpEngineBase):
|
|
45
|
+
type: Literal[WarpEngineType.WIREGUARD] = WarpEngineType.WIREGUARD
|
|
46
|
+
binary_path: Literal["/usr/local/bin/wgcf"] = "/usr/local/bin/wgcf"
|
|
47
|
+
config_dir: Literal["/etc/wgcf"] = "/etc/wgcf"
|
|
48
|
+
systemd_service_name: Literal["wg-quick@warp"] = "wg-quick@warp"
|
|
49
|
+
policy_script: Literal[""] = ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
WarpEngine = Annotated[
|
|
53
|
+
MasqueWarpEngine | WireguardWarpEngine,
|
|
54
|
+
Field(discriminator="type"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def warp_engine_factory(type: WarpEngineType) -> WarpEngine:
|
|
59
|
+
match type:
|
|
60
|
+
case WarpEngineType.MASQUE:
|
|
61
|
+
return MasqueWarpEngine()
|
|
62
|
+
case WarpEngineType.WIREGUARD:
|
|
63
|
+
return WireguardWarpEngine()
|
|
64
|
+
case _:
|
|
65
|
+
raise ValueError(f"Unknown WarpEngineType: {type}")
|