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/runes/warp.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Cloudflare WARP deployment module."""
|
|
2
|
+
|
|
3
|
+
from pyinfra.api.operation import OperationMeta
|
|
4
|
+
from pyinfra.context import host
|
|
5
|
+
from pyinfra.facts.files import File
|
|
6
|
+
from pyinfra.operations import files, server, systemd
|
|
7
|
+
from pyinfra.operations.util import any_changed
|
|
8
|
+
|
|
9
|
+
from nullforge.models.warp import WarpEngineType
|
|
10
|
+
from nullforge.molds import FeaturesMold, WarpMold
|
|
11
|
+
from nullforge.smithy.cloudflare import CLOUDFLARE_GROUP, CLOUDFLARE_USER
|
|
12
|
+
from nullforge.smithy.install import install_release_binary
|
|
13
|
+
from nullforge.smithy.network import has_ipv6
|
|
14
|
+
from nullforge.smithy.packages import get_pm
|
|
15
|
+
from nullforge.smithy.service import ensure_service_user
|
|
16
|
+
from nullforge.smithy.versions import get_versions, is_pinned_version_installed, record_installed_version
|
|
17
|
+
from nullforge.templates import get_template_path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def deploy_warp() -> None:
|
|
21
|
+
"""Deploy Cloudflare WARP configuration."""
|
|
22
|
+
|
|
23
|
+
features: FeaturesMold = host.data.features
|
|
24
|
+
warp_opts: WarpMold = features.warp
|
|
25
|
+
|
|
26
|
+
ensure_service_user(CLOUDFLARE_USER, CLOUDFLARE_GROUP, "/etc/cloudflare")
|
|
27
|
+
|
|
28
|
+
files.directory(
|
|
29
|
+
name="Ensure WARP engine configuration directory exists",
|
|
30
|
+
path=warp_opts.engine.config_dir,
|
|
31
|
+
user=CLOUDFLARE_USER,
|
|
32
|
+
group=CLOUDFLARE_GROUP,
|
|
33
|
+
mode="0755",
|
|
34
|
+
_sudo=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
match warp_opts.engine_type:
|
|
38
|
+
case WarpEngineType.WIREGUARD:
|
|
39
|
+
_deploy_wireguard_warp(warp_opts)
|
|
40
|
+
case WarpEngineType.MASQUE:
|
|
41
|
+
_deploy_masque_warp(warp_opts)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _install_wgcf(opts: WarpMold) -> None:
|
|
45
|
+
"""Install wgcf binary."""
|
|
46
|
+
|
|
47
|
+
if is_pinned_version_installed("wgcf", opts.engine.binary_path):
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
install_release_binary(
|
|
51
|
+
name="Install wgcf binary",
|
|
52
|
+
url=get_versions().wgcf_url(),
|
|
53
|
+
dest=opts.engine.binary_path,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
record_installed_version("wgcf")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _deploy_wireguard_warp(opts: WarpMold) -> None:
|
|
60
|
+
"""Deploy WARP using WireGuard."""
|
|
61
|
+
|
|
62
|
+
pm = get_pm()
|
|
63
|
+
pm.install(
|
|
64
|
+
name="Install WireGuard packages",
|
|
65
|
+
packages=["wireguard", "wireguard-tools"],
|
|
66
|
+
_sudo=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
_install_wgcf(opts)
|
|
70
|
+
|
|
71
|
+
wgcf_account_path = opts.engine.account_path
|
|
72
|
+
wgcf_profile_path = opts.engine.profile_path
|
|
73
|
+
wgcf_bin = opts.engine.binary_path
|
|
74
|
+
|
|
75
|
+
register_op = None
|
|
76
|
+
if not host.get_fact(File, wgcf_account_path):
|
|
77
|
+
register_op = server.shell(
|
|
78
|
+
name="Register wgcf account",
|
|
79
|
+
commands=f"{wgcf_bin} register --accept-tos --config {wgcf_account_path}",
|
|
80
|
+
_retries=3,
|
|
81
|
+
_retry_delay=10,
|
|
82
|
+
_ignore_errors=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
register_condition = register_op.did_succeed if register_op else (lambda: True)
|
|
86
|
+
|
|
87
|
+
generate_op = None
|
|
88
|
+
if not host.get_fact(File, wgcf_profile_path):
|
|
89
|
+
generate_op = server.shell(
|
|
90
|
+
name="Generate WireGuard profile",
|
|
91
|
+
commands=f"{wgcf_bin} generate --config {wgcf_account_path} --profile {wgcf_profile_path}",
|
|
92
|
+
_retries=3,
|
|
93
|
+
_retry_delay=10,
|
|
94
|
+
_ignore_errors=True,
|
|
95
|
+
_if=register_condition,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
server.shell(
|
|
99
|
+
name="Post-process WireGuard profile",
|
|
100
|
+
commands=f"sed -i '/^DNS = /d' {wgcf_profile_path} "
|
|
101
|
+
rf"&& sed -i '/^\[Interface\]/a Table = off' {wgcf_profile_path}",
|
|
102
|
+
_if=generate_op.did_change,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _wgcf_ok() -> bool:
|
|
106
|
+
"""True iff every declared wgcf step really succeeded (or none were declared)."""
|
|
107
|
+
|
|
108
|
+
if register_op is not None and not register_op.did_succeed():
|
|
109
|
+
return False
|
|
110
|
+
if generate_op is not None and not generate_op.did_succeed():
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
profile_link = files.link(
|
|
115
|
+
name="Link WireGuard profile to /etc/wireguard/warp.conf",
|
|
116
|
+
path="/etc/wireguard/warp.conf",
|
|
117
|
+
target=wgcf_profile_path,
|
|
118
|
+
_sudo=True,
|
|
119
|
+
_if=_wgcf_ok,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
restart_ops = [op for op in (register_op, generate_op, profile_link) if op]
|
|
123
|
+
systemd.service(
|
|
124
|
+
name="Restart WireGuard WARP on change",
|
|
125
|
+
service=opts.engine.systemd_service_name,
|
|
126
|
+
restarted=True,
|
|
127
|
+
_sudo=True,
|
|
128
|
+
_if=[_wgcf_ok, any_changed(*restart_ops)],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
systemd.service(
|
|
132
|
+
name="Ensure WireGuard WARP is running and enabled",
|
|
133
|
+
service=opts.engine.systemd_service_name,
|
|
134
|
+
running=True,
|
|
135
|
+
enabled=True,
|
|
136
|
+
_sudo=True,
|
|
137
|
+
_if=_wgcf_ok,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _install_usque(opts: WarpMold) -> OperationMeta | None:
|
|
142
|
+
"""Install usque binary."""
|
|
143
|
+
|
|
144
|
+
if is_pinned_version_installed("usque", opts.engine.binary_path):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
install_op = install_release_binary(
|
|
148
|
+
name="Extract and install usque binary",
|
|
149
|
+
url=get_versions().usque_zip(),
|
|
150
|
+
dest=opts.engine.binary_path,
|
|
151
|
+
binary_name="usque",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
files.file(
|
|
155
|
+
name="Set usque binary as executable",
|
|
156
|
+
path=opts.engine.binary_path,
|
|
157
|
+
mode="0755",
|
|
158
|
+
user="root",
|
|
159
|
+
group=CLOUDFLARE_GROUP,
|
|
160
|
+
_sudo=True,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return install_op
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _deploy_masque_warp(opts: WarpMold) -> None:
|
|
167
|
+
"""Deploy WARP using Masque."""
|
|
168
|
+
|
|
169
|
+
binary_op = _install_usque(opts)
|
|
170
|
+
|
|
171
|
+
usque_config_path = opts.engine.config_path
|
|
172
|
+
usque_bin = opts.engine.binary_path
|
|
173
|
+
if not host.get_fact(File, usque_config_path):
|
|
174
|
+
server.shell(
|
|
175
|
+
name="Enroll device in Warp",
|
|
176
|
+
commands=f"{usque_bin} enroll -c {usque_config_path}",
|
|
177
|
+
_sudo=True,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
register_op = server.shell(
|
|
181
|
+
name="Register device in Warp",
|
|
182
|
+
commands=f"{usque_bin} register -c {usque_config_path} --accept-tos",
|
|
183
|
+
_sudo=True,
|
|
184
|
+
_retries=3,
|
|
185
|
+
_retry_delay=10,
|
|
186
|
+
_ignore_errors=True,
|
|
187
|
+
)
|
|
188
|
+
config_condition = register_op.did_succeed
|
|
189
|
+
else:
|
|
190
|
+
config_condition = lambda: True # noqa: E731 # config already exists, ops run unconditionally
|
|
191
|
+
|
|
192
|
+
files.file(
|
|
193
|
+
name="Ensure WARP config file ownership to cloudflare",
|
|
194
|
+
path=usque_config_path,
|
|
195
|
+
user=CLOUDFLARE_USER,
|
|
196
|
+
group=CLOUDFLARE_GROUP,
|
|
197
|
+
mode="0640",
|
|
198
|
+
_sudo=True,
|
|
199
|
+
_if=config_condition,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
rt_tables_dir = "/etc/iproute2"
|
|
203
|
+
rt_tables_path = f"{rt_tables_dir}/rt_tables"
|
|
204
|
+
files.directory(
|
|
205
|
+
name="Create /etc/iproute2 directory",
|
|
206
|
+
path=rt_tables_dir,
|
|
207
|
+
user="root",
|
|
208
|
+
group="root",
|
|
209
|
+
mode="0755",
|
|
210
|
+
_sudo=True,
|
|
211
|
+
_if=config_condition,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
files.file(
|
|
215
|
+
name=f"Set {rt_tables_path} group to cloudflare",
|
|
216
|
+
path=rt_tables_path,
|
|
217
|
+
group=CLOUDFLARE_GROUP,
|
|
218
|
+
mode="0664",
|
|
219
|
+
_sudo=True,
|
|
220
|
+
_if=config_condition,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
policy_script = files.put(
|
|
224
|
+
name="Deploy WARP v6 policy script",
|
|
225
|
+
src=get_template_path("scripts/warp-v6-policy.sh"),
|
|
226
|
+
dest=opts.engine.policy_script,
|
|
227
|
+
user=CLOUDFLARE_USER,
|
|
228
|
+
group=CLOUDFLARE_GROUP,
|
|
229
|
+
mode="0755",
|
|
230
|
+
_sudo=True,
|
|
231
|
+
_if=config_condition,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
service_template = files.template(
|
|
235
|
+
name="Deploy Masque WARP service configuration",
|
|
236
|
+
src=get_template_path("systemd/cloudflare-warp.service.j2"),
|
|
237
|
+
dest=f"/etc/systemd/system/{opts.engine.systemd_service_name}.service",
|
|
238
|
+
mode="0644",
|
|
239
|
+
WORKDIR=opts.engine.config_dir,
|
|
240
|
+
CONFIG_PATH=opts.engine.config_path,
|
|
241
|
+
MTU=opts.mtu,
|
|
242
|
+
INET_NAME=opts.iface,
|
|
243
|
+
USE_OUTBOUND_IPV6=has_ipv6(exclude_iface=opts.iface),
|
|
244
|
+
_sudo=True,
|
|
245
|
+
_if=config_condition,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
systemd.daemon_reload(
|
|
249
|
+
name="Reload systemd daemon for Masque WARP",
|
|
250
|
+
_sudo=True,
|
|
251
|
+
_if=any_changed(policy_script, service_template),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
restart_ops = [op for op in (binary_op, policy_script, service_template) if op]
|
|
255
|
+
systemd.service(
|
|
256
|
+
name="Restart Masque WARP on change",
|
|
257
|
+
service=opts.engine.systemd_service_name,
|
|
258
|
+
restarted=True,
|
|
259
|
+
_sudo=True,
|
|
260
|
+
_if=any_changed(*restart_ops),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
systemd.service(
|
|
264
|
+
name="Ensure Masque WARP is running and enabled",
|
|
265
|
+
service=opts.engine.systemd_service_name,
|
|
266
|
+
running=True,
|
|
267
|
+
enabled=True,
|
|
268
|
+
_sudo=True,
|
|
269
|
+
_if=config_condition,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
deploy_warp()
|
nullforge/runes/xray.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Xray proxy deployment module."""
|
|
2
|
+
|
|
3
|
+
from pyinfra.context import host
|
|
4
|
+
from pyinfra.facts.files import File
|
|
5
|
+
from pyinfra.operations import files, server, systemd
|
|
6
|
+
|
|
7
|
+
from nullforge.molds import FeaturesMold, XrayCoreMold
|
|
8
|
+
from nullforge.smithy.admin import ensure_acl_access
|
|
9
|
+
from nullforge.smithy.http import CURL_ARGS
|
|
10
|
+
from nullforge.smithy.versions import STATIC_URLS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
GEOIP_DAT_URL = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
|
|
14
|
+
GEOSITE_DAT_URL = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
|
|
15
|
+
|
|
16
|
+
GEO_DATS = {
|
|
17
|
+
"geoip.dat": GEOIP_DAT_URL,
|
|
18
|
+
"geosite.dat": GEOSITE_DAT_URL,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
BASE_DIR = "/usr/local/share/xray"
|
|
22
|
+
CONFIG_DIR = "/usr/local/etc/xray"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def deploy_xray() -> None:
|
|
26
|
+
"""Deploy Xray proxy configuration."""
|
|
27
|
+
|
|
28
|
+
features: FeaturesMold = host.data.features
|
|
29
|
+
xray_opts: XrayCoreMold = features.xray
|
|
30
|
+
|
|
31
|
+
_install_xray(xray_opts)
|
|
32
|
+
_download_geo_data()
|
|
33
|
+
|
|
34
|
+
systemd.service(
|
|
35
|
+
name="Ensure Xray is running and enabled",
|
|
36
|
+
service="xray",
|
|
37
|
+
running=True,
|
|
38
|
+
enabled=True,
|
|
39
|
+
_sudo=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if features.users.manage:
|
|
43
|
+
_setup_acls(features.users.name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _install_xray(opts: XrayCoreMold) -> None:
|
|
47
|
+
"""Install Xray using official installation script."""
|
|
48
|
+
|
|
49
|
+
if host.get_fact(File, "/usr/local/bin/xray"):
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
server.shell(
|
|
53
|
+
name="Install Xray proxy",
|
|
54
|
+
commands=f'bash -lc "$(curl -L {STATIC_URLS["xray_install"]})" @ install --beta',
|
|
55
|
+
_sudo=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _download_geo_data() -> None:
|
|
60
|
+
"""Download GeoIP and GeoSite data files."""
|
|
61
|
+
|
|
62
|
+
for file, url in host.loop(GEO_DATS.items()):
|
|
63
|
+
files.download(
|
|
64
|
+
name=f"Download {file}",
|
|
65
|
+
src=url,
|
|
66
|
+
dest=f"{BASE_DIR}/{file}",
|
|
67
|
+
extra_curl_args=CURL_ARGS,
|
|
68
|
+
_sudo=True,
|
|
69
|
+
_retries=3,
|
|
70
|
+
_retry_delay=10,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _setup_acls(username: str) -> None:
|
|
75
|
+
"""Set up ACLs on Xray directories for config management."""
|
|
76
|
+
|
|
77
|
+
ensure_acl_access(username, BASE_DIR)
|
|
78
|
+
ensure_acl_access(username, CONFIG_DIR, [f"{CONFIG_DIR}/config.json"])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
deploy_xray()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Cloudflare Zero Trust Tunnel deployment module."""
|
|
2
|
+
|
|
3
|
+
from pyinfra.context import host
|
|
4
|
+
from pyinfra.operations import files, systemd
|
|
5
|
+
from pyinfra.operations.util import any_changed
|
|
6
|
+
|
|
7
|
+
from nullforge.molds import FeaturesMold, WarpMold, ZeroTrustTunnelMold
|
|
8
|
+
from nullforge.smithy.cloudflare import CLOUDFLARE_GROUP, CLOUDFLARE_USER, ensure_cloudflared_binary
|
|
9
|
+
from nullforge.smithy.service import ensure_service_user
|
|
10
|
+
from nullforge.templates import BLOCK_TRIM_ENV, get_template_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def deploy_zerotrust_tunnel() -> None:
|
|
14
|
+
features: FeaturesMold = host.data.features
|
|
15
|
+
tunnel_opts: ZeroTrustTunnelMold = features.zerotrust
|
|
16
|
+
|
|
17
|
+
_deploy_tunnel(tunnel_opts, features.warp)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _deploy_tunnel(opts: ZeroTrustTunnelMold, warp_opts: WarpMold) -> None:
|
|
21
|
+
ensure_service_user(CLOUDFLARE_USER, CLOUDFLARE_GROUP, "/etc/cloudflare")
|
|
22
|
+
binary_op = ensure_cloudflared_binary()
|
|
23
|
+
|
|
24
|
+
config_dir = "/etc/cloudflare"
|
|
25
|
+
config_path = f"{config_dir}/tunnel.yml"
|
|
26
|
+
config_template = files.template(
|
|
27
|
+
name="Deploy Zero Trust Tunnel configuration",
|
|
28
|
+
src=get_template_path("cloudflared/tunnel.yml.j2"),
|
|
29
|
+
dest=config_path,
|
|
30
|
+
mode="0600",
|
|
31
|
+
user=CLOUDFLARE_USER,
|
|
32
|
+
group=CLOUDFLARE_GROUP,
|
|
33
|
+
jinja_env_kwargs=BLOCK_TRIM_ENV,
|
|
34
|
+
TOKEN=opts.token,
|
|
35
|
+
PROTOCOL=opts.protocol,
|
|
36
|
+
HA_CONNECTIONS=opts.ha_connections,
|
|
37
|
+
POST_QUANTUM=opts.post_quantum,
|
|
38
|
+
_sudo=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
warp_route_script = None
|
|
42
|
+
if opts.route_through_warp:
|
|
43
|
+
warp_route_script = files.put(
|
|
44
|
+
name="Deploy Zero Trust tunnel WARP routing script",
|
|
45
|
+
src=get_template_path("scripts/zt-tunnel-warp.sh"),
|
|
46
|
+
dest=f"{config_dir}/zt-tunnel-warp.sh",
|
|
47
|
+
user=CLOUDFLARE_USER,
|
|
48
|
+
group=CLOUDFLARE_GROUP,
|
|
49
|
+
mode="0755",
|
|
50
|
+
_sudo=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
service_template = files.template(
|
|
54
|
+
name="Deploy Zero Trust Tunnel systemd service",
|
|
55
|
+
src=get_template_path("systemd/cloudflare-tunnel.service.j2"),
|
|
56
|
+
dest="/etc/systemd/system/cloudflare-tunnel.service",
|
|
57
|
+
mode="0644",
|
|
58
|
+
jinja_env_kwargs=BLOCK_TRIM_ENV,
|
|
59
|
+
CONFIG_PATH=config_path,
|
|
60
|
+
WORKDIR=config_dir,
|
|
61
|
+
ROUTE_THROUGH_WARP=opts.route_through_warp,
|
|
62
|
+
WARP_IFACE=warp_opts.iface,
|
|
63
|
+
WARP_SERVICE=warp_opts.engine.systemd_service_name,
|
|
64
|
+
_sudo=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
systemd.daemon_reload(
|
|
68
|
+
name="Reload systemd daemon for Zero Trust Tunnel",
|
|
69
|
+
_sudo=True,
|
|
70
|
+
_if=service_template.did_change,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
restart_ops = [op for op in (binary_op, config_template, service_template, warp_route_script) if op]
|
|
74
|
+
systemd.service(
|
|
75
|
+
name="Restart Zero Trust Tunnel on change",
|
|
76
|
+
service="cloudflare-tunnel",
|
|
77
|
+
restarted=True,
|
|
78
|
+
_sudo=True,
|
|
79
|
+
_if=any_changed(*restart_ops),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
systemd.service(
|
|
83
|
+
name="Ensure Zero Trust Tunnel is running and enabled",
|
|
84
|
+
service="cloudflare-tunnel",
|
|
85
|
+
running=True,
|
|
86
|
+
enabled=True,
|
|
87
|
+
_sudo=True,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
deploy_zerotrust_tunnel()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Helpers for NullForge."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Admin utilities for NullForge."""
|
|
2
|
+
|
|
3
|
+
from pyinfra.context import host
|
|
4
|
+
from pyinfra.facts.server import Command, User
|
|
5
|
+
from pyinfra.operations import server
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_root() -> bool:
|
|
9
|
+
cache_key = "_nullforge_is_root"
|
|
10
|
+
if hasattr(host.data, cache_key):
|
|
11
|
+
return getattr(host.data, cache_key)
|
|
12
|
+
|
|
13
|
+
result = host.get_fact(User) == "root"
|
|
14
|
+
|
|
15
|
+
setattr(host.data, cache_key, result)
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _acl_entries(path: str) -> str:
|
|
20
|
+
return host.get_fact(Command, f"getfacl {path} 2>/dev/null || true", _sudo=True) or ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ensure_acl_access(username: str, directory: str, rw_files: list[str] | None = None) -> None:
|
|
24
|
+
"""Grant a user rwx ACL access to a directory, and optional rw to specific files."""
|
|
25
|
+
|
|
26
|
+
cmds: list[str] = []
|
|
27
|
+
|
|
28
|
+
if f"user:{username}:rwx" not in _acl_entries(directory):
|
|
29
|
+
cmds += [
|
|
30
|
+
f"setfacl -m u:{username}:rwx {directory}",
|
|
31
|
+
f"setfacl -d -m u:{username}:rwx {directory}",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
for f in rw_files or []:
|
|
35
|
+
entries = _acl_entries(f)
|
|
36
|
+
if entries and f"user:{username}:rw" not in entries:
|
|
37
|
+
cmds.append(f"setfacl -m u:{username}:rw {f}")
|
|
38
|
+
|
|
39
|
+
if cmds:
|
|
40
|
+
server.shell(name=f"Set ACLs on {directory}", commands=cmds, _sudo=True)
|
nullforge/smithy/arch.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Architecture utilities for NullForge."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
|
|
5
|
+
from pyinfra.context import host
|
|
6
|
+
from pyinfra.facts.server import Arch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _normalize(a: str | None) -> str:
|
|
10
|
+
return {
|
|
11
|
+
"x86_64": "x86_64",
|
|
12
|
+
"amd64": "x86_64",
|
|
13
|
+
"aarch64": "arm64",
|
|
14
|
+
"arm64": "arm64",
|
|
15
|
+
}.get(a or "", a or "x86_64")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def arch_id() -> str:
|
|
19
|
+
"""Normalize arch id where vendors differ."""
|
|
20
|
+
|
|
21
|
+
cache_key = "_nullforge_arch_id"
|
|
22
|
+
try:
|
|
23
|
+
if hasattr(host.data, cache_key):
|
|
24
|
+
return getattr(host.data, cache_key)
|
|
25
|
+
result = _normalize(host.get_fact(Arch))
|
|
26
|
+
setattr(host.data, cache_key, result)
|
|
27
|
+
return result
|
|
28
|
+
except AttributeError:
|
|
29
|
+
return _normalize(platform.machine())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def deb_arch() -> str:
|
|
33
|
+
return {
|
|
34
|
+
"x86_64": "amd64",
|
|
35
|
+
"arm64": "arm64",
|
|
36
|
+
}.get(arch_id(), "amd64")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Blocky DNS proxy setup module."""
|
|
2
|
+
|
|
3
|
+
from pyinfra.api.operation import OperationMeta
|
|
4
|
+
from pyinfra.context import host
|
|
5
|
+
from pyinfra.operations import files
|
|
6
|
+
|
|
7
|
+
from nullforge.smithy.install import install_release_binary
|
|
8
|
+
from nullforge.smithy.versions import get_versions, is_pinned_version_installed
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
BLOCKY_GROUP = "blocky"
|
|
12
|
+
BLOCKY_USER = "blocky"
|
|
13
|
+
BLOCKY_BINARY = "/usr/bin/blocky"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_blocky_binary() -> OperationMeta | None:
|
|
17
|
+
if is_pinned_version_installed("blocky", BLOCKY_BINARY):
|
|
18
|
+
host.noop("blocky binary is already installed")
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
install_op = install_release_binary(
|
|
22
|
+
name="Install blocky binary",
|
|
23
|
+
url=get_versions().blocky_tar(),
|
|
24
|
+
dest=BLOCKY_BINARY,
|
|
25
|
+
binary_name="blocky",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
files.file(
|
|
29
|
+
name="Set blocky binary permissions",
|
|
30
|
+
path=BLOCKY_BINARY,
|
|
31
|
+
mode="0755",
|
|
32
|
+
user="root",
|
|
33
|
+
group=BLOCKY_GROUP,
|
|
34
|
+
_sudo=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return install_op
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Cloudflare service management module."""
|
|
2
|
+
|
|
3
|
+
from pyinfra.api.operation import OperationMeta
|
|
4
|
+
from pyinfra.context import host
|
|
5
|
+
from pyinfra.operations import files
|
|
6
|
+
|
|
7
|
+
from nullforge.smithy.versions import get_versions, is_pinned_version_installed
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
CLOUDFLARE_GROUP = "cloudflare"
|
|
11
|
+
CLOUDFLARE_USER = "cloudflare"
|
|
12
|
+
CLOUDFLARED_BINARY = "/usr/bin/cloudflared"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ensure_cloudflared_binary() -> OperationMeta | None:
|
|
16
|
+
if is_pinned_version_installed("cloudflared", CLOUDFLARED_BINARY):
|
|
17
|
+
host.noop("cloudflared binary is already installed")
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
return files.download(
|
|
21
|
+
name="Install cloudflared binary",
|
|
22
|
+
src=get_versions().cloudflared_url(),
|
|
23
|
+
dest=CLOUDFLARED_BINARY,
|
|
24
|
+
mode="0755",
|
|
25
|
+
user=CLOUDFLARE_USER,
|
|
26
|
+
group=CLOUDFLARE_GROUP,
|
|
27
|
+
force=True,
|
|
28
|
+
extra_curl_args=get_versions().release_curl_args(),
|
|
29
|
+
_sudo=True,
|
|
30
|
+
_retries=3,
|
|
31
|
+
_retry_delay=10,
|
|
32
|
+
)
|