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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# MEKO-style SYN rate-limiting fix for the telemt MTProto port.
|
|
4
|
+
# https://github.com/Mekotofeuka/MTPROTO_FIX_By_MEKO
|
|
5
|
+
#
|
|
6
|
+
# Non-iOS clients are throttled per source IP (default 54 SYN/minute, burst 1);
|
|
7
|
+
# excess connection attempts get a TCP reset so dead sockets are reaped in
|
|
8
|
+
# minutes. Small low-TTL SYNs (the iOS fingerprint) bypass limit for fast reconnects.
|
|
9
|
+
# Under-limit and iOS traffic RETURN to the normal INPUT path, so this coexists
|
|
10
|
+
# with UFW/firewalld (the port must still be allowed there).
|
|
11
|
+
# Best-effort: failures are logged, never fatal to startup.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
cmd=${1:?up|down}
|
|
17
|
+
PORT=${2:-443}
|
|
18
|
+
RATE=${3:-54/minute}
|
|
19
|
+
BURST=${4:-1}
|
|
20
|
+
IOS_TTL=${5:-65}
|
|
21
|
+
|
|
22
|
+
CHAIN="MTPR_SYNFIX"
|
|
23
|
+
HL_NAME="mtpr${PORT}"
|
|
24
|
+
|
|
25
|
+
log() { logger -t telemt-synfix -- "$*"; }
|
|
26
|
+
|
|
27
|
+
up_family() {
|
|
28
|
+
# $1 = iptables|ip6tables ; remaining args = iOS-bypass match (may be empty)
|
|
29
|
+
local ipt=$1
|
|
30
|
+
shift
|
|
31
|
+
|
|
32
|
+
"$ipt" -N "$CHAIN" 2>/dev/null || "$ipt" -F "$CHAIN" || return 1
|
|
33
|
+
|
|
34
|
+
if [ "$#" -gt 0 ]; then
|
|
35
|
+
"$ipt" -A "$CHAIN" -p tcp --dport "$PORT" --syn "$@" -j RETURN || return 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
"$ipt" -A "$CHAIN" -p tcp --dport "$PORT" --syn \
|
|
39
|
+
-m hashlimit \
|
|
40
|
+
--hashlimit-name "$HL_NAME" \
|
|
41
|
+
--hashlimit-mode srcip \
|
|
42
|
+
--hashlimit-upto "$RATE" \
|
|
43
|
+
--hashlimit-burst "$BURST" \
|
|
44
|
+
--hashlimit-htable-expire 60000 \
|
|
45
|
+
--hashlimit-htable-size 32768 \
|
|
46
|
+
-j RETURN || return 1
|
|
47
|
+
|
|
48
|
+
"$ipt" -A "$CHAIN" -p tcp --dport "$PORT" --syn -j REJECT --reject-with tcp-reset || return 1
|
|
49
|
+
|
|
50
|
+
# Ensure exactly one jump from INPUT, evaluated before other allow rules.
|
|
51
|
+
while "$ipt" -D INPUT -p tcp --dport "$PORT" --syn -j "$CHAIN" 2>/dev/null; do :; done
|
|
52
|
+
"$ipt" -I INPUT 1 -p tcp --dport "$PORT" --syn -j "$CHAIN" || return 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
down_family() {
|
|
56
|
+
local ipt=$1
|
|
57
|
+
while "$ipt" -D INPUT -p tcp --dport "$PORT" --syn -j "$CHAIN" 2>/dev/null; do :; done
|
|
58
|
+
"$ipt" -F "$CHAIN" 2>/dev/null || true
|
|
59
|
+
"$ipt" -X "$CHAIN" 2>/dev/null || true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case "$cmd" in
|
|
63
|
+
up)
|
|
64
|
+
if up_family iptables -m length --length 64 -m ttl --ttl-lt "$IOS_TTL"; then
|
|
65
|
+
log "up: ipv4 chain=$CHAIN port=$PORT rate=$RATE burst=$BURST ios_ttl<$IOS_TTL"
|
|
66
|
+
else
|
|
67
|
+
log "up: ipv4 synfix not applied (missing iptables modules?)"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if command -v ip6tables >/dev/null 2>&1; then
|
|
71
|
+
if up_family ip6tables -m hl --hl-lt "$IOS_TTL"; then
|
|
72
|
+
log "up: ipv6 chain=$CHAIN port=$PORT rate=$RATE burst=$BURST"
|
|
73
|
+
else
|
|
74
|
+
log "up: ipv6 synfix not applied"
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
;;
|
|
78
|
+
down)
|
|
79
|
+
down_family iptables
|
|
80
|
+
command -v ip6tables >/dev/null 2>&1 && down_family ip6tables
|
|
81
|
+
log "down: chain=$CHAIN port=$PORT cleaned"
|
|
82
|
+
;;
|
|
83
|
+
*)
|
|
84
|
+
echo "usage: $0 {up|down} [port] [rate] [burst] [ios_ttl]" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
|
|
89
|
+
exit 0
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Per-uid policy routing that steers a service user's Telegram-bound traffic
|
|
4
|
+
# through the Cloudflare WARP interface, leaving all other egress untouched.
|
|
5
|
+
#
|
|
6
|
+
# Usage: teleproxy-warp.sh {up|down} [svc_user] [iface] [table] [tid] [prio]
|
|
7
|
+
#
|
|
8
|
+
# Routing is scoped to the service uid and to Telegram's published CIDRs, so the
|
|
9
|
+
# proxy reaches Telegram over WARP while clients still connect to it directly.
|
|
10
|
+
# IPv4 fails closed for the service user when WARP has no v4 address; IPv6 is
|
|
11
|
+
# optional and skipped entirely when absent.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
cmd=${1:?up|down}
|
|
17
|
+
SVC_USER=${2:-telemt} # daemon's systemd User=
|
|
18
|
+
IFACE=${3:-warp} # WARP interface (usque -n <iface>)
|
|
19
|
+
TABLE=${4:-tpwarp} # dedicated routing table, separate from WARP's own
|
|
20
|
+
TID=${5:-124}
|
|
21
|
+
PRIO=${6:-11900} # rule priority (below the WARP policy band, above main)
|
|
22
|
+
WAIT_SECS=15
|
|
23
|
+
|
|
24
|
+
CIDR_URLS=(
|
|
25
|
+
"https://core.telegram.org/resources/cidr.txt"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Embedded fallback (core.telegram.org/resources/cidr.txt, verified 2026-05)
|
|
29
|
+
FB_V4="91.108.56.0/22 91.108.4.0/22 91.108.8.0/22 91.108.16.0/22 91.108.12.0/22 149.154.160.0/20 91.105.192.0/23 91.108.20.0/22 185.76.151.0/24"
|
|
30
|
+
FB_V6="2001:b28:f23d::/48 2001:b28:f23f::/48 2001:67c:4e8::/48 2001:b28:f23c::/48 2a0a:f280::/32"
|
|
31
|
+
|
|
32
|
+
V4=""
|
|
33
|
+
V6=""
|
|
34
|
+
|
|
35
|
+
log() { logger -t telemt-warp -- "$*"; }
|
|
36
|
+
|
|
37
|
+
ensure_table() {
|
|
38
|
+
grep -qE "^[[:space:]]*$TID[[:space:]]+$TABLE$" /etc/iproute2/rt_tables \
|
|
39
|
+
|| echo "$TID $TABLE" >> /etc/iproute2/rt_tables
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get_warp6() { ip -6 -o addr show dev "$IFACE" scope global 2>/dev/null | awk '{print $4}' | head -n1 | cut -d/ -f1; }
|
|
43
|
+
get_warp4() { ip -4 -o addr show dev "$IFACE" scope global 2>/dev/null | awk '{print $4}' | head -n1 | cut -d/ -f1; }
|
|
44
|
+
|
|
45
|
+
wait_for_warp6() {
|
|
46
|
+
local i=0 a
|
|
47
|
+
while [ $i -lt "$WAIT_SECS" ]; do
|
|
48
|
+
a=$(get_warp6 || true); [ -n "$a" ] && { echo "$a"; return 0; }
|
|
49
|
+
sleep 1; i=$((i+1))
|
|
50
|
+
done
|
|
51
|
+
return 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fetch_cidrs() { # tries all CIDR_URLS; merges + deduplicates; falls back to embedded list
|
|
55
|
+
local raw url new_v4 new_v6
|
|
56
|
+
for url in "${CIDR_URLS[@]}"; do
|
|
57
|
+
raw=$(curl --interface "$IFACE" --connect-timeout 3 --max-time 8 -fsSL "$url" 2>/dev/null || true)
|
|
58
|
+
if [ -n "$raw" ]; then
|
|
59
|
+
new_v4=$(echo "$raw" | tr ' ' '\n' | grep -E '^[0-9]+\.' | tr '\n' ' ' || true)
|
|
60
|
+
new_v6=$(echo "$raw" | tr ' ' '\n' | grep -E ':' | tr '\n' ' ' || true)
|
|
61
|
+
V4=$(printf '%s %s' "$V4" "$new_v4" | tr ' ' '\n' | sort -u | awk 'NF' | tr '\n' ' ')
|
|
62
|
+
V6=$(printf '%s %s' "$V6" "$new_v6" | tr ' ' '\n' | sort -u | awk 'NF' | tr '\n' ' ')
|
|
63
|
+
log "fetched CIDRs from $url"
|
|
64
|
+
else
|
|
65
|
+
log "cidr fetch failed from $url (expected on a filtered uplink); skipping"
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
[ -n "${V4:-}" ] || V4="$FB_V4"
|
|
69
|
+
[ -n "${V6:-}" ] || V6="$FB_V6"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
del_rules() {
|
|
73
|
+
while ip rule del pref "$PRIO" 2>/dev/null; do :; done
|
|
74
|
+
while ip -6 rule del pref "$PRIO" 2>/dev/null; do :; done
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "$cmd" in
|
|
78
|
+
up)
|
|
79
|
+
UID_N=$(id -u "$SVC_USER" 2>/dev/null) && [ -n "$UID_N" ] || { log "user $SVC_USER not found"; exit 1; }
|
|
80
|
+
ensure_table
|
|
81
|
+
|
|
82
|
+
# IPv6 is optional: non-fatal if absent; routes/rules skipped entirely when missing.
|
|
83
|
+
WARP6=$(wait_for_warp6 || true)
|
|
84
|
+
[ -n "$WARP6" ] || log "no IPv6 on $IFACE after ${WAIT_SECS}s; IPv6 routing skipped"
|
|
85
|
+
|
|
86
|
+
del_rules
|
|
87
|
+
ip -6 route flush table "$TABLE" 2>/dev/null || true
|
|
88
|
+
ip -4 route flush table "$TABLE" 2>/dev/null || true
|
|
89
|
+
|
|
90
|
+
if [ -n "$WARP6" ]; then
|
|
91
|
+
ip -6 route replace default dev "$IFACE" src "$WARP6" metric 100 table "$TABLE"
|
|
92
|
+
ip -6 route replace unreachable default metric 1024 table "$TABLE"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
WARP4=$(get_warp4 || true)
|
|
96
|
+
if [ -n "$WARP4" ]; then
|
|
97
|
+
ip -4 route replace default dev "$IFACE" src "$WARP4" metric 100 table "$TABLE"
|
|
98
|
+
log "IPv4 egress via WARP src=$WARP4"
|
|
99
|
+
else
|
|
100
|
+
log "no IPv4 on $IFACE; IPv4-to-Telegram from $SVC_USER fails closed"
|
|
101
|
+
fi
|
|
102
|
+
ip -4 route replace unreachable default metric 1024 table "$TABLE"
|
|
103
|
+
|
|
104
|
+
fetch_cidrs
|
|
105
|
+
for p in $V4; do
|
|
106
|
+
ip rule add pref "$PRIO" to "$p" uidrange "${UID_N}-${UID_N}" lookup "$TABLE" || log "v4 rule add failed for $p"
|
|
107
|
+
done
|
|
108
|
+
if [ -n "$WARP6" ]; then
|
|
109
|
+
for p in $V6; do
|
|
110
|
+
ip -6 rule add pref "$PRIO" to "$p" uidrange "${UID_N}-${UID_N}" lookup "$TABLE" || log "v6 rule add failed for $p"
|
|
111
|
+
done
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
v6_count=0; [ -n "$WARP6" ] && v6_count=$(echo $V6 | wc -w) || true
|
|
115
|
+
log "up: uid=$UID_N v6src=${WARP6:-none} table=$TABLE pref=$PRIO ($(echo $V4 | wc -w)v4 ${v6_count}v6)"
|
|
116
|
+
;;
|
|
117
|
+
down)
|
|
118
|
+
del_rules
|
|
119
|
+
ip -6 route flush table "$TABLE" 2>/dev/null || true
|
|
120
|
+
ip -4 route flush table "$TABLE" 2>/dev/null || true
|
|
121
|
+
log "down: table=$TABLE pref=$PRIO cleaned"
|
|
122
|
+
;;
|
|
123
|
+
*)
|
|
124
|
+
echo "usage: $0 {up|down} [svc_user] [iface] [table] [tid] [prio]" >&2
|
|
125
|
+
exit 1
|
|
126
|
+
;;
|
|
127
|
+
esac
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# IPv6 policy routing for Cloudflare WARP (usque nativetun).
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
cmd=${1:?up|down}
|
|
9
|
+
IFACE=${2:-warp}
|
|
10
|
+
CFG=${3:-/etc/usque/config.json}
|
|
11
|
+
|
|
12
|
+
# If omitted, values are derived from IFACE.
|
|
13
|
+
EXPLICIT_TID=${4:-}
|
|
14
|
+
EXPLICIT_PRIO=${5:-}
|
|
15
|
+
|
|
16
|
+
# This lets multiple independent warp interfacescoexist
|
|
17
|
+
_slot() {
|
|
18
|
+
printf '%s' "$1" | cksum | awk '{print $1 % 100}'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SLOT=$(_slot "$IFACE")
|
|
22
|
+
|
|
23
|
+
if [ -n "$EXPLICIT_TID" ]; then
|
|
24
|
+
TID=$EXPLICIT_TID
|
|
25
|
+
else
|
|
26
|
+
TID=$((200 + SLOT)) # 200-299: safe custom table ID range
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Unique table name per interface
|
|
30
|
+
TABLE="warp_$(printf '%s' "$IFACE" | tr -c '[:alnum:]_.-' '_')"
|
|
31
|
+
|
|
32
|
+
if [ -n "$EXPLICIT_PRIO" ]; then
|
|
33
|
+
PRIO=$EXPLICIT_PRIO
|
|
34
|
+
else
|
|
35
|
+
PRIO=$((20000 + SLOT * 10)) # 20000-20990
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
PRIO_OIF=$((PRIO + 5))
|
|
39
|
+
WAIT_SECS=8
|
|
40
|
+
|
|
41
|
+
log(){ logger -t warp-policy -- "$*"; }
|
|
42
|
+
|
|
43
|
+
get_warp6(){
|
|
44
|
+
ip -6 -o addr show dev "$IFACE" scope global 2>/dev/null \
|
|
45
|
+
| awk '{print $4}' | head -n1 | cut -d/ -f1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get_ep6(){
|
|
49
|
+
jq -r '.endpoint_v6 // empty' "$CFG" 2>/dev/null || true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ensure_table(){
|
|
53
|
+
grep -qE "^[[:space:]]*$TID[[:space:]]+$TABLE$" /etc/iproute2/rt_tables \
|
|
54
|
+
|| echo "$TID $TABLE" >> /etc/iproute2/rt_tables
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
wait_for_warp6(){
|
|
58
|
+
local i=0
|
|
59
|
+
while [ $i -lt "$WAIT_SECS" ]; do
|
|
60
|
+
local a
|
|
61
|
+
a=$(get_warp6 || true)
|
|
62
|
+
if [ -n "$a" ]; then echo "$a"; return 0; fi
|
|
63
|
+
sleep 1; i=$((i+1))
|
|
64
|
+
done
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "$cmd" in
|
|
69
|
+
up)
|
|
70
|
+
ensure_table
|
|
71
|
+
|
|
72
|
+
if ! WARP6=$(wait_for_warp6); then
|
|
73
|
+
log "No IPv6 on $IFACE after ${WAIT_SECS}s; leaving without rules"
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
EP6=$(get_ep6)
|
|
78
|
+
if [ -n "$EP6" ]; then
|
|
79
|
+
# Ensure the WARP endpoint remains reachable outside the tunnel while
|
|
80
|
+
# our default-via-warp is installed. Only possible if host has IPv6.
|
|
81
|
+
route_info=$(ip -6 route get "${EP6}" 2>/dev/null || true)
|
|
82
|
+
GW6=$(echo "$route_info" | awk '{for(i=1;i<NF;i++) if($i=="via"){print $(i+1); exit}}')
|
|
83
|
+
DEV=$(echo "$route_info" | awk '{for(i=1;i<NF;i++) if($i=="dev"){print $(i+1); exit}}')
|
|
84
|
+
if [ -n "${GW6}" ] && [ -n "${DEV}" ]; then
|
|
85
|
+
ip -6 route replace "${EP6}/128" via "$GW6" dev "$DEV" onlink
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
ip -6 route replace default dev "$IFACE" table "$TABLE"
|
|
90
|
+
|
|
91
|
+
ip -6 rule del pref "$PRIO" 2>/dev/null || true
|
|
92
|
+
ip -6 rule add pref "$PRIO" from "${WARP6}/128" lookup "$TABLE"
|
|
93
|
+
ip -6 rule del pref "$PRIO_OIF" 2>/dev/null || true
|
|
94
|
+
ip -6 rule add pref "$PRIO_OIF" oif "$IFACE" lookup "$TABLE"
|
|
95
|
+
|
|
96
|
+
log "IPv6 policy up: src=${WARP6}/128 pref=$PRIO, oif=$IFACE pref=$PRIO_OIF table=$TABLE"
|
|
97
|
+
;;
|
|
98
|
+
|
|
99
|
+
down)
|
|
100
|
+
ip -6 rule del pref "$PRIO" 2>/dev/null || true
|
|
101
|
+
ip -6 rule del pref "$PRIO_OIF" 2>/dev/null || true
|
|
102
|
+
|
|
103
|
+
EP6=$(get_ep6 || true)
|
|
104
|
+
if [ -n "${EP6:-}" ]; then
|
|
105
|
+
ip -6 route del "${EP6}/128" 2>/dev/null || true
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
ip -6 route flush table "$TABLE" 2>/dev/null || true
|
|
109
|
+
|
|
110
|
+
log "IPv6 policy down: table=$TABLE prefs=$PRIO,$PRIO_OIF cleaned"
|
|
111
|
+
;;
|
|
112
|
+
|
|
113
|
+
*)
|
|
114
|
+
echo "usage: $0 {up|down} [iface] [config] [tid] [prio]" >&2
|
|
115
|
+
echo " tid/prio: optional explicit table id and rule priority base (advanced)" >&2
|
|
116
|
+
exit 1
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
cmd=${1:?up|down}
|
|
6
|
+
IFACE=${2:-warp}
|
|
7
|
+
|
|
8
|
+
log(){ logger -t zt-tunnel-warp -- "$*"; }
|
|
9
|
+
|
|
10
|
+
CIDRS="198.41.200.0/24 104.16.0.0/12"
|
|
11
|
+
|
|
12
|
+
case "$cmd" in
|
|
13
|
+
up)
|
|
14
|
+
for cidr in $CIDRS; do
|
|
15
|
+
ip route replace "$cidr" dev "$IFACE"
|
|
16
|
+
log "Added route for $cidr via $IFACE"
|
|
17
|
+
done
|
|
18
|
+
;;
|
|
19
|
+
down)
|
|
20
|
+
for cidr in $CIDRS; do
|
|
21
|
+
ip route del "$cidr" dev "$IFACE" 2>/dev/null || true
|
|
22
|
+
log "Removed route for $cidr via $IFACE"
|
|
23
|
+
done
|
|
24
|
+
;;
|
|
25
|
+
*)
|
|
26
|
+
log "Invalid command: $cmd"
|
|
27
|
+
exit 1
|
|
28
|
+
esac
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Blocky DNS proxy
|
|
3
|
+
Wants=network-online.target nss-lookup.target
|
|
4
|
+
After=dns-internal.service
|
|
5
|
+
Before=nss-lookup.target
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
User=blocky
|
|
9
|
+
Group=blocky
|
|
10
|
+
Type=simple
|
|
11
|
+
ExecStart=/usr/bin/blocky --config {{ CONFIG_PATH }}
|
|
12
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
13
|
+
StandardOutput=journal
|
|
14
|
+
StandardError=journal
|
|
15
|
+
Restart=on-failure
|
|
16
|
+
RestartSec=5s
|
|
17
|
+
|
|
18
|
+
[Install]
|
|
19
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Cloudflare Zero Trust Tunnel
|
|
3
|
+
Wants=network-online.target
|
|
4
|
+
After=network-online.target{% if ROUTE_THROUGH_WARP %} {{ WARP_SERVICE }}.service{% endif %}
|
|
5
|
+
|
|
6
|
+
{% if ROUTE_THROUGH_WARP %}
|
|
7
|
+
Requires={{ WARP_SERVICE }}.service
|
|
8
|
+
{% endif %}
|
|
9
|
+
|
|
10
|
+
[Service]
|
|
11
|
+
User=cloudflare
|
|
12
|
+
Group=cloudflare
|
|
13
|
+
Type=simple
|
|
14
|
+
|
|
15
|
+
ExecStart=/usr/bin/cloudflared --config {{ CONFIG_PATH }} tunnel run
|
|
16
|
+
{% if ROUTE_THROUGH_WARP %}
|
|
17
|
+
ExecStartPost={{ WORKDIR }}/zt-tunnel-warp.sh up {{ WARP_IFACE }}
|
|
18
|
+
ExecStopPost={{ WORKDIR }}/zt-tunnel-warp.sh down {{ WARP_IFACE }}
|
|
19
|
+
{% endif %}
|
|
20
|
+
AmbientCapabilities=CAP_NET_ADMIN
|
|
21
|
+
StandardOutput=journal
|
|
22
|
+
StandardError=journal
|
|
23
|
+
Restart=on-failure
|
|
24
|
+
RestartSec=5s
|
|
25
|
+
|
|
26
|
+
[Install]
|
|
27
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Cloudflare Warp Tunnel (Masque)
|
|
3
|
+
Wants=network-online.target
|
|
4
|
+
After=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
User=cloudflare
|
|
8
|
+
Group=cloudflare
|
|
9
|
+
Type=simple
|
|
10
|
+
|
|
11
|
+
ExecStart=/usr/local/bin/usque nativetun -m {{ MTU }} -c {{ CONFIG_PATH }} -n {{ INET_NAME }} {% if USE_OUTBOUND_IPV6 %}--ipv6{% endif %}
|
|
12
|
+
ExecStartPost={{ WORKDIR }}/warp-v6-policy.sh up {{ INET_NAME }} {{ CONFIG_PATH }}
|
|
13
|
+
ExecStopPost={{ WORKDIR }}/warp-v6-policy.sh down {{ INET_NAME }} {{ CONFIG_PATH }}
|
|
14
|
+
AmbientCapabilities=CAP_NET_ADMIN
|
|
15
|
+
StandardOutput=journal
|
|
16
|
+
StandardError=journal
|
|
17
|
+
Restart=on-failure
|
|
18
|
+
RestartSec=5s
|
|
19
|
+
|
|
20
|
+
[Install]
|
|
21
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Setup dns-internal dummy interface for blocky
|
|
3
|
+
Before=blocky.service
|
|
4
|
+
After=network.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=oneshot
|
|
8
|
+
RemainAfterExit=yes
|
|
9
|
+
ExecStart=/bin/sh -c 'ip link show dns-internal >/dev/null 2>&1 || ip link add dns-internal type dummy'
|
|
10
|
+
ExecStart=/bin/sh -c 'ip addr show dev dns-internal | grep -q "{{ LISTEN_ADDRESS }}" || ip addr add {{ LISTEN_CIDR }} dev dns-internal'
|
|
11
|
+
ExecStart=/sbin/ip link set dns-internal up
|
|
12
|
+
ExecStop=/sbin/ip link del dns-internal
|
|
13
|
+
|
|
14
|
+
[Install]
|
|
15
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Telemt MTProto proxy
|
|
3
|
+
Wants=network-online.target
|
|
4
|
+
After=network-online.target{% if WARP_ENABLED %} {{ WARP_SERVICE }}.service{% endif %}
|
|
5
|
+
|
|
6
|
+
{% if WARP_ENABLED %}
|
|
7
|
+
Requires={{ WARP_SERVICE }}.service
|
|
8
|
+
BindsTo={{ WARP_SERVICE }}.service
|
|
9
|
+
{% endif %}
|
|
10
|
+
|
|
11
|
+
[Service]
|
|
12
|
+
Type=simple
|
|
13
|
+
User={{ USER }}
|
|
14
|
+
Group={{ GROUP }}
|
|
15
|
+
ExecStart={{ BINARY_PATH }} {{ CONFIG_PATH }}
|
|
16
|
+
{% if WARP_ENABLED %}
|
|
17
|
+
ExecStartPre=+{{ TELEPROXY_SCRIPT }} up {{ USER }} {{ WARP_IFACE }}
|
|
18
|
+
{% endif %}
|
|
19
|
+
{% if SYNFIX_ENABLED %}
|
|
20
|
+
ExecStartPre=+{{ SYNFIX_SCRIPT }} up {{ PORT }}
|
|
21
|
+
ExecStopPost=+{{ SYNFIX_SCRIPT }} down {{ PORT }}
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% if WARP_ENABLED %}
|
|
24
|
+
ExecStopPost=+{{ TELEPROXY_SCRIPT }} down {{ USER }} {{ WARP_IFACE }}
|
|
25
|
+
{% endif %}
|
|
26
|
+
Restart=on-failure
|
|
27
|
+
RestartSec=5s
|
|
28
|
+
LimitNOFILE=65536
|
|
29
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
30
|
+
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
31
|
+
NoNewPrivileges=true
|
|
32
|
+
StandardOutput=journal
|
|
33
|
+
StandardError=journal
|
|
34
|
+
|
|
35
|
+
[Install]
|
|
36
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Managed by NullForge. Local edits are overwritten on redeploy.
|
|
2
|
+
|
|
3
|
+
[general]
|
|
4
|
+
fast_mode = {{ FAST_MODE | lower }}
|
|
5
|
+
use_middle_proxy = {{ USE_MIDDLE_PROXY | lower }}
|
|
6
|
+
log_level = "silent"
|
|
7
|
+
|
|
8
|
+
[general.modes]
|
|
9
|
+
classic = {{ MODE_CLASSIC | lower }}
|
|
10
|
+
secure = {{ MODE_SECURE | lower }}
|
|
11
|
+
tls = {{ MODE_TLS | lower }}
|
|
12
|
+
|
|
13
|
+
[server]
|
|
14
|
+
max_connections = {{ MAX_CONNECTIONS }}
|
|
15
|
+
port = {{ PORT }}
|
|
16
|
+
|
|
17
|
+
[server.api]
|
|
18
|
+
enabled = {{ API_ENABLED | lower }}
|
|
19
|
+
|
|
20
|
+
[network]
|
|
21
|
+
ipv6 = {{ IPV6 | lower }}
|
|
22
|
+
prefer = {{ PREFER }}
|
|
23
|
+
|
|
24
|
+
[timeouts]
|
|
25
|
+
client_handshake = {{ CLIENT_HANDSHAKE }}
|
|
26
|
+
|
|
27
|
+
[censorship]
|
|
28
|
+
tls_domain = "{{ TLS_DOMAIN }}"
|
|
29
|
+
{% if UNKNOWN_SNI_ACTION %}
|
|
30
|
+
unknown_sni_action = "{{ UNKNOWN_SNI_ACTION }}"
|
|
31
|
+
{% else %}
|
|
32
|
+
#unknown_sni_action = "mask"
|
|
33
|
+
{% endif %}
|
|
34
|
+
|
|
35
|
+
[access.users]
|
|
36
|
+
{% for name, secret in USERS.items() %}
|
|
37
|
+
{{ name }} = "{{ secret }}"
|
|
38
|
+
{% endfor %}
|
|
39
|
+
|
|
40
|
+
[[upstreams]]
|
|
41
|
+
type = "direct"
|
|
42
|
+
{% if ROUTE_VIA_WARP %}
|
|
43
|
+
# Egress is steered through WARP by teleproxy-warp.sh (per-uid policy routing),
|
|
44
|
+
# so the upstream stays "direct" instead of binding to the "{{ WARP_IFACE }}" interface.
|
|
45
|
+
{% else %}
|
|
46
|
+
#interface = "{{ WARP_IFACE }}"
|
|
47
|
+
{% endif %}
|
|
48
|
+
weight = 10
|
|
49
|
+
enabled = true
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nullforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Forge the server's baseline from null
|
|
5
|
+
Author-email: wlix13 <github@wlix13.dev>
|
|
6
|
+
Maintainer-email: wlix13 <github@wlix13.dev>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Python: <3.14,>=3.13
|
|
11
|
+
Requires-Dist: click
|
|
12
|
+
Requires-Dist: pydantic
|
|
13
|
+
Requires-Dist: pyinfra
|
|
14
|
+
Requires-Dist: rich-click
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# NullForge
|
|
18
|
+
|
|
19
|
+
Forge the server's baseline from null — an infrastructure-as-code framework built on [pyinfra](https://pyinfra.com), themed around a blacksmith's forge: **inventories** define hosts, **molds** shape the configuration, **runes** are idempotent operation sets, the **foundry** casts them onto targets, and the **smithy** holds cross-distro helpers.
|
|
20
|
+
|
|
21
|
+
> [!WARNING]
|
|
22
|
+
> **NullForge is in active development.** Until the `v1.0.0` release, the mold schemas, runes and deploy behaviour may change at any time — breaking changes can land in **any** release, including patch versions. Pin an exact version (e.g. `nullforge==0.1.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv sync
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Deploy
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Cast the full baseline (all enabled features from the inventory)
|
|
34
|
+
uv run pyinfra nullforge/inventories/example.py nullforge/foundry/full_cast.py
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Contributing
|
|
38
|
+
|
|
39
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
|