- 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>
241 lines
8.7 KiB
Bash
Executable File
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
|