From ba4daa35977d3c7aaecc1f9dd42a6dc41794d04c Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 1 Jun 2026 13:48:47 +0800 Subject: [PATCH] fix: align bridge OpenClaw protocol 4 deployment --- README.md | 15 + group_vars/xworkmate_bridge_distributed.yml | 36 +++ host_vars/cn-xworkmate-bridge.svc.plus.yml | 2 + .../xworkmate_bridge_distributed.yml | 3 + inventory.ini | 10 + .../vhosts/gateway_openclaw/defaults/main.yml | 7 +- roles/vhosts/gateway_openclaw/tasks/main.yml | 83 ++++- .../templates/openclaw.json.j2 | 6 +- roles/vhosts/xworkmate_bridge/README.md | 9 + .../vhosts/xworkmate_bridge/defaults/main.yml | 10 +- roles/vhosts/xworkmate_bridge/tasks/main.yml | 31 +- .../xworkmate_bridge/tasks/validate.yml | 10 + .../xworkmate_bridge/templates/config.yaml.j2 | 15 +- .../README.md | 172 +++++++++++ .../defaults/main.yml | 20 ++ .../handlers/main.yml | 24 ++ .../tasks/main.yml | 286 ++++++++++++++++++ .../templates/wg-xwm.conf.j2 | 11 + .../templates/wireguard-over-vless.json.j2 | 99 ++++++ .../templates/xray-wg-tproxy.service.j2 | 18 ++ .../xworkmate-bridge-vpn-forwarder.service.j2 | 14 + scripts/deploy_bridge_with_vault.sh | 6 +- vpn-wireguard-over-vless.yml | 65 ++++ 23 files changed, 911 insertions(+), 41 deletions(-) create mode 100644 group_vars/xworkmate_bridge_distributed.yml create mode 100644 host_vars/jp-xhttp-contabo.svc.plus/xworkmate_bridge_distributed.yml create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/README.md create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/defaults/main.yml create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/handlers/main.yml create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/tasks/main.yml create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wg-xwm.conf.j2 create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wireguard-over-vless.json.j2 create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xray-wg-tproxy.service.j2 create mode 100644 roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xworkmate-bridge-vpn-forwarder.service.j2 create mode 100644 vpn-wireguard-over-vless.yml diff --git a/README.md b/README.md index 538861e..fbc5997 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,20 @@ # playbooks +## XWorkmate Bridge Distributed VPN + +The bidirectional WireGuard-over-VLESS transport for the two XWorkmate bridge +nodes is deployed by: + +```bash +ansible-playbook -i inventory.ini vpn-wireguard-over-vless.yml +``` + +The implementation uses split bridge groups (`xworkmate_bridge` and +`cn_xworkmate_bridge`) under `xworkmate_bridge_distributed`, stores private keys +and the shared management-side Xray UUID in `https://vault.svc.plus`, and keeps +the host's default `xray.service` untouched. The runbook lives in +[`roles/vhosts/xworkmate_bridge_distributed_vpn/README.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/roles/vhosts/xworkmate_bridge_distributed_vpn/README.md). + ## Cloud Dev Desktop The cloud dev desktop flow lives here as two playbooks: diff --git a/group_vars/xworkmate_bridge_distributed.yml b/group_vars/xworkmate_bridge_distributed.yml new file mode 100644 index 0000000..88bb899 --- /dev/null +++ b/group_vars/xworkmate_bridge_distributed.yml @@ -0,0 +1,36 @@ +--- +xworkmate_bridge_distributed_topology: dual-node +xworkmate_bridge_distributed_nodes: + - id: xworkmate-bridge + role: primary + public_base_url: https://xworkmate-bridge.svc.plus + bridge_endpoint: http://172.29.10.1:8787 + - id: cn-xworkmate-bridge + role: edge + public_base_url: https://cn-xworkmate-bridge.svc.plus + bridge_endpoint: http://172.29.10.2:8787 + +xworkmate_bridge_distributed_vpn_interface: wg-xwm +xworkmate_bridge_distributed_vpn_wireguard_port: 51820 +xworkmate_bridge_distributed_vpn_local_tproxy_port: 51830 +xworkmate_bridge_distributed_vpn_vless_port: 2443 +xworkmate_bridge_distributed_vpn_forwarder_port: 8787 +xworkmate_bridge_distributed_vpn_forwarder_target: 127.0.0.1:8787 +xworkmate_bridge_distributed_vpn_vault_addr: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_URL') | default('https://vault.svc.plus', true) }}" +xworkmate_bridge_distributed_vpn_vault_token: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_ROOT_ACCESS_TOKEN') | default(lookup('ansible.builtin.env', 'VAULT_TOKEN'), true) }}" +xworkmate_bridge_distributed_vpn_vault_mount: kv +xworkmate_bridge_distributed_vpn_vault_base_path: xworkmate-bridge/distributed/wireguard-over-vless + +xworkmate_bridge_distributed_vpn_nodes: + jp-xhttp-contabo.svc.plus: + node_id: xworkmate-bridge + domain: xworkmate-bridge.svc.plus + wg_ip: 172.29.10.1 + public_key: 1staGq8lmHFRFRFNj2QOFx/MPxb/1fFV4tawC6xSi1Q= + peer: cn-xworkmate-bridge.svc.plus + cn-xworkmate-bridge.svc.plus: + node_id: cn-xworkmate-bridge + domain: cn-xworkmate-bridge.svc.plus + wg_ip: 172.29.10.2 + public_key: iYlnFaWiMfMelpiN8ZV2SwCDrLihqtJXvHUsM3BN9zU= + peer: jp-xhttp-contabo.svc.plus diff --git a/host_vars/cn-xworkmate-bridge.svc.plus.yml b/host_vars/cn-xworkmate-bridge.svc.plus.yml index 4079986..3af8e5f 100644 --- a/host_vars/cn-xworkmate-bridge.svc.plus.yml +++ b/host_vars/cn-xworkmate-bridge.svc.plus.yml @@ -12,3 +12,5 @@ xworkmate_bridge_required_listeners: - host: 127.0.0.1 port: "8787" name: bridge +xworkmate_bridge_distributed_local_node_id: cn-xworkmate-bridge +xworkmate_bridge_distributed_task_forward_peer_id: xworkmate-bridge diff --git a/host_vars/jp-xhttp-contabo.svc.plus/xworkmate_bridge_distributed.yml b/host_vars/jp-xhttp-contabo.svc.plus/xworkmate_bridge_distributed.yml new file mode 100644 index 0000000..0b97912 --- /dev/null +++ b/host_vars/jp-xhttp-contabo.svc.plus/xworkmate_bridge_distributed.yml @@ -0,0 +1,3 @@ +--- +xworkmate_bridge_distributed_local_node_id: xworkmate-bridge +xworkmate_bridge_distributed_task_forward_peer_id: "" diff --git a/inventory.ini b/inventory.ini index 6d51d6b..e1cf5cb 100644 --- a/inventory.ini +++ b/inventory.ini @@ -43,6 +43,16 @@ jp-xhttp-contabo.svc.plus tky-proxy.svc.plus jp-xhttp-contabo.svc.plus +[xworkmate_bridge] +jp-xhttp-contabo.svc.plus + +[cn_xworkmate_bridge] +cn-xworkmate-bridge.svc.plus + +[xworkmate_bridge_distributed:children] +xworkmate_bridge +cn_xworkmate_bridge + [billing_service] jp-xhttp-contabo.svc.plus diff --git a/roles/vhosts/gateway_openclaw/defaults/main.yml b/roles/vhosts/gateway_openclaw/defaults/main.yml index cad8a35..b4fcd71 100644 --- a/roles/vhosts/gateway_openclaw/defaults/main.yml +++ b/roles/vhosts/gateway_openclaw/defaults/main.yml @@ -16,6 +16,8 @@ gateway_openclaw_profile_script_path: /etc/profile.d/openclaw-user-systemd.sh gateway_openclaw_home: "/home/{{ gateway_openclaw_service_user }}" gateway_openclaw_binary_path: "{{ gateway_openclaw_home }}/.local/bin/openclaw" gateway_openclaw_install_dir: "{{ gateway_openclaw_home }}/.local/lib/node_modules/openclaw" +gateway_openclaw_required_version: "2026.5.28" +gateway_openclaw_npm_package_spec: "openclaw@{{ gateway_openclaw_required_version }}" gateway_openclaw_extension_dependency_dirs: - "{{ gateway_openclaw_install_dir }}/dist/extensions/acpx" gateway_openclaw_config_path: "{{ gateway_openclaw_home }}/.openclaw/openclaw.json" @@ -24,6 +26,7 @@ gateway_openclaw_workspace_mode: "0775" gateway_openclaw_compile_cache_dir: /var/tmp/openclaw-compile-cache gateway_openclaw_service_path: "{{ gateway_openclaw_home }}/.nix-profile/bin:{{ gateway_openclaw_home }}/.local/bin:{{ gateway_openclaw_home }}/.npm-global/bin:{{ gateway_openclaw_home }}/bin:/usr/local/bin:/usr/bin:/bin" gateway_openclaw_extension_backup_dir: "{{ gateway_openclaw_home }}/.openclaw/backups/extensions" +gateway_openclaw_doctor_repair_enabled: true gateway_openclaw_upstream_host: 127.0.0.1 gateway_openclaw_upstream_port: 18789 @@ -53,7 +56,9 @@ gateway_openclaw_default_models: nvidia/nemotron-3-super-120b-a12b: {} nvidia/minimaxai/minimax-m2.5: {} nvidia/z-ai/glm5: {} - openai-codex/gpt-5.5: {} + openai/gpt-5.5: + agentRuntime: + id: codex gateway_openclaw_main_agent_model: nvidia/nemotron-3-super-120b-a12b gateway_openclaw_main_agent_skills: diff --git a/roles/vhosts/gateway_openclaw/tasks/main.yml b/roles/vhosts/gateway_openclaw/tasks/main.yml index 317f2cd..232e65e 100644 --- a/roles/vhosts/gateway_openclaw/tasks/main.yml +++ b/roles/vhosts/gateway_openclaw/tasks/main.yml @@ -68,6 +68,28 @@ group: "{{ gateway_openclaw_service_group }}" mode: "0700" +- name: Install required OpenClaw gateway package version + ansible.builtin.command: + cmd: >- + npm install --global --omit=dev --no-audit --no-fund + --prefix "{{ gateway_openclaw_home }}/.local" + "{{ gateway_openclaw_npm_package_spec }}" + environment: + HOME: "{{ gateway_openclaw_home }}" + PATH: "{{ gateway_openclaw_service_path }}" + OPENCLAW_NO_RESPAWN: "1" + NODE_COMPILE_CACHE: "{{ gateway_openclaw_compile_cache_dir }}" + become: true + become_user: "{{ gateway_openclaw_service_user }}" + register: gateway_openclaw_package_install + changed_when: >- + 'added ' in (gateway_openclaw_package_install.stdout | default('')) or + 'removed ' in (gateway_openclaw_package_install.stdout | default('')) or + 'changed ' in (gateway_openclaw_package_install.stdout | default('')) + when: + - not ansible_check_mode + notify: Restart openclaw gateway + - name: Move stale OpenClaw plugin backups out of extension scan path ansible.builtin.shell: | set -euo pipefail @@ -116,13 +138,6 @@ diff: false notify: Restart openclaw gateway -- name: Restore immutable flag on OpenClaw gateway JSON config - ansible.builtin.command: - cmd: chattr +i "{{ gateway_openclaw_config_path }}" - when: - - "'i' in (gateway_openclaw_config_attrs.stdout | default(''))" - changed_when: true - - name: Inspect OpenClaw package manifest ansible.builtin.stat: path: "{{ gateway_openclaw_install_dir }}/package.json" @@ -183,6 +198,35 @@ - item.stat.exists | default(false) - not ansible_check_mode +- name: Repair OpenClaw 2026.5 route state as service user + ansible.builtin.command: + cmd: "{{ gateway_openclaw_binary_path }} doctor --fix --non-interactive" + environment: + HOME: "{{ gateway_openclaw_home }}" + PATH: "{{ gateway_openclaw_service_path }}" + OPENCLAW_NO_RESPAWN: "1" + NODE_COMPILE_CACHE: "{{ gateway_openclaw_compile_cache_dir }}" + OPENCLAW_SERVICE_REPAIR_POLICY: external + become: true + become_user: "{{ gateway_openclaw_service_user }}" + register: gateway_openclaw_doctor_repair + changed_when: >- + ((gateway_openclaw_doctor_repair.stdout | default('')) ~ '\n' ~ + (gateway_openclaw_doctor_repair.stderr | default(''))) + is search('(?i)(fixed|repaired|migrated|rewrote|removed|set |moved |updated|archived)') + when: + - gateway_openclaw_doctor_repair_enabled | bool + - not ansible_check_mode + notify: Restart openclaw gateway + +- name: Restore immutable flag on OpenClaw gateway JSON config + ansible.builtin.command: + cmd: chattr +i "{{ gateway_openclaw_config_path }}" + when: + - "'i' in (gateway_openclaw_config_attrs.stdout | default(''))" + - not ansible_check_mode + changed_when: true + - name: Inspect OpenClaw gateway binary ansible.builtin.stat: path: "{{ gateway_openclaw_binary_path }}" @@ -196,6 +240,31 @@ - gateway_openclaw_binary.stat.executable | default(false) fail_msg: "OpenClaw gateway binary is missing or not executable: {{ gateway_openclaw_binary_path }}" +- name: Check OpenClaw gateway version + ansible.builtin.command: + cmd: "{{ gateway_openclaw_binary_path }} --version" + environment: + HOME: "{{ gateway_openclaw_home }}" + PATH: "{{ gateway_openclaw_service_path }}" + OPENCLAW_NO_RESPAWN: "1" + NODE_COMPILE_CACHE: "{{ gateway_openclaw_compile_cache_dir }}" + become: true + become_user: "{{ gateway_openclaw_service_user }}" + register: gateway_openclaw_version + changed_when: false + when: + - not ansible_check_mode + +- name: Assert OpenClaw gateway version is pinned + ansible.builtin.assert: + that: + - gateway_openclaw_version.stdout is search('OpenClaw ' ~ gateway_openclaw_required_version) + fail_msg: >- + OpenClaw gateway must run {{ gateway_openclaw_required_version }} after the + package upgrade. Actual version output: {{ gateway_openclaw_version.stdout | default('') }} + when: + - not ansible_check_mode + - name: Ensure OpenClaw user systemd unit directory exists ansible.builtin.file: path: "{{ gateway_openclaw_user_service_unit_path | dirname }}" diff --git a/roles/vhosts/gateway_openclaw/templates/openclaw.json.j2 b/roles/vhosts/gateway_openclaw/templates/openclaw.json.j2 index a649140..399bff2 100644 --- a/roles/vhosts/gateway_openclaw/templates/openclaw.json.j2 +++ b/roles/vhosts/gateway_openclaw/templates/openclaw.json.j2 @@ -91,13 +91,13 @@ }, "wizard": { "lastRunAt": "2026-04-19T10:52:37.655Z", - "lastRunVersion": "2026.4.15", + "lastRunVersion": "2026.5.28", "lastRunCommand": "configure", "lastRunMode": "local" }, "meta": { - "lastTouchedVersion": "2026.4.26", - "lastTouchedAt": "2026-04-29T04:41:12.010Z" + "lastTouchedVersion": "2026.5.28", + "lastTouchedAt": "2026-06-01T00:00:00.000Z" }, "acp": { "enabled": {{ gateway_openclaw_acp_enabled | bool | to_json }}, diff --git a/roles/vhosts/xworkmate_bridge/README.md b/roles/vhosts/xworkmate_bridge/README.md index 5c7c1ef..366cb62 100644 --- a/roles/vhosts/xworkmate_bridge/README.md +++ b/roles/vhosts/xworkmate_bridge/README.md @@ -6,6 +6,15 @@ This document records the current real deployment and runtime validation state f `roles/vhosts/xworkmate_bridge` owns the public ingress and validation contract for `xworkmate-bridge.svc.plus`. +The private distributed bridge transport is managed by +[`roles/vhosts/xworkmate_bridge_distributed_vpn`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/roles/vhosts/xworkmate_bridge_distributed_vpn/README.md) +and deployed through +[`vpn-wireguard-over-vless.yml`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/vpn-wireguard-over-vless.yml). +That role provides `wg-xwm`, `xray-wg-tproxy.service`, and the VPN-only +`172.29.10.0/24` bridge forwarders. This bridge role consumes the resulting +distributed topology config; CN forwards tasks to the primary private endpoint, +while the primary node keeps reverse task forwarding disabled. + The provider runtimes remain separate sibling roles: - [`roles/vhosts/acp_codex`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/roles/vhosts/acp_codex) diff --git a/roles/vhosts/xworkmate_bridge/defaults/main.yml b/roles/vhosts/xworkmate_bridge/defaults/main.yml index 392d1a1..e13d741 100644 --- a/roles/vhosts/xworkmate_bridge/defaults/main.yml +++ b/roles/vhosts/xworkmate_bridge/defaults/main.yml @@ -12,17 +12,23 @@ xworkmate_bridge_base_dir: /opt/cloud-neutral/xworkmate-bridge xworkmate_bridge_config_file: "{{ xworkmate_bridge_base_dir }}/config.yaml" xworkmate_bridge_binary_path: /usr/local/bin/xworkmate-go-core xworkmate_bridge_systemd_unit_path: "/etc/systemd/system/{{ xworkmate_bridge_service_name }}.service" -xworkmate_bridge_runtime_image_ref: "{{ service_compose_image | default(lookup('ansible.builtin.env', 'SERVICE_COMPOSE_IMAGE') | default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_IMAGE') | default('', true), true), true) }}" xworkmate_bridge_deprecated_container_name: xworkmate-bridge-managed xworkmate_bridge_deprecated_compose_file: "{{ xworkmate_bridge_base_dir }}/docker-compose.yml" +xworkmate_bridge_obsolete_systemd_dropin_paths: + - "/etc/systemd/system/{{ xworkmate_bridge_service_name }}.service.d/20-distributed-forward.conf" xworkmate_bridge_service_environment: BRIDGE_AUTH_TOKEN: "{{ xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token) }}" BRIDGE_REVIEW_AUTH_TOKEN: "{{ xworkmate_bridge_effective_review_auth_token | default(xworkmate_bridge_review_auth_token) }}" BRIDGE_CONFIG_PATH: "{{ xworkmate_bridge_config_file }}" - IMAGE: "{{ xworkmate_bridge_effective_runtime_image_ref | default(xworkmate_bridge_runtime_image_ref) }}" xworkmate_bridge_openclaw_gateway_max_active: 5 xworkmate_bridge_openclaw_gateway_max_queued: 20 xworkmate_bridge_openclaw_gateway_queue_timeout: 10m +xworkmate_bridge_distributed_topology: "" +xworkmate_bridge_distributed_local_node_id: "" +xworkmate_bridge_distributed_task_forward_peer_id: "" +xworkmate_bridge_distributed_task_forward_endpoint: "" +xworkmate_bridge_distributed_task_forward_token: "" +xworkmate_bridge_distributed_nodes: [] xworkmate_bridge_required_services: - acp-codex.service - acp-opencode.service diff --git a/roles/vhosts/xworkmate_bridge/tasks/main.yml b/roles/vhosts/xworkmate_bridge/tasks/main.yml index 982b71a..a30d343 100644 --- a/roles/vhosts/xworkmate_bridge/tasks/main.yml +++ b/roles/vhosts/xworkmate_bridge/tasks/main.yml @@ -40,19 +40,6 @@ failed_when: false no_log: true -- name: Read existing xworkmate-bridge image ref from systemd unit - ansible.builtin.shell: | - set -euo pipefail - if [ -f "{{ xworkmate_bridge_systemd_unit_path }}" ]; then - sed -n 's/^Environment="IMAGE=\(.*\)"$/\1/p' "{{ xworkmate_bridge_systemd_unit_path }}" | head -n 1 - fi - args: - executable: /bin/bash - register: xworkmate_bridge_existing_image_ref - check_mode: false - changed_when: false - failed_when: false - - name: Resolve xworkmate-bridge auth token ansible.builtin.set_fact: xworkmate_bridge_effective_auth_token: >- @@ -69,15 +56,6 @@ }} no_log: true -- name: Resolve xworkmate-bridge runtime image ref - ansible.builtin.set_fact: - xworkmate_bridge_effective_runtime_image_ref: >- - {{ - xworkmate_bridge_runtime_image_ref - if (xworkmate_bridge_runtime_image_ref | trim | length > 0) - else (xworkmate_bridge_existing_image_ref.stdout | default('')) - }} - - name: Assert xworkmate-bridge binary exists ansible.builtin.stat: path: "{{ xworkmate_bridge_binary_path }}" @@ -117,6 +95,13 @@ path: "{{ xworkmate_bridge_deprecated_compose_file }}" state: absent +- name: Remove obsolete xworkmate-bridge systemd drop-ins + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: "{{ xworkmate_bridge_obsolete_systemd_dropin_paths }}" + notify: Reload bridge + - name: Deploy xworkmate-bridge runtime configuration ansible.builtin.template: src: config.yaml.j2 @@ -147,6 +132,8 @@ owner: root group: root mode: "0644" + diff: false + no_log: true register: xworkmate_bridge_systemd_unit notify: Reload bridge diff --git a/roles/vhosts/xworkmate_bridge/tasks/validate.yml b/roles/vhosts/xworkmate_bridge/tasks/validate.yml index 2bcc074..0951514 100644 --- a/roles/vhosts/xworkmate_bridge/tasks/validate.yml +++ b/roles/vhosts/xworkmate_bridge/tasks/validate.yml @@ -116,6 +116,16 @@ changed_when: false no_log: true +- name: Assert xworkmate-bridge ping reports native binary metadata + ansible.builtin.assert: + that: + - xworkmate_bridge_service_ping.json.version | default('') | trim | length > 0 + - xworkmate_bridge_service_ping.json.image | default('') | trim | length == 0 + - xworkmate_bridge_service_ping.json.tag | default('') | trim | length == 0 + fail_msg: >- + xworkmate-bridge /api/ping must report native binary metadata and must + not expose stale Docker image/tag metadata. + - name: Check xworkmate-bridge capabilities contract ansible.builtin.uri: url: "https://{{ xworkmate_bridge_service_domain }}/acp/rpc" diff --git a/roles/vhosts/xworkmate_bridge/templates/config.yaml.j2 b/roles/vhosts/xworkmate_bridge/templates/config.yaml.j2 index dbd6361..6c7403a 100644 --- a/roles/vhosts/xworkmate_bridge/templates/config.yaml.j2 +++ b/roles/vhosts/xworkmate_bridge/templates/config.yaml.j2 @@ -15,6 +15,20 @@ openclaw_gateway: max_queued: {{ xworkmate_bridge_openclaw_gateway_max_queued | int }} queue_timeout: "{{ xworkmate_bridge_openclaw_gateway_queue_timeout }}" +distributed: + topology: "{{ xworkmate_bridge_distributed_topology }}" + local_node_id: "{{ xworkmate_bridge_distributed_local_node_id }}" + task_forward_peer_id: "{{ xworkmate_bridge_distributed_task_forward_peer_id }}" + task_forward_endpoint: "{{ xworkmate_bridge_distributed_task_forward_endpoint }}" + task_forward_token: "{{ xworkmate_bridge_distributed_task_forward_token }}" + nodes: +{% for node in xworkmate_bridge_distributed_nodes %} + - id: "{{ node.id }}" + role: "{{ node.role }}" + public_base_url: "{{ node.public_base_url }}" + bridge_endpoint: "{{ node.bridge_endpoint }}" +{% endfor %} + # Server orchestration settings bridge: listenAddr: "{{ xworkmate_bridge_listen_addr }}" @@ -26,4 +40,3 @@ bridge: # Operational notes for this deployment notes: - Bridge Auth Token is managed via BRIDGE_AUTH_TOKEN environment variable. - - Deployed on: {{ ansible_date_time.iso8601 | default('N/A') }} diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/README.md b/roles/vhosts/xworkmate_bridge_distributed_vpn/README.md new file mode 100644 index 0000000..5a80c2f --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/README.md @@ -0,0 +1,172 @@ +# xworkmate_bridge_distributed_vpn + +This role deploys the private transport used by the XWorkmate bridge distributed extension. + +## Topology + +The current implementation is a two-node `dual-node` topology: + +- `jp-xhttp-contabo.svc.plus` is the primary node for `xworkmate-bridge.svc.plus`. +- `cn-xworkmate-bridge.svc.plus` is the CN edge node for `cn-xworkmate-bridge.svc.plus`. + +Both nodes run the same private network path: + +```text +WireGuard peer -> 127.0.0.1:51830 -> xray-wg-tproxy -> VLESS/TLS -> peer xray-wg-tproxy -> peer UDP 51820 +``` + +The role intentionally does not manage the host's default `xray.service` or +`/usr/local/etc/xray/config.json`. WireGuard-over-VLESS uses its own config and +service: + +- `/usr/local/etc/xray/wireguard-over-vless.json` +- `xray-wg-tproxy.service` + +## Managed Services + +Each node gets: + +- WireGuard interface: `wg-xwm` +- WireGuard listen port: UDP `51820` +- local Xray dokodemo-door ingress: `127.0.0.1:51830` +- VLESS/TLS listen port: TCP `2443` +- VPN-only bridge forwarder: `:8787 -> 127.0.0.1:8787` + +Systemd units: + +- `wg-quick@wg-xwm.service` +- `xray-wg-tproxy.service` +- `xworkmate-bridge-vpn-forwarder.service` + +The WireGuard peer endpoint on both sides is local: + +```ini +Endpoint = 127.0.0.1:51830 +``` + +## Inventory And Variables + +The inventory uses split bridge groups and one distributed parent group: + +- `xworkmate_bridge` +- `cn_xworkmate_bridge` +- `xworkmate_bridge_distributed` + +Shared topology and VPN variables live in +[`group_vars/xworkmate_bridge_distributed.yml`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/group_vars/xworkmate_bridge_distributed.yml). + +Host-specific distributed bridge behavior lives in: + +- [`host_vars/jp-xhttp-contabo.svc.plus/xworkmate_bridge_distributed.yml`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/host_vars/jp-xhttp-contabo.svc.plus/xworkmate_bridge_distributed.yml) +- [`host_vars/cn-xworkmate-bridge.svc.plus.yml`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/host_vars/cn-xworkmate-bridge.svc.plus.yml) + +Important defaults: + +```yaml +xworkmate_bridge_distributed_vpn_interface: wg-xwm +xworkmate_bridge_distributed_vpn_wireguard_port: 51820 +xworkmate_bridge_distributed_vpn_local_tproxy_port: 51830 +xworkmate_bridge_distributed_vpn_vless_port: 2443 +xworkmate_bridge_distributed_vpn_forwarder_port: 8787 +``` + +## Secrets + +This role reads secrets from the Vault service, not from a local Ansible Vault +password file. + +Required controller environment: + +```bash +export VAULT_SERVER_URL=https://vault.svc.plus +export VAULT_SERVER_ROOT_ACCESS_TOKEN=... +``` + +`VAULT_TOKEN` is also accepted when `VAULT_SERVER_ROOT_ACCESS_TOKEN` is not set. +Do not commit Vault tokens, WireGuard private keys, or the shared Xray UUID. + +Vault KV base path: + +```text +kv/xworkmate-bridge/distributed/wireguard-over-vless +``` + +Expected secret layout: + +```text +common + xray_uuid +hosts/ + wireguard_private_key +``` + +The Xray UUID is the shared management-side UUID for this bridge transport. It +is not derived from tenant accounts or Xray account sync. + +## Bridge Forwarding + +The VPN forwarder exposes each bridge only on the WireGuard address: + +- primary: `172.29.10.1:8787 -> 127.0.0.1:8787` +- CN edge: `172.29.10.2:8787 -> 127.0.0.1:8787` + +Distributed task forwarding is configured through bridge topology. CN sets +`task_forward_peer_id: xworkmate-bridge`, so the bridge resolves the primary +private endpoint from `xworkmate_bridge_distributed_nodes`: + +```text +http://172.29.10.1:8787 +``` + +The primary node leaves `task_forward_peer_id` empty. That keeps the reverse +WireGuard/VLESS path available for private network reachability without sending +primary runtime tasks back to CN. + +Both sides use the same `BRIDGE_AUTH_TOKEN`. CN does not configure a separate +forwarding token; an empty forwarding token means the bridge reuses its local +auth token. + +## Deploy + +Run from the playbooks repo: + +```bash +cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks +export VAULT_SERVER_URL=https://vault.svc.plus +export VAULT_SERVER_ROOT_ACCESS_TOKEN=... + +ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i inventory.ini vpn-wireguard-over-vless.yml --check --diff +ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i inventory.ini vpn-wireguard-over-vless.yml -f 1 +``` + +Use `-f 1` for this two-host path when long SSH control sessions are unstable. + +## Verification + +On both hosts: + +```bash +systemctl is-active xray-wg-tproxy wg-quick@wg-xwm xworkmate-bridge-vpn-forwarder xworkmate-bridge +xray run -test -config /usr/local/etc/xray/wireguard-over-vless.json +wg show wg-xwm +``` + +From the primary node: + +```bash +ping -c 3 172.29.10.2 +curl -H "Authorization: Bearer $BRIDGE_AUTH_TOKEN" http://172.29.10.2:8787/api/ping +``` + +From the CN edge node: + +```bash +ping -c 3 172.29.10.1 +curl -H "Authorization: Bearer $BRIDGE_AUTH_TOKEN" http://172.29.10.1:8787/api/ping +``` + +Regression checks: + +- the primary host's `xray.service` still starts the original `/usr/local/etc/xray/config.json` +- both public bridge HTTPS endpoints still return `/api/ping` +- CN task forwarding resolves to the private `http://172.29.10.1:8787` endpoint diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/defaults/main.yml b/roles/vhosts/xworkmate_bridge_distributed_vpn/defaults/main.yml new file mode 100644 index 0000000..7e4138a --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/defaults/main.yml @@ -0,0 +1,20 @@ +--- +xworkmate_bridge_distributed_vpn_interface: wg-xwm +xworkmate_bridge_distributed_vpn_wireguard_port: 51820 +xworkmate_bridge_distributed_vpn_local_tproxy_port: 51830 +xworkmate_bridge_distributed_vpn_vless_port: 2443 +xworkmate_bridge_distributed_vpn_forwarder_port: 8787 +xworkmate_bridge_distributed_vpn_forwarder_target: 127.0.0.1:8787 +xworkmate_bridge_distributed_vpn_mtu: 1400 +xworkmate_bridge_distributed_vpn_persistent_keepalive: 25 +xworkmate_bridge_distributed_vpn_xray_bin_path: /usr/local/bin/xray +xworkmate_bridge_distributed_vpn_xray_local_cache_dir: /tmp/xray-wireguard-over-vless +xworkmate_bridge_distributed_vpn_xray_config_dir: /usr/local/etc/xray +xworkmate_bridge_distributed_vpn_xray_config_path: "{{ xworkmate_bridge_distributed_vpn_xray_config_dir }}/wireguard-over-vless.json" +xworkmate_bridge_distributed_vpn_xray_service_name: xray-wg-tproxy +xworkmate_bridge_distributed_vpn_forwarder_service_name: xworkmate-bridge-vpn-forwarder +xworkmate_bridge_distributed_vpn_vault_addr: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_URL') | default('https://vault.svc.plus', true) }}" +xworkmate_bridge_distributed_vpn_vault_token: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_ROOT_ACCESS_TOKEN') | default(lookup('ansible.builtin.env', 'VAULT_TOKEN'), true) }}" +xworkmate_bridge_distributed_vpn_vault_mount: kv +xworkmate_bridge_distributed_vpn_vault_base_path: xworkmate-bridge/distributed/wireguard-over-vless +xworkmate_bridge_distributed_vpn_nodes: {} diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/handlers/main.yml b/roles/vhosts/xworkmate_bridge_distributed_vpn/handlers/main.yml new file mode 100644 index 0000000..817d6cc --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/handlers/main.yml @@ -0,0 +1,24 @@ +--- +- name: Restart xray-wg-tproxy + ansible.builtin.systemd: + name: "{{ xworkmate_bridge_distributed_vpn_xray_service_name }}" + state: restarted + daemon_reload: true + when: + - not ansible_check_mode + +- name: Restart wg-xwm + ansible.builtin.systemd: + name: "wg-quick@{{ xworkmate_bridge_distributed_vpn_interface }}" + state: restarted + daemon_reload: true + when: + - not ansible_check_mode + +- name: Restart xworkmate-bridge-vpn-forwarder + ansible.builtin.systemd: + name: "{{ xworkmate_bridge_distributed_vpn_forwarder_service_name }}" + state: restarted + daemon_reload: true + when: + - not ansible_check_mode diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/tasks/main.yml b/roles/vhosts/xworkmate_bridge_distributed_vpn/tasks/main.yml new file mode 100644 index 0000000..bd67d68 --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/tasks/main.yml @@ -0,0 +1,286 @@ +--- +- name: Assert distributed VPN node is defined for this host + ansible.builtin.assert: + that: + - inventory_hostname in xworkmate_bridge_distributed_vpn_nodes + - xworkmate_bridge_distributed_vpn_nodes[inventory_hostname].peer in xworkmate_bridge_distributed_vpn_nodes + fail_msg: "Missing xworkmate_bridge_distributed_vpn_nodes entry for {{ inventory_hostname }} or its peer" + +- name: Resolve distributed VPN node facts + ansible.builtin.set_fact: + xworkmate_bridge_distributed_vpn_current_node: "{{ xworkmate_bridge_distributed_vpn_nodes[inventory_hostname] }}" + xworkmate_bridge_distributed_vpn_peer_node: "{{ xworkmate_bridge_distributed_vpn_nodes[xworkmate_bridge_distributed_vpn_nodes[inventory_hostname].peer] }}" + +- name: Resolve Caddy-managed TLS certificate paths for VLESS + ansible.builtin.set_fact: + xworkmate_bridge_distributed_vpn_tls_cert_path: "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{ xworkmate_bridge_distributed_vpn_current_node.domain }}/{{ xworkmate_bridge_distributed_vpn_current_node.domain }}.crt" + xworkmate_bridge_distributed_vpn_tls_key_path: "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{ xworkmate_bridge_distributed_vpn_current_node.domain }}/{{ xworkmate_bridge_distributed_vpn_current_node.domain }}.key" + +- name: Assert Vault access is configured for distributed VPN secrets + ansible.builtin.assert: + that: + - xworkmate_bridge_distributed_vpn_vault_addr | trim | length > 0 + - xworkmate_bridge_distributed_vpn_vault_token | trim | length > 0 + fail_msg: "Set VAULT_SERVER_URL and VAULT_SERVER_ROOT_ACCESS_TOKEN, or VAULT_TOKEN, before deploying distributed VPN secrets" + no_log: true + +- name: Read shared distributed VPN Xray UUID from Vault + ansible.builtin.uri: + url: "{{ xworkmate_bridge_distributed_vpn_vault_addr }}/v1/{{ xworkmate_bridge_distributed_vpn_vault_mount }}/data/{{ xworkmate_bridge_distributed_vpn_vault_base_path }}/common" + headers: + X-Vault-Token: "{{ xworkmate_bridge_distributed_vpn_vault_token }}" + return_content: true + status_code: 200 + timeout: 30 + register: xworkmate_bridge_distributed_vpn_common_secret + until: xworkmate_bridge_distributed_vpn_common_secret.status | default(0) == 200 + retries: 5 + delay: 3 + delegate_to: localhost + become: false + check_mode: false + no_log: true + +- name: Read host distributed VPN WireGuard secret from Vault + ansible.builtin.uri: + url: "{{ xworkmate_bridge_distributed_vpn_vault_addr }}/v1/{{ xworkmate_bridge_distributed_vpn_vault_mount }}/data/{{ xworkmate_bridge_distributed_vpn_vault_base_path }}/{{ inventory_hostname }}" + headers: + X-Vault-Token: "{{ xworkmate_bridge_distributed_vpn_vault_token }}" + return_content: true + status_code: 200 + timeout: 30 + register: xworkmate_bridge_distributed_vpn_host_secret + until: xworkmate_bridge_distributed_vpn_host_secret.status | default(0) == 200 + retries: 5 + delay: 3 + delegate_to: localhost + become: false + check_mode: false + no_log: true + +- name: Resolve distributed VPN secrets + ansible.builtin.set_fact: + xworkmate_bridge_distributed_vpn_xray_uuid: "{{ xworkmate_bridge_distributed_vpn_common_secret.json.data.data.xray_uuid }}" + xworkmate_bridge_distributed_vpn_wireguard_private_key: "{{ xworkmate_bridge_distributed_vpn_host_secret.json.data.data.wireguard_private_key }}" + no_log: true + +- name: Install distributed VPN packages + ansible.builtin.apt: + name: + - ca-certificates + - curl + - unzip + - wireguard-tools + - socat + state: present + update_cache: true + when: + - ansible_os_family == "Debian" + +- name: Check Xray binary + ansible.builtin.stat: + path: "{{ xworkmate_bridge_distributed_vpn_xray_bin_path }}" + register: xworkmate_bridge_distributed_vpn_xray_binary + +- name: Resolve Xray release asset architecture + ansible.builtin.set_fact: + xworkmate_bridge_distributed_vpn_xray_asset_arch: "{{ 'arm64-v8a' if ansible_architecture in ['aarch64', 'arm64'] else '64' }}" + when: + - not xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + - not ansible_check_mode + +- name: Resolve controller-side Xray cache paths + ansible.builtin.set_fact: + xworkmate_bridge_distributed_vpn_xray_local_cache_path: "{{ xworkmate_bridge_distributed_vpn_xray_local_cache_dir }}-{{ xworkmate_bridge_distributed_vpn_xray_asset_arch }}" + xworkmate_bridge_distributed_vpn_xray_local_archive_path: "{{ xworkmate_bridge_distributed_vpn_xray_local_cache_dir }}-{{ xworkmate_bridge_distributed_vpn_xray_asset_arch }}.zip" + when: + - not xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + - not ansible_check_mode + +- name: Check controller-side cached Xray binary + ansible.builtin.stat: + path: "{{ xworkmate_bridge_distributed_vpn_xray_local_cache_path }}/xray" + register: xworkmate_bridge_distributed_vpn_xray_cached_binary + delegate_to: localhost + become: false + when: + - not xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + - not ansible_check_mode + +- name: Download Xray release archive + ansible.builtin.get_url: + url: "https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-{{ xworkmate_bridge_distributed_vpn_xray_asset_arch }}.zip" + dest: "{{ xworkmate_bridge_distributed_vpn_xray_local_archive_path }}" + mode: "0644" + timeout: 120 + register: xworkmate_bridge_distributed_vpn_xray_download + until: xworkmate_bridge_distributed_vpn_xray_download is succeeded + retries: 3 + delay: 5 + delegate_to: localhost + become: false + when: + - not xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + - not (xworkmate_bridge_distributed_vpn_xray_cached_binary.stat.exists | default(false)) + - not ansible_check_mode + +- name: Create temporary Xray install directory + ansible.builtin.file: + path: "{{ xworkmate_bridge_distributed_vpn_xray_local_cache_path }}" + state: directory + mode: "0755" + delegate_to: localhost + become: false + when: + - not xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + - not (xworkmate_bridge_distributed_vpn_xray_cached_binary.stat.exists | default(false)) + - not ansible_check_mode + +- name: Extract Xray release archive + ansible.builtin.unarchive: + src: "{{ xworkmate_bridge_distributed_vpn_xray_local_archive_path }}" + dest: "{{ xworkmate_bridge_distributed_vpn_xray_local_cache_path }}" + remote_src: true + delegate_to: localhost + become: false + when: + - not xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + - not (xworkmate_bridge_distributed_vpn_xray_cached_binary.stat.exists | default(false)) + - not ansible_check_mode + +- name: Install Xray binary without creating the default xray.service + ansible.builtin.copy: + src: "{{ xworkmate_bridge_distributed_vpn_xray_local_cache_path }}/xray" + dest: "{{ xworkmate_bridge_distributed_vpn_xray_bin_path }}" + owner: root + group: root + mode: "0755" + when: + - not xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + - not ansible_check_mode + +- name: Ensure distributed VPN config directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - /etc/wireguard + - "{{ xworkmate_bridge_distributed_vpn_xray_config_dir }}" + +- name: Check VLESS TLS certificate files + ansible.builtin.stat: + path: "{{ item }}" + register: xworkmate_bridge_distributed_vpn_tls_files + loop: + - "{{ xworkmate_bridge_distributed_vpn_tls_cert_path }}" + - "{{ xworkmate_bridge_distributed_vpn_tls_key_path }}" + +- name: Assert VLESS TLS certificate files exist + ansible.builtin.assert: + that: + - item.stat.exists + - item.stat.isreg + fail_msg: "Missing VLESS TLS certificate file: {{ item.item }}" + loop: "{{ xworkmate_bridge_distributed_vpn_tls_files.results }}" + loop_control: + label: "{{ item.item }}" + +- name: Deploy Xray WireGuard over VLESS config + ansible.builtin.template: + src: wireguard-over-vless.json.j2 + dest: "{{ xworkmate_bridge_distributed_vpn_xray_config_path }}" + owner: root + group: root + mode: "0600" + notify: Restart xray-wg-tproxy + no_log: true + +- name: Deploy Xray WireGuard over VLESS service + ansible.builtin.template: + src: xray-wg-tproxy.service.j2 + dest: "/etc/systemd/system/{{ xworkmate_bridge_distributed_vpn_xray_service_name }}.service" + owner: root + group: root + mode: "0644" + notify: Restart xray-wg-tproxy + +- name: Test Xray WireGuard over VLESS config + ansible.builtin.command: + cmd: "{{ xworkmate_bridge_distributed_vpn_xray_bin_path }} run -test -config {{ xworkmate_bridge_distributed_vpn_xray_config_path }}" + changed_when: false + when: + - not ansible_check_mode or xworkmate_bridge_distributed_vpn_xray_binary.stat.exists + +- name: Deploy WireGuard distributed bridge config + ansible.builtin.template: + src: wg-xwm.conf.j2 + dest: "/etc/wireguard/{{ xworkmate_bridge_distributed_vpn_interface }}.conf" + owner: root + group: root + mode: "0600" + notify: Restart wg-xwm + no_log: true + +- name: Deploy VPN-only bridge forwarder service + ansible.builtin.template: + src: xworkmate-bridge-vpn-forwarder.service.j2 + dest: "/etc/systemd/system/{{ xworkmate_bridge_distributed_vpn_forwarder_service_name }}.service" + owner: root + group: root + mode: "0644" + notify: Restart xworkmate-bridge-vpn-forwarder + +- name: Apply distributed VPN unit changes before service checks + ansible.builtin.meta: flush_handlers + +- name: Enable and start Xray WireGuard over VLESS service + ansible.builtin.systemd: + name: "{{ xworkmate_bridge_distributed_vpn_xray_service_name }}" + enabled: true + state: started + daemon_reload: true + when: + - not ansible_check_mode + +- name: Enable and start WireGuard distributed bridge interface + ansible.builtin.systemd: + name: "wg-quick@{{ xworkmate_bridge_distributed_vpn_interface }}" + enabled: true + state: started + daemon_reload: true + when: + - not ansible_check_mode + +- name: Enable and start VPN-only bridge forwarder + ansible.builtin.systemd: + name: "{{ xworkmate_bridge_distributed_vpn_forwarder_service_name }}" + enabled: true + state: started + daemon_reload: true + when: + - not ansible_check_mode + +- name: Check distributed VPN service active states + ansible.builtin.systemd: + name: "{{ item }}" + register: xworkmate_bridge_distributed_vpn_service_status + until: xworkmate_bridge_distributed_vpn_service_status.status.ActiveState | default('') == "active" + retries: 6 + delay: 2 + loop: + - "{{ xworkmate_bridge_distributed_vpn_xray_service_name }}" + - "wg-quick@{{ xworkmate_bridge_distributed_vpn_interface }}" + - "{{ xworkmate_bridge_distributed_vpn_forwarder_service_name }}" + when: + - not ansible_check_mode + +- name: Show WireGuard distributed interface state + ansible.builtin.command: + cmd: "wg show {{ xworkmate_bridge_distributed_vpn_interface }}" + register: xworkmate_bridge_distributed_vpn_wg_show + changed_when: false + when: + - not ansible_check_mode diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wg-xwm.conf.j2 b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wg-xwm.conf.j2 new file mode 100644 index 0000000..1f0366f --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wg-xwm.conf.j2 @@ -0,0 +1,11 @@ +[Interface] +PrivateKey = {{ xworkmate_bridge_distributed_vpn_wireguard_private_key }} +Address = {{ xworkmate_bridge_distributed_vpn_current_node.wg_ip }}/32 +ListenPort = {{ xworkmate_bridge_distributed_vpn_wireguard_port }} +MTU = {{ xworkmate_bridge_distributed_vpn_mtu }} + +[Peer] +PublicKey = {{ xworkmate_bridge_distributed_vpn_peer_node.public_key }} +AllowedIPs = {{ xworkmate_bridge_distributed_vpn_peer_node.wg_ip }}/32 +Endpoint = 127.0.0.1:{{ xworkmate_bridge_distributed_vpn_local_tproxy_port }} +PersistentKeepalive = {{ xworkmate_bridge_distributed_vpn_persistent_keepalive }} diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wireguard-over-vless.json.j2 b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wireguard-over-vless.json.j2 new file mode 100644 index 0000000..26880e7 --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/wireguard-over-vless.json.j2 @@ -0,0 +1,99 @@ +{ + "log": { + "loglevel": "warning" + }, + "routing": { + "domainStrategy": "IPIfNonMatch", + "rules": [ + { + "type": "field", + "inboundTag": ["wireguard-local-udp"], + "outboundTag": "wireguard-peer" + }, + { + "type": "field", + "inboundTag": ["wireguard-vless"], + "outboundTag": "direct" + } + ] + }, + "inbounds": [ + { + "tag": "wireguard-vless", + "listen": "0.0.0.0", + "port": {{ xworkmate_bridge_distributed_vpn_vless_port | int }}, + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "{{ xworkmate_bridge_distributed_vpn_xray_uuid }}" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tlsSettings": { + "serverName": "{{ xworkmate_bridge_distributed_vpn_current_node.domain }}", + "minVersion": "1.2", + "certificates": [ + { + "certificateFile": "{{ xworkmate_bridge_distributed_vpn_tls_cert_path }}", + "keyFile": "{{ xworkmate_bridge_distributed_vpn_tls_key_path }}" + } + ] + } + } + }, + { + "tag": "wireguard-local-udp", + "listen": "127.0.0.1", + "port": {{ xworkmate_bridge_distributed_vpn_local_tproxy_port | int }}, + "protocol": "dokodemo-door", + "settings": { + "address": "127.0.0.1", + "port": {{ xworkmate_bridge_distributed_vpn_wireguard_port | int }}, + "network": "udp" + } + } + ], + "outbounds": [ + { + "tag": "wireguard-peer", + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "{{ xworkmate_bridge_distributed_vpn_peer_node.domain }}", + "port": {{ xworkmate_bridge_distributed_vpn_vless_port | int }}, + "users": [ + { + "id": "{{ xworkmate_bridge_distributed_vpn_xray_uuid }}", + "encryption": "none", + "packetEncoding": "xudp" + } + ] + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tlsSettings": { + "serverName": "{{ xworkmate_bridge_distributed_vpn_peer_node.domain }}", + "allowInsecure": false, + "fingerprint": "chrome" + } + } + }, + { + "tag": "direct", + "protocol": "freedom" + }, + { + "tag": "block", + "protocol": "blackhole" + } + ] +} diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xray-wg-tproxy.service.j2 b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xray-wg-tproxy.service.j2 new file mode 100644 index 0000000..f4baeb5 --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xray-wg-tproxy.service.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=Xray WireGuard over VLESS/TLS transport +Documentation=https://github.com/XTLS/Xray-core +After=network-online.target nss-lookup.target +Wants=network-online.target + +[Service] +Type=simple +ExecStartPre={{ xworkmate_bridge_distributed_vpn_xray_bin_path }} run -test -config {{ xworkmate_bridge_distributed_vpn_xray_config_path }} +ExecStart={{ xworkmate_bridge_distributed_vpn_xray_bin_path }} run -config {{ xworkmate_bridge_distributed_vpn_xray_config_path }} +Restart=on-failure +RestartSec=2 +RestartPreventExitStatus=23 +LimitNPROC=10000 +LimitNOFILE=1000000 + +[Install] +WantedBy=multi-user.target diff --git a/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xworkmate-bridge-vpn-forwarder.service.j2 b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xworkmate-bridge-vpn-forwarder.service.j2 new file mode 100644 index 0000000..732f0a1 --- /dev/null +++ b/roles/vhosts/xworkmate_bridge_distributed_vpn/templates/xworkmate-bridge-vpn-forwarder.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=XWorkmate bridge VPN-only private forwarder +After=network-online.target wg-quick@{{ xworkmate_bridge_distributed_vpn_interface }}.service +Requires=wg-quick@{{ xworkmate_bridge_distributed_vpn_interface }}.service +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/usr/bin/socat TCP-LISTEN:{{ xworkmate_bridge_distributed_vpn_forwarder_port }},bind={{ xworkmate_bridge_distributed_vpn_current_node.wg_ip }},fork,reuseaddr TCP:{{ xworkmate_bridge_distributed_vpn_forwarder_target }} +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/deploy_bridge_with_vault.sh b/scripts/deploy_bridge_with_vault.sh index 3d09d5a..ec11b7c 100755 --- a/scripts/deploy_bridge_with_vault.sh +++ b/scripts/deploy_bridge_with_vault.sh @@ -21,15 +21,11 @@ if [ -z "$INTERNAL_TOKEN" ]; then exit 1 fi -# 3. Resolve Image (get latest from online if not provided) -IMAGE="${SERVICE_COMPOSE_IMAGE:-ghcr.io/x-evor/xworkmate-bridge:f30c8d481615933448535b15c0ed9099ed7c4ac9}" - -# 4. Run Ansible +# 3. Run Ansible echo "[Ansible] Starting dry-run validation..." cd "$(dirname "$0")/.." ansible-playbook -i inventory.ini deploy_xworkmate_bridge_vhosts.yml \ -l jp-xhttp-contabo.svc.plus \ -e "INTERNAL_SERVICE_TOKEN=$INTERNAL_TOKEN" \ -e "xworkmate_bridge_auth_token=$INTERNAL_TOKEN" \ - -e "service_compose_image=$IMAGE" \ "$@" diff --git a/vpn-wireguard-over-vless.yml b/vpn-wireguard-over-vless.yml new file mode 100644 index 0000000..9017ff2 --- /dev/null +++ b/vpn-wireguard-over-vless.yml @@ -0,0 +1,65 @@ +--- +- name: Deploy bidirectional WireGuard over VLESS for XWorkmate bridge distribution + hosts: xworkmate_bridge_distributed + become: true + gather_facts: true + roles: + - role: roles/vhosts/xworkmate_bridge_distributed_vpn/ + +- name: Refresh xworkmate-bridge distributed runtime config + hosts: xworkmate_bridge_distributed + become: true + gather_facts: true + roles: + - role: roles/vhosts/xworkmate_bridge/ + tags: [xworkmate_bridge] + +- name: Validate XWorkmate bridge private distributed path + hosts: xworkmate_bridge_distributed + become: true + gather_facts: false + tasks: + - name: Resolve current distributed VPN node + ansible.builtin.set_fact: + xworkmate_bridge_distributed_current_node: "{{ xworkmate_bridge_distributed_vpn_nodes[inventory_hostname] }}" + + - name: Resolve peer distributed VPN node + ansible.builtin.set_fact: + xworkmate_bridge_distributed_peer_node: "{{ xworkmate_bridge_distributed_vpn_nodes[xworkmate_bridge_distributed_current_node.peer] }}" + + - name: Verify peer WireGuard address is reachable + ansible.builtin.command: + cmd: "ping -c 3 -W 2 {{ xworkmate_bridge_distributed_peer_node.wg_ip }}" + changed_when: false + when: not ansible_check_mode + + - name: Read local bridge auth token for private peer validation + ansible.builtin.shell: | + set -euo pipefail + systemctl cat xworkmate-bridge.service | + sed -n 's/^Environment="BRIDGE_AUTH_TOKEN=\(.*\)"$/\1/p' | + head -n 1 + args: + executable: /bin/bash + register: xworkmate_bridge_private_validation_token + changed_when: false + no_log: true + when: not ansible_check_mode + + - name: Verify peer bridge API through WireGuard private endpoint + ansible.builtin.uri: + url: "http://{{ xworkmate_bridge_distributed_peer_node.wg_ip }}:{{ xworkmate_bridge_distributed_vpn_forwarder_port }}/api/ping" + headers: + Authorization: "Bearer {{ xworkmate_bridge_private_validation_token.stdout }}" + return_content: true + register: xworkmate_bridge_private_peer_ping + changed_when: false + no_log: true + when: not ansible_check_mode + + - name: Assert peer bridge private ping succeeded + ansible.builtin.assert: + that: + - xworkmate_bridge_private_peer_ping.status == 200 + - xworkmate_bridge_private_peer_ping.json.status | default('') == 'ok' + when: not ansible_check_mode