From d45391a261c1b816fd66959356258a595cfafc48 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 3 Sep 2026 10:17:26 +0300 Subject: [PATCH] Revert: drop the phone road-warrior OpenVPN attempt Rolled back per the user's request. Three variants were tried on ru-vps (static key; TLS peer-fingerprint p2p; server mode with push routes and an inline ). The server side worked each time, but the "OpenVPN for Android" client consistently failed at config build ("Used 101 tries to get current version of the profile"), which looks like an app/OS issue rather than the config. Repo: remove playbooks/openvpn-phone.yml, its Make target, and the shared homelab_vpn_client_routes var; restore openvpn-laptop.yml to its prior state (its pre-existing `become: false` on delegate_to: localhost is noted in plan.md, left untouched). ru-vps teardown done out of band: unit, tun2, ufw/nat rules for 9444 and 10.80.0.0/29, and /etc/openvpn/homelab-phone removed; the site tunnel (homelab-openvpn, tun0) was not touched and is verified active. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KbuZrUoevfBgCpf5DCF4DG --- ansible/Makefile | 4 - ansible/inventory/group_vars/all/main.yml | 13 - ansible/playbooks/openvpn-laptop.yml | 23 +- ansible/playbooks/openvpn-phone.yml | 377 ---------------------- docs/ai/plan.md | 48 ++- 5 files changed, 36 insertions(+), 429 deletions(-) delete mode 100644 ansible/playbooks/openvpn-phone.yml diff --git a/ansible/Makefile b/ansible/Makefile index be337dd..b5ab643 100644 --- a/ansible/Makefile +++ b/ansible/Makefile @@ -221,10 +221,6 @@ openvpn: ## Поднять OpenVPN-транспорт ru-vps <-> ovpn-mini (ну openvpn-laptop: ## Настроить OpenVPN-профиль ноутбука $(ANSIBLE_PLAYBOOK) playbooks/openvpn-laptop.yml $(EXTRA) -.PHONY: openvpn-phone -openvpn-phone: ## Настроить OpenVPN-профиль телефона (Android), профиль -> ansible/generated/ada-phone.ovpn - $(ANSIBLE_PLAYBOOK) playbooks/openvpn-phone.yml $(EXTRA) - .PHONY: bash-config bash-config: ## Раскатать единый bash-конфиг на shell_hosts $(ANSIBLE_PLAYBOOK) playbooks/bash-config.yml $(EXTRA) diff --git a/ansible/inventory/group_vars/all/main.yml b/ansible/inventory/group_vars/all/main.yml index 95a98ff..01b79d3 100644 --- a/ansible/inventory/group_vars/all/main.yml +++ b/ansible/inventory/group_vars/all/main.yml @@ -23,19 +23,6 @@ homelab_service_range: 192.168.1.5-192.168.1.40 # Total votes меньше Expected votes и флаг NV у узлов. homelab_pve_egress_ip: 85.143.112.108 -# Маршруты, которые road-warrior профили (openvpn-laptop.yml, openvpn-phone.yml) -# добавляют клиенту. НЕ весь homelab_lan_cidr: домашняя сеть клиента почти -# наверняка тоже 192.168.1.0/24, полный /24 перебил бы её. Диапазон покрывает -# 192.168.1.5-.7, .8-.15, .16-.31, .32-.39, .40 — все managed-хосты и дашборд -# (192.168.1.30), но не роутер (.1) и не игровой сервер (.100). -homelab_vpn_client_routes: - - {network: 192.168.1.5, netmask: 255.255.255.255} - - {network: 192.168.1.6, netmask: 255.255.255.254} - - {network: 192.168.1.8, netmask: 255.255.255.248} - - {network: 192.168.1.16, netmask: 255.255.255.240} - - {network: 192.168.1.32, netmask: 255.255.255.248} - - {network: 192.168.1.40, netmask: 255.255.255.255} - openvpn_network_cidr: 10.78.0.0/30 openvpn_listen_port: 8443 openvpn_forwarded_services: diff --git a/ansible/playbooks/openvpn-laptop.yml b/ansible/playbooks/openvpn-laptop.yml index 974eb7a..bddf80a 100644 --- a/ansible/playbooks/openvpn-laptop.yml +++ b/ansible/playbooks/openvpn-laptop.yml @@ -16,8 +16,19 @@ laptop_openvpn_client_ip: 10.79.0.2 laptop_openvpn_remote: 157.22.231.198 laptop_openvpn_client_config_local: "{{ playbook_dir }}/../generated/{{ laptop_profile_name }}.ovpn" - # Общий список с openvpn-phone.yml — см. group_vars/all/main.yml. - laptop_openvpn_home_routes: "{{ homelab_vpn_client_routes }}" + laptop_openvpn_home_routes: + - network: 192.168.1.5 + netmask: 255.255.255.255 + - network: 192.168.1.6 + netmask: 255.255.255.254 + - network: 192.168.1.8 + netmask: 255.255.255.248 + - network: 192.168.1.16 + netmask: 255.255.255.240 + - network: 192.168.1.32 + netmask: 255.255.255.248 + - network: 192.168.1.40 + netmask: 255.255.255.255 tasks: - name: Install OpenVPN packages @@ -145,10 +156,8 @@ path: "{{ playbook_dir }}/../generated" state: directory mode: "0700" + become: false delegate_to: localhost - vars: - ansible_connection: local - ansible_become: false - name: Write local laptop OpenVPN profile ansible.builtin.copy: @@ -173,10 +182,8 @@ {{ laptop_openvpn_static_key_raw.content | b64decode }} + become: false delegate_to: localhost - vars: - ansible_connection: local - ansible_become: false no_log: true handlers: diff --git a/ansible/playbooks/openvpn-phone.yml b/ansible/playbooks/openvpn-phone.yml deleted file mode 100644 index dd031a3..0000000 --- a/ansible/playbooks/openvpn-phone.yml +++ /dev/null @@ -1,377 +0,0 @@ ---- -# ============================================================================ -# Road-warrior OpenVPN профиль для телефона (Android). -# -# Отдельный OpenVPN server-инстанс на ru-vps (порт 9444/tcp, tun2, пул -# 10.80.0.0/29), параллельный тоннелю site-to-site (tun0, homelab-openvpn) -# и профилю ноутбука (tun1, openvpn-laptop.yml). -# -# КРИПТО: peer-fingerprint, БЕЗ CA/PKI. -# На ru-vps генерятся две самоподписанные EC-пары (server.* и client.*), -# стороны пинят друг друга по SHA256-отпечатку. Прежняя схема со static -# key (--secret) отброшена: OpenVPN 2.7 в Android-клиенте её не запускает. -# -# РЕЖИМ: полноценный `server` (не p2p), маршруты в LAN раздаются через -# `push "route ..."`. Через p2p static-key/tls «OpenVPN for Android» -# локальные `route` из конфига не подхватывал — тоннель вставал, но -# трафик в LAN не заворачивался. Через push маршруты ставит любой клиент. -# -# КЛИЕНТ -# Android — «OpenVPN for Android» (Arne Schwabe) или OpenVPN Connect: -# TLS-конфиг понимают оба. Профиль самодостаточный (cert+key внутри). -# -# ЧТО ДЕЛАЕТ -# Ставит openvpn на ru-vps, генерит EC-пары (один раз), открывает -# 9444/tcp в UFW, поднимает systemd-юнит homelab-openvpn-phone, NAT'ит -# 10.80.0.0/29 -> LAN через tun0, кладёт готовый профиль в -# ansible/generated/ada-phone.ovpn (mode 0600, содержит приватный ключ). -# -# ЗАПУСК -# make openvpn-phone -# ============================================================================ -- name: Configure OpenVPN phone access through ru-vps - hosts: ru-vps - gather_facts: true - become: true - vars: - phone_profile_name: ada-phone - phone_openvpn_dir: /etc/openvpn/homelab-phone - phone_openvpn_config: /etc/openvpn/homelab-phone/server.conf - phone_openvpn_server_cert: /etc/openvpn/homelab-phone/server.crt - phone_openvpn_server_key: /etc/openvpn/homelab-phone/server.key - phone_openvpn_client_cert: /etc/openvpn/homelab-phone/client.crt - phone_openvpn_client_key: /etc/openvpn/homelab-phone/client.key - phone_openvpn_service: homelab-openvpn-phone - phone_openvpn_port: 9444 - phone_openvpn_interface: tun2 - phone_openvpn_site_interface: tun0 - # server-режим: нужен пул адресов, поэтому /29, а не /30 p2p. - phone_openvpn_network_cidr: 10.80.0.0/29 - phone_openvpn_server_subnet: 10.80.0.0 - phone_openvpn_server_netmask: 255.255.255.248 - phone_openvpn_remote: 157.22.231.198 - phone_openvpn_data_ciphers: "AES-256-GCM:AES-128-GCM" - phone_openvpn_client_config_local: "{{ playbook_dir }}/../generated/{{ phone_profile_name }}.ovpn" - phone_openvpn_routes: "{{ homelab_vpn_client_routes }}" - - tasks: - - name: Install OpenVPN packages - ansible.builtin.apt: - name: - - openvpn - - iptables - - openssl - state: present - update_cache: true - - - name: Ensure phone OpenVPN config directory exists - ansible.builtin.file: - path: "{{ phone_openvpn_dir }}" - state: directory - owner: root - group: root - mode: "0700" - - - name: Remove the obsolete static key (crypto model changed to TLS) - ansible.builtin.file: - path: "{{ phone_openvpn_dir }}/static.key" - state: absent - - - name: Generate the server EC keypair if missing - ansible.builtin.command: - argv: - - openssl - - req - - -x509 - - -newkey - - ec - - -pkeyopt - - ec_paramgen_curve:prime256v1 - - -keyout - - "{{ phone_openvpn_server_key }}" - - -out - - "{{ phone_openvpn_server_cert }}" - - -nodes - - -sha256 - - -days - - "3650" - - -subj - - /CN=homelab-phone-server - args: - creates: "{{ phone_openvpn_server_cert }}" - no_log: true - - - name: Generate the client EC keypair if missing - ansible.builtin.command: - argv: - - openssl - - req - - -x509 - - -newkey - - ec - - -pkeyopt - - ec_paramgen_curve:prime256v1 - - -keyout - - "{{ phone_openvpn_client_key }}" - - -out - - "{{ phone_openvpn_client_cert }}" - - -nodes - - -sha256 - - -days - - "3650" - - -subj - - /CN=homelab-phone-client - args: - creates: "{{ phone_openvpn_client_cert }}" - no_log: true - - - name: Enforce key permissions - ansible.builtin.file: - path: "{{ item }}" - owner: root - group: root - mode: "0600" - loop: - - "{{ phone_openvpn_server_key }}" - - "{{ phone_openvpn_client_key }}" - - - name: Read the server certificate fingerprint - ansible.builtin.command: - argv: - - openssl - - x509 - - -in - - "{{ phone_openvpn_server_cert }}" - - -noout - - -fingerprint - - -sha256 - register: phone_openvpn_server_fp_raw - changed_when: false - - - name: Read the client certificate fingerprint - ansible.builtin.command: - argv: - - openssl - - x509 - - -in - - "{{ phone_openvpn_client_cert }}" - - -noout - - -fingerprint - - -sha256 - register: phone_openvpn_client_fp_raw - changed_when: false - - - name: Extract the bare colon-separated fingerprints - # openssl печатает "sha256 Fingerprint=AB:CD:...", отпечаток без '='. - ansible.builtin.set_fact: - phone_openvpn_server_fp: "{{ phone_openvpn_server_fp_raw.stdout | regex_replace('^.*=', '') | trim }}" - phone_openvpn_client_fp: "{{ phone_openvpn_client_fp_raw.stdout | regex_replace('^.*=', '') | trim }}" - - - name: Enable IPv4 forwarding on ru-vps - ansible.posix.sysctl: - name: net.ipv4.ip_forward - value: "1" - state: present - reload: true - - - name: Allow phone OpenVPN TCP in UFW - ansible.builtin.command: "ufw allow {{ phone_openvpn_port }}/tcp" - register: ufw_allow_phone_openvpn - changed_when: "'Rule added' in ufw_allow_phone_openvpn.stdout or 'Rules updated' in ufw_allow_phone_openvpn.stdout" - failed_when: false - - # Road-warrior трафик tun2 -> tun0 ФОРВАРДИТСЯ через ru-vps, а policy - # FORWARD = DROP. Сайт-тоннель этого правила не требовал (он несёт - # собственный трафик ru-vps, не forward). ufw route allow переживает - # `ufw reload`, в отличие от сырого iptables -I FORWARD. Обратное - # направление покрыто глобальным RELATED,ESTABLISHED в ufw-before-forward. - - name: Allow forwarding phone VPN traffic into the LAN - community.general.ufw: - route: true - rule: allow - interface_in: "{{ phone_openvpn_interface }}" - interface_out: "{{ phone_openvpn_site_interface }}" - src: "{{ phone_openvpn_network_cidr }}" - dest: "{{ homelab_lan_cidr }}" - - # Переходная уборка: p2p-версия использовала /30. Убрать после ~2026-11. - - name: Drop the legacy /30 ufw forward rule - community.general.ufw: - route: true - rule: allow - delete: true - interface_in: "{{ phone_openvpn_interface }}" - interface_out: "{{ phone_openvpn_site_interface }}" - src: 10.80.0.0/30 - dest: "{{ homelab_lan_cidr }}" - - - name: Drop the legacy /30 MASQUERADE rule - ansible.builtin.iptables: - table: nat - chain: POSTROUTING - source: 10.80.0.0/30 - destination: "{{ homelab_lan_cidr }}" - out_interface: "{{ phone_openvpn_site_interface }}" - jump: MASQUERADE - state: absent - - - name: Render phone OpenVPN server config - ansible.builtin.copy: - dest: "{{ phone_openvpn_config }}" - owner: root - group: root - mode: "0600" - content: | - dev {{ phone_openvpn_interface }} - dev-type tun - proto tcp-server - port {{ phone_openvpn_port }} - server {{ phone_openvpn_server_subnet }} {{ phone_openvpn_server_netmask }} - topology subnet - dh none - cert {{ phone_openvpn_server_cert }} - key {{ phone_openvpn_server_key }} - peer-fingerprint {{ phone_openvpn_client_fp }} - data-ciphers {{ phone_openvpn_data_ciphers }} - auth SHA256 - keepalive 10 60 - {% for route in phone_openvpn_routes %} - push "route {{ route.network }} {{ route.netmask }}" - {% endfor %} - persist-key - persist-tun - verb 3 - notify: restart phone openvpn - - - name: Render phone OpenVPN up script - ansible.builtin.copy: - dest: "{{ phone_openvpn_dir }}/up.sh" - owner: root - group: root - mode: "0700" - content: | - #!/bin/sh - iptables -t nat -C POSTROUTING -s {{ phone_openvpn_network_cidr }} -d {{ homelab_lan_cidr }} -o {{ phone_openvpn_site_interface }} -j MASQUERADE 2>/dev/null || \ - iptables -t nat -A POSTROUTING -s {{ phone_openvpn_network_cidr }} -d {{ homelab_lan_cidr }} -o {{ phone_openvpn_site_interface }} -j MASQUERADE - notify: restart phone openvpn - - - name: Render phone OpenVPN down script - ansible.builtin.copy: - dest: "{{ phone_openvpn_dir }}/down.sh" - owner: root - group: root - mode: "0700" - content: | - #!/bin/sh - iptables -t nat -D POSTROUTING -s {{ phone_openvpn_network_cidr }} -d {{ homelab_lan_cidr }} -o {{ phone_openvpn_site_interface }} -j MASQUERADE 2>/dev/null || true - notify: restart phone openvpn - - - name: Render phone OpenVPN systemd service - ansible.builtin.copy: - dest: "/etc/systemd/system/{{ phone_openvpn_service }}.service" - owner: root - group: root - mode: "0644" - content: | - [Unit] - Description=HomeLab OpenVPN phone access - After=network-online.target homelab-openvpn.service - Wants=network-online.target - Requires=homelab-openvpn.service - - [Service] - Type=simple - ExecStartPre={{ phone_openvpn_dir }}/up.sh - ExecStart=/usr/sbin/openvpn --config {{ phone_openvpn_config }} - ExecStopPost={{ phone_openvpn_dir }}/down.sh - Restart=on-failure - RestartSec=5 - - [Install] - WantedBy=multi-user.target - notify: - - reload systemd - - restart phone openvpn - - - name: Flush handlers before starting phone OpenVPN - ansible.builtin.meta: flush_handlers - - - name: Enable and start phone OpenVPN - ansible.builtin.systemd: - name: "{{ phone_openvpn_service }}" - state: started - enabled: true - - - name: Read the server certificate for the profile CA block - # client-режим ics-openvpn отказывается собирать конфиг без . - # Серверный самоподписанный сертификат сам себе trust anchor; реальную - # проверку всё равно делает peer-fingerprint. - ansible.builtin.slurp: - src: "{{ phone_openvpn_server_cert }}" - register: phone_openvpn_server_cert_raw - no_log: true - - - name: Read the client certificate for the profile - ansible.builtin.slurp: - src: "{{ phone_openvpn_client_cert }}" - register: phone_openvpn_client_cert_raw - no_log: true - - - name: Read the client key for the profile - ansible.builtin.slurp: - src: "{{ phone_openvpn_client_key }}" - register: phone_openvpn_client_key_raw - no_log: true - - - name: Ensure local generated directory exists - ansible.builtin.file: - path: "{{ playbook_dir }}/../generated" - state: directory - mode: "0700" - delegate_to: localhost - vars: - ansible_connection: local - ansible_become: false - - - name: Write local phone OpenVPN profile - ansible.builtin.copy: - dest: "{{ phone_openvpn_client_config_local }}" - mode: "0600" - content: | - client - dev tun - dev-type tun - proto tcp-client - remote {{ phone_openvpn_remote }} {{ phone_openvpn_port }} - peer-fingerprint {{ phone_openvpn_server_fp }} - data-ciphers {{ phone_openvpn_data_ciphers }} - auth SHA256 - persist-key - persist-tun - resolv-retry infinite - nobind - verb 3 - - {{ phone_openvpn_server_cert_raw.content | b64decode | trim }} - - - {{ phone_openvpn_client_cert_raw.content | b64decode | trim }} - - - {{ phone_openvpn_client_key_raw.content | b64decode | trim }} - - delegate_to: localhost - vars: - ansible_connection: local - ansible_become: false - no_log: true - - handlers: - - name: reload systemd - ansible.builtin.systemd: - daemon_reload: true - - - name: restart phone openvpn - ansible.builtin.systemd: - name: "{{ phone_openvpn_service }}" - state: restarted diff --git a/docs/ai/plan.md b/docs/ai/plan.md index 2e3c5bf..4bd9520 100644 --- a/docs/ai/plan.md +++ b/docs/ai/plan.md @@ -2,36 +2,30 @@ ## Активные задачи -### Road-warrior OpenVPN для телефона +### Road-warrior OpenVPN для телефона — откачено -Выполнено 2026-09-03. `playbooks/openvpn-phone.yml` (`make openvpn-phone`) -поднимает на ru-vps отдельный TLS point-to-point инстанс: порт 9444/tcp, -`tun2`, сеть 10.80.0.0/30, юнит `homelab-openvpn-phone`, NAT -`10.80.0.0/30 -> LAN` через `tun0`. Профиль (с приватным ключом) — -`ansible/generated/ada-phone.ovpn` (gitignored). +2026-09-03: пробовали поднять клиентский OpenVPN для телефона (Android), +чтобы смотреть дашборд вне LAN. Перебрали три варианта на `ru-vps` (static +key -> TLS peer-fingerprint p2p -> `server`-режим с `push route` и ``). +На стороне ru-vps всё работало (тоннель вставал, TLS OK), но клиент +«OpenVPN for Android» стабильно падал на «сборка конфигурации / Used 101 +tries to get current version of the profile». По решению пользователя всё +откачено: `homelab-openvpn-phone`, `tun2`, правила UFW/NAT для 9444 и +`10.80.0.0/29`, `/etc/openvpn/homelab-phone` сняты с `ru-vps`; `playbooks/ +openvpn-phone.yml`, Make-цель и `homelab_vpn_client_routes` удалены из +репозитория. Сайт-тоннель `homelab-openvpn` (tun0) не затрагивался. -Крипто: **peer-fingerprint, без CA/PKI**. На ru-vps генерятся две -самоподписанные EC-пары (`server.*`, `client.*`), стороны пинят друг друга -по SHA256-отпечатку (`peer-fingerprint`), `data-ciphers AES-256-GCM`. Первая -попытка была на static key (`--secret`) — Android-клиент несёт OpenVPN 2.7 и -запускать её отказался («No tls-client or tls-server option»; 2.8 уберёт -совсем). Клиент на Android — «OpenVPN for Android» (Arne Schwabe) или -OpenVPN Connect: TLS-конфиг понимают оба. +Если возвращаться: скорее всего проблема на стороне приложения (battery +optimization / сохранение профиля), а не конфига; либо собрать полноценный +PKI через `community.crypto` и ``+`remote-cert-tls server`, либо +попробовать WireGuard. -Прогон идемпотентен (`changed=0` на повторе). Проверено: сервис active, -OpenVPN 2.6.19 слушает 9444, `tun2` up, `Using certificate fingerprint to -verify peer`, UFW 9444/tcp, NAT-правило на месте, с ru-vps -`192.168.1.30:8082` достижим. Маршруты клиента вынесены в -`homelab_vpn_client_routes` (`group_vars/all/main.yml`) — общий список с -`openvpn-laptop.yml`, не весь `/24` (домашняя сеть телефона тоже -192.168.1.0/24). - -Побочно: в `openvpn-laptop.yml` и `openvpn-phone.yml` локальная запись -профиля переведена с `become: false` на `vars: {ansible_connection: local, -ansible_become: false}` — ключ `become: false` на `delegate_to: localhost` -не подавлял унаследованный `ansible_become: true` (sudo password required). -`openvpn-laptop.yml` по-прежнему на static key и на живом ru-vps не -применялся — при реальном использовании ему нужен тот же перевод на TLS. +Предсуществующий баг, замеченный по ходу и НЕ тронутый: в +`openvpn-laptop.yml` (в проде не применялся) задачи с `delegate_to: +localhost` используют ключ `become: false`, который не подавляет +унаследованный `ansible_become: true` — при запуске упрётся в «sudo: a +password is required». Чинится `vars: {ansible_connection: local, +ansible_become: false}`. ### Дашборд-обзор инфраструктуры (Homepage)