Files
Das-OS/srcpkgs/das-helpers/files/dfw
T
dasandClaude Opus 5.5 acb7532a4f Stage 7: command-line helpers dsv, duser, dfw, dsys (0.5.0)
- das-helpers replaces the dsv package and adds shared functions in
  /usr/lib/das/helpers.sh (bilingual messages, sudo re-exec, prompts).
- dsv new creates a runit service (command, user, directory, environment,
  svlogd log) and checks it stays up; dsv rm deletes one not from a package.
- duser: list, add (installer groups, --admin, key, password), del, passwd,
  lock/unlock (key logins too), key add|list|rm, groups +/-.
- dfw: allow/deny/status/reload for the tcp_ports and udp_ports sets of the
  inet das table, kept in /etc/nftables.d/dfw.nft and checked with nft -c.
- dsys: info, hostname, timezone, locale, keymap.
- xtools-minimal on both images.
- nftables.conf no longer suggests restarting the nftables service: its
  finish script flushes the whole ruleset, fail2ban's and Docker's included.
- das-server-install 0.2.0 mentions the helpers when it finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:58:25 +03:00

241 lines
8.7 KiB
Bash
Executable File

#!/bin/bash
# dfw - open and close ports in the Das OS server firewall (nftables).
#
# /etc/nftables.conf defines the "das" table with the sets tcp_ports and
# udp_ports; dfw keeps their elements in /etc/nftables.d/dfw.nft, which that
# file includes, and reloads only the "das" table. SSH is always open.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
conf=/etc/nftables.conf
ports_file=/etc/nftables.d/dfw.nft
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
Використання: dfw КОМАНДА [ПОРТ...]
status стан брандмауера й відкриті порти (типова команда)
allow ПОРТ... відкрити порти для вхідних з'єднань
deny ПОРТ... закрити порти, відкриті через allow
reload перечитати /etc/nftables.conf після редагування
ПОРТ: 80, 80/tcp, 53/udp, 8000-8010/tcp або назва служби (http, https).
Без /udp мається на увазі tcp. SSH відкритий завжди.
Порти, які публікують контейнери Docker (-p 8080:80), відкриває сам Docker,
dfw на них не впливає.
EOF
else
cat <<'EOF'
Usage: dfw COMMAND [PORT...]
status firewall state and open ports (the default)
allow PORT... open ports for incoming connections
deny PORT... close ports opened with allow
reload load /etc/nftables.conf again after editing it
PORT: 80, 80/tcp, 53/udp, 8000-8010/tcp or a service name (http, https).
Without /udp it means tcp. SSH is always open.
Ports that Docker containers publish (-p 8080:80) are opened by Docker
itself, dfw does not affect them.
EOF
fi
}
check_config() {
grep -qs 'set tcp_ports' "$conf" ||
die "$conf не з Das OS Server (немає набору tcp_ports); dfw працює лише з ним" \
"$conf is not the Das OS Server one (no tcp_ports set); dfw only works with that"
}
# Opened ports of one protocol from the ports file, one per line
saved_ports() {
[[ -r $ports_file ]] || return 0
sed -n "s/^add element inet das $1_ports { \(.*\) }$/\1/p" "$ports_file" | tr -d ' ' | tr ',' '\n'
}
# parse_port SPEC: prints "PORT PROTO" with PORT a number or a FIRST-LAST range
parse_port() {
local spec=$1 port proto=tcp first last
if [[ $spec == */* ]]; then
port=${spec%/*}
proto=${spec##*/}
else
port=$spec
fi
[[ $proto == tcp || $proto == udp ]] || die "'$spec': протокол tcp або udp" "'$spec': the protocol is tcp or udp"
if [[ $port =~ ^[a-z][a-z0-9-]*$ ]]; then
# A service name from /etc/services
first=$(getent services "$port/$proto" | awk '{ print $2 }')
[[ -n $first ]] || die "'$spec': невідома служба $port/$proto" "'$spec': unknown service $port/$proto"
port=${first%/*}
fi
if [[ $port =~ ^([0-9]+)-([0-9]+)$ ]]; then
first=${BASH_REMATCH[1]}
last=${BASH_REMATCH[2]}
elif [[ $port =~ ^[0-9]+$ ]]; then
first=$port
last=$port
else
die "'$spec': порт — число, діапазон 8000-8010 або назва служби" \
"'$spec': a port is a number, a range like 8000-8010 or a service name"
fi
((first >= 1 && last <= 65535 && first <= last)) ||
die "'$spec': порти від 1 до 65535" "'$spec': ports are 1 to 65535"
if ((first == last)); then
echo "$first $proto"
else
echo "$first-$last $proto"
fi
}
firewall_on() {
[[ -e /etc/runit/runsvdir/default/nftables ]]
}
# save TCP_PORTS UDP_PORTS (newline-separated): writes the ports file, checks
# the whole configuration and loads it, restoring the old file on an error
save() {
local tcp udp backup=""
# sort -n -u would take 80 and 80-90 for duplicates
tcp=$(sort -u <<<"$1" | sort -n | grep . | paste -sd, - | sed 's/,/, /g' || true)
udp=$(sort -u <<<"$2" | sort -n | grep . | paste -sd, - | sed 's/,/, /g' || true)
mkdir -p "${ports_file%/*}"
if [[ -e $ports_file ]]; then
backup=$(mktemp)
cp "$ports_file" "$backup"
fi
{
echo "# Ports opened with dfw; change them with dfw allow/deny"
[[ -z $tcp ]] || echo "add element inet das tcp_ports { $tcp }"
[[ -z $udp ]] || echo "add element inet das udp_ports { $udp }"
} >"$ports_file"
if ! nft -c -f "$conf"; then
if [[ -n $backup ]]; then mv "$backup" "$ports_file"; else rm -f "$ports_file"; fi
die "nftables не прийняв правила, нічого не змінено" "nftables rejected the rules, nothing changed"
fi
rm -f "$backup"
if firewall_on; then
nft -f "$conf"
else
warn "брандмауер вимкнений, зміни збережено; увімкнути: dsv enable nftables" \
"the firewall is off, changes are saved; to turn it on: dsv enable nftables"
fi
}
cmd_allow() {
local spec port proto tcp udp
tcp=$(saved_ports tcp)
udp=$(saved_ports udp)
for spec in "$@"; do
read -r port proto < <(parse_port "$spec")
if [[ $port == 22 && $proto == tcp ]]; then
say "22/tcp (SSH) відкритий завжди" "22/tcp (SSH) is always open"
continue
fi
if grep -qx "$port" <<<"$(if [[ $proto == tcp ]]; then echo "$tcp"; else echo "$udp"; fi)"; then
say "$port/$proto: уже відкритий" "$port/$proto: already open"
continue
fi
if [[ $proto == tcp ]]; then tcp+=$'\n'$port; else udp+=$'\n'$port; fi
say "$port/$proto: відкрито" "$port/$proto: open"
done
save "$tcp" "$udp"
}
cmd_deny() {
local spec port proto tcp udp list
tcp=$(saved_ports tcp)
udp=$(saved_ports udp)
for spec in "$@"; do
read -r port proto < <(parse_port "$spec")
if [[ $port == 22 && $proto == tcp ]]; then
die "SSH не закривається через dfw, щоб не втратити доступ до сервера; правило — у $conf" \
"SSH is not closed with dfw, so as not to lose access to the server; the rule is in $conf"
fi
if [[ $proto == tcp ]]; then list=$tcp; else list=$udp; fi
if ! grep -qx "$port" <<<"$list"; then
say "$port/$proto: не був відкритий через dfw" "$port/$proto: was not opened with dfw"
continue
fi
list=$(grep -vx "$port" <<<"$list" || true)
if [[ $proto == tcp ]]; then tcp=$list; else udp=$list; fi
say "$port/$proto: закрито" "$port/$proto: closed"
done
save "$tcp" "$udp"
}
cmd_status() {
local p state banned ports
if firewall_on && nft list table inet das >/dev/null 2>&1; then
state=$(say "увімкнений" "on")
else
state=$(say "ВИМКНЕНИЙ (dsv enable nftables)" "OFF (dsv enable nftables)")
fi
say "Брандмауер: $state" "Firewall: $state"
say "Вхідні з'єднання закриті, крім:" "Incoming connections are closed except:"
echo " 22/tcp (ssh)"
for p in tcp udp; do
while read -r ports; do
[[ -n $ports ]] && echo " $ports/$p"
done < <(saved_ports "$p")
done
# Docker opens its published ports itself
if command -v docker >/dev/null && ports=$(docker ps --format '{{.Names}}: {{.Ports}}' 2>/dev/null) &&
grep -q -- '->' <<<"$ports"; then
say "Опубліковані контейнерами Docker (відкриті незалежно від dfw):" \
"Published by Docker containers (open regardless of dfw):"
grep -- '->' <<<"$ports" | sed 's/^/ /'
fi
if banned=$(fail2ban-client status sshd 2>/dev/null | sed -n 's/.*Currently banned:[[:space:]]*//p') &&
[[ -n $banned && $banned != 0 ]]; then
say "fail2ban: заблоковано адрес за невдалі входи SSH: $banned" \
"fail2ban: addresses banned for failed SSH logins: $banned"
fi
}
cmd=${1:-status}
[[ $# -gt 0 ]] && shift
case $cmd in
status | list | ls)
need_root status
check_config
cmd_status
;;
allow | open)
[[ $# -gt 0 ]] || die "вкажіть порт: dfw allow 80/tcp" "give a port: dfw allow 80/tcp"
need_root allow "$@"
check_config
cmd_allow "$@"
;;
deny | close)
[[ $# -gt 0 ]] || die "вкажіть порт: dfw deny 80/tcp" "give a port: dfw deny 80/tcp"
need_root deny "$@"
check_config
cmd_deny "$@"
;;
reload)
need_root reload
nft -c -f "$conf"
if firewall_on; then
nft -f "$conf"
say "правила завантажено" "rules loaded"
else
say "правила правильні, але брандмауер вимкнений: dsv enable nftables" \
"the rules are valid, but the firewall is off: dsv enable nftables"
fi
;;
-h | --help | help) usage ;;
*)
usage >&2
exit 1
;;
esac