Files
Wireguard_server/client/install_client.sh

497 lines
17 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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=""
ADVERTISE_SUBNETS=""
SERVER_HOST=""
SERVER_USER="root"
SSH_PORT="22"
SSH_AUTH_METHOD="key"
SSH_PASSWORD=""
KEEPALIVE="25"
CLIENT_ADDRESS_PREFIX="24"
FORWARDING_MODE="disabled"
usage() {
cat <<'USAGE'
Установка WireGuard-клиента и автоматическая регистрация на сервере.
Каждый запуск выполняет полный reset предыдущей клиентской конфигурации для выбранного интерфейса.
Использование:
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 для клиента (если не задан, берется с сервера)
--client-address-prefix <1-32> Префикс маски адреса интерфейса клиента (по умолчанию: 24)
--advertise-subnets <cidr,...> Сети за клиентом, которые нужно маршрутизировать через него (например 192.168.33.0/24)
--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 ;;
--client-address-prefix)
CLIENT_ADDRESS_PREFIX="$2"; shift 2 ;;
--advertise-subnets)
ADVERTISE_SUBNETS="$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"
[[ "$TUNNEL_MODE" == "full" || "$TUNNEL_MODE" == "split" ]] || die "--mode должен быть full или split"
[[ "$CLIENT_ADDRESS_PREFIX" =~ ^[0-9]+$ ]] || die "--client-address-prefix должен быть числом 1..32"
((CLIENT_ADDRESS_PREFIX >= 1 && CLIENT_ADDRESS_PREFIX <= 32)) || die "--client-address-prefix должен быть в диапазоне 1..32"
if [[ -n "$ADVERTISE_SUBNETS" ]]; then
is_valid_cidr_list "$ADVERTISE_SUBNETS" || die "Некорректный список --advertise-subnets"
fi
if [[ -z "$SERVER_HOST" ]]; then
die "Не указан --server-host"
fi
if [[ -n "$SPLIT_ALLOWED_IPS" ]]; then
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 [[ "$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
}
detect_client_lan_subnets() {
ip -o -4 route show scope link 2>/dev/null | awk '
{
cidr=$1
dev=""
for (i=1; i<=NF; i++) {
if ($i=="dev") { dev=$(i+1); break }
}
if (cidr ~ /^127\./ || cidr ~ /^169\.254\./) next
if (dev ~ /^(lo|wg|docker|veth|br-|tun|tap)/) next
if (cidr ~ /^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]+$/) print cidr
}
' | sort -u | paste -sd, -
}
install_packages() {
pkg_has_install_candidate() {
local pkg="$1"
local candidate
candidate="$(apt-cache policy "$pkg" 2>/dev/null | awk '/Candidate:/ {print $2; exit}')"
[[ -n "$candidate" && "$candidate" != "(none)" ]]
}
apt_install_if_missing wireguard wireguard-tools iproute2 openssh-client sshpass ca-certificates
if ! command -v resolvconf >/dev/null 2>&1; then
if pkg_has_install_candidate openresolv; then
apt_install_if_missing openresolv
elif pkg_has_install_candidate resolvconf; then
apt_install_if_missing resolvconf
else
log_warn "Пакеты openresolv/resolvconf недоступны. DNS-строка в wg0.conf будет пропущена."
fi
fi
}
reset_existing_client_install() {
log_warn "Выполняю полный reset предыдущей конфигурации клиента (${WG_INTERFACE})"
local unit="wg-quick@${WG_INTERFACE}.service"
systemctl disable --now "$unit" >/dev/null 2>&1 || true
wg-quick down "$WG_INTERFACE" >/dev/null 2>&1 || true
rm -f \
"/etc/wireguard/${WG_INTERFACE}.conf" \
"/etc/wireguard/${WG_INTERFACE}_client_private.key" \
"/etc/wireguard/${WG_INTERFACE}_client_public.key" \
"/etc/wireguard/${WG_INTERFACE}_client_psk.key"
log_info "Старые конфиг/ключи клиента удалены для интерфейса ${WG_INTERFACE}"
}
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}'"
if [[ -n "$ADVERTISE_SUBNETS" ]]; then
remote_cmd+=" --client-routes '${ADVERTISE_SUBNETS}'"
fi
response="$(run_ssh "$remote_cmd")"
SERVER_RESPONSE_RAW="$response"
kv_get() {
local key="$1"
printf '%s\n' "$response" | sed -n "s/^${key}=//p" | head -n1
}
SERVER_STATUS="$(kv_get STATUS)"
CLIENT_ADDRESS="$(kv_get CLIENT_ADDRESS)"
SERVER_PUBLIC_KEY="$(kv_get SERVER_PUBLIC_KEY)"
SERVER_ENDPOINT="$(kv_get SERVER_ENDPOINT)"
SERVER_DNS_REMOTE="$(kv_get SERVER_DNS)"
SERVER_WG_NETWORK="$(kv_get WG_NETWORK)"
[[ -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
if [[ -n "$SPLIT_ALLOWED_IPS" ]]; then
ALLOWED_IPS="$SPLIT_ALLOWED_IPS"
elif [[ -n "${SERVER_WG_NETWORK:-}" ]]; then
ALLOWED_IPS="$SERVER_WG_NETWORK"
log_info "Режим split: --allowed-ips не задан, использую сеть WG сервера: ${ALLOWED_IPS}"
else
die "Режим split: не удалось определить --allowed-ips автоматически"
fi
fi
}
build_route_hooks_if_needed() {
PRE_UP=""
POST_DOWN=""
POST_UP_EXTRA_1=""
POST_UP_EXTRA_2=""
POST_UP_EXTRA_3=""
POST_DOWN_EXTRA_1=""
POST_DOWN_EXTRA_2=""
POST_DOWN_EXTRA_3=""
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
}
detect_iface_for_cidr() {
local cidr="$1"
ip -4 route show "$cidr" 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="dev") {print $(i+1); exit}}'
}
build_lan_nat_hooks_if_needed() {
[[ -n "$ADVERTISE_SUBNETS" ]] || return
local first_cidr lan_iface wg_net
first_cidr="$(echo "$ADVERTISE_SUBNETS" | awk -F',' '{gsub(/ /,"",$1); print $1}')"
lan_iface="$(detect_iface_for_cidr "$first_cidr" || true)"
[[ -n "$lan_iface" ]] || lan_iface="$(detect_default_iface || true)"
[[ -n "$lan_iface" ]] || { log_warn "Не удалось определить LAN-интерфейс для NAT/forwarding."; return; }
wg_net="${SERVER_WG_NETWORK:-10.66.66.0/24}"
local fwd="/etc/sysctl.d/99-wireguard-client-forwarding.conf"
cat > "$fwd" <<EOF_FWD
net.ipv4.ip_forward=1
EOF_FWD
sysctl --system >/dev/null || true
POST_UP_EXTRA_1="PostUp = iptables -C FORWARD -i ${WG_INTERFACE} -o ${lan_iface} -j ACCEPT 2>/dev/null || iptables -A FORWARD -i ${WG_INTERFACE} -o ${lan_iface} -j ACCEPT"
POST_UP_EXTRA_2="PostUp = iptables -C FORWARD -i ${lan_iface} -o ${WG_INTERFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -A FORWARD -i ${lan_iface} -o ${WG_INTERFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT"
POST_UP_EXTRA_3="PostUp = iptables -t nat -C POSTROUTING -s ${wg_net} -o ${lan_iface} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s ${wg_net} -o ${lan_iface} -j MASQUERADE"
POST_DOWN_EXTRA_1="PostDown = iptables -D FORWARD -i ${WG_INTERFACE} -o ${lan_iface} -j ACCEPT 2>/dev/null || true"
POST_DOWN_EXTRA_2="PostDown = iptables -D FORWARD -i ${lan_iface} -o ${WG_INTERFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true"
POST_DOWN_EXTRA_3="PostDown = iptables -t nat -D POSTROUTING -s ${wg_net} -o ${lan_iface} -j MASQUERADE 2>/dev/null || true"
FORWARDING_MODE="enabled via ${lan_iface} (wg:${wg_net})"
}
build_client_interface_address() {
local ip_only
ip_only="${CLIENT_ADDRESS%%/*}"
CLIENT_INTERFACE_ADDRESS="${ip_only}/${CLIENT_ADDRESS_PREFIX}"
}
write_client_config() {
local conf="/etc/wireguard/${WG_INTERFACE}.conf"
local dns
local include_dns=0
dns="$CLIENT_DNS"
if [[ -z "$dns" ]]; then
dns="${SERVER_DNS_REMOTE:-1.1.1.1}"
fi
if command -v resolvconf >/dev/null 2>&1; then
include_dns=1
else
log_warn "Команда resolvconf не найдена. Пропускаю строку DNS в конфиге WireGuard."
fi
if [[ -f "$conf" ]]; then
backup_file "$conf"
fi
{
echo "[Interface]"
echo "PrivateKey = $(cat "$CLIENT_PRIV_KEY_PATH")"
echo "Address = ${CLIENT_INTERFACE_ADDRESS}"
if ((include_dns)); then
echo "DNS = ${dns}"
fi
if [[ -n "$PRE_UP" ]]; then
echo "$PRE_UP"
fi
if [[ -n "$POST_DOWN" ]]; then
echo "$POST_DOWN"
fi
if [[ -n "$POST_UP_EXTRA_1" ]]; then
echo "$POST_UP_EXTRA_1"
echo "$POST_UP_EXTRA_2"
echo "$POST_UP_EXTRA_3"
echo "$POST_DOWN_EXTRA_1"
echo "$POST_DOWN_EXTRA_2"
echo "$POST_DOWN_EXTRA_3"
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}
Адрес интерфейса: ${CLIENT_INTERFACE_ADDRESS}
Режим туннеля: ${TUNNEL_MODE}
SSH сервер: ${SERVER_USER}@${SERVER_HOST}:${SSH_PORT}
Статус регистрации: ${SERVER_STATUS}
Сети за клиентом: ${ADVERTISE_SUBNETS:-не объявлены}
LAN forwarding/NAT: ${FORWARDING_MODE}
Лог: ${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
reset_existing_client_install
print_routes_info
install_packages
generate_keys
if [[ -z "$ADVERTISE_SUBNETS" ]]; then
ADVERTISE_SUBNETS="$(detect_client_lan_subnets || true)"
if [[ -n "$ADVERTISE_SUBNETS" ]]; then
log_info "Автоопределены сети за клиентом: ${ADVERTISE_SUBNETS}"
fi
fi
register_peer_on_server
build_allowed_ips
build_client_interface_address
build_route_hooks_if_needed
build_lan_nat_hooks_if_needed
write_client_config
apply_client_config
print_summary
# Не держим пароль в переменной дольше необходимого.
unset SSH_PASSWORD
log_success "Настройка клиента завершена"
}
main "$@"