Files
Wireguard_server/client/install_client.sh
T

499 lines
17 KiB
Bash
Executable File

#!/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
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
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 "$@"