Revert: drop the phone road-warrior OpenVPN attempt
lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s

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 <ca>). 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbuZrUoevfBgCpf5DCF4DG
This commit is contained in:
Dmitry
2026-09-03 10:17:26 +03:00
co-authored by Claude Sonnet 5
parent c5c33986da
commit d45391a261
5 changed files with 36 additions and 429 deletions
-4
View File
@@ -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)
-13
View File
@@ -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:
+15 -8
View File
@@ -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 @@
<secret>
{{ laptop_openvpn_static_key_raw.content | b64decode }}
</secret>
become: false
delegate_to: localhost
vars:
ansible_connection: local
ansible_become: false
no_log: true
handlers:
-377
View File
@@ -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 отказывается собирать конфиг без <ca>.
# Серверный самоподписанный сертификат сам себе 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
<ca>
{{ phone_openvpn_server_cert_raw.content | b64decode | trim }}
</ca>
<cert>
{{ phone_openvpn_client_cert_raw.content | b64decode | trim }}
</cert>
<key>
{{ phone_openvpn_client_key_raw.content | b64decode | trim }}
</key>
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
+21 -27
View File
@@ -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` и `<ca>`).
На стороне 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` и `<ca>`+`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)