feat: автоматизация установки и настройки WireGuard сервера и клиента

This commit is contained in:
Ruslan
2026-04-14 00:04:06 +03:00
commit a31f1a1090
8 changed files with 1416 additions and 0 deletions

350
client/install_client.sh Executable file
View File

@@ -0,0 +1,350 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=../lib/common.sh
source "${PROJECT_ROOT}/lib/common.sh"
LOG_FILE="/var/log/wireguard-client-install.log"
NON_INTERACTIVE=0
WG_INTERFACE="wg0"
CLIENT_NAME=""
CLIENT_DNS=""
TUNNEL_MODE=""
SPLIT_ALLOWED_IPS=""
SERVER_HOST=""
SERVER_USER="root"
SSH_PORT="22"
SSH_AUTH_METHOD="key"
SSH_PASSWORD=""
KEEPALIVE="25"
usage() {
cat <<'USAGE'
Установка WireGuard-клиента и автоматическая регистрация на сервере.
Использование:
install_client.sh [опции]
Опции:
--non-interactive Режим без вопросов
--interface <name> Интерфейс клиента (по умолчанию: wg0)
--client-name <name> Имя клиента (по умолчанию: hostname)
--mode <full|split> full: весь трафик, split: только выбранные сети
--allowed-ips <cidr,cidr> Для режима split
--dns <ip> DNS для клиента (если не задан, берется с сервера)
--server-host <host> IP/домен WireGuard-сервера
--server-user <user> SSH-пользователь (по умолчанию: root)
--ssh-port <port> SSH-порт (по умолчанию: 22)
--ssh-auth <key|password> Тип SSH-аутентификации (по умолчанию: key)
--ssh-password <password> Пароль SSH (используйте осторожно)
-h, --help Показать помощь
USAGE
}
on_error() {
local code=$?
log_error "Клиентская настройка завершилась с ошибкой (код: ${code}). Лог: ${LOG_FILE}"
exit "$code"
}
trap on_error ERR
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--non-interactive)
NON_INTERACTIVE=1; shift ;;
--interface)
WG_INTERFACE="$2"; shift 2 ;;
--client-name)
CLIENT_NAME="$2"; shift 2 ;;
--mode)
TUNNEL_MODE="$2"; shift 2 ;;
--allowed-ips)
SPLIT_ALLOWED_IPS="$2"; shift 2 ;;
--dns)
CLIENT_DNS="$2"; shift 2 ;;
--server-host)
SERVER_HOST="$2"; shift 2 ;;
--server-user)
SERVER_USER="$2"; shift 2 ;;
--ssh-port)
SSH_PORT="$2"; shift 2 ;;
--ssh-auth)
SSH_AUTH_METHOD="$2"; shift 2 ;;
--ssh-password)
SSH_PASSWORD="$2"; shift 2 ;;
-h|--help)
usage; exit 0 ;;
*)
die "Неизвестный аргумент: $1"
;;
esac
done
}
validate_inputs() {
is_valid_port "$SSH_PORT" || die "Некорректный SSH-порт: $SSH_PORT"
[[ "$SSH_AUTH_METHOD" == "key" || "$SSH_AUTH_METHOD" == "password" ]] || die "--ssh-auth должен быть key или password"
if [[ -z "$SERVER_HOST" ]]; then
die "Не указан --server-host"
fi
if [[ "$TUNNEL_MODE" == "split" ]]; then
[[ -n "$SPLIT_ALLOWED_IPS" ]] || die "Для режима split укажите --allowed-ips"
is_valid_cidr_list "$SPLIT_ALLOWED_IPS" || die "Некорректный список --allowed-ips"
fi
}
collect_inputs() {
if [[ -z "$CLIENT_NAME" ]]; then
CLIENT_NAME="$(hostname -s)"
fi
CLIENT_NAME="$(sanitize_name "$CLIENT_NAME")"
[[ -n "$CLIENT_NAME" ]] || die "Некорректное имя клиента"
if [[ -z "$SERVER_HOST" && ! $NON_INTERACTIVE -eq 1 ]]; then
read -r -p "Введите IP/домен сервера: " SERVER_HOST
fi
if [[ -z "$TUNNEL_MODE" ]]; then
if ((NON_INTERACTIVE)); then
TUNNEL_MODE="full"
else
echo "Выберите режим маршрутизации:"
echo " 1) весь трафик через VPN (full)"
echo " 2) только выбранные сети (split)"
read -r -p "Ваш выбор [1/2, по умолчанию 1]: " mode_choice
case "${mode_choice:-1}" in
1) TUNNEL_MODE="full" ;;
2) TUNNEL_MODE="split" ;;
*) die "Некорректный выбор" ;;
esac
fi
fi
if [[ "$TUNNEL_MODE" == "split" && -z "$SPLIT_ALLOWED_IPS" ]]; then
if ((NON_INTERACTIVE)); then
die "В non-interactive для split укажите --allowed-ips"
fi
read -r -p "Введите CIDR-сети через запятую (например 10.0.0.0/8,192.168.0.0/16): " SPLIT_ALLOWED_IPS
fi
if [[ "$SSH_AUTH_METHOD" == "password" && -z "$SSH_PASSWORD" ]]; then
if ((NON_INTERACTIVE)); then
die "Для --ssh-auth password нужно указать --ssh-password"
fi
ask_secret "Введите SSH-пароль для ${SERVER_USER}@${SERVER_HOST}" SSH_PASSWORD
fi
validate_inputs
}
install_packages() {
apt_install_if_missing wireguard wireguard-tools iproute2 openssh-client sshpass ca-certificates
}
ssh_base_cmd() {
echo "ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -p ${SSH_PORT} ${SERVER_USER}@${SERVER_HOST}"
}
run_ssh() {
local remote_cmd="$1"
local base
base="$(ssh_base_cmd)"
if [[ "$SSH_AUTH_METHOD" == "password" ]]; then
require_cmd sshpass
SSHPASS="$SSH_PASSWORD" sshpass -e bash -c "$base \"$remote_cmd\""
else
bash -c "$base \"$remote_cmd\""
fi
}
resolve_server_ip() {
getent ahostsv4 "$SERVER_HOST" | awk '{print $1; exit}'
}
generate_keys() {
mkdir -p /etc/wireguard
chmod 700 /etc/wireguard
CLIENT_PRIV_KEY_PATH="/etc/wireguard/${WG_INTERFACE}_client_private.key"
CLIENT_PUB_KEY_PATH="/etc/wireguard/${WG_INTERFACE}_client_public.key"
CLIENT_PSK_PATH="/etc/wireguard/${WG_INTERFACE}_client_psk.key"
if [[ ! -f "$CLIENT_PRIV_KEY_PATH" ]]; then
umask 077
wg genkey | tee "$CLIENT_PRIV_KEY_PATH" | wg pubkey > "$CLIENT_PUB_KEY_PATH"
wg genpsk > "$CLIENT_PSK_PATH"
log_success "Сгенерированы клиентские ключи"
else
if [[ ! -f "$CLIENT_PUB_KEY_PATH" ]]; then
wg pubkey < "$CLIENT_PRIV_KEY_PATH" > "$CLIENT_PUB_KEY_PATH"
fi
if [[ ! -f "$CLIENT_PSK_PATH" ]]; then
wg genpsk > "$CLIENT_PSK_PATH"
fi
log_info "Ключи клиента уже существуют, переиспользую"
fi
safe_chmod_600 "$CLIENT_PRIV_KEY_PATH"
safe_chmod_600 "$CLIENT_PUB_KEY_PATH"
safe_chmod_600 "$CLIENT_PSK_PATH"
}
register_peer_on_server() {
local pub psk remote_cmd response
pub="$(cat "$CLIENT_PUB_KEY_PATH")"
psk="$(cat "$CLIENT_PSK_PATH")"
remote_cmd="/usr/local/sbin/wg-peerctl add --client-name '${CLIENT_NAME}' --client-public-key '${pub}' --client-preshared-key '${psk}' --persistent-keepalive '${KEEPALIVE}'"
response="$(run_ssh "$remote_cmd")"
SERVER_RESPONSE_RAW="$response"
SERVER_STATUS="$(echo "$response" | awk -F= '/^STATUS=/{print $2; exit}')"
CLIENT_ADDRESS="$(echo "$response" | awk -F= '/^CLIENT_ADDRESS=/{print $2; exit}')"
SERVER_PUBLIC_KEY="$(echo "$response" | awk -F= '/^SERVER_PUBLIC_KEY=/{print $2; exit}')"
SERVER_ENDPOINT="$(echo "$response" | awk -F= '/^SERVER_ENDPOINT=/{print $2; exit}')"
SERVER_DNS_REMOTE="$(echo "$response" | awk -F= '/^SERVER_DNS=/{print $2; exit}')"
[[ -n "$SERVER_PUBLIC_KEY" ]] || die "Сервер не вернул SERVER_PUBLIC_KEY"
[[ -n "$CLIENT_ADDRESS" ]] || die "Сервер не вернул CLIENT_ADDRESS"
[[ -n "$SERVER_ENDPOINT" ]] || die "Сервер не вернул SERVER_ENDPOINT"
}
build_allowed_ips() {
if [[ "$TUNNEL_MODE" == "full" ]]; then
ALLOWED_IPS="0.0.0.0/0,::/0"
else
ALLOWED_IPS="$SPLIT_ALLOWED_IPS"
fi
}
build_route_hooks_if_needed() {
PRE_UP=""
POST_DOWN=""
if [[ "$TUNNEL_MODE" != "full" ]]; then
return
fi
local server_ip gw iface
server_ip="$(resolve_server_ip || true)"
gw="$(detect_default_gateway || true)"
iface="$(detect_default_iface || true)"
if [[ -n "$server_ip" && -n "$gw" && -n "$iface" ]]; then
PRE_UP="PreUp = ip route add ${server_ip}/32 via ${gw} dev ${iface} 2>/dev/null || true"
POST_DOWN="PostDown = ip route del ${server_ip}/32 via ${gw} dev ${iface} 2>/dev/null || true"
else
log_warn "Не удалось вычислить route-exception до сервера. Использую стандартное поведение wg-quick."
fi
}
write_client_config() {
local conf="/etc/wireguard/${WG_INTERFACE}.conf"
local dns
dns="$CLIENT_DNS"
if [[ -z "$dns" ]]; then
dns="${SERVER_DNS_REMOTE:-1.1.1.1}"
fi
if [[ -f "$conf" ]]; then
backup_file "$conf"
fi
{
echo "[Interface]"
echo "PrivateKey = $(cat "$CLIENT_PRIV_KEY_PATH")"
echo "Address = ${CLIENT_ADDRESS}"
echo "DNS = ${dns}"
if [[ -n "$PRE_UP" ]]; then
echo "$PRE_UP"
fi
if [[ -n "$POST_DOWN" ]]; then
echo "$POST_DOWN"
fi
echo
echo "[Peer]"
echo "PublicKey = ${SERVER_PUBLIC_KEY}"
echo "PresharedKey = $(cat "$CLIENT_PSK_PATH")"
echo "Endpoint = ${SERVER_ENDPOINT}"
echo "AllowedIPs = ${ALLOWED_IPS}"
echo "PersistentKeepalive = ${KEEPALIVE}"
} > "$conf"
safe_chmod_600 "$conf"
}
apply_client_config() {
local unit="wg-quick@${WG_INTERFACE}.service"
if systemctl list-unit-files | grep -q "^${unit}"; then
systemctl restart "$unit" || true
fi
systemd_enable_now "$unit"
}
print_routes_info() {
log_info "Текущая таблица маршрутов клиента (до применения уже была прочитана системой):"
ip route show | sed 's/^/ /'
}
print_summary() {
local st
st="$(systemctl is-active "wg-quick@${WG_INTERFACE}" 2>/dev/null || true)"
cat <<EOF_SUMMARY
================ ИТОГОВАЯ СВОДКА ================
Клиент: ${CLIENT_NAME}
Интерфейс: ${WG_INTERFACE}
Сервис: ${st}
Конфиг клиента: /etc/wireguard/${WG_INTERFACE}.conf
Endpoint сервера: ${SERVER_ENDPOINT}
Маршруты (AllowedIPs):${ALLOWED_IPS}
Режим туннеля: ${TUNNEL_MODE}
SSH сервер: ${SERVER_USER}@${SERVER_HOST}:${SSH_PORT}
Статус регистрации: ${SERVER_STATUS}
Лог: ${LOG_FILE}
=================================================
EOF_SUMMARY
}
main() {
parse_args "$@"
require_root
check_os_supported
require_cmd awk
require_cmd sed
require_cmd ip
require_cmd ssh
collect_inputs
print_routes_info
install_packages
generate_keys
register_peer_on_server
build_allowed_ips
build_route_hooks_if_needed
write_client_config
apply_client_config
print_summary
# Не держим пароль в переменной дольше необходимого.
unset SSH_PASSWORD
log_success "Настройка клиента завершена"
}
main "$@"