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)