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:
das
2026-10-20 06:58:25 +03:00
co-authored by Claude Opus 5.5
parent 84768962fc
commit acb7532a4f
21 changed files with 1342 additions and 99 deletions
+240
View File
@@ -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
@@ -0,0 +1,22 @@
# bash completion for dfw
# shellcheck shell=bash
_dfw() {
local cur=${COMP_WORDS[COMP_CWORD]}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "status allow deny reload help" -- "$cur")
return
fi
case ${COMP_WORDS[1]} in
deny)
# The ports opened with dfw
local ports
ports=$(sed -n 's/^add element inet das \(tcp\|udp\)_ports { \(.*\) }$/\2 \1/p' /etc/nftables.d/dfw.nft 2>/dev/null |
awk '{ for (i = 1; i < NF; i++) { p = $i; sub(/,$/, "", p); print p "/" $NF } }')
mapfile -t COMPREPLY < <(compgen -W "$ports" -- "$cur")
;;
allow)
mapfile -t COMPREPLY < <(compgen -W "http https 80/tcp 443/tcp 53/udp" -- "$cur")
;;
esac
}
complete -F _dfw dfw
@@ -7,6 +7,9 @@
# services that log to syslog, from socklog.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
sv_dir=/etc/sv
runlevel=/etc/runit/runsvdir/default
# Outside a running system (a chroot, an image) /var/service may not exist
@@ -14,22 +17,6 @@ service_dir=/var/service
[[ -d $service_dir ]] || service_dir=$runlevel
socklog=/var/log/socklog
if [[ ${LANG:-} == uk* || ${LC_ALL:-} == uk* || ${LC_MESSAGES:-} == uk* ]]; then
uk=1
else
uk=""
fi
# say UKRAINIAN ENGLISH: prints the message in the user's language
say() {
if [[ -n $uk ]]; then echo "$1"; else echo "$2"; fi
}
die() {
say "dsv: $1" "dsv: $2" >&2
exit 1
}
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
@@ -42,6 +29,16 @@ usage() {
start|stop|restart СЛУЖБА...
запустити, зупинити, перезапустити
logs СЛУЖБА [-f] журнал служби (-f: стежити за новими рядками)
new [СЛУЖБА] [ПАРАМЕТРИ]
створити службу для своєї програми; чого не
вказано, те запитає:
--cmd КОМАНДА що запускати (програма не має йти у фон)
--user КОРИСТУВАЧ від чийого імені
--dir КАТАЛОГ робочий каталог
--env ІМ'Я=ЗНАЧЕННЯ змінна середовища, можна кілька разів
--no-log без власного журналу в /var/log/СЛУЖБА
--enable одразу ввімкнути й запустити
rm СЛУЖБА вимкнути й видалити службу, створену через new
Служби — каталоги в /etc/sv. Без команди виконується list.
EOF
@@ -56,21 +53,23 @@ Usage: dsv COMMAND [SERVICE...]
start|stop|restart SERVICE...
start, stop, restart
logs SERVICE [-f] the service log (-f: follow new lines)
new [SERVICE] [OPTIONS] create a service for your own program; asks for
what is not given:
--cmd COMMAND what to run (the program must stay in the foreground)
--user USER who runs it
--dir DIR working directory
--env NAME=VALUE an environment variable, may be repeated
--no-log no log of its own in /var/log/SERVICE
--enable enable and start it right away
rm SERVICE disable and delete a service made with new
Services are the directories in /etc/sv. Without a command, runs list.
EOF
fi
}
# Service states live in root-only supervise directories
need_root() {
if [[ $EUID -ne 0 ]]; then
exec sudo "$0" "$@"
fi
}
check_service() {
[[ -d $sv_dir/$1 ]] || die "немає служби '$1' (див. ls $sv_dir)" "no service '$1' (see ls $sv_dir)"
[[ -d $sv_dir/$1 ]] || die "немає служби '$1' (див. dsv list)" "no service '$1' (see dsv list)"
}
is_enabled() {
@@ -87,14 +86,12 @@ wait_supervised() {
return 1
}
# row NAME ENABLED STATE PID UPTIME: one line of the list. printf pads by
# bytes, ${#s} counts characters, so Cyrillic columns stay aligned.
# row NAME ENABLED STATE PID UPTIME: one line of the list
row() {
local s widths=(24 9 6 8) i=0
for s in "$1" "$2" "$3" "$4"; do
printf '%s%*s ' "$s" $((widths[i] - ${#s})) ''
i=$((i + 1))
done
pad "$1" 24
pad "$2" 9
pad "$3" 6
pad "$4" 8
printf '%s\n' "$5"
}
@@ -240,6 +237,151 @@ cmd_logs() {
fi
}
# check_running NAME: after a start, tells whether the service stays up or
# keeps restarting (runsv restarts a program that exits, once a second)
check_running() {
local line up='^run:[^;]*\) ([0-9]+)s'
sleep 3
line=$(sv status "$service_dir/$1" 2>/dev/null || true)
if [[ $line =~ $up ]] && ((BASH_REMATCH[1] >= 2)); then
say "$1: працює" "$1: running"
else
warn "$1 не працює стабільно (${line%%;*}); див. dsv logs $1" \
"$1 does not stay up (${line%%;*}); see dsv logs $1"
fi
}
cmd_new() {
local name="" run_cmd="" user="" dir="" log=1 enable="" envs=() e home bin
while (($#)); do
case $1 in
--cmd) run_cmd=${2:?}; shift ;;
--user) user=${2:?}; shift ;;
--dir) dir=${2:?}; shift ;;
--env) envs+=("${2:?}"); shift ;;
--no-log) log="" ;;
--enable) enable=1 ;;
-*) die "невідомий параметр $1 (dsv help)" "unknown option $1 (dsv help)" ;;
*) [[ -z $name ]] || die "зайвий аргумент $1" "extra argument $1"; name=$1 ;;
esac
shift
done
if [[ -z $name ]]; then
interactive || die "вкажіть назву служби" "name the service"
ask name "Назва служби" "Service name"
fi
[[ $name =~ ^[A-Za-z0-9][A-Za-z0-9_.@-]*$ ]] ||
die "назва '$name': лише латинські літери, цифри, '.', '_', '-', '@'" \
"name '$name': only Latin letters, digits, '.', '_', '-', '@'"
[[ ! -e $sv_dir/$name ]] || die "служба $name уже є" "service $name already exists"
if [[ -z $run_cmd ]]; then
interactive || die "вкажіть команду: --cmd" "give the command: --cmd"
say "Команда запуску. Програма має працювати на передньому плані (без -d/--daemon)." \
"The command to run. The program must stay in the foreground (no -d/--daemon)."
ask run_cmd "Команда" "Command"
[[ -n $run_cmd ]] || die "команду не вказано" "no command given"
fi
read -r bin _ <<<"$run_cmd"
if [[ $bin == */* ]]; then
[[ -x $bin ]] || die "$bin не існує або не виконуваний" "$bin does not exist or is not executable"
elif ! command -v "$bin" >/dev/null; then
die "команду $bin не знайдено; вкажіть повний шлях" "command $bin not found; give the full path"
fi
if [[ -z $user ]] && interactive; then
ask user "Від імені користувача" "Run as user" "${SUDO_USER:-root}"
fi
user=${user:-root}
id -u "$user" >/dev/null 2>&1 || die "немає користувача $user" "no user $user"
home=$(getent passwd "$user" | cut -d: -f6)
if [[ -z $dir ]] && interactive; then
ask dir "Робочий каталог" "Working directory" "$home"
fi
dir=${dir:-$home}
[[ -d $dir ]] || die "немає каталогу $dir" "no directory $dir"
if ((${#envs[@]} == 0)) && interactive; then
ask e "Змінні середовища ІМ'Я=ЗНАЧЕННЯ через пробіл (Enter — без них)" \
"Environment variables NAME=VALUE separated by spaces (Enter for none)"
read -ra envs <<<"$e"
fi
for e in "${envs[@]}"; do
[[ $e =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || die "'$e' не має вигляду ІМ'Я=ЗНАЧЕННЯ" "'$e' is not NAME=VALUE"
done
# Build it aside, then move it in place in one step
local tmp=$sv_dir/.$name.new
rm -rf "$tmp"
mkdir -p "$tmp/env"
# chpst does not set these for the new user
printf '%s\n' "$home" >"$tmp/env/HOME"
printf '%s\n' "$user" >"$tmp/env/USER"
for e in "${envs[@]}"; do
printf '%s\n' "${e#*=}" >"$tmp/env/${e%%=*}"
done
cat >"$tmp/run" <<EOF
#!/bin/sh
# Created by dsv new. After editing: dsv restart $name
# Environment variables: one file per variable in $sv_dir/$name/env
exec 2>&1
cd '$dir' || exit 1
exec chpst -u $user -e $sv_dir/$name/env $run_cmd
EOF
chmod 755 "$tmp/run"
if [[ -n $log ]]; then
mkdir -p "$tmp/log" "/var/log/$name"
cat >"$tmp/log/run" <<EOF
#!/bin/sh
# Log of $name, read with: dsv logs $name
mkdir -p /var/log/$name
exec svlogd -tt /var/log/$name
EOF
chmod 755 "$tmp/log/run"
fi
mv "$tmp" "$sv_dir/$name"
say "$name: створена в $sv_dir/$name" "$name: created in $sv_dir/$name"
if [[ -z $enable ]] && interactive; then
confirm "Увімкнути й запустити зараз?" "Enable and start it now?" && enable=1
fi
if [[ -n $enable ]]; then
cmd_enable "$name"
[[ $service_dir == /var/service ]] && check_running "$name"
else
say "Запустити: dsv enable $name" "To start it: dsv enable $name"
fi
}
cmd_rm() {
local name=${1:-} i
[[ -n $name ]] || die "вкажіть службу" "name a service"
check_service "$name"
# Services from packages come back with the next update; disable them instead
# (xbps-query -o succeeds even when no package owns the file)
if [[ -n $(xbps-query -o "$sv_dir/$name/run" 2>/dev/null) ]]; then
die "$name встановлена пакетом, її можна лише вимкнути: dsv disable $name" \
"$name comes from a package, it can only be disabled: dsv disable $name"
fi
if interactive && ! confirm "Видалити службу $name?" "Delete service $name?"; then
return 1
fi
if is_enabled "$name"; then
cmd_disable "$name"
# runsvdir stops its runsv at the next scan; wait for it before deleting
for ((i = 0; i < 20; i++)); do
pgrep -x -f "runsv $name" >/dev/null || break
sleep 0.5
done
fi
rm -rf "${sv_dir:?}/$name"
say "$name: видалена" "$name: deleted"
[[ -d /var/log/$name ]] && say "Журнал залишився в /var/log/$name" "The log is kept in /var/log/$name"
return 0
}
cmd=${1:-list}
[[ $# -gt 0 ]] && shift
@@ -269,6 +411,14 @@ case $cmd in
[[ -r $socklog/messages/current ]] || need_root logs "$@"
cmd_logs "$@"
;;
new | create)
need_root new "$@"
cmd_new "$@"
;;
rm | remove | delete)
need_root rm "$@"
cmd_rm "$@"
;;
-h | --help | help)
usage
;;
@@ -3,7 +3,7 @@
_dsv() {
local cur=${COMP_WORDS[COMP_CWORD]} cmd=${COMP_WORDS[1]:-}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "list status enable disable start stop restart logs help" -- "$cur")
mapfile -t COMPREPLY < <(compgen -W "list status enable disable start stop restart logs new rm help" -- "$cur")
return
fi
case $cmd in
@@ -20,7 +20,16 @@ _dsv() {
disable | start | stop | restart)
mapfile -t COMPREPLY < <(compgen -W "$(ls /etc/runit/runsvdir/default 2>/dev/null)" -- "$cur")
;;
status | logs | log)
new)
case ${COMP_WORDS[COMP_CWORD - 1]} in
--user) mapfile -t COMPREPLY < <(compgen -u -- "$cur") ;;
--dir) mapfile -t COMPREPLY < <(compgen -d -- "$cur") ;;
--cmd) mapfile -t COMPREPLY < <(compgen -c -- "$cur") ;;
--env) ;;
*) mapfile -t COMPREPLY < <(compgen -W "--cmd --user --dir --env --no-log --enable" -- "$cur") ;;
esac
;;
status | logs | log | rm)
if [[ $cmd == logs* ]] && ((COMP_CWORD == 3)); then
mapfile -t COMPREPLY < <(compgen -W "-f" -- "$cur")
else
+247
View File
@@ -0,0 +1,247 @@
#!/bin/bash
# dsys - system settings on Das OS: host name, time zone, language and
# console keyboard, in place of hostnamectl, timedatectl and localectl.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
zoneinfo=/usr/share/zoneinfo
libc_locales=/etc/default/libc-locales
keymaps=/usr/share/kbd/keymaps
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
Використання: dsys [КОМАНДА] [ЗНАЧЕННЯ]
info огляд системи (типова команда)
hostname [ІМ'Я] показати або змінити ім'я хоста
timezone [ЗОНА] показати або змінити часовий пояс (Europe/Kyiv,
достатньо й частини назви: Kyiv)
timezone list [ФІЛЬТР] усі часові пояси
locale [МОВА] показати або змінити мову системи (uk_UA, en_US)
locale list [ФІЛЬТР] доступні мови; * — уже зібрані
keymap [РОЗКЛАДКА] показати або змінити розкладку консолі (us, ua-utf)
keymap list [ФІЛЬТР] доступні розкладки консолі
Мову й розкладку графічного сеансу кожен користувач задає в налаштуваннях
Xfce і в das-welcome; dsys змінює типові для системи й консолі.
EOF
else
cat <<'EOF'
Usage: dsys [COMMAND] [VALUE]
info system overview (the default)
hostname [NAME] show or change the host name
timezone [ZONE] show or change the time zone (Europe/Kyiv,
part of the name will do: Kyiv)
timezone list [FILTER] all time zones
locale [LANGUAGE] show or change the system language (uk_UA, en_US)
locale list [FILTER] available languages; * marks the built ones
keymap [MAP] show or change the console keyboard map (us, ua-utf)
keymap list [FILTER] available console keyboard maps
In the graphical session every user picks the language and keyboard in the
Xfce settings and in das-welcome; dsys changes the system and console defaults.
EOF
fi
}
current_hostname() {
cat /etc/hostname 2>/dev/null || hostname
}
current_timezone() {
local tz
tz=$(readlink /etc/localtime 2>/dev/null || true)
echo "${tz#*zoneinfo/}"
}
current_locale() {
sed -n 's/^LANG=//p' /etc/locale.conf 2>/dev/null | tr -d '"'
}
rc_value() {
sed -n "s/^$1=//p" /etc/rc.conf 2>/dev/null | tr -d '"' | tail -n 1
}
# A graphical session keeps its X authority under the old host name
session_warning() {
if pgrep -x lightdm >/dev/null || [[ -n ${DISPLAY:-} ]]; then
say "Перезавантажте комп'ютер: графічний сеанс не відкриватиме нових вікон зі старим іменем." \
"Reboot: the graphical session will not open new windows under the old name."
fi
}
# 3 д 4 год 5 хв / 3 d 4 h 5 min
uptime_text() {
local s d h m
read -r s _ </proc/uptime
s=${s%.*}
d=$((s / 86400))
h=$((s % 86400 / 3600))
m=$((s % 3600 / 60))
if ((d > 0)); then
say "$d д $h год $m хв" "$d d $h h $m min"
elif ((h > 0)); then
say "$h год $m хв" "$h h $m min"
else
say "$m хв" "$m min"
fi
}
cmd_info() {
local ntp line
pad "$(say "Ім'я хоста:" "Host name:")" 16; current_hostname
pad "$(say "Система:" "System:")" 16; echo "$(uname -sr), $(uname -m)"
pad "$(say "Працює:" "Up:")" 16; uptime_text
pad "$(say "Час:" "Time:")" 16; date '+%Y-%m-%d %H:%M:%S %Z'
pad "$(say "Часовий пояс:" "Time zone:")" 16; current_timezone
if ntp=$(chronyc -n tracking 2>/dev/null); then
if grep -q '^Leap status *: Normal' <<<"$ntp"; then
ntp=$(say "синхронізовано (chrony)" "synchronized (chrony)")
else
ntp=$(say "не синхронізовано (chrony)" "not synchronized (chrony)")
fi
else
ntp=$(say "не працює (dsv enable chronyd)" "not running (dsv enable chronyd)")
fi
pad "$(say "Синхронізація:" "Time sync:")" 16; echo "$ntp"
pad "$(say "Мова:" "Language:")" 16; current_locale
pad "$(say "Консоль:" "Console:")" 16
echo "$(say розкладка keymap) $(rc_value KEYMAP || true), $(say шрифт font) $(rc_value FONT || true)"
say "Мережа:" "Network:"
ip -brief address 2>/dev/null | while read -r line; do
[[ $line == lo\ * ]] || echo " $line"
done
}
cmd_hostname() {
local new=${1:-} old
if [[ -z $new ]]; then
current_hostname
return
fi
[[ $new =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$ ]] ||
die "'$new': малі латинські літери, цифри й дефіс (не на початку й не в кінці)" \
"'$new': lowercase Latin letters, digits and hyphens (not first or last)"
need_root hostname "$new"
old=$(current_hostname)
echo "$new" >/etc/hostname
# The installers add "127.0.1.1 <name>"; keep the line in step
if grep -qw -- "$old" /etc/hosts; then
sed -i "/^[^#]/s/\([[:space:]]\)${old//./\\.}\([[:space:]]\|$\)/\1$new\2/g" /etc/hosts
fi
grep -qw -- "$new" /etc/hosts || printf '127.0.1.1\t%s\n' "$new" >>/etc/hosts
hostname "$new"
say "Ім'я хоста: $new" "Host name: $new"
session_warning
}
# All time zones, from tzdata's table plus UTC
timezones() {
{ awk '!/^#/ { print $3 }' "$zoneinfo/zone1970.tab"; echo UTC; } | sort -u
}
cmd_timezone() {
local zone=${1:-} matches line
if [[ -z $zone ]]; then
current_timezone
return
fi
if [[ $zone == list ]]; then
timezones | grep -i -- "${2:-}" || true
return
fi
if ! timezones | grep -qx -- "$zone"; then
# Kyiv -> Europe/Kyiv, when only one zone matches
matches=$(timezones | grep -i -- "$zone" || true)
if [[ $(grep -c . <<<"$matches") -eq 1 ]]; then
zone=$matches
elif [[ -n $matches ]]; then
say "Кілька часових поясів підходять, уточніть:" "Several time zones match, be more specific:" >&2
head -n 20 <<<"$matches" | while read -r line; do echo " $line"; done >&2
(($(grep -c . <<<"$matches") <= 20)) || echo " ..." >&2
exit 1
else
die "невідомий часовий пояс '$zone' (dsys timezone list)" "unknown time zone '$zone' (dsys timezone list)"
fi
fi
need_root timezone "$zone"
ln -sfn "$zoneinfo/$zone" /etc/localtime
say "Часовий пояс: $zone, зараз $(date '+%H:%M %Z')" "Time zone: $zone, now $(date '+%H:%M %Z')"
}
# Every UTF-8 locale glibc can build, with * for the enabled ones
locales() {
sed -n 's/^\(#\?\)\([^ ]*\.UTF-8\) UTF-8[[:space:]]*$/\2 \1/p' "$libc_locales" |
awk '{ print ($2 == "#" ? " " : "* ") $1 }'
}
cmd_locale() {
local lang=${1:-} re
if [[ -z $lang ]]; then
current_locale
return
fi
if [[ $lang == list ]]; then
locales | grep -i -- "${2:-}" || true
return
fi
[[ $lang == *.* ]] || lang+=.UTF-8
re=${lang//./\\.}
locales | grep -q "^. $re\$" ||
die "невідома мова '$lang' (dsys locale list)" "unknown language '$lang' (dsys locale list)"
need_root locale "$lang"
if grep -q "^#$re " "$libc_locales"; then
sed -i "s/^#\($re \)/\1/" "$libc_locales"
say "Збираю $lang..." "Building $lang..."
xbps-reconfigure -f glibc-locales >/dev/null
fi
# Keep the other settings, Void's default file also has LC_COLLATE=C
if grep -q '^LANG=' /etc/locale.conf 2>/dev/null; then
sed -i "s/^LANG=.*/LANG=$lang/" /etc/locale.conf
else
echo "LANG=$lang" >>/etc/locale.conf
fi
say "Мова системи: $lang (діє з наступного входу)" "System language: $lang (from the next login)"
}
cmd_keymap() {
local map=${1:-}
if [[ -z $map ]]; then
rc_value KEYMAP
return
fi
if [[ $map == list ]]; then
find "$keymaps" -name '*.map.gz' -printf '%f\n' | sed 's/\.map\.gz$//' | sort -u | grep -i -- "${2:-}" || true
return
fi
[[ -n $(find "$keymaps" -name "$map.map.gz" -print -quit) ]] ||
die "невідома розкладка '$map' (dsys keymap list)" "unknown keyboard map '$map' (dsys keymap list)"
need_root keymap "$map"
set_rc KEYMAP "$map"
# The default console font has no Cyrillic letters
if [[ $map == @(ua|ru|by|bg|mk|sr)* && -z $(rc_value FONT) ]]; then
set_rc FONT cyr-sun16
fi
loadkeys "$map" >/dev/null 2>&1 || true
say "Розкладка консолі: $map" "Console keyboard map: $map"
}
cmd=${1:-info}
[[ $# -gt 0 ]] && shift
case $cmd in
info | status) cmd_info ;;
hostname | host) cmd_hostname "$@" ;;
timezone | tz) cmd_timezone "$@" ;;
locale | lang) cmd_locale "$@" ;;
keymap | keyboard) cmd_keymap "$@" ;;
-h | --help | help) usage ;;
*)
usage >&2
exit 1
;;
esac
@@ -0,0 +1,22 @@
# bash completion for dsys
# shellcheck shell=bash
_dsys() {
local cur=${COMP_WORDS[COMP_CWORD]}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "info hostname timezone locale keymap help" -- "$cur")
return
fi
((COMP_CWORD == 2)) || return
case ${COMP_WORDS[1]} in
timezone | tz)
mapfile -t COMPREPLY < <(compgen -W "list UTC $(awk '!/^#/ { print $3 }' /usr/share/zoneinfo/zone1970.tab 2>/dev/null)" -- "$cur")
;;
locale | lang)
mapfile -t COMPREPLY < <(compgen -W "list $(sed -n 's/^#\?\([^ ]*\)\.UTF-8 UTF-8[[:space:]]*$/\1/p' /etc/default/libc-locales 2>/dev/null)" -- "$cur")
;;
keymap | keyboard)
mapfile -t COMPREPLY < <(compgen -W "list $(find /usr/share/kbd/keymaps -name '*.map.gz' -printf '%f\n' 2>/dev/null | sed 's/\.map\.gz$//')" -- "$cur")
;;
esac
}
complete -F _dsys dsys
+396
View File
@@ -0,0 +1,396 @@
#!/bin/bash
# duser - manage user accounts on Das OS: create them with the right groups,
# set passwords, lock them, add SSH keys.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
# Groups of every new user, as the desktop installer gives them; only the ones
# that exist on this system are used
user_groups="users audio video input storage network optical scanner lp kvm plugdev bluetooth"
# Administrators also get sudo, Docker (root-equivalent) and the system logs
admin_groups="wheel docker socklog"
uid_min=$(awk '$1 == "UID_MIN" { print $2 }' /etc/login.defs 2>/dev/null)
uid_max=$(awk '$1 == "UID_MAX" { print $2 }' /etc/login.defs 2>/dev/null)
uid_min=${uid_min:-1000}
uid_max=${uid_max:-60000}
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
Використання: duser КОМАНДА [КОРИСТУВАЧ] [...]
list користувачі: адміністратор, вхід, SSH-ключі, групи
add [КОРИСТУВАЧ] створити; чого не вказано, те запитає:
--admin адміністратор (sudo, docker, журнали)
--key КЛЮЧ|ФАЙЛ публічний SSH-ключ
--no-password без пароля, вхід лише за ключем
--password-stdin пароль зі стандартного входу (для скриптів)
del КОРИСТУВАЧ [--home] видалити (--home: разом із домашнім каталогом)
passwd [КОРИСТУВАЧ] змінити пароль (без імені — свій)
lock КОРИСТУВАЧ заборонити вхід (і за паролем, і за ключем)
unlock КОРИСТУВАЧ дозволити вхід знову
key КОРИСТУВАЧ [list] SSH-ключі користувача
key КОРИСТУВАЧ add [КЛЮЧ|ФАЙЛ|-]
додати ключ (рядок, файл .pub або stdin)
key КОРИСТУВАЧ rm НОМЕР видалити ключ за номером зі списку
groups КОРИСТУВАЧ [+ГРУПА|-ГРУПА...]
показати групи, додати (+) чи прибрати (-);
+wheel робить адміністратором
Зміни груп діють із наступного входу користувача.
EOF
else
cat <<'EOF'
Usage: duser COMMAND [USER] [...]
list users: administrator, login, SSH keys, groups
add [USER] create one; asks for what is not given:
--admin an administrator (sudo, docker, system logs)
--key KEY|FILE a public SSH key
--no-password no password, key login only
--password-stdin read the password from stdin (for scripts)
del USER [--home] delete (--home: with the home directory)
passwd [USER] change the password (without a name: your own)
lock USER forbid logins (by password and by key)
unlock USER allow logins again
key USER [list] the user's SSH keys
key USER add [KEY|FILE|-]
add a key (a line, a .pub file or stdin)
key USER rm NUMBER remove a key by its number in the list
groups USER [+GROUP|-GROUP...]
show the groups, add (+) or remove (-) some;
+wheel makes an administrator
Group changes apply from the user's next login.
EOF
fi
}
# A regular (human) account, not a system one
check_user() {
local uid
uid=$(id -u "$1" 2>/dev/null) || die "немає користувача $1" "no user $1"
((uid >= uid_min && uid <= uid_max)) ||
die "$1 — системний обліковий запис, duser його не змінює" "$1 is a system account, duser leaves it alone"
}
in_group() {
id -nG "$1" | tr ' ' '\n' | grep -qx "$2"
}
# existing GROUPS...: the given groups that exist, comma-separated
existing() {
local g out=()
for g in "$@"; do
getent group "$g" >/dev/null && out+=("$g")
done
local IFS=,
echo "${out[*]}"
}
# login_state USER: how the user can log in, from /etc/shadow
login_state() {
local hash expire today
IFS=: read -r _ hash _ _ _ _ _ expire _ < <(getent shadow "$1")
today=$(($(date +%s) / 86400))
if [[ -n $expire ]] && ((expire <= today)); then
say заблоковано locked
elif [[ $hash == \$* ]]; then
say пароль password
else
# "!", "*" or a "!"-locked hash: SSH keys still work
say "лише ключ" "key only"
fi
}
keys_file() {
echo "$(getent passwd "$1" | cut -d: -f6)/.ssh/authorized_keys"
}
# The keys of a file, one per line, without comments and blank lines
key_lines() {
[[ -r $1 ]] || return 0
grep -v '^[[:space:]]*\(#\|$\)' "$1" || true
}
cmd_list() {
local name uid home admin keys
pad "$(say КОРИСТУВАЧ USER)" 16
pad UID 6
pad "$(say АДМІН ADMIN)" 6
pad "$(say ВХІД LOGIN)" 12
pad "$(say КЛЮЧІ KEYS)" 6
say ГРУПИ GROUPS
while IFS=: read -r name _ uid _ _ home _; do
((uid >= uid_min && uid <= uid_max)) || continue
admin=$(say ні no)
in_group "$name" wheel && admin=$(say так yes)
keys=$(key_lines "$home/.ssh/authorized_keys" | wc -l)
pad "$name" 16
pad "$uid" 6
pad "$admin" 6
pad "$(login_state "$name")" 12
pad "$keys" 6
id -nG "$name" | sed "s/^$name //; s/ /,/g"
done < <(getent passwd | sort -t: -k3n)
}
# add_key USER KEY|FILE|-: appends a validated key to the user's authorized_keys
add_key() {
local user=$1 src=$2 key file tmp group
if [[ $src == - ]]; then
key=$(cat)
elif [[ -f $src ]]; then
key=$(cat "$src")
else
key=$src
fi
key=$(printf '%s\n' "$key" | grep -v '^[[:space:]]*$' | head -n 1)
tmp=$(mktemp)
printf '%s\n' "$key" >"$tmp"
if ! ssh-keygen -l -f "$tmp" >/dev/null 2>&1; then
rm -f "$tmp"
die "це не публічний SSH-ключ (потрібен рядок на кшталт 'ssh-ed25519 AAAA... ім'я')" \
"this is not a public SSH key (expected a line like 'ssh-ed25519 AAAA... name')"
fi
rm -f "$tmp"
file=$(keys_file "$user")
# Same key type and body, whatever the comment
if key_lines "$file" | awk -v k="$(awk '{ print $1, $2 }' <<<"$key")" '$1 " " $2 == k { found = 1 } END { exit !found }'; then
say "$user: цей ключ уже є" "$user: this key is already there"
return 0
fi
group=$(id -gn "$user")
install -d -m 700 -o "$user" -g "$group" "$(dirname "$file")"
printf '%s\n' "$key" >>"$file"
chown "$user:$group" "$file"
chmod 600 "$file"
say "$user: ключ додано" "$user: key added"
}
# set_password USER: asks on a terminal, reads one line from stdin otherwise
set_password() {
local password
if interactive; then
passwd "$1"
else
IFS= read -r password || die "пароль не передано" "no password given"
[[ -n $password ]] || die "порожній пароль" "empty password"
# -c: Void's PAM stack for chpasswd does not hash passwords itself
printf '%s:%s\n' "$1" "$password" | chpasswd -c SHA512
say "$1: пароль змінено" "$1: password changed"
fi
}
cmd_add() {
local name="" admin="" key="" password=ask groups
while (($#)); do
case $1 in
--admin) admin=1 ;;
--key) key=${2:?}; shift ;;
--no-password) password=none ;;
--password-stdin) password=stdin ;;
-*) die "невідомий параметр $1 (duser help)" "unknown option $1 (duser help)" ;;
*) [[ -z $name ]] || die "зайвий аргумент $1" "extra argument $1"; name=$1 ;;
esac
shift
done
if [[ -z $name ]]; then
interactive || die "вкажіть ім'я користувача" "name the user"
ask name "Ім'я користувача (латиницею)" "User name"
fi
[[ $name =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] ||
die "ім'я '$name': малі латинські літери, цифри, '_', '-', до 32 символів" \
"name '$name': lowercase Latin letters, digits, '_', '-', up to 32 characters"
! id "$name" >/dev/null 2>&1 || die "користувач $name уже є" "user $name already exists"
if [[ -z $admin ]] && interactive && [[ $password != stdin ]]; then
confirm "Адміністратор (sudo, docker, журнали)?" "Administrator (sudo, docker, system logs)?" && admin=1
fi
if [[ -z $key ]] && interactive && [[ $password != stdin ]]; then
ask key "Публічний SSH-ключ: рядок або файл .pub (Enter — без ключа)" \
"Public SSH key: a line or a .pub file (Enter for none)"
fi
[[ $password == ask ]] && ! interactive && password=none
# shellcheck disable=SC2086 # word lists
groups=$(existing $user_groups ${admin:+$admin_groups})
useradd -m -s /bin/bash -G "$groups" "$name"
say "$name: створено, групи $groups" "$name: created, groups $groups"
# From here on the account exists: on a failure, say so instead of leaving
# it half set up silently
# shellcheck disable=SC2064 # $name is fixed by now
trap "[[ \$? -eq 0 ]] || warn '$name створено не повністю; виправте через duser або видаліть: duser del $name --home' \
'$name is only partly set up; fix it with duser or delete it: duser del $name --home'" EXIT
[[ -n $key ]] && add_key "$name" "$key"
case $password in
ask | stdin) set_password "$name" ;;
none)
[[ -n $key ]] || warn "$name не має ні пароля, ні ключа і поки не зможе увійти: duser passwd $name" \
"$name has neither a password nor a key and cannot log in yet: duser passwd $name"
;;
esac
trap - EXIT
}
# The current user (the one who ran sudo)
me() {
echo "${SUDO_USER:-$(id -un)}"
}
cmd_del() {
local name=${1:-} home="" others
[[ -n $name ]] || die "вкажіть користувача" "name the user"
[[ ${2:-} == --home ]] && home=1
check_user "$name"
[[ $name != "$(me)" ]] || die "не можна видалити себе" "you cannot delete yourself"
if in_group "$name" wheel; then
others=$(getent group wheel | cut -d: -f4 | tr ',' '\n' | grep -vx "$name" || true)
[[ -n $others ]] || die "$name — єдиний адміністратор" "$name is the only administrator"
fi
if interactive; then
confirm "Видалити користувача $name?" "Delete user $name?" || return 1
if [[ -z $home ]] && confirm "Видалити й домашній каталог $(getent passwd "$name" | cut -d: -f6)?" \
"Delete the home directory $(getent passwd "$name" | cut -d: -f6) too?"; then
home=1
fi
fi
# userdel refuses while the user has running processes, which is right.
# Void creates no mail spools, so drop userdel's warning about it.
local out
if ! out=$(userdel ${home:+-r} "$name" 2>&1); then
echo "$out" >&2
exit 1
fi
grep -v 'mail spool' <<<"$out" >&2 || true
say "$name: видалено" "$name: deleted"
}
cmd_passwd() {
local name=${1:-}
if [[ -z $name || $name == "$(id -un)" ]] && [[ $EUID -ne 0 ]]; then
# Your own password: no sudo, passwd asks for the current one
exec passwd
fi
need_root passwd "$@"
name=${name:-$(me)}
check_user "$name"
set_password "$name"
}
cmd_lock() {
local name=${1:-}
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
[[ $name != "$(me)" ]] || die "не можна заблокувати себе" "you cannot lock yourself out"
# -L disables the password; an expired account also stops SSH key logins
usermod -L -e 1 "$name"
say "$name: вхід заборонено (поточні сеанси тривають; завершити: pkill -KILL -u $name)" \
"$name: logins forbidden (open sessions go on; to end them: pkill -KILL -u $name)"
}
cmd_unlock() {
local name=${1:-} hash
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
# (usermod -e '' complains "no changes" when the account never expired)
chage -E -1 "$name"
# Re-enable the password only when there is one under the "!"
hash=$(getent shadow "$name" | cut -d: -f2)
[[ $hash == \!\$* ]] && usermod -U "$name"
say "$name: вхід дозволено" "$name: logins allowed"
}
cmd_key() {
local name=${1:-} action=${2:-list} file n i line
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
file=$(keys_file "$name")
case $action in
list | ls)
if [[ -z $(key_lines "$file") ]]; then
say "$name: SSH-ключів немає" "$name: no SSH keys"
return 0
fi
i=0
while IFS= read -r line; do
i=$((i + 1))
printf '%2d %s\n' "$i" "$(ssh-keygen -l -f - <<<"$line" 2>/dev/null || echo "? $line")"
done < <(key_lines "$file")
;;
add)
local src=${3:-}
if [[ -z $src ]]; then
interactive || die "вкажіть ключ, файл або -" "give a key, a file or -"
ask src "Публічний SSH-ключ: рядок або файл .pub" "Public SSH key: a line or a .pub file"
fi
add_key "$name" "$src"
;;
rm | del)
n=${3:-}
[[ $n =~ ^[1-9][0-9]*$ ]] || die "вкажіть номер ключа (duser key $name)" "give the key number (duser key $name)"
line=$(key_lines "$file" | sed -n "${n}p")
[[ -n $line ]] || die "немає ключа номер $n" "no key number $n"
# Remove that exact line, keeping comments and the other keys
awk -v k="$line" '$0 != k' "$file" >"$file.new"
chown --reference="$file" "$file.new"
chmod 600 "$file.new"
mv "$file.new" "$file"
say "$name: ключ $n видалено" "$name: key $n removed"
;;
*) die "невідома дія $action (list, add, rm)" "unknown action $action (list, add, rm)" ;;
esac
}
cmd_groups() {
local name=${1:-} arg g
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
shift
for arg in "$@"; do
g=${arg:1}
case $arg in
+?*)
getent group "$g" >/dev/null || die "немає групи $g" "no group $g"
gpasswd -a "$name" "$g" >/dev/null
;;
-?*)
if [[ $g == wheel && $name == "$(me)" ]]; then
die "не можна позбавити себе прав адміністратора" "you cannot drop your own administrator rights"
fi
gpasswd -d "$name" "$g" >/dev/null
;;
*) die "'$arg': потрібно +ГРУПА або -ГРУПА" "'$arg': expected +GROUP or -GROUP" ;;
esac
done
echo "$name: $(id -nG "$name" | sed "s/^$name //; s/ /, /g")"
}
cmd=${1:-list}
[[ $# -gt 0 ]] && shift
case $cmd in
list | ls)
# /etc/shadow is root-only
need_root list
cmd_list
;;
add | new) need_root add "$@"; cmd_add "$@" ;;
del | rm | remove) need_root del "$@"; cmd_del "$@" ;;
passwd | password) cmd_passwd "$@" ;;
lock) need_root lock "$@"; cmd_lock "$@" ;;
unlock) need_root unlock "$@"; cmd_unlock "$@" ;;
key | keys) need_root key "$@"; cmd_key "$@" ;;
groups | group) need_root groups "$@"; cmd_groups "$@" ;;
-h | --help | help) usage ;;
*)
usage >&2
exit 1
;;
esac
@@ -0,0 +1,42 @@
# bash completion for duser
# shellcheck shell=bash
_duser_users() {
local min max
min=$(awk '$1 == "UID_MIN" { print $2 }' /etc/login.defs 2>/dev/null)
max=$(awk '$1 == "UID_MAX" { print $2 }' /etc/login.defs 2>/dev/null)
getent passwd | awk -F: -v min="${min:-1000}" -v max="${max:-60000}" '$3 >= min && $3 <= max { print $1 }'
}
_duser() {
local cur=${COMP_WORDS[COMP_CWORD]} cmd=${COMP_WORDS[1]:-}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "list add del passwd lock unlock key groups help" -- "$cur")
return
fi
case $cmd in
add)
if [[ ${COMP_WORDS[COMP_CWORD - 1]} == --key ]]; then
mapfile -t COMPREPLY < <(compgen -f -- "$cur")
else
mapfile -t COMPREPLY < <(compgen -W "--admin --key --no-password --password-stdin" -- "$cur")
fi
;;
del | passwd | lock | unlock | key | groups)
if ((COMP_CWORD == 2)); then
mapfile -t COMPREPLY < <(compgen -W "$(_duser_users)" -- "$cur")
elif [[ $cmd == del ]]; then
mapfile -t COMPREPLY < <(compgen -W "--home" -- "$cur")
elif [[ $cmd == key ]] && ((COMP_CWORD == 3)); then
mapfile -t COMPREPLY < <(compgen -W "list add rm" -- "$cur")
elif [[ $cmd == key && ${COMP_WORDS[3]} == add ]]; then
mapfile -t COMPREPLY < <(compgen -f -- "$cur")
elif [[ $cmd == groups ]]; then
# +GROUP and -GROUP
local sign=${cur:0:1}
[[ $sign == [+-] ]] || sign=+
mapfile -t COMPREPLY < <(compgen -P "$sign" -W "$(getent group | cut -d: -f1)" -- "${cur#[+-]}")
fi
;;
esac
}
complete -F _duser duser
+75
View File
@@ -0,0 +1,75 @@
# Shared functions of the Das OS helpers (dsv, duser, dfw, dsys).
# Sourced, not run: each helper sets "set -euo pipefail" itself.
# shellcheck shell=bash
self=${0##*/}
if [[ ${LC_ALL:-} == uk* || ${LC_MESSAGES:-} == uk* || ${LANG:-} == uk* ]]; then
uk=1
else
uk=""
fi
# say UKRAINIAN ENGLISH: prints the message in the user's language
say() {
if [[ -n $uk ]]; then echo "$1"; else echo "$2"; fi
}
warn() {
say "$self: $1" "$self: $2" >&2
}
die() {
warn "$1" "$2"
exit 1
}
# need_root ARGS...: runs the helper again through sudo with the same arguments
need_root() {
if [[ $EUID -ne 0 ]]; then
exec sudo "$0" "$@"
fi
}
# Questions are only asked on a terminal; scripts must pass every argument
interactive() {
[[ -t 0 ]]
}
# ask VAR UKRAINIAN ENGLISH [DEFAULT]: reads a line into VAR; the prompt shows
# the default, which an empty answer keeps
ask() {
local -n _answer=$1
local prompt
prompt=$(say "$2" "$3")
if [[ -n ${4:-} ]]; then
prompt+=" [$4]"
fi
read -r -e -p "$prompt: " _answer || exit 1
[[ -n $_answer ]] || _answer=${4:-}
}
# confirm UKRAINIAN ENGLISH: a yes/no question, "no" by default
confirm() {
local reply
read -r -p "$(say "$1 [т/Н]" "$2 [y/N]") " reply || return 1
[[ ${reply,,} == @(y|yes|т|так) ]]
}
# pad TEXT WIDTH: TEXT padded with spaces to WIDTH characters. printf pads by
# bytes, ${#s} counts characters, so Cyrillic columns stay aligned.
pad() {
local width=$(($2 - ${#1}))
((width > 0)) || width=0
printf '%s%*s ' "$1" "$width" ''
}
# set_rc VAR VALUE: sets VAR in /etc/rc.conf, uncommenting Void's example line
set_rc() {
local rc=${RC_CONF:-/etc/rc.conf}
if grep -q "^#\?$1=" "$rc"; then
sed -i "s|^#\?$1=.*|$1=\"$2\"|" "$rc"
else
echo "$1=\"$2\"" >>"$rc"
fi
}
+21
View File
@@ -0,0 +1,21 @@
# Template file for 'das-helpers'
pkgname=das-helpers
version=0.1.0
revision=1
depends="bash runit sudo shadow openssh nftables kbd"
short_desc="Das OS helpers: services (dsv), users (duser), firewall (dfw), system (dsys)"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
# dsv used to be a package of its own
replaces="dsv>=0"
provides="dsv-${version}_${revision}"
do_install() {
vinstall ${FILESDIR}/helpers.sh 644 usr/lib/das
for tool in dsv duser dfw dsys; do
vbin ${FILESDIR}/${tool}
vcompletion ${FILESDIR}/${tool}.bash-completion bash ${tool}
done
vlicense ${FILESDIR}/LICENSE
}
@@ -66,7 +66,7 @@ t() {
s_config) echo "Налаштування системи..." ;;
s_post) echo "Локалі, знімки, initramfs (кілька хвилин)..." ;;
s_boot) echo "Встановлення завантажувача..." ;;
finished) echo "Das OS Server встановлено. Вийміть інсталяційний носій і перезавантажте сервер.\n\nВхід: %s, далі sudo. Служби: dsv list. Оновлення: das-update." ;;
finished) echo "Das OS Server встановлено. Вийміть інсталяційний носій і перезавантажте сервер.\n\nВхід: %s, далі sudo. Служби: dsv. Користувачі: duser. Порти: dfw. Налаштування: dsys. Оновлення: das-update." ;;
reboot) echo "Перезавантажити зараз?" ;;
failed) echo "Помилка встановлення. Журнал: %s" ;;
root) echo "Інсталятор треба запускати від root (sudo das-server-install)." ;;
@@ -105,7 +105,7 @@ t() {
s_config) echo "Configuring the system..." ;;
s_post) echo "Locales, snapshots, initramfs (a few minutes)..." ;;
s_boot) echo "Installing the boot loader..." ;;
finished) echo "Das OS Server is installed. Remove the installation medium and reboot.\n\nLog in as %s, then use sudo. Services: dsv list. Updates: das-update." ;;
finished) echo "Das OS Server is installed. Remove the installation medium and reboot.\n\nLog in as %s, then use sudo. Services: dsv. Users: duser. Ports: dfw. Settings: dsys. Updates: das-update." ;;
reboot) echo "Reboot now?" ;;
failed) echo "Installation failed. Log: %s" ;;
root) echo "The installer must run as root (sudo das-server-install)." ;;
+1 -1
View File
@@ -1,6 +1,6 @@
# Template file for 'das-server-installer'
pkgname=das-server-installer
version=0.1.0
version=0.2.0
revision=1
depends="bash dialog das-install-common btrfs-progs dosfstools gptfdisk parted
rsync util-linux grub grub-x86_64-efi efibootmgr"
+22 -6
View File
@@ -1,15 +1,26 @@
#!/usr/sbin/nft -f
# Das OS server firewall: incoming connections are closed except SSH.
#
# Only the "das" table is replaced, so loading this file again
# (dsv restart nftables) keeps the rules of Docker and fail2ban.
# Ports that Docker containers publish (-p 8080:80) pass through Docker's
# own forwarding rules, not the input chain below.
# Open ports with dfw (dfw allow 80/tcp), which keeps them in
# /etc/nftables.d/dfw.nft. Ports that Docker containers publish (-p 8080:80)
# pass through Docker's own forwarding rules, not the input chain below.
#
# After editing this file: dfw reload. Only the "das" table is replaced, so the
# rules of Docker and fail2ban stay. Do not restart the nftables service for
# that: its finish script flushes the whole ruleset, theirs too.
table inet das
delete table inet das
table inet das {
# Filled by dfw
set tcp_ports {
type inet_service; flags interval; auto-merge;
}
set udp_ports {
type inet_service; flags interval; auto-merge;
}
chain input {
type filter hook input priority filter; policy drop;
@@ -24,8 +35,13 @@ table inet das {
iifname "br-*" accept
tcp dport ssh accept
tcp dport @tcp_ports accept
udp dport @udp_ports accept
# Open more ports here, then: dsv restart nftables
# tcp dport { http, https } accept
# Rules of your own go here, e.g. a port for one network only:
# ip saddr 192.168.1.0/24 tcp dport 5432 accept
}
}
# dfw's ports and drop-ins of your own
include "/etc/nftables.d/*.nft"
+2 -1
View File
@@ -4,7 +4,7 @@ version=0.2.0
revision=1
depends="openssh chrony dhcpcd NetworkManager docker docker-compose nftables
fail2ban socklog-void python3 curl lazydocker lazygit mc tmux htop
das-snapshots das-update dsv"
das-snapshots das-update das-helpers xtools-minimal"
short_desc="Das OS server: packages and secure defaults for SSH, firewall and Docker"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
@@ -18,6 +18,7 @@ conf_files="
do_install() {
vinstall ${FILESDIR}/nftables.conf 644 etc
vmkdir etc/nftables.d
vinstall ${FILESDIR}/fail2ban-das.conf 644 etc/fail2ban/jail.d das.conf
vinstall ${FILESDIR}/sshd-das.conf 644 etc/ssh/sshd_config.d 50-das.conf
vinstall ${FILESDIR}/docker-daemon.json 644 etc/docker daemon.json
-15
View File
@@ -1,15 +0,0 @@
# Template file for 'dsv'
pkgname=dsv
version=0.1.0
revision=1
depends="bash runit sudo"
short_desc="Das OS service manager: enable, start and read logs of runit services"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
do_install() {
vbin ${FILESDIR}/dsv
vcompletion ${FILESDIR}/dsv.bash-completion bash
vlicense ${FILESDIR}/LICENSE
}