--- locate backend/source roots --- path=/opt/wg-access exists_rc=0 path=/opt/wg-access/backend exists_rc=0 path=/opt/wg-access/agent exists_rc=0 path=/opt/wg-access/backend/app exists_rc=0 path=/usr/local/sbin exists_rc=0 --- files containing allocator/reuse terms --- /opt/wg-access/.env.BEFORE_MGTS_CLIENTCFG_20260706-185027 /opt/wg-access/.env.BEFORE_MGTS_CLIENTCFG_20260706-185140 /opt/wg-access/STATUS-STEP-002-BACKEND-DB-OK.txt /opt/wg-access/STATUS-STEP-006_AGENT_PULL_API_OK.txt /opt/wg-access/STATUS-STEP-012_REAL_AGENT_PROVISION_VM100_OK.txt /opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt /opt/wg-access/STATUS-STEP-015_DISABLE_DEPROVISION_OK.txt /opt/wg-access/STATUS-STEP-016_CLIENT_CONFIG_RESPONSE_OK.txt /opt/wg-access/STATUS-STEP-017_GENERATED_CLIENT_CONFIG_HANDSHAKE_OK.txt /opt/wg-access/STATUS-STEP-018_POOL_16_AND_REUSE_ALLOCATOR_OK.txt /opt/wg-access/STATUS-STEP-019A_TEST_PEERS_CLEANED_OK.txt /opt/wg-access/STATUS-STEP-019B_CLEAN_POOL_SMOKE_OK.txt /opt/wg-access/STATUS-STEP-020C_AGENT_TIMER_REAL_PERIODIC_OK.txt /opt/wg-access/STATUS-STEP-020D_TIMER_AUTO_PROVISION_DEPROVISION_OK.txt /opt/wg-access/STATUS-STEP-021C_ADMIN_SUBSCRIPTION_FLOW_OK.txt /opt/wg-access/STATUS-STEP-022B_DEV_ENDPOINT_USES_SHARED_WG_SERVICE_OK.txt /opt/wg-access/STATUS-STEP-023A_DEV_ENDPOINT_GUARD_OK.txt /opt/wg-access/STATUS-STEP-024A_ADMIN_USER_VIEW_OK.txt /opt/wg-access/STATUS-STEP-025A_CANCEL_SUBSCRIPTION_OK.txt /opt/wg-access/STATUS-STEP-026A_EXPIRE_SUBSCRIPTIONS_OK.txt /opt/wg-access/STATUS-STEP-028A_MGTS_WG_PAID_E2E_OK.txt /opt/wg-access/agent/logs/agent.log /opt/wg-access/agent/state/active-peers.json /opt/wg-access/agent/wg_access_agent.py /opt/wg-access/agent/wg_access_agent.py.BEFORE_REAL_SSH_WG_SET.20260704-132120 /opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532 /opt/wg-access/backend/alembic/versions/0001_initial_schema.py /opt/wg-access/backend/app/api/admin.py /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253 /opt/wg-access/backend/app/api/agent.py /opt/wg-access/backend/app/api/dev.py /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636 /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545 /opt/wg-access/backend/app/models/core.py /opt/wg-access/backend/app/services/wireguard.py /opt/wg-access/backups/step018b/core.py.20260704-155437.bak /opt/wg-access/backups/step018b/dev.py.20260704-155255.bak /opt/wg-access/backups/step018b/peers.20260704-155255.sql /opt/wg-access/backups/step021b/admin.py.20260704-162930.bak /opt/wg-access/backups/step021b/dev.py.20260704-162930.bak /opt/wg-access/backups/step022a/dev.py.20260704-163658.bak /opt/wg-access/backups/step022a/wireguard.py.20260704-163658.bak /opt/wg-access/backups/step023a/dev.py.20260704-172733.bak /opt/wg-access/backups/step024a/admin.py.20260704-174718.bak /opt/wg-access/backups/step025a/admin.py.20260704-180655.bak /opt/wg-access/backups/step026a/admin.py.20260704-182045.bak /opt/wg-access/tmp/mgts-smoke-20260706-183921/peers-before.json /opt/wg-access/tmp/mgts-smoke-20260706-184120/allowed-ips-after-enable.txt /opt/wg-access/tmp/mgts-smoke-20260706-184120/create.json /usr/local/sbin/wg-access-agent-job-selector-plan.py /usr/local/sbin/wg-access-agent-selector-apply.py /usr/local/sbin/wg-access-agent-selector-contract-dry-run.py /usr/local/sbin/wg-access-agent-selector-remote-plan.py /usr/local/sbin/wg-access-slot-allocator-dry-run.py --- allocator/reuse source snippets, sanitized --- ### FILE: /opt/wg-access/.env.BEFORE_MGTS_CLIENTCFG_20260706-185027 9-ENVIRONMENT=dev 10- 11-AGENT_TOKEN=ffVg3A8uoPkb9t0YRNfoLbKIGOXWZ4Ao1DAvPiIrdh3Jgqcr 12-WG_SERVER_PUBLIC_KEY=rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 13-WG_CLIENT_ENDPOINT=192.168.30.1:51830 14-WG_CLIENT_ALLOWED_IPS=10.253.1.1/32 15:WG_CLIENT_POOL_CIDR=10.253.0.0/16 16:WG_CLIENT_FIRST_IP=10.253.1.10 ### FILE: /opt/wg-access/.env.BEFORE_MGTS_CLIENTCFG_20260706-185140 9-ENVIRONMENT=dev 10- 11-AGENT_TOKEN=ffVg3A8uoPkb9t0YRNfoLbKIGOXWZ4Ao1DAvPiIrdh3Jgqcr 12-WG_SERVER_PUBLIC_KEY=FD1J+KVZ965pCjgtO0YWxJsKANY9uz6v1P54JLkiZzY= 13-WG_CLIENT_ENDPOINT=10.71.100.1:51830 14-WG_CLIENT_ALLOWED_IPS=0.0.0.0/0 15:WG_CLIENT_POOL_CIDR=10.253.0.0/16 16:WG_CLIENT_FIRST_IP=10.253.1.10 ### FILE: /opt/wg-access/STATUS-STEP-012_REAL_AGENT_PROVISION_VM100_OK.txt 31- host: 192.168.30.1 32- interface: wg_paid 33- address: 10.253.1.1/24 34- listen_port: 51830 35- 36-Verified peer: 37: tunnel_ip: 10.253.1.14/32 38- public_key: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 39- job status: completed 40- attempts: 1 41- 42-Important: 43- wg-access-agent.timer is still disabled. ### FILE: /opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt 1-WG Access Dev checkpoint 2- 3-STEP: 014 4-STATUS: OK 5- 6:Agent reconciliation / sync enabled peers completed. 7- 8-Problem solved: 9- VM100 wg_paid peers added with runtime wg set are not persistent across reboot, 10- network reload, or manual removal. 11- 12-Implemented: 13- Backend agent endpoint: 14- GET /agent/peers?node_id=ddn-test 15- 16: It returns enabled peers for the node, including: 17- - public_key 18- - preshared_key 19: - tunnel_ip 20- - paid_until 21: - enabled 22- 23- Agent behavior: 24: 1. Fetch enabled peers from backend. 25- 2. Read current VM100 runtime peers using: 26- wg show wg_paid allowed-ips 27- 3. Add/update missing or changed peers using: 28- wg set wg_paid peer ... preshared-key ... allowed-ips ... 29: 4. Remove extra runtime peers that are not enabled in backend. 30- 5. Then process pending provisioning jobs. 31- 32-Test performed: 33- 1. Manually removed real peer from VM100 wg_paid. 34- 2. Ran agent once manually. 35: 3. Agent restored the peer from backend enabled peers. 36- 37-Verified restored peer: 38- public_key: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 39: tunnel_ip: 10.253.1.14/32 40- 41-Log evidence: 42: sync desired enabled peers: 1 43- sync current runtime peers: 0 44: sync set peer tunnel_ip=10.253.1.14 45- no pending jobs 46- 47-Current state: 48: - Backend has one enabled real peer. 49- - Old fake peers are disabled. 50: - VM100 wg_paid contains the enabled real peer. 51- - wg-access-agent.timer remains disabled. 52- 53-Next steps: 54- - add disable/deprovision API/job and test sync removal 55- - generate client config from backend 56- - then re-enable timer ### FILE: /opt/wg-access/STATUS-STEP-015_DISABLE_DEPROVISION_OK.txt 7- 8-Implemented: 9- Backend endpoint: 10- POST /admin/peers/{peer_id}/disable 11- 12-Behavior: 13: - sets Peer.enabled = false 14- - sets Peer.disabled_at 15- - creates ProvisioningJob: 16- action: disable_peer 17- status: pending 18- payload_json: 19- public_key 20: tunnel_ip 21- 22-Tested peer: 23- peer_id: 813515db-bf25-46c9-bc83-c9c45d51ba68 24- public_key: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 25: tunnel_ip: 10.253.1.14 26- 27-Verified: 28- - disable endpoint returned HTTP 200 29: - peer became enabled=false 30- - disable_peer job was created 31: - agent reconciliation saw desired enabled peers: 0 32- - agent removed runtime peer from VM100 wg_paid 33- - agent processed disable_peer job 34- - disable_peer job became completed, attempts=1 35- - VM100 wg_paid has no peers after agent run 36- 37-Current state: -- 39- all dev peers are disabled 40- VM100: 41- wg_paid is up 42- wg_paid has no peers 43- Agent: 44- real SSH wg set mode 45: reconciliation enabled 46- timer still disabled 47- 48-Note: 49- Reconciliation removes disabled peers before pending disable_peer jobs are processed. 50- This is safe because WireGuard peer removal is idempotent. 51- ### FILE: /opt/wg-access/STATUS-STEP-016_CLIENT_CONFIG_RESPONSE_OK.txt 24- private_key 25- public_key 26- preshared_key 27- client_config 28- 29-Created test peer: 30: tunnel_ip: 10.253.1.15/32 31- public_key: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= 32- job_id: ca3e15c1-91ba-435c-8a40-37faa9e101c9 33- 34-Agent: 35- processed pending enable_peer job 36- job status became completed -- 40- wg_paid contains peer: 41- cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= 42- allowed ips: 10.253.1.15/32 43- 44-Current state: 45- wg_paid is up 46: one enabled real test peer remains active: 10.253.1.15/32 47- wg-access-agent.timer remains disabled 48- 49-Note: 50- The generated client_config for this diagnostic peer was visible in curl output, 51- but was not saved to /tmp/wg-step016-peer.json because the diagnostic request 52- used curl -i directly. ### FILE: /opt/wg-access/STATUS-STEP-017_GENERATED_CLIENT_CONFIG_HANDSHAKE_OK.txt 17- 8. VM100 showed latest handshake for the generated client peer. 18- 9. Local test interface was brought down after the test. 19- 20-Test peer: 21- peer_id: 1b19ed83-1cad-4d57-be9f-fded5270213c 22- job_id: 7a8fe618-7dc9-4eea-80e7-c4fd09ac2172 23: tunnel_ip: 10.253.1.16/32 24- public_key: mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 25- 26-Client config contained: 27- Address = 10.253.1.16/32 28- Endpoint = 192.168.30.1:51830 29- AllowedIPs = 10.253.1.1/32 -- 35- VM100 endpoint observed: 192.168.30.83:37504 36- job status: completed 37- attempts: 1 38- 39-Current state: 40- VM100 wg_paid is up. 41: Backend has two enabled real test peers: 42- - 10.253.1.15/32 43- - 10.253.1.16/32 44- VM100 wg_paid has both peers. 45- Local VM121 test interface wg-step017 is down. 46- wg-access-agent.timer remains disabled. 47- ### FILE: /opt/wg-access/STATUS-STEP-018_POOL_16_AND_REUSE_ALLOCATOR_OK.txt 9- wg_paid address changed from: 10- 10.253.1.1/24 11- to: 12- 10.253.1.1/16 13- 14- Route verified: 15: 10.253.0.0/16 dev wg_paid 16- 17- After wg_paid reload runtime peers disappeared as expected. 18: Agent reconciliation restored enabled peers successfully. 19- 20-Database: 21- Old global unique constraint was removed: 22: uq_peers_node_tunnel_ip UNIQUE (node_id, tunnel_ip) 23- 24- New partial unique index was created: 25: uq_peers_node_tunnel_ip_enabled 26: ON peers (node_id, tunnel_ip) 27: WHERE enabled = true 28- 29- Result: 30: Disabled historical peers may keep old tunnel_ip values. 31: Enabled peers cannot duplicate tunnel_ip on the same node. 32- 33-Backend: 34: backend/app/api/dev.py next_tunnel_ip() changed from count-based allocation 35- to first-free-IP allocation. 36- 37- Pool settings: 38: WG_CLIENT_POOL_CIDR=10.253.0.0/16 39: WG_CLIENT_FIRST_IP=10.253.1.10 40- 41- Allocation rule: 42: Only enabled peers reserve tunnel IPs. 43- Disabled peers keep history but their addresses can be reused. 44- 45-SQLAlchemy model: 46: backend/app/models/core.py old global tunnel_ip UniqueConstraint removed/commented. 47: DB-level partial unique index is now the source of truth for enabled peer uniqueness. 48- 49-Verification: 50- Existing disabled historical peer had: 51: tunnel_ip: 10.253.1.10 52: enabled: false 53- 54- New allocator test peer was created and correctly reused: 55- peer_id: 9298ec04-26cf-44a7-8079-7a13664612fa 56- job_id: bda6e96e-ef56-44f6-8bc0-867b91ac59bb 57: tunnel_ip: 10.253.1.10 58- public_key: CJMx+fPoiz5TUmUHKF7JcKo9IrxBZZMEDk7aS/Temwk= 59- 60- Agent processed the enable_peer job: 61- status: completed 62- attempts: 1 63- 64: VM100 wg_paid contains enabled peers: 65: 10.253.1.10/32 66- 10.253.1.15/32 67- 10.253.1.16/32 68- 69-Current state: 70- VM100 wg_paid is /16 and up. 71- Backend is healthy. 72- wg-access-agent.timer remains disabled. 73: Three real test peers are currently enabled. ### FILE: /opt/wg-access/STATUS-STEP-019A_TEST_PEERS_CLEANED_OK.txt 3-STEP: 019A 4-STATUS: OK 5- 6-All active test peers were cleaned through disable API. 7- 8-Disabled test peers: 9: 10.253.1.10 10- peer_id: 9298ec04-26cf-44a7-8079-7a13664612fa 11- disable_job_id: fb2c12a5-3f22-4c55-94f1-091240e0019a 12- 13- 10.253.1.15 14- peer_id: 13543224-39f7-4cfc-917d-66a16e2e0e55 15- disable_job_id: 652f82c5-4243-4618-bf17-ccfea3e44d54 16- 17- 10.253.1.16 18- peer_id: 1b19ed83-1cad-4d57-be9f-fded5270213c 19- disable_job_id: d607c313-cacd-4650-9d39-47c7d43817d6 20- 21-Agent: 22: sync desired enabled peers: 0 23- removed all extra runtime peers from VM100 wg_paid 24- processed all three disable_peer jobs 25- all disable jobs completed with attempts=1 26- 27-VM100: 28- wg_paid remains up 29- address: 10.253.1.1/16 30: route: 10.253.0.0/16 dev wg_paid 31- runtime peers: none 32- 33-Backend: 34: enabled peers count: 0 35- 36-Current state: 37- Clean /16 wg_paid network. 38: No enabled test peers. 39- wg-access-agent.timer remains disabled. ### FILE: /opt/wg-access/STATUS-STEP-019B_CLEAN_POOL_SMOKE_OK.txt 3-STEP: 019B 4-STATUS: OK 5- 6-Clean-pool smoke test completed successfully. 7- 8-Initial state: 9: Backend enabled peers count: 0 10- VM100 wg_paid was clean: 11- address: 10.253.1.1/16 12: route: 10.253.0.0/16 13- runtime peers: none 14- 15-Created fresh smoke-test peer: 16- peer_id: 6a5f1655-8ebe-435e-bba6-1af2412d40c2 17- enable_job_id: cb23b57d-7ec7-4804-a045-495a23705f5d 18: tunnel_ip: 10.253.1.10 19- public_key: TuuRNB7urYUrpRq9gHqqMlqWfte0jSJooLXjZIIYKjs= 20- 21-Allocator verification: 22: Clean-pool allocator returned 10.253.1.10 as expected. 23- 24-Provision verification: 25- Agent ran once manually. 26- Agent added peer to VM100 wg_paid. 27- enable_peer job completed with attempts=1. 28- -- 36- 4 received 37- 0% packet loss 38- 39- VM100 observed: 40- endpoint: 192.168.30.83:44404 41- latest handshake: OK 42: allowed ips: 10.253.1.10/32 43- 44-Cleanup: 45- Local wg-step019b interface was brought down. 46- Smoke-test peer was disabled through disable API. 47- 48-Disabled smoke-test peer: 49- peer_id: 6a5f1655-8ebe-435e-bba6-1af2412d40c2 50- disable_job_id: 8b21d24d-d575-4224-a281-bd2456cccb37 51: tunnel_ip: 10.253.1.10 52: enabled: false 53- 54-Deprovision verification: 55- Agent ran once manually. 56- Agent removed runtime peer from VM100 wg_paid. 57- disable_peer job completed with attempts=1. 58- 59-Final state: 60: Backend enabled peers count: 0 61- VM100 wg_paid: 62- address: 10.253.1.1/16 63: route: 10.253.0.0/16 64- runtime peers: none 65- 66-Current state: 67- Backend is clean. 68- VM100 wg_paid is clean and ready. 69- wg-access-agent.timer remains disabled. ### FILE: /opt/wg-access/STATUS-STEP-020C_AGENT_TIMER_REAL_PERIODIC_OK.txt 31-Automatic timer run observed: 32- service started automatically after timer activation 33- service ran as: 34- WG Access Agent real poller 35- 36-Agent run result: 37: sync desired enabled peers: 0 38- sync current runtime peers: 0 39- no pending jobs 40- 41-Final clean checks: 42: Backend enabled peers count: 0 43- Backend active pending/running jobs count: 0 44- VM100 wg_paid: 45- address: 10.253.1.1/16 46: route: 10.253.0.0/16 dev wg_paid 47- runtime peers: none 48- 49-Current state: 50: wg-access-agent.timer is enabled and active. 51- Agent is now running periodically in real mode. 52- Backend and VM100 are clean. ### FILE: /opt/wg-access/STATUS-STEP-020D_TIMER_AUTO_PROVISION_DEPROVISION_OK.txt 7- 8-Important: 9- Agent was NOT run manually during provision/deprovision. 10- All provisioning/deprovisioning actions were handled by systemd timer. 11- 12-Timer: 13: wg-access-agent.timer is enabled and active. 14- Timer status: 15- active (waiting) 16- next trigger scheduled 17- 18-Created timer-provision test peer: 19- peer_id: 267835b6-84b6-4527-bd47-eee963e7a5c9 20- enable_job_id: 1912c96e-bda5-4af1-a9ea-9a81a27344fd 21: tunnel_ip: 10.253.1.10 22- public_key: gXUwVRUraYFaUDv1BJ6GdxbZi6IoliYNBUe8wYRNERU= 23- 24-Timer provision verification: 25- systemd timer started wg-access-agent.service automatically. 26- Agent processed enable_peer job. 27- enable job completed: 28- status: completed 29- attempts: 1 30- 31-VM100 after timer provision: 32- wg_paid had peer: 33- public_key: gXUwVRUraYFaUDv1BJ6GdxbZi6IoliYNBUe8wYRNERU= 34: allowed ips: 10.253.1.10/32 35- 36-Generated client config verification: 37- config saved to: 38- /etc/wireguard/wg-step020d.conf 39- 40- wg-quick up wg-step020d: OK -- 51- Local wg-step020d interface was brought down. 52- Peer was disabled through backend API. 53- 54-Disabled peer: 55- peer_id: 267835b6-84b6-4527-bd47-eee963e7a5c9 56- disable_job_id: 9057b4c7-68b5-4b9e-8cf0-f624a07e30d0 57: tunnel_ip: 10.253.1.10 58: enabled: false 59- 60-Timer deprovision verification: 61- Agent was NOT run manually. 62- systemd timer started wg-access-agent.service automatically. 63- Agent processed disable_peer job. 64- disable job completed: 65- status: completed 66- attempts: 1 67- 68-Final state: 69: Backend enabled peers count: 0 70- VM100 wg_paid: 71- address: 10.253.1.1/16 72: route: 10.253.0.0/16 dev wg_paid 73- runtime peers: none 74- 75-Current state: 76- Backend clean. 77- VM100 wg_paid clean. 78: wg-access-agent.timer enabled and active. ### FILE: /opt/wg-access/STATUS-STEP-021C_ADMIN_SUBSCRIPTION_FLOW_OK.txt 14-Shared WireGuard helpers: 15- backend/app/services/wireguard.py 16- 17-Endpoint behavior: 18- Creates User 19- Creates active Subscription 20: Creates enabled Peer 21- Creates pending ProvisioningJob enable_peer 22- Returns ready WireGuard client_config once 23- 24-Backend status: 25- /health OK 26- OpenAPI lists: -- 29-Test subscription: 30- user_id: 3303b0a8-450a-4154-9322-9ddfc5757208 31- subscription_id: 54b8c5b7-430d-425a-8fc0-909eeb0cab9d 32- peer_id: 00fde00e-a169-4397-841f-5cc99de475d5 33- enable_job_id: bb71b4cd-a615-45f5-9135-4aa0efca9cb7 34- node_id: ddn-test 35: tunnel_ip: 10.253.1.10 36- public_key: H+yLpZTzxVq4o9RSD6b70hu6JTIY5WkH6Tx74qP1dwM= 37- plan_code: manual_test 38- months: 2 39- auto_renew: false 40- 41-Timer provision: -- 52- 4 received 53- 0% packet loss 54- 55-VM100 handshake: 56- endpoint: 192.168.30.83:42824 57- latest handshake: OK 58: allowed ips: 10.253.1.10/32 59- 60-Cleanup: 61- local wg-step021c interface was brought down. 62- peer disabled through existing API: 63- POST /admin/peers/{peer_id}/disable 64- -- 66- disable_job_id: 6e273fdf-781f-4992-ad42-4bf83bc7b6f5 67- status: completed 68- attempts: 1 69- processed automatically by wg-access-agent.timer 70- 71-Final state: 72: Backend enabled peers count: 0 73- VM100 wg_paid: 74- address: 10.253.1.1/16 75: route: 10.253.0.0/16 dev wg_paid 76- runtime peers: none 77- wg-access-agent.timer: 78: enabled 79- active (waiting) 80- next trigger scheduled 81- 82-Current state: 83- Backend clean. 84- VM100 wg_paid clean. ### FILE: /opt/wg-access/STATUS-STEP-022B_DEV_ENDPOINT_USES_SHARED_WG_SERVICE_OK.txt 12-Single source of truth for WireGuard logic: 13- backend/app/services/wireguard.py 14- 15-Shared helper now provides: 16- generate_wg_keypair 17- generate_wg_psk 18: next_tunnel_ip 19- build_client_config 20- 21-Backend status: 22- /health OK 23- OpenAPI routes present: 24- POST /dev/create-test-peer -- 26- 27-Test peer from refactored dev endpoint: 28- user_id: ead942f1-f354-496a-b2e6-857604fea5c3 29- peer_id: da134a09-caa8-4209-bbfa-93dd06f310fb 30- enable_job_id: 13a61a08-8eeb-4402-8a5c-624bc606c023 31- node_id: ddn-test 32: tunnel_ip: 10.253.1.10 33- public_key: OtlMSSUslhQufF939tNN3quNVY0Y+bqmfyohE1K6sBE= 34- 35-Timer provision: 36- Agent was NOT run manually. 37- wg-access-agent.timer processed enable_peer automatically. 38- enable job status: completed -- 46- 4 received 47- 0% packet loss 48- 49-VM100 handshake: 50- endpoint: 192.168.30.83:44891 51- latest handshake: OK 52: allowed ips: 10.253.1.10/32 53- 54-Cleanup: 55- local wg-step022b interface was brought down. 56- peer disabled through existing API: 57- POST /admin/peers/{peer_id}/disable 58- -- 60- disable_job_id: ef0e8472-6e80-45eb-9bcb-ec0a9d7be598 61- status: completed 62- attempts: 1 63- processed automatically by wg-access-agent.timer 64- 65-Final state: 66: Backend enabled peers count: 0 67- VM100 wg_paid: 68- address: 10.253.1.1/16 69: route: 10.253.0.0/16 dev wg_paid 70- runtime peers: none 71- wg-access-agent.timer: 72: enabled 73- active (waiting) 74- next trigger scheduled 75- 76-Current state: 77- Backend clean. 78- VM100 wg_paid clean. ### FILE: /opt/wg-access/STATUS-STEP-023A_DEV_ENDPOINT_GUARD_OK.txt 29- 30-Test peer: 31- user_id: cd6b00a0-8cc5-48df-8413-8e645505db8c 32- peer_id: 486ee88e-2515-4203-8541-44f89cdaba89 33- enable_job_id: cef3e913-d69a-4f61-a5af-7d10eeb8b2da 34- node_id: ddn-test 35: tunnel_ip: 10.253.1.10 36- public_key: WkjrGK7JKsYkhzFz5Bggunzlw5BNY+WxkcYp4D7IulA= 37- 38-Timer provision: 39- Agent was NOT run manually. 40- wg-access-agent.timer processed enable_peer automatically. 41- enable job status: completed 42- 43-VM100 provision check: 44- peer appeared on wg_paid: 45- public_key: WkjrGK7JKsYkhzFz5Bggunzlw5BNY+WxkcYp4D7IulA= 46: allowed ips: 10.253.1.10/32 47- 48-Cleanup: 49- peer disabled through existing API: 50- POST /admin/peers/{peer_id}/disable 51- 52-Disable job: 53- disable_job_id: de335c08-7807-465b-9065-bce2c77a955f 54- status: completed 55- attempts: 1 56- processed automatically by wg-access-agent.timer 57- 58-Final state: 59: Backend enabled peers count: 0 60- Backend active pending/running jobs count: 0 61- VM100 wg_paid: 62- address: 10.253.1.1/16 63: route: 10.253.0.0/16 dev wg_paid 64- runtime peers: none 65- wg-access-agent.timer: 66: enabled 67- active (waiting) 68- next trigger scheduled 69- 70-Note: 71- Runtime non-dev 404 behavior was not separately tested by switching environment. 72- Code-level guard is present and dev-mode behavior is smoke-tested. ### FILE: /opt/wg-access/STATUS-STEP-024A_ADMIN_USER_VIEW_OK.txt 26- Returns user summaries: 27- id 28- email 29- created_at 30- subscriptions_count 31- peers_count 32: enabled_peers_count 33- 34-GET /admin/users/{user_id}: 35- Returns user detail: 36- user 37- subscriptions 38- peers -- 46- email: dev-guard-20260704-172805@example.com 47- 48-Detail response included: 49- peers: 50- peer_id: 486ee88e-2515-4203-8541-44f89cdaba89 51- public_key: WkjrGK7JKsYkhzFz5Bggunzlw5BNY+WxkcYp4D7IulA= 52: tunnel_ip: 10.253.1.10 53: enabled: false 54- 55- jobs: 56- disable_peer: 57- job_id: de335c08-7807-465b-9065-bce2c77a955f 58- status: completed 59- attempts: 1 -- 69- private_key 70- 71- Result: 72- OK, no private key leaked in admin user responses. 73- 74-Final state: 75: Backend enabled peers count: 0 76- Backend active pending/running jobs count: 0 77- VM100 wg_paid: 78- address: 10.253.1.1/16 79: route: 10.253.0.0/16 dev wg_paid 80- runtime peers: none 81- wg-access-agent.timer: 82: enabled 83- active (waiting) 84- next trigger scheduled 85- 86-Current state: 87- Backend clean. 88- VM100 wg_paid clean. ### FILE: /opt/wg-access/STATUS-STEP-025A_CANCEL_SUBSCRIPTION_OK.txt 15- Finds subscription by ID. 16- Sets: 17- subscription.status = canceled 18- subscription.auto_renew = false 19- subscription.canceled_at = now, if not already set 20- 21: Finds all enabled peers for subscription.user_id. 22: For each enabled peer: 23: peer.enabled = false 24- peer.disabled_at = now 25- creates pending disable_peer ProvisioningJob 26- 27- Returns: 28- subscription_id 29- user_id -- 40- POST /admin/subscriptions/{subscription_id}/cancel 41- GET /admin/users 42- GET /admin/users/{user_id} 43- GET /admin/users/by-email/{email} 44- 45-Clean precheck: 46: Backend enabled peers count: 0 47- Backend active pending/running jobs count: 0 48- 49-Cancel smoke test subscription: 50- user_id: 1b42ec72-1f39-483d-ae1e-85669d3ecad2 51- subscription_id: 26794caf-ab42-4cb7-8270-5e87e7b1d47d 52- peer_id: 3460b486-358c-44d6-8f47-c960d6254e3a 53- enable_job_id: 74ccc2cf-bbb2-4621-8bbe-ce3096d8366d 54- node_id: ddn-test 55: tunnel_ip: 10.253.1.10 56- public_key: dYuBNktWxl/PIqV49T9BIiHIvP4ZumswWZIEFD/Cwxg= 57- 58-Timer provision: 59- Agent was NOT run manually. 60- wg-access-agent.timer processed enable_peer automatically. 61- enable job status: completed 62- 63-VM100 before cancel: 64- peer appeared on wg_paid: 65- public_key: dYuBNktWxl/PIqV49T9BIiHIvP4ZumswWZIEFD/Cwxg= 66: allowed ips: 10.253.1.10/32 67- 68-Cancel response: 69- subscription_id: 26794caf-ab42-4cb7-8270-5e87e7b1d47d 70- user_id: 1b42ec72-1f39-483d-ae1e-85669d3ecad2 71- status: canceled 72- canceled_at: 2026-07-04T18:08:12.285041Z -- 78- subscription: 79- status: canceled 80- auto_renew: false 81- canceled_at set 82- 83- peer: 84: enabled: false 85- disabled_at set 86- 87-Timer deprovision: 88- Agent was NOT run manually. 89- wg-access-agent.timer processed cancel-created disable_peer job automatically. 90- disable job status: completed -- 94- Second POST /admin/subscriptions/{subscription_id}/cancel returned: 95- status: canceled 96- disabled_peer_count: 0 97- job_ids: [] 98- 99-Final state: 100: Backend enabled peers count: 0 101- Backend active pending/running jobs count: 0 102- VM100 wg_paid: 103- address: 10.253.1.1/16 104: route: 10.253.0.0/16 dev wg_paid 105- runtime peers: none 106- wg-access-agent.timer: 107: enabled 108- active (waiting) 109- next trigger scheduled 110- 111-Current state: 112- Backend clean. 113- VM100 wg_paid clean. ### FILE: /opt/wg-access/STATUS-STEP-026A_EXPIRE_SUBSCRIPTIONS_OK.txt 14-Endpoint behavior: 15- Finds active subscriptions where paid_until <= now. 16- For each expired subscription: 17- subscription.status = expired 18- subscription.auto_renew = false 19- 20: Finds all enabled peers for subscription.user_id. 21: For each enabled peer: 22: peer.enabled = false 23- peer.disabled_at = now 24- creates pending disable_peer ProvisioningJob 25- 26- Returns: 27- expired_subscription_count 28- disabled_peer_count -- 36- POST /admin/maintenance/expire-subscriptions 37- POST /admin/subscriptions 38- POST /admin/subscriptions/{subscription_id}/cancel 39- GET /health 40- 41-Clean precheck: 42: Backend enabled peers count: 0 43- Backend active pending/running jobs count: 0 44- 45-Expiration smoke test subscription: 46- user_id: 7c1eb9f8-f220-43ae-840b-14f4cb362c1e 47- subscription_id: 7a3dfe11-ebf3-4f5e-966b-73fdbe464426 48- peer_id: f45a349d-25a7-426d-b88a-4479b28aec18 49- enable_job_id: 838a2aaf-64ed-4346-9996-1ef7889fda2f 50- node_id: ddn-test 51: tunnel_ip: 10.253.1.10 52- public_key: 239TtHnxaJefv7kKJeNOVjW6YLchjRcTdctI3nJrAXE= 53- 54-Timer provision: 55- Agent was NOT run manually. 56- wg-access-agent.timer processed enable_peer automatically. 57- enable job status: completed 58- 59-VM100 before expiration: 60- peer appeared on wg_paid: 61- public_key: 239TtHnxaJefv7kKJeNOVjW6YLchjRcTdctI3nJrAXE= 62: allowed ips: 10.253.1.10/32 63- 64-Forced expiration: 65- subscriptions.paid_until moved into the past 66- peers.paid_until moved into the past 67- 68-Expire endpoint response: -- 77- subscription: 78- status: expired 79- auto_renew: false 80- paid_until in the past 81- 82- peer: 83: enabled: false 84- disabled_at set 85- 86-Timer deprovision: 87- Agent was NOT run manually. 88- wg-access-agent.timer processed expiration-created disable_peer job automatically. 89- disable job status: completed -- 94- expired_subscription_count: 0 95- disabled_peer_count: 0 96- job_ids: [] 97- subscription_ids: [] 98- 99-Final state: 100: Backend enabled peers count: 0 101- Backend active pending/running jobs count: 0 102- VM100 wg_paid: 103- address: 10.253.1.1/16 104: route: 10.253.0.0/16 dev wg_paid 105- runtime peers: none 106- wg-access-agent.timer: 107: enabled 108- active (waiting) 109- next trigger scheduled 110- 111-Current state: 112- Backend clean. 113- VM100 wg_paid clean. ### FILE: /opt/wg-access/STATUS-STEP-028A_MGTS_WG_PAID_E2E_OK.txt 23- -> agent fetched enable_peer job 24- -> agent executed runtime wg set on MGTS VM100 wg_paid 25- -> peer appeared on MGTS VM100 wg_paid 26- 27- - Temporary client smoke OK: 28- interface: wg-paid-test 29: address: 10.253.1.10/32 30- endpoint used for internal smoke: 10.71.100.1:51830 31- 32- - E2E checks OK: 33- ping 10.253.1.1 from wg-paid-test: OK 34- ping 77.88.8.8 from wg-paid-test: OK 35- ping 1.1.1.1 from wg-paid-test: OK, short test had 25% loss 36- ping 9.9.9.9 from wg-paid-test: OK, short test had 25% loss 37- 38- - VM100 PBR route decision OK: 39: 1.1.1.1 from 10.253.1.10 -> table pbr_transit_vpn via 10.200.0.2 40: 77.88.8.8 from 10.253.1.10 -> table pbr_transit_direct via 10.201.0.2 41- 42- - Cleanup OK: 43- POST /admin/subscriptions//cancel 44- -> disabled_peer_count=1 45- -> disable_peer job created 46- ### FILE: /opt/wg-access/agent/logs/agent.log 1-[2026-07-04T10:37:52.238900+00:00] fetched pending jobs: 1 2-[2026-07-04T10:37:52.239091+00:00] starting job_id=7196bb14-b3ba-4aaf-85f8-aa393fe1b4dc action=enable_peer 3:[2026-07-04T10:37:52.249864+00:00] DRY-RUN enable_peer peer_id=4b39aa87-4b59-4c71-950b-cf2dca3e606f tunnel_ip=10.253.1.12 4-[2026-07-04T10:37:52.256558+00:00] completed job_id=7196bb14-b3ba-4aaf-85f8-aa393fe1b4dc status=completed attempts=1 5-[2026-07-04T10:40:57.781938+00:00] no pending jobs 6-[2026-07-04T10:41:28.672378+00:00] fetched pending jobs: 1 7-[2026-07-04T10:41:28.672498+00:00] starting job_id=338006fa-9b17-45ac-b53f-bc2383421e70 action=enable_peer 8:[2026-07-04T10:41:28.685370+00:00] DRY-RUN enable_peer peer_id=754b76bd-0e69-4184-ac0b-a9c5706dbf19 tunnel_ip=10.253.1.13 9-[2026-07-04T10:41:28.691422+00:00] completed job_id=338006fa-9b17-45ac-b53f-bc2383421e70 status=completed attempts=1 10-[2026-07-04T10:42:03.672218+00:00] no pending jobs 11-[2026-07-04T10:42:38.680486+00:00] no pending jobs 12-[2026-07-04T10:43:13.672162+00:00] no pending jobs 13-[2026-07-04T10:43:48.672656+00:00] no pending jobs 14-[2026-07-04T10:44:23.672263+00:00] no pending jobs -- 20- private key: (hidden) 21- listening port: 51830 22- 23-peer: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 24- preshared key: (hidden) 25- allowed ips: 10.253.1.14/32 26:[2026-07-04T13:28:03.185952+00:00] REAL enable_peer peer_id=813515db-bf25-46c9-bc83-c9c45d51ba68 tunnel_ip=10.253.1.14 27-[2026-07-04T13:28:03.192685+00:00] completed job_id=a16ef565-d195-4302-acb3-c7d963b2f3a5 status=completed attempts=1 28:[2026-07-04T13:47:56.360799+00:00] sync desired enabled peers: 1 29-[2026-07-04T13:47:56.479781+00:00] sync current runtime peers: 0 30:[2026-07-04T13:47:56.597862+00:00] sync set peer tunnel_ip=10.253.1.14 31-[2026-07-04T13:47:56.696020+00:00] remote stdout: interface: wg_paid 32- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 33- private key: (hidden) 34- listening port: 51830 35- 36-peer: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 37- preshared key: (hidden) 38- allowed ips: 10.253.1.14/32 39-[2026-07-04T13:47:56.703737+00:00] no pending jobs 40:[2026-07-04T15:12:54.576715+00:00] sync desired enabled peers: 1 41-[2026-07-04T15:12:55.806538+00:00] remote stdout: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 10.253.1.14/32 42-[2026-07-04T15:12:55.806675+00:00] sync current runtime peers: 1 43:[2026-07-04T15:12:55.806757+00:00] sync keep peer tunnel_ip=10.253.1.14 44-[2026-07-04T15:12:56.858872+00:00] remote stdout: interface: wg_paid 45- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 46- private key: (hidden) 47- listening port: 51830 48- 49-peer: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 50- preshared key: (hidden) 51- allowed ips: 10.253.1.14/32 52-[2026-07-04T15:12:56.867442+00:00] no pending jobs 53:[2026-07-04T15:13:42.063808+00:00] sync desired enabled peers: 0 54-[2026-07-04T15:13:42.495341+00:00] remote stdout: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 10.253.1.14/32 55-[2026-07-04T15:13:42.495447+00:00] sync current runtime peers: 1 56-[2026-07-04T15:13:42.589731+00:00] sync removed extra runtime peer public_key=TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 57-[2026-07-04T15:13:42.700820+00:00] remote stdout: interface: wg_paid 58- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 59- private key: (hidden) -- 63-[2026-07-04T15:13:43.088872+00:00] remote stdout: interface: wg_paid 64- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 65- private key: (hidden) 66- listening port: 51830 67-[2026-07-04T15:13:43.089290+00:00] REAL disable_peer peer_id=813515db-bf25-46c9-bc83-c9c45d51ba68 68-[2026-07-04T15:13:43.097094+00:00] completed job_id=8594e558-1c0d-4c49-872e-a1c47dc0f377 status=completed attempts=1 69:[2026-07-04T15:19:03.615898+00:00] sync desired enabled peers: 1 70-[2026-07-04T15:19:03.954892+00:00] sync current runtime peers: 0 71:[2026-07-04T15:19:04.221203+00:00] sync set peer tunnel_ip=10.253.1.15 72-[2026-07-04T15:19:05.259935+00:00] remote stdout: interface: wg_paid 73- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 74- private key: (hidden) 75- listening port: 51830 76- 77-peer: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= -- 84- private key: (hidden) 85- listening port: 51830 86- 87-peer: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= 88- preshared key: (hidden) 89- allowed ips: 10.253.1.15/32 90:[2026-07-04T15:19:05.670502+00:00] REAL enable_peer peer_id=13543224-39f7-4cfc-917d-66a16e2e0e55 tunnel_ip=10.253.1.15 91-[2026-07-04T15:19:05.678181+00:00] completed job_id=ca3e15c1-91ba-435c-8a40-37faa9e101c9 status=completed attempts=1 92:[2026-07-04T15:26:17.199390+00:00] sync desired enabled peers: 2 93-[2026-07-04T15:26:17.337590+00:00] remote stdout: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= 10.253.1.15/32 94-[2026-07-04T15:26:17.337698+00:00] sync current runtime peers: 1 95:[2026-07-04T15:26:17.337783+00:00] sync keep peer tunnel_ip=10.253.1.15 96:[2026-07-04T15:26:17.449846+00:00] sync set peer tunnel_ip=10.253.1.16 97-[2026-07-04T15:26:17.542992+00:00] remote stdout: interface: wg_paid 98- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 99- private key: (hidden) 100- listening port: 51830 101- 102-peer: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= -- 117- preshared key: (hidden) 118- allowed ips: 10.253.1.15/32 119- 120-peer: mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 121- preshared key: (hidden) 122- allowed ips: 10.253.1.16/32 123:[2026-07-04T15:26:17.798837+00:00] REAL enable_peer peer_id=1b19ed83-1cad-4d57-be9f-fded5270213c tunnel_ip=10.253.1.16 124-[2026-07-04T15:26:17.805374+00:00] completed job_id=7a8fe618-7dc9-4eea-80e7-c4fd09ac2172 status=completed attempts=1 125:[2026-07-04T15:35:52.741243+00:00] sync desired enabled peers: 2 126-[2026-07-04T15:35:53.306310+00:00] sync current runtime peers: 0 127:[2026-07-04T15:35:53.401701+00:00] sync set peer tunnel_ip=10.253.1.15 128:[2026-07-04T15:35:53.526725+00:00] sync set peer tunnel_ip=10.253.1.16 129-[2026-07-04T15:35:53.621782+00:00] remote stdout: interface: wg_paid 130- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 131- private key: (hidden) 132- listening port: 51830 133- 134-peer: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= -- 136- allowed ips: 10.253.1.15/32 137- 138-peer: mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 139- preshared key: (hidden) 140- allowed ips: 10.253.1.16/32 141-[2026-07-04T15:35:53.627096+00:00] no pending jobs 142:[2026-07-04T15:56:30.286651+00:00] sync desired enabled peers: 3 143-[2026-07-04T15:56:30.380215+00:00] remote stdout: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= 10.253.1.15/32 144-mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 10.253.1.16/32 145-[2026-07-04T15:56:30.380320+00:00] sync current runtime peers: 2 146:[2026-07-04T15:56:30.380410+00:00] sync keep peer tunnel_ip=10.253.1.15 147:[2026-07-04T15:56:30.380482+00:00] sync keep peer tunnel_ip=10.253.1.16 148:[2026-07-04T15:56:30.487352+00:00] sync set peer tunnel_ip=10.253.1.10 149-[2026-07-04T15:56:30.583337+00:00] remote stdout: interface: wg_paid 150- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 151- private key: (hidden) 152- listening port: 51830 153- 154-peer: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= -- 158-peer: mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 159- preshared key: (hidden) 160- allowed ips: 10.253.1.16/32 161- 162-peer: CJMx+fPoiz5TUmUHKF7JcKo9IrxBZZMEDk7aS/Temwk= 163- preshared key: (hidden) 164: allowed ips: 10.253.1.10/32 165-[2026-07-04T15:56:30.590052+00:00] fetched pending jobs: 1 166-[2026-07-04T15:56:30.590256+00:00] starting job_id=bda6e96e-ef56-44f6-8bc0-867b91ac59bb action=enable_peer 167-[2026-07-04T15:56:30.796077+00:00] remote stdout: interface: wg_paid 168- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 169- private key: (hidden) 170- listening port: 51830 -- 176-peer: mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 177- preshared key: (hidden) 178- allowed ips: 10.253.1.16/32 179- 180-peer: CJMx+fPoiz5TUmUHKF7JcKo9IrxBZZMEDk7aS/Temwk= 181- preshared key: (hidden) 182: allowed ips: 10.253.1.10/32 183:[2026-07-04T15:56:30.796545+00:00] REAL enable_peer peer_id=9298ec04-26cf-44a7-8079-7a13664612fa tunnel_ip=10.253.1.10 184-[2026-07-04T15:56:30.803748+00:00] completed job_id=bda6e96e-ef56-44f6-8bc0-867b91ac59bb status=completed attempts=1 185:[2026-07-04T15:58:57.370294+00:00] sync desired enabled peers: 0 186-[2026-07-04T15:58:57.477394+00:00] remote stdout: cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= 10.253.1.15/32 187-mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 10.253.1.16/32 188:CJMx+fPoiz5TUmUHKF7JcKo9IrxBZZMEDk7aS/Temwk= 10.253.1.10/32 189-[2026-07-04T15:58:57.477509+00:00] sync current runtime peers: 3 190-[2026-07-04T15:58:57.653127+00:00] sync removed extra runtime peer public_key=CJMx+fPoiz5TUmUHKF7JcKo9IrxBZZMEDk7aS/Temwk= 191-[2026-07-04T15:58:57.746038+00:00] sync removed extra runtime peer public_key=cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= 192-[2026-07-04T15:58:57.850056+00:00] sync removed extra runtime peer public_key=mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= 193-[2026-07-04T15:58:57.946401+00:00] remote stdout: interface: wg_paid 194- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= -- 213-[2026-07-04T15:58:58.786321+00:00] remote stdout: interface: wg_paid 214- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 215- private key: (hidden) 216- listening port: 51830 217-[2026-07-04T15:58:58.786748+00:00] REAL disable_peer peer_id=1b19ed83-1cad-4d57-be9f-fded5270213c 218-[2026-07-04T15:58:58.793366+00:00] completed job_id=d607c313-cacd-4650-9d39-47c7d43817d6 status=completed attempts=1 219:[2026-07-04T16:01:57.846873+00:00] sync desired enabled peers: 1 220-[2026-07-04T16:01:57.935512+00:00] sync current runtime peers: 0 221:[2026-07-04T16:01:58.111619+00:00] sync set peer tunnel_ip=10.253.1.10 222-[2026-07-04T16:01:58.206415+00:00] remote stdout: interface: wg_paid 223- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 224- private key: (hidden) 225- listening port: 51830 226- 227-peer: TuuRNB7urYUrpRq9gHqqMlqWfte0jSJooLXjZIIYKjs= 228- preshared key: (hidden) 229: allowed ips: 10.253.1.10/32 230-[2026-07-04T16:01:58.210830+00:00] fetched pending jobs: 1 231-[2026-07-04T16:01:58.211032+00:00] starting job_id=cb23b57d-7ec7-4804-a045-495a23705f5d action=enable_peer 232-[2026-07-04T16:01:58.417209+00:00] remote stdout: interface: wg_paid 233- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 234- private key: (hidden) 235- listening port: 51830 236- 237-peer: TuuRNB7urYUrpRq9gHqqMlqWfte0jSJooLXjZIIYKjs= 238- preshared key: (hidden) 239: allowed ips: 10.253.1.10/32 240:[2026-07-04T16:01:58.417633+00:00] REAL enable_peer peer_id=6a5f1655-8ebe-435e-bba6-1af2412d40c2 tunnel_ip=10.253.1.10 241-[2026-07-04T16:01:58.426171+00:00] completed job_id=cb23b57d-7ec7-4804-a045-495a23705f5d status=completed attempts=1 242:[2026-07-04T16:02:01.901657+00:00] sync desired enabled peers: 0 243:[2026-07-04T16:02:01.995042+00:00] remote stdout: TuuRNB7urYUrpRq9gHqqMlqWfte0jSJooLXjZIIYKjs= 10.253.1.10/32 244-[2026-07-04T16:02:01.995271+00:00] sync current runtime peers: 1 245-[2026-07-04T16:02:02.116483+00:00] sync removed extra runtime peer public_key=TuuRNB7urYUrpRq9gHqqMlqWfte0jSJooLXjZIIYKjs= 246-[2026-07-04T16:02:02.206011+00:00] remote stdout: interface: wg_paid 247- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 248- private key: (hidden) 249- listening port: 51830 -- 252-[2026-07-04T16:02:02.421568+00:00] remote stdout: interface: wg_paid 253- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 254- private key: (hidden) 255- listening port: 51830 256-[2026-07-04T16:02:02.422058+00:00] REAL disable_peer peer_id=6a5f1655-8ebe-435e-bba6-1af2412d40c2 257-[2026-07-04T16:02:02.428942+00:00] completed job_id=8b21d24d-d575-4224-a281-bd2456cccb37 status=completed attempts=1 258:[2026-07-04T16:05:36.732089+00:00] sync desired enabled peers: 0 259-[2026-07-04T16:05:36.828585+00:00] sync current runtime peers: 0 260-[2026-07-04T16:05:36.981360+00:00] remote stdout: interface: wg_paid 261- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= 262- private key: (hidden) 263- listening port: 51830 264-[2026-07-04T16:05:36.985822+00:00] no pending jobs 265:[2026-07-04T16:06:57.680005+00:00] sync desired enabled peers: 0 266-[2026-07-04T16:06:57.779254+00:00] sync current runtime peers: 0 267-[2026-07-04T16:06:57.870563+00:00] remote stdout: interface: wg_paid 268- public key: rNdcZimQJKT6nb4wPmUFN/95bZfKDDsXsp1ie8nn1SU= ### FILE: /opt/wg-access/agent/state/active-peers.json 1-{ 2- "node_id": "ddn-test", 3- "peers": { 4- "HknQ+VpqOIvsd3RxkrgaUqiR7zkS9nMfDRzDAvdqUVw=": { 5: "enabled": true, 6: "last_action": "sync_enabled_peers", 7- "paid_until": "2026-08-07T14:03:52.367757Z", 8- "peer_id": "a3cbe3b5-8416-4847-b5ee-21f42359b465", 9- "preshared_key": "gijtbk0KhkQlSugeeDUvTwx6DEyaPdiwySi4ZdMIj2g=", 10- "public_key": "HknQ+VpqOIvsd3RxkrgaUqiR7zkS9nMfDRzDAvdqUVw=", 11: "tunnel_ip": "10.253.1.12", 12- "updated_at": "2026-07-08T15:05:42.288728+00:00" 13- }, 14- "JQXornv9vzH7nn9E1laZehYLnwMXuitP+eP35Gde0D4=": { 15: "enabled": true, 16- "last_action": "enable_peer", 17- "last_job_id": "d6e07a1e-78d5-401f-a8c7-501cf75a9bf7", 18- "paid_until": "2026-08-07T15:05:41.531656+00:00", 19- "peer_id": "219a1f63-ec47-4f72-9663-c1a17d82301e", 20- "preshared_key": "eD7QAhrBHZysFfUVNbLJ7Qku+OJLrBg+7gUBo7OdQHE=", 21- "public_key": "JQXornv9vzH7nn9E1laZehYLnwMXuitP+eP35Gde0D4=", 22: "tunnel_ip": "10.253.1.13", 23- "updated_at": "2026-07-08T15:05:42.776063+00:00" 24- }, 25- "fmcqSUy1fErBV5RrcoVoex2VAgaxz2i79GzK8qd88UI=": { 26: "enabled": true, 27: "last_action": "sync_enabled_peers", 28- "paid_until": "2026-08-07T12:12:18.368562Z", 29- "peer_id": "376dfa07-0ba4-4182-af39-389a2cb3badd", 30- "preshared_key": "rF56aOQU6rflYyWTkg0N9DFRYDn6WI0LenrdiaEa+mg=", 31- "public_key": "fmcqSUy1fErBV5RrcoVoex2VAgaxz2i79GzK8qd88UI=", 32: "tunnel_ip": "10.253.1.10", 33- "updated_at": "2026-07-08T15:05:42.288717+00:00" 34- }, 35- "zwzkWJkpX7q5PfcT9pG2v52Es4yM0VRP3w1drj7LJmw=": { 36: "enabled": true, 37: "last_action": "sync_enabled_peers", 38- "paid_until": "2026-08-07T13:42:56.372306Z", 39- "peer_id": "cb533a1a-7070-416d-9272-846327545060", 40- "preshared_key": "TQx4ho/n8GLd+d+zBe6/qLc7/LJ+BwrUP1DKvzS0HOM=", 41- "public_key": "zwzkWJkpX7q5PfcT9pG2v52Es4yM0VRP3w1drj7LJmw=", 42: "tunnel_ip": "10.253.1.11", 43- "updated_at": "2026-07-08T15:05:42.288725+00:00" 44- } 45- }, 46- "updated_at": "2026-07-08T15:05:42.776095+00:00" 47-} ### FILE: /opt/wg-access/agent/wg_access_agent.py 104-def require_wg_key(name, value): 105- if not isinstance(value, str) or not WG_KEY_RE.match(value): 106- raise RuntimeError(f"invalid WireGuard key in {name}") 107- return value 108- 109- 110:def require_tunnel_ip(value): 111- ip = ipaddress.ip_address(str(value)) 112- if ip.version != 4: 113: raise RuntimeError("only IPv4 tunnel_ip is supported") 114- return str(ip) 115- 116- 117-def normalize_peer_payload(peer): 118- public_key = require_wg_key("public_key", peer["public_key"]) 119- preshared_key = require_wg_key("preshared_key", peer["preshared_key"]) 120: tunnel_ip = require_tunnel_ip(peer["tunnel_ip"]) 121- 122- return { 123- "id": str(peer.get("id") or peer.get("peer_id") or ""), 124- "node_id": peer.get("node_id", NODE_ID), 125- "public_key": public_key, 126- "preshared_key": preshared_key, 127: "tunnel_ip": tunnel_ip, 128: "allowed_ips": f"{tunnel_ip}/32", 129- "paid_until": peer.get("paid_until"), 130: "enabled": bool(peer.get("enabled", True)), 131- } 132- 133- 134-def remote_wg(script, stdin_text=None): 135- cmd = [ 136- "ssh", -- 205- + shlex.quote(public_key) 206- + " remove" 207- ) 208- remote_wg(script) 209- 210- 211:def sync_enabled_peers(): 212- query = parse.urlencode({"node_id": NODE_ID}) 213- rows = http_json("GET", f"/agent/peers?{query}") or [] 214- desired = {} 215- 216- for row in rows: 217- peer = normalize_peer_payload(row) 218- desired[peer["public_key"]] = peer 219- 220: log(f"sync desired enabled peers: {len(desired)}") 221- 222- current = remote_current_allowed_ips() 223- log(f"sync current runtime peers: {len(current)}") 224- 225- for public_key, peer in desired.items(): 226- current_allowed = current.get(public_key) 227- if current_allowed == [peer["allowed_ips"]]: 228: log(f"sync keep peer tunnel_ip={peer['tunnel_ip']}") 229- continue 230- 231- remote_set_peer(peer) 232: log(f"sync set peer tunnel_ip={peer['tunnel_ip']}") 233- 234- for public_key in sorted(set(current.keys()) - set(desired.keys())): 235- remote_remove_peer(public_key) 236- log(f"sync removed extra runtime peer public_key={public_key}") 237- 238- # Финальная проверка для лога. -- 242- state["peers"] = {} 243- for public_key, peer in desired.items(): 244- state["peers"][public_key] = { 245- "peer_id": peer["id"], 246- "public_key": public_key, 247- "preshared_key": peer["preshared_key"], 248: "tunnel_ip": peer["tunnel_ip"], 249- "paid_until": peer["paid_until"], 250: "enabled": True, 251: "last_action": "sync_enabled_peers", 252- "updated_at": utc_now(), 253- } 254- save_state(state) 255- 256- 257-def apply_job_real(job): -- 262- if action == "enable_peer": 263- peer = normalize_peer_payload({ 264- "id": peer_id, 265- "node_id": NODE_ID, 266- "public_key": payload["public_key"], 267- "preshared_key": payload["preshared_key"], 268: "tunnel_ip": payload["tunnel_ip"], 269- "paid_until": payload.get("paid_until"), 270: "enabled": True, 271- }) 272- 273- remote_set_peer(peer) 274- remote_wg("wg show " + shlex.quote(REMOTE_WG_INTERFACE)) 275- 276- state = load_state() 277- state["peers"][peer["public_key"]] = { 278- "peer_id": peer_id, 279- "public_key": peer["public_key"], 280- "preshared_key": peer["preshared_key"], 281: "tunnel_ip": peer["tunnel_ip"], 282- "paid_until": peer["paid_until"], 283: "enabled": True, 284- "last_action": action, 285- "last_job_id": job["id"], 286- "updated_at": utc_now(), 287- } 288- save_state(state) 289: log(f"REAL enable_peer peer_id={peer_id} tunnel_ip={peer['tunnel_ip']}") 290- return 291- 292- if action == "disable_peer": 293- public_key = require_wg_key("public_key", payload["public_key"]) 294- 295- remote_remove_peer(public_key) -- 299- if public_key not in state["peers"]: 300- state["peers"][public_key] = { 301- "peer_id": peer_id, 302- "public_key": public_key, 303- } 304- 305: state["peers"][public_key]["enabled"] = False 306- state["peers"][public_key]["last_action"] = action 307- state["peers"][public_key]["last_job_id"] = job["id"] 308- state["peers"][public_key]["updated_at"] = utc_now() 309- 310- save_state(state) 311- log(f"REAL disable_peer peer_id={peer_id}") 312- return 313- 314- raise RuntimeError(f"unsupported action: {action}") 315- 316- 317-def run_once(): 318: # Reconciliation first: VM100 wg_paid runtime state must match backend enabled peers. 319: sync_enabled_peers() 320- 321- query = parse.urlencode({"node_id": NODE_ID, "limit": 20}) 322- jobs = http_json("GET", f"/agent/jobs?{query}") or [] 323- 324- if not jobs: 325- log("no pending jobs") ### FILE: /opt/wg-access/agent/wg_access_agent.py.BEFORE_REAL_SSH_WG_SET.20260704-132120 99- if action == "enable_peer": 100- public_key = payload["public_key"] 101- state["peers"][public_key] = { 102- "peer_id": peer_id, 103- "public_key": public_key, 104- "preshared_key": payload["preshared_key"], 105: "tunnel_ip": payload["tunnel_ip"], 106- "paid_until": payload["paid_until"], 107: "enabled": True, 108- "last_action": action, 109- "last_job_id": job["id"], 110- "updated_at": utc_now(), 111- } 112- save_state(state) 113: log(f"DRY-RUN enable_peer peer_id={peer_id} tunnel_ip={payload['tunnel_ip']}") 114- return 115- 116- if action == "disable_peer": 117- public_key = payload["public_key"] 118- if public_key in state["peers"]: 119: state["peers"][public_key]["enabled"] = False 120- state["peers"][public_key]["last_action"] = action 121- state["peers"][public_key]["last_job_id"] = job["id"] 122- state["peers"][public_key]["updated_at"] = utc_now() 123- save_state(state) 124- log(f"DRY-RUN disable_peer peer_id={peer_id}") 125- return ### FILE: /opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532 104-def require_wg_key(name, value): 105- if not isinstance(value, str) or not WG_KEY_RE.match(value): 106- raise RuntimeError(f"invalid WireGuard key in {name}") 107- return value 108- 109- 110:def require_tunnel_ip(value): 111- ip = ipaddress.ip_address(str(value)) 112- if ip.version != 4: 113: raise RuntimeError("only IPv4 tunnel_ip is supported") 114- return str(ip) 115- 116- 117-def remote_wg(script, stdin_text=None): 118- cmd = [ 119- "ssh", -- 155- payload = job["payload_json"] 156- peer_id = str(job.get("peer_id") or "") 157- 158- if action == "enable_peer": 159- public_key = require_wg_key("public_key", payload["public_key"]) 160- preshared_key = require_wg_key("preshared_key", payload["preshared_key"]) 161: tunnel_ip = require_tunnel_ip(payload["tunnel_ip"]) 162: allowed_ips = f"{tunnel_ip}/32" 163- 164- script = "\n".join([ 165- "set -eu", 166- 'tmp="$(mktemp /tmp/wg_access_psk.XXXXXX)"', 167- 'trap \'rm -f "$tmp"\' EXIT', 168- 'cat > "$tmp"', -- 178- remote_wg(script, preshared_key + "\n") 179- 180- state["peers"][public_key] = { 181- "peer_id": peer_id, 182- "public_key": public_key, 183- "preshared_key": preshared_key, 184: "tunnel_ip": tunnel_ip, 185- "paid_until": payload.get("paid_until"), 186: "enabled": True, 187- "last_action": action, 188- "last_job_id": job["id"], 189- "updated_at": utc_now(), 190- } 191- save_state(state) 192: log(f"REAL enable_peer peer_id={peer_id} tunnel_ip={tunnel_ip}") 193- return 194- 195- if action == "disable_peer": 196- public_key = require_wg_key("public_key", payload["public_key"]) 197- 198- script = "\n".join([ -- 210- if public_key not in state["peers"]: 211- state["peers"][public_key] = { 212- "peer_id": peer_id, 213- "public_key": public_key, 214- } 215- 216: state["peers"][public_key]["enabled"] = False 217- state["peers"][public_key]["last_action"] = action 218- state["peers"][public_key]["last_job_id"] = job["id"] 219- state["peers"][public_key]["updated_at"] = utc_now() 220- 221- save_state(state) 222- log(f"REAL disable_peer peer_id={peer_id}") ### FILE: /opt/wg-access/backend/alembic/versions/0001_initial_schema.py 75- "peers", 76- sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), 77- sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False), 78- sa.Column("node_id", sa.String(length=64), nullable=False), 79- sa.Column("public_key", sa.String(length=128), nullable=False), 80- sa.Column("preshared_key", sa.String(length=128), nullable=False), 81: sa.Column("tunnel_ip", sa.String(length=64), nullable=False), 82- sa.Column("paid_until", sa.DateTime(timezone=True), nullable=False), 83: sa.Column("enabled", sa.Boolean(), nullable=False), 84- sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), 85- sa.Column("disabled_at", sa.DateTime(timezone=True), nullable=True), 86: sa.UniqueConstraint("node_id", "tunnel_ip", name="uq_peers_node_tunnel_ip"), 87- ) 88- op.create_index("ix_peers_user_id", "peers", ["user_id"]) 89- op.create_index("ix_peers_node_id", "peers", ["node_id"]) 90: op.create_index("ix_peers_enabled", "peers", ["enabled"]) 91- op.create_unique_constraint("uq_peers_public_key", "peers", ["public_key"]) 92- 93- op.create_table( 94- "provisioning_jobs", 95- sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), 96- sa.Column("node_id", sa.String(length=64), nullable=False), ### FILE: /opt/wg-access/backend/app/api/admin.py 10- 11-from app.db.session import get_db 12-from app.models import InviteCode, User, Subscription, Peer, ProvisioningJob 13-from app.services.wireguard import ( 14- generate_wg_keypair, 15- generate_wg_psk, 16: next_tunnel_ip, 17- build_client_config, 18-) 19- 20-router = APIRouter(prefix="/admin", tags=["admin"]) 21- 22- -- 62- 63-class PeerResponse(BaseModel): 64- id: UUID 65- user_id: UUID 66- node_id: str 67- public_key: str 68: tunnel_ip: str 69- paid_until: datetime 70: enabled: bool 71- created_at: datetime 72- 73- class Config: 74- from_attributes = True 75- 76- -- 92-class SubscriptionCreateResponse(BaseModel): 93- user_id: UUID 94- subscription_id: UUID 95- peer_id: UUID 96- job_id: UUID 97- node_id: str 98: tunnel_ip: str 99- paid_until: datetime 100- private_key: REDACTED 101- public_key: str 102- preshared_key: str 103- client_config: str 104- -- 121- auto_renew=payload.auto_renew, 122- paid_until=paid_until, 123- ) 124- db.add(subscription) 125- db.flush() 126- 127: tunnel_ip = next_tunnel_ip(db, payload.node_id) 128- private_key, public_key = generate_wg_keypair() 129- preshared_key = generate_wg_psk() 130: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 131- 132- peer = Peer( 133- user_id=user.id, 134- node_id=payload.node_id, 135- public_key=public_key, 136- preshared_key=preshared_key, 137: tunnel_ip=tunnel_ip, 138- paid_until=paid_until, 139: enabled=True, 140- ) 141- db.add(peer) 142- db.flush() 143- 144- job = ProvisioningJob( 145- node_id=payload.node_id, 146- action="enable_peer", 147- peer_id=peer.id, 148- payload_json={ 149- "public_key": peer.public_key, 150- "preshared_key": peer.preshared_key, 151: "tunnel_ip": peer.tunnel_ip, 152- "paid_until": peer.paid_until.isoformat(), 153- }, 154- status="pending", 155- attempts=0, 156- ) 157- db.add(job) -- 164- return SubscriptionCreateResponse( 165- user_id=user.id, 166- subscription_id=subscription.id, 167- peer_id=peer.id, 168- job_id=job.id, 169- node_id=peer.node_id, 170: tunnel_ip=peer.tunnel_ip, 171- paid_until=peer.paid_until, 172- private_key=REDACTED, 173- public_key=peer.public_key, 174- preshared_key=peer.preshared_key, 175- client_config=client_config, 176- ) -- 200- disabled_peer_count = 0 201- 202- for subscription in expired_subscriptions: 203- subscription.status = "expired" 204- subscription.auto_renew = False 205- 206: enabled_peers = db.execute( 207- select(Peer) 208- .where(Peer.user_id == subscription.user_id) 209: .where(Peer.enabled == True) # noqa: E712 210- .order_by(Peer.created_at.desc()) 211- ).scalars().all() 212- 213: for peer in enabled_peers: 214: peer.enabled = False 215- peer.disabled_at = now 216- disabled_peer_count += 1 217- 218- job = ProvisioningJob( 219- node_id=peer.node_id, 220- action="disable_peer", 221- peer_id=peer.id, 222- payload_json={ 223- "public_key": peer.public_key, 224: "tunnel_ip": peer.tunnel_ip, 225- }, 226- status="pending", 227- attempts=0, 228- ) 229- db.add(job) 230- jobs.append(job) -- 257- if not subscription: 258- raise HTTPException(status_code=404, detail="subscription not found") 259- 260- now = datetime.now(timezone.utc) 261- 262- # Idempotent business-level cancel: 263: # cancel subscription once, then disable any currently enabled peers for this user. 264- subscription.status = "canceled" 265- subscription.auto_renew = False 266- if subscription.canceled_at is None: 267- subscription.canceled_at = now 268- 269: enabled_peers = db.execute( 270- select(Peer) 271- .where(Peer.user_id == subscription.user_id) 272: .where(Peer.enabled == True) # noqa: E712 273- .order_by(Peer.created_at.desc()) 274- ).scalars().all() 275- 276- jobs = [] 277: for peer in enabled_peers: 278: peer.enabled = False 279- peer.disabled_at = now 280- 281- job = ProvisioningJob( 282- node_id=peer.node_id, 283- action="disable_peer", 284- peer_id=peer.id, 285- payload_json={ 286- "public_key": peer.public_key, 287: "tunnel_ip": peer.tunnel_ip, 288- }, 289- status="pending", 290- attempts=0, 291- ) 292- db.add(job) 293- jobs.append(job) -- 299- 300- return CancelSubscriptionResponse( 301- subscription_id=subscription.id, 302- user_id=subscription.user_id, 303- status=subscription.status, 304- canceled_at=subscription.canceled_at, 305: disabled_peer_count=len(enabled_peers), 306- job_ids=[job.id for job in jobs], 307- ) 308- 309- 310-class DisablePeerResponse(BaseModel): 311- peer_id: UUID 312- job_id: UUID 313- node_id: str 314- public_key: str 315: tunnel_ip: str 316: enabled: bool 317- 318- 319-@router.post("/peers/{peer_id}/disable", response_model=DisablePeerResponse) 320-def disable_peer(peer_id: UUID, db: Session = Depends(get_db)): 321- peer = db.get(Peer, peer_id) 322- if not peer: 323- from fastapi import HTTPException 324- raise HTTPException(status_code=404, detail="peer not found") 325- 326: peer.enabled = False 327- peer.disabled_at = datetime.now(timezone.utc) 328- 329- job = ProvisioningJob( 330- node_id=peer.node_id, 331- action="disable_peer", 332- peer_id=peer.id, 333- payload_json={ 334- "public_key": peer.public_key, 335: "tunnel_ip": peer.tunnel_ip, 336- }, 337- status="pending", 338- attempts=0, 339- ) 340- db.add(job) 341- db.commit() -- 344- 345- return DisablePeerResponse( 346- peer_id=peer.id, 347- job_id=job.id, 348- node_id=peer.node_id, 349- public_key=peer.public_key, 350: tunnel_ip=peer.tunnel_ip, 351: enabled=peer.enabled, 352- ) 353- ### FILE: /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253 55- 56-class PeerResponse(BaseModel): 57- id: UUID 58- user_id: UUID 59- node_id: str 60- public_key: str 61: tunnel_ip: str 62- paid_until: datetime 63: enabled: bool 64- created_at: datetime 65- 66- class Config: 67- from_attributes = True 68- 69- ### FILE: /opt/wg-access/backend/app/api/agent.py 20- 21-class AgentPeerResponse(BaseModel): 22- id: UUID 23- node_id: str 24- public_key: str 25- preshared_key: str 26: tunnel_ip: str 27- paid_until: datetime 28: enabled: bool 29- 30- class Config: 31- from_attributes = True 32- 33- 34-@router.get("/peers", response_model=list[AgentPeerResponse]) 35:def get_enabled_peers( 36- node_id: str, 37- db: Session = Depends(get_db), 38- _: None = Depends(check_agent_token), 39-): 40- rows = db.execute( 41- select(Peer) 42- .where(Peer.node_id == node_id) 43: .where(Peer.enabled == True) 44- .order_by(Peer.created_at.asc()) 45- ).scalars().all() 46- return rows 47- 48- 49- ### FILE: /opt/wg-access/backend/app/api/dev.py 8-from app.config import settings 9-from app.db.session import get_db 10-from app.models import User, Peer, ProvisioningJob 11-from app.services.wireguard import ( 12- generate_wg_keypair, 13- generate_wg_psk, 14: next_tunnel_ip, 15- build_client_config, 16-) 17- 18-router = APIRouter(prefix="/dev", tags=["dev"]) 19- 20-def require_dev_environment(): -- 30- 31- 32-class TestPeerResponse(BaseModel): 33- user_id: UUID 34- peer_id: UUID 35- job_id: UUID 36: tunnel_ip: str 37- paid_until: datetime 38- private_key: REDACTED 39- public_key: str 40- preshared_key: str 41- client_config: str 42- -- 52- # The private key is returned once and is not stored in DB. 53- user = User(email=payload.email) 54- db.add(user) 55- db.flush() 56- 57- paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 58: tunnel_ip = next_tunnel_ip(db, payload.node_id) 59- 60- private_key, public_key = generate_wg_keypair() 61- preshared_key = generate_wg_psk() 62: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 63- 64- peer = Peer( 65- user_id=user.id, 66- node_id=payload.node_id, 67- public_key=public_key, 68- preshared_key=preshared_key, 69: tunnel_ip=tunnel_ip, 70- paid_until=paid_until, 71: enabled=True, 72- ) 73- db.add(peer) 74- db.flush() 75- 76- job = ProvisioningJob( 77- node_id=payload.node_id, 78- action="enable_peer", 79- peer_id=peer.id, 80- payload_json={ 81- "public_key": peer.public_key, 82- "preshared_key": peer.preshared_key, 83: "tunnel_ip": peer.tunnel_ip, 84- "paid_until": peer.paid_until.isoformat(), 85- }, 86- status="pending", 87- attempts=0, 88- ) 89- db.add(job) -- 93- db.refresh(job) 94- 95- return TestPeerResponse( 96- user_id=user.id, 97- peer_id=peer.id, 98- job_id=job.id, 99: tunnel_ip=peer.tunnel_ip, 100- paid_until=peer.paid_until, 101- private_key=REDACTED, 102- public_key=peer.public_key, 103- preshared_key=peer.preshared_key, 104- client_config=client_config, 105- ) ### FILE: /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636 23- 24- 25-class TestPeerResponse(BaseModel): 26- user_id: UUID 27- peer_id: UUID 28- job_id: UUID 29: tunnel_ip: str 30- paid_until: datetime 31- private_key: REDACTED 32- public_key: str 33- preshared_key: str 34- 35- -- 57- 58- 59-def generate_wg_psk() -> str: 60- return b64(secrets.token_bytes(32)) 61- 62- 63:def next_tunnel_ip(db: Session, node_id: str) -> str: 64- # MVP-простота: 10.253.1.N, начиная с 10. 65- count = db.execute( 66- select(func.count()).select_from(Peer).where(Peer.node_id == node_id) 67- ).scalar_one() 68- return f"10.253.1.{10 + count}" 69- -- 72-def create_test_peer(payload: TestPeerRequest, db: Session = Depends(get_db)): 73- user = User(email=payload.email) 74- db.add(user) 75- db.flush() 76- 77- paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 78: tunnel_ip = next_tunnel_ip(db, payload.node_id) 79- 80- private_key, public_key = generate_wg_keypair() 81- preshared_key = generate_wg_psk() 82- 83- peer = Peer( 84- user_id=user.id, 85- node_id=payload.node_id, 86- public_key=public_key, 87- preshared_key=preshared_key, 88: tunnel_ip=tunnel_ip, 89- paid_until=paid_until, 90: enabled=True, 91- ) 92- db.add(peer) 93- db.flush() 94- 95- job = ProvisioningJob( 96- node_id=payload.node_id, 97- action="enable_peer", 98- peer_id=peer.id, 99- payload_json={ 100- "public_key": peer.public_key, 101- "preshared_key": peer.preshared_key, 102: "tunnel_ip": peer.tunnel_ip, 103- "paid_until": peer.paid_until.isoformat(), 104- }, 105- status="pending", 106- attempts=0, 107- ) 108- db.add(job) -- 112- db.refresh(job) 113- 114- return TestPeerResponse( 115- user_id=user.id, 116- peer_id=peer.id, 117- job_id=job.id, 118: tunnel_ip=peer.tunnel_ip, 119- paid_until=peer.paid_until, 120- private_key=REDACTED, 121- public_key=peer.public_key, 122- preshared_key=peer.preshared_key, 123- ) ### FILE: /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545 20- 21- 22-class TestPeerResponse(BaseModel): 23- user_id: UUID 24- peer_id: UUID 25- job_id: UUID 26: tunnel_ip: str 27- paid_until: datetime 28- public_key: str 29- preshared_key: str 30- 31- 32-def fake_wg_key(prefix: str) -> str: 33- return prefix + "_" + secrets.token_urlsafe(32) 34- 35- 36:def next_tunnel_ip(db: Session, node_id: str) -> str: 37- # MVP-простота: 10.253.1.N, начиная с 10. 38- count = db.execute( 39- select(func.count()).select_from(Peer).where(Peer.node_id == node_id) 40- ).scalar_one() 41- return f"10.253.1.{10 + count}" 42- -- 45-def create_test_peer(payload: TestPeerRequest, db: Session = Depends(get_db)): 46- user = User(email=payload.email) 47- db.add(user) 48- db.flush() 49- 50- paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 51: tunnel_ip = next_tunnel_ip(db, payload.node_id) 52- 53- peer = Peer( 54- user_id=user.id, 55- node_id=payload.node_id, 56- public_key=fake_wg_key("pub"), 57- preshared_key=fake_wg_key("psk"), 58: tunnel_ip=tunnel_ip, 59- paid_until=paid_until, 60: enabled=True, 61- ) 62- db.add(peer) 63- db.flush() 64- 65- job = ProvisioningJob( 66- node_id=payload.node_id, 67- action="enable_peer", 68- peer_id=peer.id, 69- payload_json={ 70- "public_key": peer.public_key, 71- "preshared_key": peer.preshared_key, 72: "tunnel_ip": peer.tunnel_ip, 73- "paid_until": peer.paid_until.isoformat(), 74- }, 75- status="pending", 76- attempts=0, 77- ) 78- db.add(job) -- 82- db.refresh(job) 83- 84- return TestPeerResponse( 85- user_id=user.id, 86- peer_id=peer.id, 87- job_id=job.id, 88: tunnel_ip=peer.tunnel_ip, 89- paid_until=peer.paid_until, 90- public_key=peer.public_key, 91- preshared_key=peer.preshared_key, 92- ) ### FILE: /opt/wg-access/backend/app/models/core.py 78- canceled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 79- 80- 81-class Peer(Base): 82- __tablename__ = "peers" 83- __table_args__ = ( 84: # tunnel_ip uniqueness is enforced in PostgreSQL by partial unique index: 85: # uq_peers_node_tunnel_ip_enabled (node_id, tunnel_ip) WHERE enabled = true, 86- ) 87- 88- id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 89- user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) 90- 91- node_id: Mapped[str] = mapped_column(String(64), default="ddn-test", nullable=False, index=True) 92- public_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False) 93- preshared_key: Mapped[str] = mapped_column(String(128), nullable=False) 94: tunnel_ip: Mapped[str] = mapped_column(String(64), nullable=False) 95- 96- paid_until: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) 97: enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) 98- 99- created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.utcnow, nullable=False) 100- disabled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 101- 102- user: Mapped["User"] = relationship(back_populates="peers") 103- ### FILE: /opt/wg-access/backend/app/services/wireguard.py 35- 36- 37-def generate_wg_psk() -> str: 38- return b64(secrets.token_bytes(32)) 39- 40- 41:def next_tunnel_ip(db: Session, node_id: str) -> str: 42- # MVP allocator: 43: # - pool is /16 by default: 10.253.0.0/16 44: # - client addresses start at 10.253.1.10 45: # - only enabled peers reserve addresses 46- # - disabled peers keep history but their addresses can be reused 47: pool_cidr = os.environ.get("WG_CLIENT_POOL_CIDR", "10.253.0.0/16").strip() 48: first_client_ip = os.environ.get("WG_CLIENT_FIRST_IP", "10.253.1.10").strip() 49- 50- pool = ipaddress.ip_network(pool_cidr, strict=False) 51- first_ip = ipaddress.ip_address(first_client_ip) 52- 53- if first_ip not in pool: 54- raise RuntimeError(f"WG_CLIENT_FIRST_IP {first_client_ip} is outside {pool_cidr}") 55- 56- used = set( 57- db.execute( 58: select(Peer.tunnel_ip).where( 59- Peer.node_id == node_id, 60: Peer.enabled.is_(True), 61- ) 62- ).scalars().all() 63- ) 64- 65- current = first_ip 66- last_ip = pool.broadcast_address - 1 -- 71- return ip_text 72- current += 1 73- 74- raise RuntimeError(f"No free tunnel IPs left in pool {pool_cidr}") 75- 76- 77:def build_client_config(private_key: REDACTED, tunnel_ip: str, preshared_key: str) -> str: 78- server_public_key = os.environ.get("WG_SERVER_PUBLIC_KEY", "").strip() 79- endpoint = os.environ.get("WG_CLIENT_ENDPOINT", "").strip() 80- allowed_ips = os.environ.get("WG_CLIENT_ALLOWED_IPS", "10.253.1.1/32").strip() 81- dns = os.environ.get("WG_CLIENT_DNS", "").strip() 82- 83- if not server_public_key: -- 85- if not endpoint: 86- raise RuntimeError("WG_CLIENT_ENDPOINT is not configured") 87- 88- lines = [ 89- "[Interface]", 90- f"PrivateKey = REDACTED 91: f"Address = {tunnel_ip}/32", 92- ] 93- 94- if dns: 95- lines.append(f"DNS = {dns}") 96- 97- lines.extend([ ### FILE: /opt/wg-access/backups/step018b/core.py.20260704-155437.bak 78- canceled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 79- 80- 81-class Peer(Base): 82- __tablename__ = "peers" 83- __table_args__ = ( 84: UniqueConstraint("node_id", "tunnel_ip", name="uq_peers_node_tunnel_ip"), 85- ) 86- 87- id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 88- user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) 89- 90- node_id: Mapped[str] = mapped_column(String(64), default="ddn-test", nullable=False, index=True) 91- public_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False) 92- preshared_key: Mapped[str] = mapped_column(String(128), nullable=False) 93: tunnel_ip: Mapped[str] = mapped_column(String(64), nullable=False) 94- 95- paid_until: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) 96: enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) 97- 98- created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.utcnow, nullable=False) 99- disabled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 100- 101- user: Mapped["User"] = relationship(back_populates="peers") 102- ### FILE: /opt/wg-access/backups/step018b/dev.py.20260704-155255.bak 24- 25- 26-class TestPeerResponse(BaseModel): 27- user_id: UUID 28- peer_id: UUID 29- job_id: UUID 30: tunnel_ip: str 31- paid_until: datetime 32- private_key: REDACTED 33- public_key: str 34- preshared_key: str 35- client_config: str 36- -- 59- 60- 61-def generate_wg_psk() -> str: 62- return b64(secrets.token_bytes(32)) 63- 64- 65:def next_tunnel_ip(db: Session, node_id: str) -> str: 66- # MVP-простота: 10.253.1.N, начиная с 10. 67- count = db.execute( 68- select(func.count()).select_from(Peer).where(Peer.node_id == node_id) 69- ).scalar_one() 70- return f"10.253.1.{10 + count}" 71- 72- 73:def build_client_config(private_key: REDACTED, tunnel_ip: str, preshared_key: str) -> str: 74- server_public_key = os.environ.get("WG_SERVER_PUBLIC_KEY", "").strip() 75- endpoint = os.environ.get("WG_CLIENT_ENDPOINT", "").strip() 76- allowed_ips = os.environ.get("WG_CLIENT_ALLOWED_IPS", "10.253.1.1/32").strip() 77- dns = os.environ.get("WG_CLIENT_DNS", "").strip() 78- 79- if not server_public_key: -- 81- if not endpoint: 82- raise RuntimeError("WG_CLIENT_ENDPOINT is not configured") 83- 84- lines = [ 85- "[Interface]", 86- f"PrivateKey = REDACTED 87: f"Address = {tunnel_ip}/32", 88- ] 89- 90- if dns: 91- lines.append(f"DNS = {dns}") 92- 93- lines.extend([ -- 108-def create_test_peer(payload: TestPeerRequest, db: Session = Depends(get_db)): 109- user = User(email=payload.email) 110- db.add(user) 111- db.flush() 112- 113- paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 114: tunnel_ip = next_tunnel_ip(db, payload.node_id) 115- 116- private_key, public_key = generate_wg_keypair() 117- preshared_key = generate_wg_psk() 118: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 119- 120- peer = Peer( 121- user_id=user.id, 122- node_id=payload.node_id, 123- public_key=public_key, 124- preshared_key=preshared_key, 125: tunnel_ip=tunnel_ip, 126- paid_until=paid_until, 127: enabled=True, 128- ) 129- db.add(peer) 130- db.flush() 131- 132- job = ProvisioningJob( 133- node_id=payload.node_id, 134- action="enable_peer", 135- peer_id=peer.id, 136- payload_json={ 137- "public_key": peer.public_key, 138- "preshared_key": peer.preshared_key, 139: "tunnel_ip": peer.tunnel_ip, 140- "paid_until": peer.paid_until.isoformat(), 141- }, 142- status="pending", 143- attempts=0, 144- ) 145- db.add(job) -- 149- db.refresh(job) 150- 151- return TestPeerResponse( 152- user_id=user.id, 153- peer_id=peer.id, 154- job_id=job.id, 155: tunnel_ip=peer.tunnel_ip, 156- paid_until=peer.paid_until, 157- private_key=REDACTED, 158- public_key=peer.public_key, 159- preshared_key=peer.preshared_key, 160- client_config=client_config, 161- ) ### FILE: /opt/wg-access/backups/step018b/peers.20260704-155255.sql 29-CREATE TABLE public.peers ( 30- id uuid NOT NULL, 31- user_id uuid NOT NULL, 32- node_id character varying(64) NOT NULL, 33- public_key character varying(128) NOT NULL, 34- preshared_key character varying(128) NOT NULL, 35: tunnel_ip character varying(64) NOT NULL, 36- paid_until timestamp with time zone NOT NULL, 37: enabled boolean NOT NULL, 38- created_at timestamp with time zone NOT NULL, 39- disabled_at timestamp with time zone 40-); 41- 42- 43-ALTER TABLE public.peers OWNER TO wg_access; 44- 45--- 46--- Data for Name: peers; Type: TABLE DATA; Schema: public; Owner: wg_access 47--- 48- 49:COPY public.peers (id, user_id, node_id, public_key, preshared_key, tunnel_ip, paid_until, enabled, created_at, disabled_at) FROM stdin; 50:1c152b56-8f35-4bee-bdd7-154ff551077e 10de9cee-ffc9-4065-96fe-401ec593e250 ddn-test pub_2aWI36SAPwbgiUQLZCVeTmoOAPSd2gjMX_uVNgYN1ag psk_9bCVUY2U-mcNQ8NtymqJSMNTUbmqIB655VOKbk3PCKQ 10.253.1.10 2026-09-02 10:28:02.539201+00 f 2026-07-04 10:28:02.545211+00 2026-07-04 13:38:41.691553+00 51-37361fae-0ad5-4a7a-8096-a59e1dde2d78 abe41583-990e-4d08-9572-a586acaf816f ddn-test pub_X1c2s8NhHk-msYA4Pe25gvmnZmhSRSsmrVob-exBUaw psk_sIShZqy2__7O7UM7iPIPcv1Q4wyZoqk9gR6t9peFt98 10.253.1.11 2026-09-02 10:28:54.187046+00 f 2026-07-04 10:28:54.18809+00 2026-07-04 13:38:41.691553+00 52-4b39aa87-4b59-4c71-950b-cf2dca3e606f 1f0b0e4a-e3ea-4d11-89d1-18d0db370ba7 ddn-test pub_BFUhgsp-ejx-tAwttmNzzPupN-c0-5sSS3YRaevqpes psk_Go31Cw6iyQNo5EducR7hZdckh6F-_9J_k9bFp_uYP94 10.253.1.12 2026-08-03 10:37:34.52517+00 f 2026-07-04 10:37:34.530114+00 2026-07-04 13:38:41.691553+00 53-754b76bd-0e69-4184-ac0b-a9c5706dbf19 8c8e9a3f-740d-4f24-baa1-801904b4b50e ddn-test pub_cdnODJUM3pZBIS_Z3dOyBsHVi-Jq-4j_WiDuve84qDw psk_ys8c0VJVIx-Af-VZKi3X6qR3kCSvtw2VvGBuE7a_8CM 10.253.1.13 2026-08-03 10:41:13.816901+00 f 2026-07-04 10:41:13.818018+00 2026-07-04 13:38:41.691553+00 54-813515db-bf25-46c9-bc83-c9c45d51ba68 ade5215e-4229-4e83-9012-f09f4691b4e6 ddn-test TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM= 3bSmNkXz95F0wXWDiTsz9y/wSdP6/QmzAv9lODnMsPE= 10.253.1.14 2026-09-02 13:28:02.954738+00 f 2026-07-04 13:28:02.964577+00 2026-07-04 15:13:41.913497+00 55-13543224-39f7-4cfc-917d-66a16e2e0e55 d360d32e-c7db-4688-846b-ecef626f12db ddn-test cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik= OLhe7KFgKYHSMuHsXcp0Nhttofj6LnjSHt25yh/H6Xw= 10.253.1.15 2026-09-02 15:17:46.085909+00 t 2026-07-04 15:17:46.096446+00 \N 56-1b19ed83-1cad-4d57-be9f-fded5270213c 2109fe84-fde0-4856-8268-030a43112782 ddn-test mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg= d4JT7vhXYExxGz8BqU/CxZ51gdh6nC6bLr0G0dJbX14= 10.253.1.16 2026-09-02 15:26:17.089248+00 t 2026-07-04 15:26:17.090574+00 \N -- 63- 64-ALTER TABLE ONLY public.peers 65- ADD CONSTRAINT peers_pkey PRIMARY KEY (id); 66- 67- 68--- 69:-- Name: peers uq_peers_node_tunnel_ip; Type: CONSTRAINT; Schema: public; Owner: wg_access 70--- 71- 72-ALTER TABLE ONLY public.peers 73: ADD CONSTRAINT uq_peers_node_tunnel_ip UNIQUE (node_id, tunnel_ip); 74- 75- 76--- 77--- Name: peers uq_peers_public_key; Type: CONSTRAINT; Schema: public; Owner: wg_access 78--- 79- 80-ALTER TABLE ONLY public.peers 81- ADD CONSTRAINT uq_peers_public_key UNIQUE (public_key); 82- 83- 84--- 85:-- Name: ix_peers_enabled; Type: INDEX; Schema: public; Owner: wg_access 86--- 87- 88:CREATE INDEX ix_peers_enabled ON public.peers USING btree (enabled); 89- 90- 91--- 92--- Name: ix_peers_node_id; Type: INDEX; Schema: public; Owner: wg_access 93--- 94- ### FILE: /opt/wg-access/backups/step021b/admin.py.20260704-162930.bak 55- 56-class PeerResponse(BaseModel): 57- id: UUID 58- user_id: UUID 59- node_id: str 60- public_key: str 61: tunnel_ip: str 62- paid_until: datetime 63: enabled: bool 64- created_at: datetime 65- 66- class Config: 67- from_attributes = True 68- 69- -- 75- 76-class DisablePeerResponse(BaseModel): 77- peer_id: UUID 78- job_id: UUID 79- node_id: str 80- public_key: str 81: tunnel_ip: str 82: enabled: bool 83- 84- 85-@router.post("/peers/{peer_id}/disable", response_model=DisablePeerResponse) 86-def disable_peer(peer_id: UUID, db: Session = Depends(get_db)): 87- peer = db.get(Peer, peer_id) 88- if not peer: 89- from fastapi import HTTPException 90- raise HTTPException(status_code=404, detail="peer not found") 91- 92: peer.enabled = False 93- peer.disabled_at = datetime.now(timezone.utc) 94- 95- job = ProvisioningJob( 96- node_id=peer.node_id, 97- action="disable_peer", 98- peer_id=peer.id, 99- payload_json={ 100- "public_key": peer.public_key, 101: "tunnel_ip": peer.tunnel_ip, 102- }, 103- status="pending", 104- attempts=0, 105- ) 106- db.add(job) 107- db.commit() -- 110- 111- return DisablePeerResponse( 112- peer_id=peer.id, 113- job_id=job.id, 114- node_id=peer.node_id, 115- public_key=peer.public_key, 116: tunnel_ip=peer.tunnel_ip, 117: enabled=peer.enabled, 118- ) 119- 120- 121- 122-class JobResponse(BaseModel): 123- id: UUID ### FILE: /opt/wg-access/backups/step021b/dev.py.20260704-162930.bak 25- 26- 27-class TestPeerResponse(BaseModel): 28- user_id: UUID 29- peer_id: UUID 30- job_id: UUID 31: tunnel_ip: str 32- paid_until: datetime 33- private_key: REDACTED 34- public_key: str 35- preshared_key: str 36- client_config: str 37- -- 60- 61- 62-def generate_wg_psk() -> str: 63- return b64(secrets.token_bytes(32)) 64- 65- 66:def next_tunnel_ip(db: Session, node_id: str) -> str: 67- # MVP allocator: 68: # - pool is /16 by default: 10.253.0.0/16 69: # - client addresses start at 10.253.1.10 70: # - only enabled peers reserve addresses 71- # - disabled peers keep history but their addresses can be reused 72: pool_cidr = os.environ.get("WG_CLIENT_POOL_CIDR", "10.253.0.0/16").strip() 73: first_client_ip = os.environ.get("WG_CLIENT_FIRST_IP", "10.253.1.10").strip() 74- 75- pool = ipaddress.ip_network(pool_cidr, strict=False) 76- first_ip = ipaddress.ip_address(first_client_ip) 77- 78- if first_ip not in pool: 79- raise RuntimeError(f"WG_CLIENT_FIRST_IP {first_client_ip} is outside {pool_cidr}") 80- 81- used = set( 82- db.execute( 83: select(Peer.tunnel_ip).where( 84- Peer.node_id == node_id, 85: Peer.enabled.is_(True), 86- ) 87- ).scalars().all() 88- ) 89- 90- current = first_ip 91- last_ip = pool.broadcast_address - 1 -- 96- return ip_text 97- current += 1 98- 99- raise RuntimeError(f"No free tunnel IPs left in pool {pool_cidr}") 100- 101- 102:def build_client_config(private_key: REDACTED, tunnel_ip: str, preshared_key: str) -> str: 103- server_public_key = os.environ.get("WG_SERVER_PUBLIC_KEY", "").strip() 104- endpoint = os.environ.get("WG_CLIENT_ENDPOINT", "").strip() 105- allowed_ips = os.environ.get("WG_CLIENT_ALLOWED_IPS", "10.253.1.1/32").strip() 106- dns = os.environ.get("WG_CLIENT_DNS", "").strip() 107- 108- if not server_public_key: -- 110- if not endpoint: 111- raise RuntimeError("WG_CLIENT_ENDPOINT is not configured") 112- 113- lines = [ 114- "[Interface]", 115- f"PrivateKey = REDACTED 116: f"Address = {tunnel_ip}/32", 117- ] 118- 119- if dns: 120- lines.append(f"DNS = {dns}") 121- 122- lines.extend([ -- 137-def create_test_peer(payload: TestPeerRequest, db: Session = Depends(get_db)): 138- user = User(email=payload.email) 139- db.add(user) 140- db.flush() 141- 142- paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 143: tunnel_ip = next_tunnel_ip(db, payload.node_id) 144- 145- private_key, public_key = generate_wg_keypair() 146- preshared_key = generate_wg_psk() 147: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 148- 149- peer = Peer( 150- user_id=user.id, 151- node_id=payload.node_id, 152- public_key=public_key, 153- preshared_key=preshared_key, 154: tunnel_ip=tunnel_ip, 155- paid_until=paid_until, 156: enabled=True, 157- ) 158- db.add(peer) 159- db.flush() 160- 161- job = ProvisioningJob( 162- node_id=payload.node_id, 163- action="enable_peer", 164- peer_id=peer.id, 165- payload_json={ 166- "public_key": peer.public_key, 167- "preshared_key": peer.preshared_key, 168: "tunnel_ip": peer.tunnel_ip, 169- "paid_until": peer.paid_until.isoformat(), 170- }, 171- status="pending", 172- attempts=0, 173- ) 174- db.add(job) -- 178- db.refresh(job) 179- 180- return TestPeerResponse( 181- user_id=user.id, 182- peer_id=peer.id, 183- job_id=job.id, 184: tunnel_ip=peer.tunnel_ip, 185- paid_until=peer.paid_until, 186- private_key=REDACTED, 187- public_key=peer.public_key, 188- preshared_key=peer.preshared_key, 189- client_config=client_config, 190- ) ### FILE: /opt/wg-access/backups/step022a/dev.py.20260704-163658.bak 25- 26- 27-class TestPeerResponse(BaseModel): 28- user_id: UUID 29- peer_id: UUID 30- job_id: UUID 31: tunnel_ip: str 32- paid_until: datetime 33- private_key: REDACTED 34- public_key: str 35- preshared_key: str 36- client_config: str 37- -- 60- 61- 62-def generate_wg_psk() -> str: 63- return b64(secrets.token_bytes(32)) 64- 65- 66:def next_tunnel_ip(db: Session, node_id: str) -> str: 67- # MVP allocator: 68: # - pool is /16 by default: 10.253.0.0/16 69: # - client addresses start at 10.253.1.10 70: # - only enabled peers reserve addresses 71- # - disabled peers keep history but their addresses can be reused 72: pool_cidr = os.environ.get("WG_CLIENT_POOL_CIDR", "10.253.0.0/16").strip() 73: first_client_ip = os.environ.get("WG_CLIENT_FIRST_IP", "10.253.1.10").strip() 74- 75- pool = ipaddress.ip_network(pool_cidr, strict=False) 76- first_ip = ipaddress.ip_address(first_client_ip) 77- 78- if first_ip not in pool: 79- raise RuntimeError(f"WG_CLIENT_FIRST_IP {first_client_ip} is outside {pool_cidr}") 80- 81- used = set( 82- db.execute( 83: select(Peer.tunnel_ip).where( 84- Peer.node_id == node_id, 85: Peer.enabled.is_(True), 86- ) 87- ).scalars().all() 88- ) 89- 90- current = first_ip 91- last_ip = pool.broadcast_address - 1 -- 96- return ip_text 97- current += 1 98- 99- raise RuntimeError(f"No free tunnel IPs left in pool {pool_cidr}") 100- 101- 102:def build_client_config(private_key: REDACTED, tunnel_ip: str, preshared_key: str) -> str: 103- server_public_key = os.environ.get("WG_SERVER_PUBLIC_KEY", "").strip() 104- endpoint = os.environ.get("WG_CLIENT_ENDPOINT", "").strip() 105- allowed_ips = os.environ.get("WG_CLIENT_ALLOWED_IPS", "10.253.1.1/32").strip() 106- dns = os.environ.get("WG_CLIENT_DNS", "").strip() 107- 108- if not server_public_key: -- 110- if not endpoint: 111- raise RuntimeError("WG_CLIENT_ENDPOINT is not configured") 112- 113- lines = [ 114- "[Interface]", 115- f"PrivateKey = REDACTED 116: f"Address = {tunnel_ip}/32", 117- ] 118- 119- if dns: 120- lines.append(f"DNS = {dns}") 121- 122- lines.extend([ -- 137-def create_test_peer(payload: TestPeerRequest, db: Session = Depends(get_db)): 138- user = User(email=payload.email) 139- db.add(user) 140- db.flush() 141- 142- paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 143: tunnel_ip = next_tunnel_ip(db, payload.node_id) 144- 145- private_key, public_key = generate_wg_keypair() 146- preshared_key = generate_wg_psk() 147: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 148- 149- peer = Peer( 150- user_id=user.id, 151- node_id=payload.node_id, 152- public_key=public_key, 153- preshared_key=preshared_key, 154: tunnel_ip=tunnel_ip, 155- paid_until=paid_until, 156: enabled=True, 157- ) 158- db.add(peer) 159- db.flush() 160- 161- job = ProvisioningJob( 162- node_id=payload.node_id, 163- action="enable_peer", 164- peer_id=peer.id, 165- payload_json={ 166- "public_key": peer.public_key, 167- "preshared_key": peer.preshared_key, 168: "tunnel_ip": peer.tunnel_ip, 169- "paid_until": peer.paid_until.isoformat(), 170- }, 171- status="pending", 172- attempts=0, 173- ) 174- db.add(job) -- 178- db.refresh(job) 179- 180- return TestPeerResponse( 181- user_id=user.id, 182- peer_id=peer.id, 183- job_id=job.id, 184: tunnel_ip=peer.tunnel_ip, 185- paid_until=peer.paid_until, 186- private_key=REDACTED, 187- public_key=peer.public_key, 188- preshared_key=peer.preshared_key, 189- client_config=client_config, 190- ) ### FILE: /opt/wg-access/backups/step022a/wireguard.py.20260704-163658.bak 35- 36- 37-def generate_wg_psk() -> str: 38- return b64(secrets.token_bytes(32)) 39- 40- 41:def next_tunnel_ip(db: Session, node_id: str) -> str: 42- # MVP allocator: 43: # - pool is /16 by default: 10.253.0.0/16 44: # - client addresses start at 10.253.1.10 45: # - only enabled peers reserve addresses 46- # - disabled peers keep history but their addresses can be reused 47: pool_cidr = os.environ.get("WG_CLIENT_POOL_CIDR", "10.253.0.0/16").strip() 48: first_client_ip = os.environ.get("WG_CLIENT_FIRST_IP", "10.253.1.10").strip() 49- 50- pool = ipaddress.ip_network(pool_cidr, strict=False) 51- first_ip = ipaddress.ip_address(first_client_ip) 52- 53- if first_ip not in pool: 54- raise RuntimeError(f"WG_CLIENT_FIRST_IP {first_client_ip} is outside {pool_cidr}") 55- 56- used = set( 57- db.execute( 58: select(Peer.tunnel_ip).where( 59- Peer.node_id == node_id, 60: Peer.enabled.is_(True), 61- ) 62- ).scalars().all() 63- ) 64- 65- current = first_ip 66- last_ip = pool.broadcast_address - 1 -- 71- return ip_text 72- current += 1 73- 74- raise RuntimeError(f"No free tunnel IPs left in pool {pool_cidr}") 75- 76- 77:def build_client_config(private_key: REDACTED, tunnel_ip: str, preshared_key: str) -> str: 78- server_public_key = os.environ.get("WG_SERVER_PUBLIC_KEY", "").strip() 79- endpoint = os.environ.get("WG_CLIENT_ENDPOINT", "").strip() 80- allowed_ips = os.environ.get("WG_CLIENT_ALLOWED_IPS", "10.253.1.1/32").strip() 81- dns = os.environ.get("WG_CLIENT_DNS", "").strip() 82- 83- if not server_public_key: -- 85- if not endpoint: 86- raise RuntimeError("WG_CLIENT_ENDPOINT is not configured") 87- 88- lines = [ 89- "[Interface]", 90- f"PrivateKey = REDACTED 91: f"Address = {tunnel_ip}/32", 92- ] 93- 94- if dns: 95- lines.append(f"DNS = {dns}") 96- 97- lines.extend([ ### FILE: /opt/wg-access/backups/step023a/dev.py.20260704-172733.bak 7- 8-from app.db.session import get_db 9-from app.models import User, Peer, ProvisioningJob 10-from app.services.wireguard import ( 11- generate_wg_keypair, 12- generate_wg_psk, 13: next_tunnel_ip, 14- build_client_config, 15-) 16- 17-router = APIRouter(prefix="/dev", tags=["dev"]) 18- 19- -- 24- 25- 26-class TestPeerResponse(BaseModel): 27- user_id: UUID 28- peer_id: UUID 29- job_id: UUID 30: tunnel_ip: str 31- paid_until: datetime 32- private_key: REDACTED 33- public_key: str 34- preshared_key: str 35- client_config: str 36- -- 42- # The private key is returned once and is not stored in DB. 43- user = User(email=payload.email) 44- db.add(user) 45- db.flush() 46- 47- paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 48: tunnel_ip = next_tunnel_ip(db, payload.node_id) 49- 50- private_key, public_key = generate_wg_keypair() 51- preshared_key = generate_wg_psk() 52: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 53- 54- peer = Peer( 55- user_id=user.id, 56- node_id=payload.node_id, 57- public_key=public_key, 58- preshared_key=preshared_key, 59: tunnel_ip=tunnel_ip, 60- paid_until=paid_until, 61: enabled=True, 62- ) 63- db.add(peer) 64- db.flush() 65- 66- job = ProvisioningJob( 67- node_id=payload.node_id, 68- action="enable_peer", 69- peer_id=peer.id, 70- payload_json={ 71- "public_key": peer.public_key, 72- "preshared_key": peer.preshared_key, 73: "tunnel_ip": peer.tunnel_ip, 74- "paid_until": peer.paid_until.isoformat(), 75- }, 76- status="pending", 77- attempts=0, 78- ) 79- db.add(job) -- 83- db.refresh(job) 84- 85- return TestPeerResponse( 86- user_id=user.id, 87- peer_id=peer.id, 88- job_id=job.id, 89: tunnel_ip=peer.tunnel_ip, 90- paid_until=peer.paid_until, 91- private_key=REDACTED, 92- public_key=peer.public_key, 93- preshared_key=peer.preshared_key, 94- client_config=client_config, 95- ) ### FILE: /opt/wg-access/backups/step024a/admin.py.20260704-174718.bak 9- 10-from app.db.session import get_db 11-from app.models import InviteCode, User, Subscription, Peer, ProvisioningJob 12-from app.services.wireguard import ( 13- generate_wg_keypair, 14- generate_wg_psk, 15: next_tunnel_ip, 16- build_client_config, 17-) 18- 19-router = APIRouter(prefix="/admin", tags=["admin"]) 20- 21- -- 61- 62-class PeerResponse(BaseModel): 63- id: UUID 64- user_id: UUID 65- node_id: str 66- public_key: str 67: tunnel_ip: str 68- paid_until: datetime 69: enabled: bool 70- created_at: datetime 71- 72- class Config: 73- from_attributes = True 74- 75- -- 91-class SubscriptionCreateResponse(BaseModel): 92- user_id: UUID 93- subscription_id: UUID 94- peer_id: UUID 95- job_id: UUID 96- node_id: str 97: tunnel_ip: str 98- paid_until: datetime 99- private_key: REDACTED 100- public_key: str 101- preshared_key: str 102- client_config: str 103- -- 120- auto_renew=payload.auto_renew, 121- paid_until=paid_until, 122- ) 123- db.add(subscription) 124- db.flush() 125- 126: tunnel_ip = next_tunnel_ip(db, payload.node_id) 127- private_key, public_key = generate_wg_keypair() 128- preshared_key = generate_wg_psk() 129: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 130- 131- peer = Peer( 132- user_id=user.id, 133- node_id=payload.node_id, 134- public_key=public_key, 135- preshared_key=preshared_key, 136: tunnel_ip=tunnel_ip, 137- paid_until=paid_until, 138: enabled=True, 139- ) 140- db.add(peer) 141- db.flush() 142- 143- job = ProvisioningJob( 144- node_id=payload.node_id, 145- action="enable_peer", 146- peer_id=peer.id, 147- payload_json={ 148- "public_key": peer.public_key, 149- "preshared_key": peer.preshared_key, 150: "tunnel_ip": peer.tunnel_ip, 151- "paid_until": peer.paid_until.isoformat(), 152- }, 153- status="pending", 154- attempts=0, 155- ) 156- db.add(job) -- 163- return SubscriptionCreateResponse( 164- user_id=user.id, 165- subscription_id=subscription.id, 166- peer_id=peer.id, 167- job_id=job.id, 168- node_id=peer.node_id, 169: tunnel_ip=peer.tunnel_ip, 170- paid_until=peer.paid_until, 171- private_key=REDACTED, 172- public_key=peer.public_key, 173- preshared_key=peer.preshared_key, 174- client_config=client_config, 175- ) -- 177- 178-class DisablePeerResponse(BaseModel): 179- peer_id: UUID 180- job_id: UUID 181- node_id: str 182- public_key: str 183: tunnel_ip: str 184: enabled: bool 185- 186- 187-@router.post("/peers/{peer_id}/disable", response_model=DisablePeerResponse) 188-def disable_peer(peer_id: UUID, db: Session = Depends(get_db)): 189- peer = db.get(Peer, peer_id) 190- if not peer: 191- from fastapi import HTTPException 192- raise HTTPException(status_code=404, detail="peer not found") 193- 194: peer.enabled = False 195- peer.disabled_at = datetime.now(timezone.utc) 196- 197- job = ProvisioningJob( 198- node_id=peer.node_id, 199- action="disable_peer", 200- peer_id=peer.id, 201- payload_json={ 202- "public_key": peer.public_key, 203: "tunnel_ip": peer.tunnel_ip, 204- }, 205- status="pending", 206- attempts=0, 207- ) 208- db.add(job) 209- db.commit() -- 212- 213- return DisablePeerResponse( 214- peer_id=peer.id, 215- job_id=job.id, 216- node_id=peer.node_id, 217- public_key=peer.public_key, 218: tunnel_ip=peer.tunnel_ip, 219: enabled=peer.enabled, 220- ) 221- 222- 223- 224-class JobResponse(BaseModel): 225- id: UUID ### FILE: /opt/wg-access/backups/step025a/admin.py.20260704-180655.bak 10- 11-from app.db.session import get_db 12-from app.models import InviteCode, User, Subscription, Peer, ProvisioningJob 13-from app.services.wireguard import ( 14- generate_wg_keypair, 15- generate_wg_psk, 16: next_tunnel_ip, 17- build_client_config, 18-) 19- 20-router = APIRouter(prefix="/admin", tags=["admin"]) 21- 22- -- 62- 63-class PeerResponse(BaseModel): 64- id: UUID 65- user_id: UUID 66- node_id: str 67- public_key: str 68: tunnel_ip: str 69- paid_until: datetime 70: enabled: bool 71- created_at: datetime 72- 73- class Config: 74- from_attributes = True 75- 76- -- 92-class SubscriptionCreateResponse(BaseModel): 93- user_id: UUID 94- subscription_id: UUID 95- peer_id: UUID 96- job_id: UUID 97- node_id: str 98: tunnel_ip: str 99- paid_until: datetime 100- private_key: REDACTED 101- public_key: str 102- preshared_key: str 103- client_config: str 104- -- 121- auto_renew=payload.auto_renew, 122- paid_until=paid_until, 123- ) 124- db.add(subscription) 125- db.flush() 126- 127: tunnel_ip = next_tunnel_ip(db, payload.node_id) 128- private_key, public_key = generate_wg_keypair() 129- preshared_key = generate_wg_psk() 130: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 131- 132- peer = Peer( 133- user_id=user.id, 134- node_id=payload.node_id, 135- public_key=public_key, 136- preshared_key=preshared_key, 137: tunnel_ip=tunnel_ip, 138- paid_until=paid_until, 139: enabled=True, 140- ) 141- db.add(peer) 142- db.flush() 143- 144- job = ProvisioningJob( 145- node_id=payload.node_id, 146- action="enable_peer", 147- peer_id=peer.id, 148- payload_json={ 149- "public_key": peer.public_key, 150- "preshared_key": peer.preshared_key, 151: "tunnel_ip": peer.tunnel_ip, 152- "paid_until": peer.paid_until.isoformat(), 153- }, 154- status="pending", 155- attempts=0, 156- ) 157- db.add(job) -- 164- return SubscriptionCreateResponse( 165- user_id=user.id, 166- subscription_id=subscription.id, 167- peer_id=peer.id, 168- job_id=job.id, 169- node_id=peer.node_id, 170: tunnel_ip=peer.tunnel_ip, 171- paid_until=peer.paid_until, 172- private_key=REDACTED, 173- public_key=peer.public_key, 174- preshared_key=peer.preshared_key, 175- client_config=client_config, 176- ) -- 178- 179-class DisablePeerResponse(BaseModel): 180- peer_id: UUID 181- job_id: UUID 182- node_id: str 183- public_key: str 184: tunnel_ip: str 185: enabled: bool 186- 187- 188-@router.post("/peers/{peer_id}/disable", response_model=DisablePeerResponse) 189-def disable_peer(peer_id: UUID, db: Session = Depends(get_db)): 190- peer = db.get(Peer, peer_id) 191- if not peer: 192- from fastapi import HTTPException 193- raise HTTPException(status_code=404, detail="peer not found") 194- 195: peer.enabled = False 196- peer.disabled_at = datetime.now(timezone.utc) 197- 198- job = ProvisioningJob( 199- node_id=peer.node_id, 200- action="disable_peer", 201- peer_id=peer.id, 202- payload_json={ 203- "public_key": peer.public_key, 204: "tunnel_ip": peer.tunnel_ip, 205- }, 206- status="pending", 207- attempts=0, 208- ) 209- db.add(job) 210- db.commit() -- 213- 214- return DisablePeerResponse( 215- peer_id=peer.id, 216- job_id=job.id, 217- node_id=peer.node_id, 218- public_key=peer.public_key, 219: tunnel_ip=peer.tunnel_ip, 220: enabled=peer.enabled, 221- ) 222- 223- 224- 225-class JobResponse(BaseModel): 226- id: UUID -- 257- 258-class AdminUserPeerResponse(BaseModel): 259- id: UUID 260- user_id: UUID 261- node_id: str 262- public_key: str 263: tunnel_ip: str 264- paid_until: datetime 265: enabled: bool 266- created_at: datetime 267- disabled_at: datetime | None 268- 269- class Config: 270- from_attributes = True 271- -- 287-class AdminUserSummaryResponse(BaseModel): 288- id: UUID 289- email: str | None 290- created_at: datetime 291- subscriptions_count: int 292- peers_count: int 293: enabled_peers_count: int 294- 295- class Config: 296- from_attributes = True 297- 298- 299-class AdminUserDetailResponse(BaseModel): -- 322- result.append(AdminUserSummaryResponse( 323- id=user.id, 324- email=user.email, 325- created_at=user.created_at, 326- subscriptions_count=len(subscriptions), 327- peers_count=len(peers), 328: enabled_peers_count=len([p for p in peers if p.enabled]), 329- )) 330- 331- return result 332- 333- 334-def build_admin_user_detail(user: User, db: Session) -> AdminUserDetailResponse: ### FILE: /opt/wg-access/backups/step026a/admin.py.20260704-182045.bak 10- 11-from app.db.session import get_db 12-from app.models import InviteCode, User, Subscription, Peer, ProvisioningJob 13-from app.services.wireguard import ( 14- generate_wg_keypair, 15- generate_wg_psk, 16: next_tunnel_ip, 17- build_client_config, 18-) 19- 20-router = APIRouter(prefix="/admin", tags=["admin"]) 21- 22- -- 62- 63-class PeerResponse(BaseModel): 64- id: UUID 65- user_id: UUID 66- node_id: str 67- public_key: str 68: tunnel_ip: str 69- paid_until: datetime 70: enabled: bool 71- created_at: datetime 72- 73- class Config: 74- from_attributes = True 75- 76- -- 92-class SubscriptionCreateResponse(BaseModel): 93- user_id: UUID 94- subscription_id: UUID 95- peer_id: UUID 96- job_id: UUID 97- node_id: str 98: tunnel_ip: str 99- paid_until: datetime 100- private_key: REDACTED 101- public_key: str 102- preshared_key: str 103- client_config: str 104- -- 121- auto_renew=payload.auto_renew, 122- paid_until=paid_until, 123- ) 124- db.add(subscription) 125- db.flush() 126- 127: tunnel_ip = next_tunnel_ip(db, payload.node_id) 128- private_key, public_key = generate_wg_keypair() 129- preshared_key = generate_wg_psk() 130: client_config = build_client_config(private_key, tunnel_ip, preshared_key) 131- 132- peer = Peer( 133- user_id=user.id, 134- node_id=payload.node_id, 135- public_key=public_key, 136- preshared_key=preshared_key, 137: tunnel_ip=tunnel_ip, 138- paid_until=paid_until, 139: enabled=True, 140- ) 141- db.add(peer) 142- db.flush() 143- 144- job = ProvisioningJob( 145- node_id=payload.node_id, 146- action="enable_peer", 147- peer_id=peer.id, 148- payload_json={ 149- "public_key": peer.public_key, 150- "preshared_key": peer.preshared_key, 151: "tunnel_ip": peer.tunnel_ip, 152- "paid_until": peer.paid_until.isoformat(), 153- }, 154- status="pending", 155- attempts=0, 156- ) 157- db.add(job) -- 164- return SubscriptionCreateResponse( 165- user_id=user.id, 166- subscription_id=subscription.id, 167- peer_id=peer.id, 168- job_id=job.id, 169- node_id=peer.node_id, 170: tunnel_ip=peer.tunnel_ip, 171- paid_until=peer.paid_until, 172- private_key=REDACTED, 173- public_key=peer.public_key, 174- preshared_key=peer.preshared_key, 175- client_config=client_config, 176- ) -- 192- if not subscription: 193- raise HTTPException(status_code=404, detail="subscription not found") 194- 195- now = datetime.now(timezone.utc) 196- 197- # Idempotent business-level cancel: 198: # cancel subscription once, then disable any currently enabled peers for this user. 199- subscription.status = "canceled" 200- subscription.auto_renew = False 201- if subscription.canceled_at is None: 202- subscription.canceled_at = now 203- 204: enabled_peers = db.execute( 205- select(Peer) 206- .where(Peer.user_id == subscription.user_id) 207: .where(Peer.enabled == True) # noqa: E712 208- .order_by(Peer.created_at.desc()) 209- ).scalars().all() 210- 211- jobs = [] 212: for peer in enabled_peers: 213: peer.enabled = False 214- peer.disabled_at = now 215- 216- job = ProvisioningJob( 217- node_id=peer.node_id, 218- action="disable_peer", 219- peer_id=peer.id, 220- payload_json={ 221- "public_key": peer.public_key, 222: "tunnel_ip": peer.tunnel_ip, 223- }, 224- status="pending", 225- attempts=0, 226- ) 227- db.add(job) 228- jobs.append(job) -- 234- 235- return CancelSubscriptionResponse( 236- subscription_id=subscription.id, 237- user_id=subscription.user_id, 238- status=subscription.status, 239- canceled_at=subscription.canceled_at, 240: disabled_peer_count=len(enabled_peers), 241- job_ids=[job.id for job in jobs], 242- ) 243- 244- 245-class DisablePeerResponse(BaseModel): 246- peer_id: UUID 247- job_id: UUID 248- node_id: str 249- public_key: str 250: tunnel_ip: str 251: enabled: bool 252- 253- 254-@router.post("/peers/{peer_id}/disable", response_model=DisablePeerResponse) 255-def disable_peer(peer_id: UUID, db: Session = Depends(get_db)): 256- peer = db.get(Peer, peer_id) 257- if not peer: 258- from fastapi import HTTPException 259- raise HTTPException(status_code=404, detail="peer not found") 260- 261: peer.enabled = False 262- peer.disabled_at = datetime.now(timezone.utc) 263- 264- job = ProvisioningJob( 265- node_id=peer.node_id, 266- action="disable_peer", 267- peer_id=peer.id, 268- payload_json={ 269- "public_key": peer.public_key, 270: "tunnel_ip": peer.tunnel_ip, 271- }, 272- status="pending", 273- attempts=0, 274- ) 275- db.add(job) 276- db.commit() -- 279- 280- return DisablePeerResponse( 281- peer_id=peer.id, 282- job_id=job.id, 283- node_id=peer.node_id, 284- public_key=peer.public_key, 285: tunnel_ip=peer.tunnel_ip, 286: enabled=peer.enabled, 287- ) 288- 289- 290- 291-class JobResponse(BaseModel): 292- id: UUID -- 323- 324-class AdminUserPeerResponse(BaseModel): 325- id: UUID 326- user_id: UUID 327- node_id: str 328- public_key: str 329: tunnel_ip: str 330- paid_until: datetime 331: enabled: bool 332- created_at: datetime 333- disabled_at: datetime | None 334- 335- class Config: 336- from_attributes = True 337- -- 353-class AdminUserSummaryResponse(BaseModel): 354- id: UUID 355- email: str | None 356- created_at: datetime 357- subscriptions_count: int 358- peers_count: int 359: enabled_peers_count: int 360- 361- class Config: 362- from_attributes = True 363- ### FILE: /opt/wg-access/tmp/mgts-smoke-20260706-183921/peers-before.json 1:[{"id":"0f808400-74ad-437b-a716-c6389aa3f057","user_id":"2cc7ce32-acc1-4258-92c6-bccd2febb7b6","node_id":"ddn-test","public_key":"Y9RXkNshIhljMAjSX9nyGZQLephRVV0TfIZfMhFn6R0=","tunnel_ip":"10.253.1.10","paid_until":"2026-07-03T10:37:18.258073Z","enabled":false,"created_at":"2026-07-05T10:37:15.949762Z"},{"id":"28aefe9e-25b0-434a-aedc-a4df15609e98","user_id":"a1957370-4921-4639-b4a9-6e6e05ab8c95","node_id":"ddn-test","public_key":"4U6GMvepAeuuXDaGryZQ9RSeomQfBCBbuKRdvNqbIQw=","tunnel_ip":"10.253.1.10","paid_until":"2026-07-03T10:35:48.458083Z","enabled":false,"created_at":"2026-07-05T10:35:47.463427Z"},{"id":"40b76a1d-e1ef-4a26-ac11-f39043a6a190","user_id":"07f03547-19ee-4068-ba4b-3ba504490337","node_id":"ddn-test","public_key":"7unzXkfaw3lD0tiSVT9UlMWesbD2OrzuT/CwLc10klE=","tunnel_ip":"10.253.1.10","paid_until":"2026-07-03T10:30:53.275935Z","enabled":false,"created_at":"2026-07-05T10:30:52.304156Z"},{"id":"f45a349d-25a7-426d-b88a-4479b28aec18","user_id":"7c1eb9f8-f220-43ae-840b-14f4cb362c1e","node_id":"ddn-test","public_key":"239TtHnxaJefv7kKJeNOVjW6YLchjRcTdctI3nJrAXE=","tunnel_ip":"10.253.1.10","paid_until":"2026-07-04T18:20:43.380875Z","enabled":false,"created_at":"2026-07-04T18:21:22.899633Z"},{"id":"3460b486-358c-44d6-8f47-c960d6254e3a","user_id":"1b42ec72-1f39-483d-ae1e-85669d3ecad2","node_id":"ddn-test","public_key":"dYuBNktWxl/PIqV49T9BIiHIvP4ZumswWZIEFD/Cwxg=","tunnel_ip":"10.253.1.10","paid_until":"2026-08-03T18:07:51.577444Z","enabled":false,"created_at":"2026-07-04T18:07:51.596325Z"},{"id":"486ee88e-2515-4203-8541-44f89cdaba89","user_id":"cd6b00a0-8cc5-48df-8413-8e645505db8c","node_id":"ddn-test","public_key":"WkjrGK7JKsYkhzFz5Bggunzlw5BNY+WxkcYp4D7IulA=","tunnel_ip":"10.253.1.10","paid_until":"2026-08-03T17:28:05.654224Z","enabled":false,"created_at":"2026-07-04T17:28:05.667811Z"},{"id":"da134a09-caa8-4209-bbfa-93dd06f310fb","user_id":"ead942f1-f354-496a-b2e6-857604fea5c3","node_id":"ddn-test","public_key":"OtlMSSUslhQufF939tNN3quNVY0Y+bqmfyohE1K6sBE=","tunnel_ip":"10.253.1.10","paid_until":"2026-09-02T17:22:24.372601Z","enabled":false,"created_at":"2026-07-04T17:22:24.373856Z"},{"id":"966f8262-b25c-4a13-ae1d-694b0378910a","user_id":"2096c7ba-76f1-4c39-8bb4-53d629e23387","node_id":"ddn-test","public_key":"lAQxl4ykPcSpXxFNfz+pGS5Pn+im/uXlVmc+fd3UPB8=","tunnel_ip":"10.253.1.10","paid_until":"2026-09-02T16:38:53.198957Z","enabled":false,"created_at":"2026-07-04T16:38:53.204965Z"},{"id":"00fde00e-a169-4397-841f-5cc99de475d5","user_id":"3303b0a8-450a-4154-9322-9ddfc5757208","node_id":"ddn-test","public_key":"H+yLpZTzxVq4o9RSD6b70hu6JTIY5WkH6Tx74qP1dwM=","tunnel_ip":"10.253.1.10","paid_until":"2026-09-02T16:32:01.766833Z","enabled":false,"created_at":"2026-07-04T16:32:01.782547Z"},{"id":"267835b6-84b6-4527-bd47-eee963e7a5c9","user_id":"6a7eeddb-d012-403c-bf31-ea29a82c1d13","node_id":"ddn-test","public_key":"gXUwVRUraYFaUDv1BJ6GdxbZi6IoliYNBUe8wYRNERU=","tunnel_ip":"10.253.1.10","paid_until":"2026-09-02T16:19:03.151534Z","enabled":false,"created_at":"2026-07-04T16:19:03.152686Z"},{"id":"6a5f1655-8ebe-435e-bba6-1af2412d40c2","user_id":"46a582e8-3822-4686-b9b9-8751061fde97","node_id":"ddn-test","public_key":"TuuRNB7urYUrpRq9gHqqMlqWfte0jSJooLXjZIIYKjs=","tunnel_ip":"10.253.1.10","paid_until":"2026-09-02T16:01:57.735658Z","enabled":false,"created_at":"2026-07-04T16:01:57.736841Z"},{"id":"9298ec04-26cf-44a7-8079-7a13664612fa","user_id":"9bc16072-f8e5-489f-8817-c6778237d6d0","node_id":"ddn-test","public_key":"CJMx+fPoiz5TUmUHKF7JcKo9IrxBZZMEDk7aS/Temwk=","tunnel_ip":"10.253.1.10","paid_until":"2026-09-02T15:56:30.168829Z","enabled":false,"created_at":"2026-07-04T15:56:30.178510Z"},{"id":"1b19ed83-1cad-4d57-be9f-fded5270213c","user_id":"2109fe84-fde0-4856-8268-030a43112782","node_id":"ddn-test","public_key":"mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg=","tunnel_ip":"10.253.1.16","paid_until":"2026-09-02T15:26:17.089248Z","enabled":false,"created_at":"2026-07-04T15:26:17.090574Z"},{"id":"13543224-39f7-4cfc-917d-66a16e2e0e55","user_id":"d360d32e-c7db-4688-846b-ecef626f12db","node_id":"ddn-test","public_key":"cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik=","tunnel_ip":"10.253.1.15","paid_until":"2026-09-02T15:17:46.085909Z","enabled":false,"created_at":"2026-07-04T15:17:46.096446Z"},{"id":"813515db-bf25-46c9-bc83-c9c45d51ba68","user_id":"ade5215e-4229-4e83-9012-f09f4691b4e6","node_id":"ddn-test","public_key":"TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM=","tunnel_ip":"10.253.1.14","paid_until":"2026-09-02T13:28:02.954738Z","enabled":false,"created_at":"2026-07-04T13:28:02.964577Z"},{"id":"754b76bd-0e69-4184-ac0b-a9c5706dbf19","user_id":"8c8e9a3f-740d-4f24-baa1-801904b4b50e","node_id":"ddn-test","public_key":"pub_cdnODJUM3pZBIS_Z3dOyBsHVi-Jq-4j_WiDuve84qDw","tunnel_ip":"10.253.1.13","paid_until":"2026-08-03T10:41:13.816901Z","enabled":false,"created_at":"2026-07-04T10:41:13.818018Z"},{"id":"4b39aa87-4b59-4c71-950b-cf2dca3e606f","user_id":"1f0b0e4a-e3ea-4d11-89d1-18d0db370ba7","node_id":"ddn-test","public_key":"pub_BFUhgsp-ejx-tAwttmNzzPupN-c0-5sSS3YRaevqpes","tunnel_ip":"10.253.1.12","paid_until":"2026-08-03T10:37:34.525170Z","enabled":false,"created_at":"2026-07-04T10:37:34.530114Z"},{"id":"37361fae-0ad5-4a7a-8096-a59e1dde2d78","user_id":"abe41583-990e-4d08-9572-a586acaf816f","node_id":"ddn-test","public_key":"pub_X1c2s8NhHk-msYA4Pe25gvmnZmhSRSsmrVob-exBUaw","tunnel_ip":"10.253.1.11","paid_until":"2026-09-02T10:28:54.187046Z","enabled":false,"created_at":"2026-07-04T10:28:54.188090Z"},{"id":"1c152b56-8f35-4bee-bdd7-154ff551077e","user_id":"10de9cee-ffc9-4065-96fe-401ec593e250","node_id":"ddn-test","public_key":"pub_2aWI36SAPwbgiUQLZCVeTmoOAPSd2gjMX_uVNgYN1ag","tunnel_ip":"10.253.1.10","paid_until":"2026-09-02T10:28:02.539201Z","enabled":false,"created_at":"2026-07-04T10:28:02.545211Z"}] ### FILE: /opt/wg-access/tmp/mgts-smoke-20260706-184120/allowed-ips-after-enable.txt 1:zUUEdc+Lac9C6fmMDVosuO3ZIYHIUDWi34UVgdjhbCU= 10.253.1.10/32 ### FILE: /opt/wg-access/tmp/mgts-smoke-20260706-184120/create.json 1:{"user_id":"13783bf9-94bb-4c90-b1c6-685a956a5c43","subscription_id":"8adb06a9-713a-46b0-8759-d11c28a797fc","peer_id":"3d03a7a2-c009-48a4-b6ff-214326d26fc0","job_id":"814ba665-f6e5-4ba4-b1ba-8af1ec4954e6","node_id":"ddn-test","tunnel_ip":"10.253.1.10","paid_until":"2026-08-05T18:41:20.288079Z","private_key":"REDACTED","public_key":"zUUEdc+Lac9C6fmMDVosuO3ZIYHIUDWi34UVgdjhbCU=","preshared_key":"jDmX4j1ipVr4r45cRb4nbKaeBqQYQeeaCpsBG4U2sMQ=","client_config":"[Interface]\nPrivateKey = REDACTED --- selector/job/source snippets, sanitized --- ### FILE: /usr/local/sbin/wg-access-agent-selector-apply.py exists_rc=0 4-import json 5-import sys 6-from pathlib import Path 7- 8-EXPECTED = { 9: "vpn1": {"selector_class": "cs4", "selector_label": "canary_vpn1", "fwmark": "0x201", "table_id": 201, "interface_name": "vpn1"}, 10: "vpn2": {"selector_class": "cs5", "selector_label": "canary_vpn2", "fwmark": "0x202", "table_id": 202, "interface_name": "vpn2"}, 11: "vpn3": {"selector_class": "cs1", "selector_label": "canary_vpn3", "fwmark": "0x203", "table_id": 203, "interface_name": "vpn3"}, 12: "vpn4": {"selector_class": "cs2", "selector_label": "canary_vpn4", "fwmark": "0x204", "table_id": 204, "interface_name": "vpn4"}, 13: "vpn5": {"selector_class": "cs3", "selector_label": "canary_vpn5", "fwmark": "0x205", "table_id": 205, "interface_name": "vpn5"}, 14-} 15- 16-def load_json(path): 17- return json.loads(Path(path).read_text(encoding="utf-8", errors="replace")) 18- -- 30- entries.append({ 31- "line_no": line_no, 32- "raw": line, 33- "valid": True, 34- "tunnel_ip": parts[0], 35: "selector_class": parts[1], 36- "selector_label": parts[2], 37- }) 38- return entries 39- 40-def load_slots(status_json): 41- data = load_json(status_json) 42: return {slot.get("slot_code"): slot for slot in data.get("slots", [])} 43- 44:def validate_and_plan(payload, selector_entries, slots, require_peer_visible=False, peer_visible=False): 45- errors = [] 46- warnings = [] 47- 48: tunnel_ip = str(payload.get("tunnel_ip", "")) 49: slot_code = str(payload.get("slot_code", "")) 50: selector_class = str(payload.get("selector_class", "")) 51: selector_label = str(payload.get("selector_label", "")) 52: fwmark = str(payload.get("fwmark", "")) 53: table_id = payload.get("table_id") 54: interface_name = str(payload.get("interface_name", "")) 55- 56- try: 57- ipaddress.ip_address(tunnel_ip) 58- except Exception: 59- errors.append("invalid_tunnel_ip") 60- 61: expected = EXPECTED.get(slot_code) 62- if not expected: 63: errors.append("unknown_slot_code") 64- else: 65: for key in ("selector_class", "selector_label", "fwmark", "table_id", "interface_name"): 66: if payload.get(key) != expected[key]: 67- errors.append(f"mapping_mismatch_{key}") 68- 69: slot = slots.get(slot_code) 70- if not slot: 71- errors.append("slot_status_missing") 72- else: 73- if slot.get("status") != "healthy": 74- errors.append("slot_not_healthy") 75: for key in ("selector_class", "fwmark", "table_id", "interface_name"): 76: if slot.get(key) != payload.get(key): 77- errors.append(f"slot_status_mismatch_{key}") 78- 79- if require_peer_visible and not peer_visible: 80- errors.append("wg_peer_not_visible") 81- 82: exact_line = f"{tunnel_ip} {selector_class} {selector_label}" 83- 84- valid_entries = [entry for entry in selector_entries if entry.get("valid")] 85- exact_present = any(entry["raw"] == exact_line for entry in valid_entries) 86- 87- same_ip_conflicts = [ 88- entry for entry in valid_entries 89- if entry.get("tunnel_ip") == tunnel_ip and entry["raw"] != exact_line 90- ] 91- same_class_conflicts = [ 92- entry for entry in valid_entries 93: if entry.get("selector_class") == selector_class and entry["raw"] != exact_line 94- ] 95- same_label_conflicts = [ 96- entry for entry in valid_entries 97- if entry.get("selector_label") == selector_label and entry["raw"] != exact_line 98- ] 99- 100- if same_ip_conflicts: 101- errors.append("same_ip_conflict") 102- if same_class_conflicts: 103: errors.append("same_selector_class_conflict") 104- if same_label_conflicts: 105- errors.append("same_selector_label_conflict") 106- 107- if errors: 108- action = "error" -- 111- else: 112- action = "append" 113- 114- return { 115- "schema": "wg-access-agent-selector-apply-plan-v1", 116: "contract_valid": not errors, 117- "errors": errors, 118- "warnings": warnings, 119- "action": action, 120- "exact_line": exact_line, 121- "selector_entry_count_before": len(valid_entries), 122- "selector_entry_count_after_planned": len(valid_entries) + (1 if action == "append" else 0), 123- "exact_present": exact_present, 124- "would_append": action == "append", 125- "idempotent_noop": action == "noop", 126- "same_ip_conflict_count": len(same_ip_conflicts), 127: "same_selector_class_conflict_count": len(same_class_conflicts), 128- "same_selector_label_conflict_count": len(same_label_conflicts), 129- "slot_status": slot.get("status") if slot else "", 130: "payload_summary": { 131: "peer_id": payload.get("peer_id", ""), 132: "subscription_id": payload.get("subscription_id", ""), 133- "tunnel_ip": tunnel_ip, 134: "slot_code": slot_code, 135: "selector_class": selector_class, 136- "selector_label": selector_label, 137: "fwmark": fwmark, 138: "table_id": table_id, 139: "interface_name": interface_name, 140: "provider_type": payload.get("provider_type", ""), 141: "manager_id": payload.get("manager_id", ""), 142- }, 143- } 144- 145-def main(): 146- parser = argparse.ArgumentParser() 147: parser.add_argument("--payload", required=True) 148- parser.add_argument("--selector-file", required=True) 149- parser.add_argument("--slot-status-json", required=True) 150: parser.add_argument("--mode", choices=["dry-run", "apply-local"], default="dry-run") 151- parser.add_argument("--output-json", action="store_true") 152- args = parser.parse_args() 153- 154: payload = load_json(args.payload) 155- selector_entries = parse_selector(Path(args.selector_file)) 156- slots = load_slots(Path(args.slot_status_json)) 157- 158: plan = validate_and_plan(payload, selector_entries, slots) 159- 160- plan["mode"] = args.mode 161- plan["apply_performed"] = False 162- 163- if args.mode == "apply-local": 164: if not plan["contract_valid"]: 165- pass 166- elif plan["action"] == "append": 167- selector_path = Path(args.selector_file) 168- text = selector_path.read_text(encoding="utf-8", errors="replace") 169- if text and not text.endswith("\n"): -- 180- for key, value in plan.items(): 181- if isinstance(value, (dict, list)): 182- continue 183- print(f"{key}={value}") 184- 185: return 0 if plan["contract_valid"] else 2 186- 187-if __name__ == "__main__": 188- raise SystemExit(main()) ### FILE: /usr/local/sbin/wg-access-agent-selector-remote-plan.py exists_rc=0 10- return json.loads(Path(path).read_text(encoding="utf-8", errors="replace")) 11- 12-def q(value): 13- return shlex.quote(str(value)) 14- 15:def build_remote_script(payload, selector_plan, selector_conf, selector_service): 16- exact_line = selector_plan.get("exact_line", "") 17- action = selector_plan.get("action", "") 18: tunnel_ip = str(payload.get("tunnel_ip", "")) 19: selector_class = str(payload.get("selector_class", "")) 20- 21- if action == "append": 22- apply_block = """ 23-if grep -Fxq "$LINE" "$CONF"; then 24- echo "selector_apply_action=noop_already_present" -- 43- 44-CONF={q(selector_conf)} 45-SERVICE={q(selector_service)} 46-LINE={q(exact_line)} 47-TUNNEL_IP={q(tunnel_ip)} 48:SELECTOR_CLASS={q(selector_class)} 49-BACKUP="/root/peers.conf.before-agent-selector-apply-$(date +%Y%m%d-%H%M%S)" 50-ROLLBACK="/root/rollback-agent-selector-apply.sh" 51- 52:echo "remote_script_schema=wg-access-agent-selector-remote-script-v3" 53:echo "remote_script_action={action}" 54- 55-test -f "$CONF" 56- 57-cp "$CONF" "$BACKUP" 58-echo "selector_backup_path=$BACKUP" -- 80-""" 81- return script 82- 83-def main(): 84- parser = argparse.ArgumentParser() 85: parser.add_argument("--payload", required=True) 86- parser.add_argument("--selector-file", required=True) 87- parser.add_argument("--slot-status-json", required=True) 88- parser.add_argument("--selector-helper", default="/usr/local/sbin/wg-access-agent-selector-apply.py") 89- parser.add_argument("--target-host", default="10.71.100.1") 90- parser.add_argument("--target-name", default="vm100") 91- parser.add_argument("--selector-conf", default="/etc/router-wgpay-selector.d/peers.conf") 92- parser.add_argument("--selector-service", default="router-wgpay-canary") 93: parser.add_argument("--mode", choices=["remote-dry-run"], default="remote-dry-run") 94- parser.add_argument("--output-json", action="store_true") 95- args = parser.parse_args() 96- 97: payload = load_json(args.payload) 98- 99- helper_cmd = [ 100- args.selector_helper, 101: "--payload", args.payload, 102- "--selector-file", args.selector_file, 103- "--slot-status-json", args.slot_status_json, 104: "--mode", "dry-run", 105- "--output-json", 106- ] 107- 108- proc = subprocess.run(helper_cmd, text=True, capture_output=True) 109- 110- try: 111- selector_plan = json.loads(proc.stdout) 112- except Exception as e: 113- selector_plan = { 114- "schema": "parse-error", 115: "contract_valid": False, 116- "errors": ["selector_plan_json_parse_error:" + type(e).__name__], 117- } 118- 119: contract_valid = bool(selector_plan.get("contract_valid", False)) 120- action = selector_plan.get("action", "") 121: remote_script = build_remote_script(payload, selector_plan, args.selector_conf, args.selector_service) 122- 123- result = { 124- "schema": "wg-access-agent-selector-remote-plan-v3", 125- "mode": args.mode, 126: "remote_script_model": "single_shell_script", 127- "target": { 128- "name": args.target_name, 129- "host": args.target_host, 130- "selector_conf": args.selector_conf, 131- "selector_service": args.selector_service 132- }, 133: "payload_summary": { 134: "tunnel_ip": payload.get("tunnel_ip", ""), 135: "slot_code": payload.get("slot_code", ""), 136: "selector_class": payload.get("selector_class", ""), 137: "selector_label": payload.get("selector_label", ""), 138: "fwmark": payload.get("fwmark", ""), 139: "table_id": payload.get("table_id", ""), 140: "interface_name": payload.get("interface_name", ""), 141: "provider_type": payload.get("provider_type", ""), 142: "manager_id": payload.get("manager_id", "") 143- }, 144- "selector_plan_rc": proc.returncode, 145- "selector_plan": selector_plan, 146: "contract_valid": contract_valid, 147- "action": action, 148- "would_append": bool(selector_plan.get("would_append", False)), 149- "idempotent_noop": bool(selector_plan.get("idempotent_noop", False)), 150- "exact_line": selector_plan.get("exact_line", ""), 151- "requires_wg_peer_before_real_apply": True, 152- "remote_commands_executed": False, 153- "apply_performed": False, 154: "remote_script_executed": False, 155: "remote_script_sha256": hashlib.sha256(remote_script.encode("utf-8")).hexdigest(), 156: "remote_script_line_count": len(remote_script.splitlines()), 157: "remote_script_contains_runtime_backup_date": "$(date +%Y%m%d-%H%M%S)" in remote_script, 158: "remote_script_contains_actual_backup_rollback": 'cp "$BACKUP" "$CONF"' in remote_script, 159: "remote_script_contains_placeholder_backup": "YYYYmmdd-HHMMSS" in remote_script, 160: "remote_script_contains_bad_etc_initd_quoted_service": '/etc/init.d "$SERVICE"' in remote_script, 161: "remote_script_contains_clean_service_start": "/etc/init.d/$SERVICE start" in remote_script, 162: "remote_script": remote_script, 163- "errors": selector_plan.get("errors", []), 164- "error_count": len(selector_plan.get("errors", [])), 165- } 166- 167- if args.output_json: 168- print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) 169- else: 170: print("contract_valid=" + ("YES" if contract_valid else "NO")) 171- print("action=" + str(action)) 172: print("remote_script_executed=NO") 173- 174: return 0 if contract_valid else 2 175- 176-if __name__ == "__main__": 177- raise SystemExit(main()) ### FILE: /usr/local/sbin/wg-access-agent-job-selector-plan.py exists_rc=0 7- 8-def load_json(path): 9- return json.loads(Path(path).read_text(encoding="utf-8", errors="replace")) 10- 11-def flatten_job(job): 12: payload = job.get("payload", {}) 13: peer = payload.get("peer", {}) 14: selector = payload.get("selector", {}) 15: provider = payload.get("provider", {}) 16- 17- return { 18- "job_type": job.get("job_type", ""), 19- "dry_run": True, 20: "peer_id": peer.get("peer_id", payload.get("peer_id", "")), 21: "subscription_id": peer.get("subscription_id", payload.get("subscription_id", "")), 22: "tunnel_ip": peer.get("tunnel_ip", payload.get("tunnel_ip", "")), 23: "slot_code": selector.get("slot_code", payload.get("slot_code", "")), 24: "selector_class": selector.get("selector_class", payload.get("selector_class", "")), 25: "selector_label": selector.get("selector_label", payload.get("selector_label", "")), 26: "fwmark": selector.get("fwmark", payload.get("fwmark", "")), 27: "table_id": selector.get("table_id", payload.get("table_id", "")), 28: "interface_name": selector.get("interface_name", payload.get("interface_name", "")), 29: "provider_type": provider.get("provider_type", payload.get("provider_type", "")), 30: "manager_id": provider.get("manager_id", payload.get("manager_id", "")), 31- } 32- 33-def main(): 34- parser = argparse.ArgumentParser() 35- parser.add_argument("--job", required=True) 36- parser.add_argument("--selector-file", required=True) 37- parser.add_argument("--slot-status-json", required=True) 38- parser.add_argument("--selector-helper", default="/usr/local/sbin/wg-access-agent-selector-apply.py") 39: parser.add_argument("--mode", choices=["dry-run"], default="dry-run") 40- parser.add_argument("--output-json", action="store_true") 41- args = parser.parse_args() 42- 43- job = load_json(args.job) 44- flat = flatten_job(job) -- 50- "schema": job.get("schema", ""), 51- "job_id": job.get("job_id", ""), 52- "job_type": job.get("job_type", ""), 53- "dry_run": bool(job.get("dry_run", False)), 54- }, 55: "flattened_payload": flat, 56- "selector_helper": args.selector_helper, 57- "selector_plan_rc": None, 58- "selector_plan": None, 59- "apply_performed": False, 60- } 61- 62- with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as tmp: 63- json.dump(flat, tmp, ensure_ascii=False, indent=2, sort_keys=True) 64: tmp_payload = tmp.name 65- 66- cmd = [ 67- args.selector_helper, 68: "--payload", tmp_payload, 69- "--selector-file", args.selector_file, 70- "--slot-status-json", args.slot_status_json, 71: "--mode", "dry-run", 72- "--output-json", 73- ] 74- 75- proc = subprocess.run(cmd, text=True, capture_output=True) 76- result["selector_plan_rc"] = proc.returncode -- 80- try: 81- plan = json.loads(proc.stdout) 82- except Exception as e: 83- plan = { 84- "schema": "parse-error", 85: "contract_valid": False, 86- "errors": ["selector_plan_json_parse_error:" + type(e).__name__], 87- } 88- 89- result["selector_plan"] = plan 90: result["contract_valid"] = bool(plan.get("contract_valid", False)) 91- result["action"] = plan.get("action", "") 92- result["would_append"] = bool(plan.get("would_append", False)) 93- result["idempotent_noop"] = bool(plan.get("idempotent_noop", False)) 94- result["selector_entry_count_before"] = plan.get("selector_entry_count_before") 95- result["selector_entry_count_after_planned"] = plan.get("selector_entry_count_after_planned") -- 98- result["apply_performed"] = bool(plan.get("apply_performed", False)) 99- 100- if args.output_json: 101- print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) 102- else: 103: print("contract_valid=" + ("YES" if result["contract_valid"] else "NO")) 104- print("action=" + str(result["action"])) 105- print("selector_plan_rc=" + str(result["selector_plan_rc"])) 106- 107: return 0 if result["contract_valid"] else 2 108- 109-if __name__ == "__main__": 110- raise SystemExit(main()) --- backend OpenAPI paths/mentions --- openapi_rc=0 path=/admin/jobs path=/admin/maintenance/expire-subscriptions path=/admin/peers path=/admin/peers/{peer_id}/disable path=/admin/subscriptions path=/admin/subscriptions/{subscription_id}/cancel path=/agent/jobs path=/agent/jobs/{job_id}/complete path=/agent/jobs/{job_id}/fail path=/agent/jobs/{job_id}/start path=/agent/peers path=/dev/create-test-peer mentions_next_tunnel_ip=false mentions_tunnel_ip=true mentions_selector=false mentions_slot=false mentions_fwmark=false mentions_table_id=false mentions_allocator=false mentions_reuse=false mentions_create_peer=false mentions_disable_peer=true