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)