fix: align bridge OpenClaw protocol 4 deployment

This commit is contained in:
Haitao Pan 2026-06-01 13:48:47 +08:00
parent 402faa02e1
commit ba4daa3597
23 changed files with 911 additions and 41 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
---
xworkmate_bridge_distributed_local_node_id: xworkmate-bridge
xworkmate_bridge_distributed_task_forward_peer_id: ""

View File

@ -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

View File

@ -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:

View File

@ -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 }}"

View File

@ -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 }},

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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') }}

View File

@ -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: `<wg_ip>: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/<inventory_hostname>
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

View File

@ -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: {}

View File

@ -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

View File

@ -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

View File

@ -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 }}

View File

@ -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"
}
]
}

View File

@ -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

View File

@ -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

View File

@ -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" \
"$@"

View File

@ -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