Repository navigation
Expand file tree
/
Copy pathpacket-shield-nft.sh
More file actions
executable file
·786 lines (717 loc) · 27.1 KB
/
Copy pathpacket-shield-nft.sh
File metadata and controls
executable file
·786 lines (717 loc) · 27.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
#!/usr/bin/env bash
# nftables packet shield — drops inbound tiny floods to Xbox game ports.
set -euo pipefail
CONF="${ARRAY_FW_CONF:-/etc/array-firewall/array-firewall.conf}"
STATE="/var/lib/array-firewall/packet-shield.state"
TABLE="inet gaming"
PERSISTENT_PEERS="/var/lib/array-firewall/persistent-peers.txt"
MM_ALLOWLIST="/opt/array-firewall/config/matchmaking-allowlist.json"
IN_MATCH_ALLOWLIST="/opt/array-firewall/config/in-match-allowlist.json"
PER_SRC_UDP_RATE="${PER_SRC_UDP_RATE:-500}"
CONN_CAP_PER_PEER="${CONN_CAP_PER_PEER:-40}"
PROBE_SINK_PORT="${PROBE_SINK_PORT:-39217}"
HONEYPOT_TCP="{ 23, 21, 445, 135, 139, 3389, 5900, 8080, 8443, 31337, 4444, 5555, 6667 }"
# shellcheck disable=SC1090
source "$CONF"
PEER_UDP_RATE="${PEER_UDP_RATE:-15}"
PEER_UDP_BURST="${PEER_UDP_BURST:-30}"
PROBE_CONFUSION_UDP="${PROBE_CONFUSION_UDP:-0}"
PROBE_CONFUSION_QUEUE="${PROBE_CONFUSION_QUEUE:-42}"
load_probe_confusion() {
PROBE_CONFUSION_UDP=0
PROBE_CONFUSION_QUEUE=42
local f="/var/lib/array-firewall/probe-confusion.env"
[[ -f "$f" ]] || return 0
set -a
# shellcheck disable=SC1090
source "$f"
set +a
}
log() { printf '[packet-shield-nft] %s\n' "$*"; }
ensure_wan_nat_py() {
command -v python3 >/dev/null 2>&1 || return 0
python3 - <<'PY'
import sys
sys.path.insert(0, "/opt/array-firewall/api")
from lib import nat
nat.ensure_wan_nat()
PY
}
need_root() { [[ "${EUID:-$(id -u)}" -eq 0 ]] || { echo "Run as root"; exit 1; }; }
require_xbox() {
if [[ -z "${XBOX_IP:-}" ]]; then
echo "XBOX_IP not set in $CONF" >&2
exit 1
fi
}
sentinel_tiny_only() {
command -v python3 >/dev/null 2>&1 || return 1
python3 - <<'PY'
import sys
sys.path.insert(0, "/opt/array-firewall/api")
from lib import policies
raise SystemExit(0 if policies.sentinel_tiny_only() else 1)
PY
}
sentinel_wrapper_level() {
command -v python3 >/dev/null 2>&1 || { echo console; return; }
python3 - <<'PY'
import sys
sys.path.insert(0, "/opt/array-firewall/api")
from lib import policies
print(policies.sentinel_wrapper_level())
PY
}
udp_ports_csv() {
local IFS=,
echo "${XBOX_UDP_PORTS[*]}"
}
tcp_ports_csv() {
local IFS=,
echo "${XBOX_TCP_PORTS[*]}"
}
cmd_status() {
[[ -f "$STATE" ]] && cat "$STATE" || echo "mode=inactive"
nft list table "$TABLE" 2>/dev/null | head -80 || echo "table inactive"
nft list chain ip nat probe_blackhole 2>/dev/null | head -20 || true
}
relax_honeypot_nat() {
nft flush chain ip nat probe_blackhole 2>/dev/null || true
}
setup_honeypot_nat() {
ensure_wan_nat_py
local gw="${LAN_GATEWAY_IP:-192.0.2.1}"
local wan_if="${WAN_IF:-eth1}"
local sink="127.0.0.1:${PROBE_SINK_PORT}"
nft list table ip nat >/dev/null 2>&1 || nft add table ip nat
if ! nft list chain ip nat probe_blackhole >/dev/null 2>&1; then
nft add chain ip nat probe_blackhole '{ type nat hook prerouting priority dstnat - 5; policy accept; }'
fi
nft flush chain ip nat probe_blackhole
# Internet scans hit WAN IP (eth1) before DMZ DNAT — sink obvious probe ports here.
nft add rule ip nat probe_blackhole iifname "$wan_if" tcp dport "$HONEYPOT_TCP" \
dnat to "$sink" comment '"wan-honeypot-tcp-sink"'
nft add rule ip nat probe_blackhole ip daddr "$XBOX_IP" tcp dport "$HONEYPOT_TCP" \
dnat to "${gw}:${PROBE_SINK_PORT}" comment '"honeypot-tcp-sink"'
systemctl enable array-firewall-probe-sink 2>/dev/null || true
systemctl start array-firewall-probe-sink 2>/dev/null || true
}
_ensure_peer_guard_table() {
if nft list table "$TABLE" >/dev/null 2>&1; then
return 0
fi
nft add table "$TABLE"
nft add chain "$TABLE" xbox_shield '{ type filter hook forward priority filter; policy accept; }'
nft add chain "$TABLE" xbox_in '{ }'
nft add rule "$TABLE" xbox_shield ip daddr "$XBOX_IP" jump xbox_in
nft add rule "$TABLE" xbox_shield ip6 daddr "$XBOX_IP" jump xbox_in 2>/dev/null || true
}
_add_global_tiny_game_drops() {
local udp_set tcp_set tcp_meta_max
udp_set="{ $(udp_ports_csv) }"
tcp_set="{ $(tcp_ports_csv) }"
tcp_meta_max=$((TINY_MAX + 60))
if nft -a list chain "$TABLE" xbox_in 2>/dev/null | grep -q 'global tiny udp game port'; then
return 0
fi
nft add counter "$TABLE" global_tiny_drop 2>/dev/null || true
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" \
udp length 0-"$TINY_MAX" counter name global_tiny_drop drop \
comment '"global tiny udp game port"'
nft add rule "$TABLE" xbox_in ip protocol tcp tcp dport "$tcp_set" tcp flags syn \
meta length 0-"$tcp_meta_max" counter name global_tiny_drop drop \
comment '"global tiny tcp game port"'
}
cmd_peer_guard() {
need_root
require_xbox
mapfile -t all_peers < <(merge_peer_ips "$@")
if ((${#all_peers[@]} == 0)); then
log "peer-guard — no blocked peers"
return 0
fi
_ensure_peer_guard_table
write_peer_list "${all_peers[@]}"
local added
added="$(ensure_suspicious_peer_rules "${all_peers[@]}")"
_add_global_tiny_game_drops
mkdir -p "$(dirname "$STATE")"
{
echo "mode=peer-guard"
echo "level=peer-guard"
echo "persistent_peers=$(grep -cve '^[[:space:]]*$' "$PERSISTENT_PEERS" 2>/dev/null || echo 0)"
echo "suspicious_peers=${#all_peers[@]}"
echo "tiny_max_bytes=${TINY_MAX}"
} >"$STATE"
log "PEER-GUARD — ${added}/${#all_peers[@]} blocked peer(s); tiny game-port drops active"
}
cmd_relax() {
need_root
mapfile -t guard_peers < <(merge_peer_ips)
nft delete table "$TABLE" 2>/dev/null || true
relax_honeypot_nat
rm -f "$STATE" "/var/lib/array-firewall/suspicious-peers.txt"
if ((${#guard_peers[@]} > 0)); then
cmd_peer_guard "${guard_peers[@]}"
return
fi
log "RELAX — gaming nft table removed (persistent peer list retained)"
}
_first_xbox_in_handle() {
nft -a list chain "$TABLE" xbox_in 2>/dev/null \
| awk '/^[[:space:]]+(ip |meta |ct |counter )/ && /# handle/ { sub(/.*# handle /, ""); sub(/ .*/, ""); print; exit }'
}
_suspicious_peer_rules_present() {
nft -a list chain "$TABLE" xbox_in 2>/dev/null | grep -q 'blocked peer priority'
}
_populate_suspicious_peers_set() {
local -a peers=("$@")
if ! nft list set "$TABLE" suspicious_peers >/dev/null 2>&1; then
nft add set "$TABLE" suspicious_peers '{ type ipv4_addr; flags dynamic; size 256; }'
else
nft flush set "$TABLE" suspicious_peers 2>/dev/null || true
fi
local peer_ip added=0
for peer_ip in "${peers[@]}"; do
[[ -n "$peer_ip" ]] || continue
nft add element "$TABLE" suspicious_peers "{ $peer_ip }" 2>/dev/null && added=$((added + 1)) || true
done
echo "$added"
}
_add_suspicious_peer_nft_rules() {
local mode="${1:-add}"
local pos="${2:-}"
load_probe_confusion
_addrule() {
if [[ "$mode" == "insert" && -n "$pos" ]]; then
nft insert rule "$TABLE" xbox_in position "$pos" "$@"
else
nft add rule "$TABLE" xbox_in "$@"
fi
}
_addrule ip saddr @suspicious_peers ip protocol udp udp length 0-"$TINY_MAX" \
counter drop comment '"peer tiny probe"'
_addrule ip saddr @suspicious_peers ip protocol tcp tcp flags syn \
meta length 0-$((TINY_MAX + 60)) counter reject with tcp reset comment '"peer tcp probe rst"'
_addrule ip saddr @suspicious_peers ip protocol udp \
limit rate 15/second burst 30 packets accept
_addrule ip saddr @suspicious_peers ip protocol udp counter drop comment '"peer flood cap"'
_addrule ip saddr @suspicious_peers counter drop comment '"blocked peer priority"'
}
_reload_suspicious_peer_rules() {
local h
while read -r h; do
[[ -n "$h" ]] || continue
nft delete rule "$TABLE" xbox_in handle "$h" 2>/dev/null || true
done < <(
nft -a list chain "$TABLE" xbox_in 2>/dev/null | grep -E \
'blocked peer priority|peer tiny probe|probe udp confusion|peer flood cap|peer tcp probe rst' \
| awk '{print $NF}'
)
local pos="$(_first_xbox_in_handle)"
_add_suspicious_peer_nft_rules insert "$pos"
}
_add_suspicious_peer_block_rules() {
local pos="$(_first_xbox_in_handle)"
_add_suspicious_peer_nft_rules insert "$pos"
}
ensure_suspicious_peer_rules() {
local -a all_peers=("$@")
if ((${#all_peers[@]} == 0)); then
echo 0
return 0
fi
if ! _suspicious_peer_rules_present; then
if ! nft list set "$TABLE" suspicious_peers >/dev/null 2>&1; then
nft add set "$TABLE" suspicious_peers '{ type ipv4_addr; flags dynamic; size 256; }'
fi
_add_suspicious_peer_block_rules
fi
_populate_suspicious_peers_set "${all_peers[@]}"
}
cmd_refresh_peers() {
need_root
require_xbox
if ! nft list table "$TABLE" >/dev/null 2>&1; then
cmd_peer_guard "$@"
return $?
fi
mapfile -t all_peers < <(merge_peer_ips "$@")
if ((${#all_peers[@]} == 0)); then
log "refresh-peers — no peers"
return 0
fi
write_peer_list "${all_peers[@]}"
local added
added="$(ensure_suspicious_peer_rules "${all_peers[@]}")"
log "refresh-peers — ${added}/${#all_peers[@]} peer(s) in nft"
}
write_peer_list() {
local peers_file="/var/lib/array-firewall/suspicious-peers.txt"
mkdir -p "$(dirname "$peers_file")"
if [[ $# -gt 0 ]]; then
printf '%s\n' "$@" >"$peers_file"
else
rm -f "$peers_file"
fi
}
merge_peer_ips() {
local -a merged=()
local -A seen=()
local ip
for ip in "$@"; do
ip="${ip%%#*}"
ip="${ip// /}"
[[ -n "$ip" ]] || continue
[[ -n "${seen[$ip]+x}" ]] && continue
seen[$ip]=1
merged+=("$ip")
done
if [[ -f "$PERSISTENT_PEERS" ]]; then
while IFS= read -r ip || [[ -n "$ip" ]]; do
ip="${ip%%#*}"
ip="${ip// /}"
[[ -n "$ip" ]] || continue
[[ -n "${seen[$ip]+x}" ]] && continue
seen[$ip]=1
merged+=("$ip")
done <"$PERSISTENT_PEERS"
fi
printf '%s\n' "${merged[@]}"
}
load_allowlist_cidrs() {
local file="${1:-$MM_ALLOWLIST}"
if [[ ! -f "$file" ]]; then
return 0
fi
python3 - "$file" <<'PY'
import json, sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text())
for c in data.get("cidrs") or []:
print(c.strip())
PY
}
load_mm_cidrs() {
load_allowlist_cidrs "$MM_ALLOWLIST"
}
add_non_game_syn_rst() {
# Remaining TCP SYN (non-game ports) — send RST instead of silent drop (port scan feedback).
nft add rule "$TABLE" xbox_in ip protocol tcp tcp flags syn \
counter name probe_tcp_rst reject with tcp reset comment '"non-game syn rst"'
}
add_wan_scan_policy() {
# DMZ exposes WAN IP — block internet port scans; only Xbox Live / game ports reach console.
local wan_if="${WAN_IF:-eth1}"
local udp_set="$1"
local tcp_set="$2"
nft add counter "$TABLE" wan_scan_drop
nft add rule "$TABLE" xbox_in iifname "$wan_if" ip protocol udp udp dport "$udp_set" accept \
comment '"wan game udp"'
nft add rule "$TABLE" xbox_in iifname "$wan_if" ip protocol tcp tcp dport "$tcp_set" \
ct state new,established,related accept comment '"wan game tcp"'
nft add rule "$TABLE" xbox_in iifname "$wan_if" ip protocol udp udp dport "$HONEYPOT_TCP" \
counter name probe_udp_sink drop comment '"wan honeypot udp"'
nft add rule "$TABLE" xbox_in iifname "$wan_if" ip protocol tcp tcp dport "$HONEYPOT_TCP" \
counter name wan_scan_drop drop comment '"wan honeypot tcp"'
nft add rule "$TABLE" xbox_in iifname "$wan_if" ip protocol tcp tcp flags syn \
counter name probe_tcp_rst reject with tcp reset comment '"wan scan syn rst"'
nft add rule "$TABLE" xbox_in iifname "$wan_if" counter name wan_scan_drop drop \
comment '"wan unsolicited drop"'
log "WAN scan filter — only Xbox game ports reach console from ${wan_if}"
}
setup_wan_scanner_block() {
nft add set "$TABLE" wan_scanners '{ type ipv4_addr; flags timeout; size 4096; }'
nft add counter "$TABLE" wan_scanner_block
local elems=""
if command -v python3 >/dev/null 2>&1; then
elems="$(python3 - <<'PY'
import sys
sys.path.insert(0, "/opt/array-firewall/api")
from lib import wan_scan_block
print(wan_scan_block.render_nft_elements())
PY
)"
fi
if [[ -n "$elems" ]]; then
nft add element "$TABLE" wan_scanners "{ $elems }" 2>/dev/null || true
fi
}
setup_blocked_subnets() {
nft add set "$TABLE" blocked_subnets '{ type ipv4_addr; flags interval, timeout; size 65536; }'
nft add counter "$TABLE" subnet_block
local elems=""
if command -v python3 >/dev/null 2>&1; then
elems="$(python3 - <<'PY'
import sys
sys.path.insert(0, "/opt/array-firewall/api")
from lib import subnet_blocklist
print(subnet_blocklist.render_nft_elements())
PY
)"
fi
if [[ -n "$elems" ]]; then
nft add element "$TABLE" blocked_subnets "{ $elems }" 2>/dev/null || true
fi
}
cmd_shield() {
need_root
require_xbox
ensure_wan_nat_py
local level="${1:-normal}"
shift || true
local -a session_peers=("$@")
if sentinel_tiny_only; then
if ((${#session_peers[@]} == 0)); then
case "$level" in
console|matchmaking|peer-strict|in-match|whitelist|strict)
;; # keep explicit attack profile (abnormal peers use persistent list)
*)
level="$(sentinel_wrapper_level)"
;;
esac
fi
if [[ -f "$STATE" ]] && grep -q '^mode=shield$' "$STATE"; then
current_level="$(grep '^level=' "$STATE" 2>/dev/null | head -1 | cut -d= -f2 || true)"
upgrade_from_line_rate=0
if [[ "$current_level" == "line-rate" && "$level" != "line-rate" && ${#session_peers[@]} -gt 0 ]]; then
upgrade_from_line_rate=1
fi
if (( upgrade_from_line_rate == 0 )) && grep -q "^level=${level}$" "$STATE"; then
mapfile -t _skip_peers < <(merge_peer_ips "${session_peers[@]}")
if ((${#_skip_peers[@]} > 0)) && ! _suspicious_peer_rules_present; then
log "SHIELD active but blocked-peer priority missing — rebuilding"
elif nft list set "$TABLE" wan_scanners >/dev/null 2>&1 \
&& nft list set "$TABLE" blocked_subnets >/dev/null 2>&1; then
if ((${#session_peers[@]} > 0)); then
cmd_refresh_peers "${session_peers[@]}"
return 0
fi
log "SHIELD (${level}/sentinel-wrapper) already active — skip rebuild"
return 0
fi
log "SHIELD active but wan_scanners/blocked_subnets missing — rebuilding"
elif (( upgrade_from_line_rate == 1 )); then
log "SHIELD upgrading line-rate → ${level} (${#session_peers[@]} abnormal peer(s))"
fi
fi
fi
mapfile -t all_peers < <(merge_peer_ips "${session_peers[@]}")
if ((${#all_peers[@]} > 0)); then
write_peer_list "${all_peers[@]}"
else
write_peer_list
fi
cmd_relax
local tiny_rate=30
local tiny_burst=60
local whitelist=0
local peer_strict=0
local matchmaking=0
local console_mode=0
local in_match_mode=0
local allowlist_file="$MM_ALLOWLIST"
local peers_file="/var/lib/array-firewall/suspicious-peers.txt"
case "$level" in
in-match)
in_match_mode=1
console_mode=1
matchmaking=1
whitelist=1
allowlist_file="$IN_MATCH_ALLOWLIST"
tiny_rate=12
tiny_burst=24
;;
console)
console_mode=1
matchmaking=1
whitelist=1
tiny_rate=12
tiny_burst=24
;;
matchmaking)
matchmaking=1
console_mode=1
whitelist=1
tiny_rate=20
tiny_burst=40
;;
strict|whitelist)
whitelist=1
tiny_rate=12
tiny_burst=24
;;
peer-strict)
peer_strict=1
tiny_rate=20
tiny_burst=40
;;
normal|*) ;;
esac
setup_honeypot_nat
local wan_if="${WAN_IF:-eth1}"
local xbox_dmz=0
if command -v python3 >/dev/null 2>&1; then
xbox_dmz="$(python3 - <<PY
import json
from pathlib import Path
p = Path("/var/lib/array-firewall/policies.json")
c = Path("/etc/array-firewall/array-firewall.conf")
xbox = ""
if c.is_file():
for line in c.read_text().splitlines():
if line.startswith("XBOX_IP="):
xbox = line.split("=", 1)[1].strip()
if p.is_file():
d = json.loads(p.read_text())
dmz = d.get("dmz") or {}
if dmz.get("enabled") and (dmz.get("host_ip") or "") == xbox:
print("1")
raise SystemExit
print("0")
PY
)"
fi
nft add table "$TABLE"
nft add chain "$TABLE" xbox_shield '{ type filter hook forward priority filter; policy accept; }'
nft add chain "$TABLE" xbox_in '{ }'
nft add counter "$TABLE" probe_tcp_rst
nft add counter "$TABLE" probe_udp_sink
nft add rule "$TABLE" xbox_shield ip daddr "$XBOX_IP" jump xbox_in
nft add rule "$TABLE" xbox_shield ip6 daddr "$XBOX_IP" jump xbox_in 2>/dev/null || true
if [[ "$peer_strict" == "1" || ${#all_peers[@]} -gt 0 ]]; then
nft add set "$TABLE" suspicious_peers '{ type ipv4_addr; flags dynamic; size 256; }'
for peer_ip in "${all_peers[@]}"; do
[[ -n "$peer_ip" ]] || continue
nft add element "$TABLE" suspicious_peers "{ $peer_ip }" 2>/dev/null || true
done
_add_suspicious_peer_nft_rules add
fi
nft add rule "$TABLE" xbox_in ct state established,related accept
setup_wan_scanner_block
setup_blocked_subnets
nft add rule "$TABLE" xbox_in ip saddr @wan_scanners counter name wan_scanner_block drop \
comment '"wan-scanner-block"'
nft add rule "$TABLE" xbox_in ip saddr @blocked_subnets counter name subnet_block drop \
comment '"auto-blocked-subnet"'
# Full WAN DMZ DNAT marks every inbound scan as dnat — do not blanket-accept those.
if [[ "$xbox_dmz" != "1" ]]; then
nft add rule "$TABLE" xbox_in ct status dnat accept comment '"wan port-forward"'
fi
local lan_cidr="${LAN_CIDR:-}"
if [[ -z "$lan_cidr" && -n "${XBOX_IP:-}" ]]; then
lan_cidr="${XBOX_IP%.*}.0/24"
fi
if [[ -n "$lan_cidr" ]]; then
nft add rule "$TABLE" xbox_in ip saddr "$lan_cidr" accept comment '"lan-local"'
fi
local udp_set tcp_set
udp_set="{ $(udp_ports_csv) }"
tcp_set="{ $(tcp_ports_csv) }"
local tcp_meta_max=$((TINY_MAX + 60))
# Tiny probes on game ports — drop before any wide accept (including WAN DMZ).
nft add counter "$TABLE" global_tiny_drop
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" \
udp length 0-"$TINY_MAX" counter name global_tiny_drop drop \
comment '"global tiny udp game port"'
nft add rule "$TABLE" xbox_in ip protocol tcp tcp dport "$tcp_set" tcp flags syn \
meta length 0-"$tcp_meta_max" counter name global_tiny_drop drop \
comment '"global tiny tcp game port"'
if [[ "$xbox_dmz" == "1" ]]; then
add_wan_scan_policy "$udp_set" "$tcp_set"
fi
if [[ "$matchmaking" == "1" || "$console_mode" == "1" ]]; then
mapfile -t mm_cidrs < <(load_allowlist_cidrs "$allowlist_file")
if ((${#mm_cidrs[@]} > 0)); then
local mm_csv
mm_csv=$(IFS=,; echo "${mm_cidrs[*]}")
nft add set "$TABLE" mm_allow '{ type ipv4_addr; flags interval; }'
nft add element "$TABLE" mm_allow "{ $mm_csv }" 2>/dev/null || true
local service_udp="{ 53, 88, 500, 3544, 4500, 9002 }"
local service_tcp="{ 53, 80, 443, 2869 }"
# Xbox Live / Azure — service ports only (never P2P game ports).
nft add rule "$TABLE" xbox_in ip saddr @mm_allow ip protocol tcp tcp dport "$service_tcp" accept \
comment '"allowlist service tcp"'
nft add rule "$TABLE" xbox_in ip saddr @mm_allow ip protocol udp udp dport "$service_udp" accept \
comment '"allowlist service udp"'
# Outbound: explicit allow for dedicated-server / allowlisted backends (default accept).
nft add chain "$TABLE" xbox_out '{ }'
nft add rule "$TABLE" xbox_shield ip saddr "$XBOX_IP" jump xbox_out
nft add rule "$TABLE" xbox_out ip daddr @mm_allow ip protocol udp udp dport "$udp_set" accept \
comment '"allowlist game udp out"'
nft add rule "$TABLE" xbox_out ip daddr @mm_allow ip protocol tcp tcp dport "$tcp_set" accept \
comment '"allowlist game tcp out"'
nft add rule "$TABLE" xbox_out accept comment '"xbox outbound default accept"'
fi
fi
# Block all inbound P2P to Xbox — unless Open NAT mode (Xbox Live needs 3074 inbound).
local xbox_nat_open=0
if [[ "${XBOX_NAT_OPEN:-0}" == "1" ]]; then
xbox_nat_open=1
elif command -v python3 >/dev/null 2>&1; then
xbox_nat_open="$(python3 - <<'PY'
import json
from pathlib import Path
p = Path("/var/lib/array-firewall/policies.json")
if p.is_file():
g = json.loads(p.read_text()).get("gaming") or {}
print("1" if g.get("nat_open") else "0")
else:
print("0")
PY
)"
fi
if [[ "$xbox_nat_open" != "1" && "$xbox_dmz" != "1" ]]; then
nft add counter "$TABLE" p2p_block
nft add rule "$TABLE" xbox_in ip protocol udp udp dport { 3074, 3075 } \
counter name p2p_block drop comment '"block all p2p udp"'
nft add rule "$TABLE" xbox_in ip protocol tcp tcp dport 3074 tcp flags syn \
counter name p2p_block drop comment '"block all p2p tcp syn"'
nft add rule "$TABLE" xbox_in ip protocol udp udp dport 1024-65535 udp dport != { 53, 88, 500, 3544, 4500, 9002 } \
counter name p2p_block drop comment '"block high-port p2p udp"'
else
log "NAT open — inbound Xbox Live ports allowed (3074/88/500/3544/4500)"
fi
if [[ "$console_mode" == "1" ]]; then
nft add counter "$TABLE" console_non_game_drop
if [[ "$xbox_dmz" != "1" ]]; then
nft add counter "$TABLE" console_p2p_drop
# Non-allowlist P2P-shaped game-port traffic — not real lobby players; drop all sizes.
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" \
counter name console_p2p_drop drop comment '"console non-allowlist game udp"'
nft add rule "$TABLE" xbox_in ip protocol tcp tcp dport "$tcp_set" \
counter name console_p2p_drop drop comment '"console non-allowlist game tcp"'
fi
else
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" \
udp length 0-"$TINY_MAX" limit rate "${tiny_rate}/second" burst "${tiny_burst} packets" accept
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" \
udp length 0-"$TINY_MAX" counter drop comment '"tiny udp game"'
nft add rule "$TABLE" xbox_in ip protocol tcp tcp dport "$tcp_set" tcp flags syn \
meta length 0-"$tcp_meta_max" limit rate "${tiny_rate}/second" burst "${tiny_burst} packets" accept
nft add rule "$TABLE" xbox_in ip protocol tcp tcp dport "$tcp_set" tcp flags syn \
meta length 0-"$tcp_meta_max" counter drop comment '"tiny tcp game"'
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" \
meter xbox_udp_per_src "{ ip saddr limit rate over ${PER_SRC_UDP_RATE}/second burst 200 packets }" accept \
comment '"per-src game udp rate"'
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" \
counter drop comment '"per-src game udp flood"'
nft add rule "$TABLE" xbox_in ct state new ip protocol udp \
meter xbox_conn_cap "{ ip saddr ct count over ${CONN_CAP_PER_PEER} }" drop \
comment '"conn cap per peer"'
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$udp_set" accept
nft add rule "$TABLE" xbox_in ip protocol tcp tcp dport "$tcp_set" ct state new,established,related accept
fi
# Obvious probe UDP ports (not game) — log and drop
nft add rule "$TABLE" xbox_in ip protocol udp udp dport "$HONEYPOT_TCP" \
counter name probe_udp_sink drop comment '"honeypot udp sink"'
if [[ "$whitelist" == "1" ]]; then
add_non_game_syn_rst
if [[ "$console_mode" == "1" ]]; then
nft add rule "$TABLE" xbox_in counter name console_non_game_drop drop comment '"console non-game inbound"'
else
nft add rule "$TABLE" xbox_in counter drop comment '"non-game inbound"'
fi
else
nft add rule "$TABLE" xbox_in ip protocol udp udp length 0-"$TINY_MAX" drop
nft add rule "$TABLE" xbox_in ip protocol udp limit rate 600/second burst 1200 packets accept
nft add rule "$TABLE" xbox_in ip protocol udp drop
add_non_game_syn_rst
nft add rule "$TABLE" xbox_in accept
fi
{
echo "mode=shield"
echo "level=${level}"
echo "whitelist_non_game=${whitelist}"
echo "console_mode=${console_mode}"
echo "in_match_mode=${in_match_mode}"
echo "allowlist=${allowlist_file}"
echo "peer_strict=${peer_strict}"
echo "matchmaking_allowlist=${matchmaking}"
echo "honeypot_sink_port=${PROBE_SINK_PORT}"
echo "tcp_rst_non_game=1"
echo "persistent_peers=$(grep -cve '^[[:space:]]*$' "$PERSISTENT_PEERS" 2>/dev/null || echo 0)"
if [[ -f "$peers_file" ]]; then
echo "suspicious_peers=$(grep -cve '^[[:space:]]*$' "$peers_file" 2>/dev/null || echo 0)"
fi
echo "p2p_block=$([[ "$xbox_nat_open" == "1" || "$xbox_dmz" == "1" ]] && echo off || echo all_inbound)"
echo "xbox_wan_dmz=$([[ "$xbox_dmz" == "1" ]] && echo 1 || echo 0)"
echo "xbox_out_allowlist=1"
echo "backend=nftables"
echo "tiny_max_bytes=${TINY_MAX}"
echo "tiny_rate_per_src=${tiny_rate}/sec"
echo "per_src_udp_rate=${PER_SRC_UDP_RATE}/sec"
echo "conn_cap_per_peer=${CONN_CAP_PER_PEER}"
echo "xbox_ip=${XBOX_IP}"
echo "tiny_packet_only=$(sentinel_tiny_only && echo 1 || echo 0)"
echo "updated=$(date -Is)"
} >"$STATE"
if ! sentinel_tiny_only; then
if command -v conntrack >/dev/null 2>&1; then
conntrack -D -d "$XBOX_IP" 2>/dev/null || true
conntrack -D -s "$XBOX_IP" 2>/dev/null || true
fi
fi
log "SHIELD (${level}) active for ${XBOX_IP} — RST on non-game SYN, honeypot :${PROBE_SINK_PORT}"
}
cmd_line_rate() {
need_root
require_xbox
ensure_wan_nat_py
cmd_relax
nft add table "$TABLE"
nft add chain "$TABLE" xbox_shield '{ type filter hook forward priority filter - 10; policy accept; }'
nft add chain "$TABLE" xbox_in '{ }'
nft add chain "$TABLE" xbox_out '{ }'
nft add rule "$TABLE" xbox_shield ip daddr "$XBOX_IP" jump xbox_in
nft add rule "$TABLE" xbox_shield ip saddr "$XBOX_IP" jump xbox_out
nft add rule "$TABLE" xbox_in ct state established,related accept comment '"line-rate established"'
nft add rule "$TABLE" xbox_in accept comment '"line-rate inbound"'
nft add rule "$TABLE" xbox_out accept comment '"line-rate outbound"'
{
echo "mode=shield"
echo "level=line-rate"
echo "line_rate=1"
echo "console_mode=0"
echo "xbox_ip=${XBOX_IP}"
echo "p2p_block=off"
echo "updated=$(date -Is)"
} >"$STATE"
log "LINE-RATE forward for ${XBOX_IP} — minimal nft (throughput / speed-test path)"
}
cmd_refresh_probe_rules() {
need_root
require_xbox
if ! nft list table "$TABLE" >/dev/null 2>&1; then
log "refresh-probe-rules — gaming table missing"
return 0
fi
load_probe_confusion
mapfile -t all_peers < <(merge_peer_ips)
if ! nft list set "$TABLE" suspicious_peers >/dev/null 2>&1; then
nft add set "$TABLE" suspicious_peers '{ type ipv4_addr; flags dynamic; size 256; }'
fi
_reload_suspicious_peer_rules
if ((${#all_peers[@]} > 0)); then
write_peer_list "${all_peers[@]}"
_populate_suspicious_peers_set "${all_peers[@]}"
fi
systemctl enable array-firewall-probe-udp-confusion 2>/dev/null || true
systemctl restart array-firewall-probe-udp-confusion 2>/dev/null || true
log "refresh-probe-rules — confusion=${PROBE_CONFUSION_UDP} queue=${PROBE_CONFUSION_QUEUE} peers=${#all_peers[@]}"
}
case "${1:-status}" in
status) cmd_status ;;
shield) shift; cmd_shield "$@" ;;
line-rate|linerate) cmd_line_rate ;;
console) cmd_shield console ;;
in-match) cmd_shield in-match ;;
strict) cmd_shield strict ;;
peer-strict) cmd_shield peer-strict ;;
matchmaking) cmd_shield matchmaking ;;
refresh-peers) shift; cmd_refresh_peers "$@" ;;
refresh-probe-rules) cmd_refresh_probe_rules ;;
peer-guard) shift; cmd_peer_guard "$@" ;;
relax|off) cmd_relax ;;
*) echo "Usage: $0 {status|line-rate|shield [normal|strict|...]|console|relax}"; exit 2 ;;
esac