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>
This commit is contained in:
Executable
+240
@@ -0,0 +1,240 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user