Files
claudetools/clients/internal-infrastructure/scripts/cloudflared-tunnel-setup/revert_broken.py
Mike Swanson 9ab36352ae Session log: Tunnel expansion + WHM fix (ix. grey-cloud)
Audited all 25 proxied zone records and expanded tunnel ingress to cover
9 hostnames total (azcomputerguru + analytics + community + radio +
git + plexrequest + rmm + rmm-api + sync). All verified HTTP 200.

Reverted 3 hostnames to original A records after discovering they
require backend work, not tunnel changes:
- plex/rustdesk: NPM on Jupiter has no vhost for these (returned
  'tls: unrecognized name' when tunneled)
- secure: Jupiter can't route to its backend subnet 172.16.1.0/24

Reverted ix.azcomputerguru.com to DNS-only A record after user
reported :2087 WHM access broken. Cloudflare Tunnel is hostname-bound,
not port-bound, so non-standard admin ports can't pass through. Direct
NAT to 72.194.62.5 restored WHM/cPanel access.

Adds four new helper scripts under clients/internal-infrastructure/
scripts/cloudflared-tunnel-setup/ (audit_proxied, discover_backends,
expand_tunnel, revert_broken). All use SOPS vault / env var for creds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:59:49 -07:00

124 lines
5.2 KiB
Python

"""Revert the 3 hostnames that have no functional backend:
- plex (NPM has no vhost)
- rustdesk (NPM has no vhost)
- secure (Jupiter can't route to 172.16.1.16)
Removes them from tunnel ingress and restores their original A records.
"""
import json, os, subprocess, urllib.error, urllib.request, time
import paramiko, yaml
ZONE = '1beb9917c22b54be32e5215df2c227ce'
CF_TOKEN = os.environ.get('CF_API_TOKEN_FULL_DNS','')
if not CF_TOKEN: raise SystemExit('set CF_API_TOKEN_FULL_DNS')
REVERT = {
# hostname: original A content
'plex.azcomputerguru.com': '72.194.62.4',
'rustdesk.azcomputerguru.com': '72.194.62.10',
'secure.azcomputerguru.com': '72.194.62.2',
}
def cfapi(method, path, body=None):
req = urllib.request.Request(
f'https://api.cloudflare.com/client/v4{path}',
data=json.dumps(body).encode() if body else None,
method=method,
headers={'Authorization': f'Bearer {CF_TOKEN}','Content-Type':'application/json'},
)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
except urllib.error.HTTPError as e:
try: return json.loads(e.read())
except: return {'success':False,'errors':[{'message':str(e)}]}
def _pwd(v): return yaml.safe_load(subprocess.run(['sops','-d',v],capture_output=True,text=True,timeout=30,check=True).stdout)['credentials']['password']
j = paramiko.SSHClient(); j.set_missing_host_key_policy(paramiko.AutoAddPolicy())
j.connect('172.16.3.20', username='root', password=_pwd('D:/vault/infrastructure/jupiter-unraid-primary.sops.yaml'),
timeout=30, look_for_keys=False, allow_agent=False)
def jrun(cmd, to=60):
_, o, _ = j.exec_command(cmd, timeout=to)
return o.read().decode()
try:
print('=== [1] rewrite config.yml without the 3 broken hosts ===')
APPDATA = '/mnt/cache/appdata/cloudflared'
# Read UUID
UUID = jrun(f'grep "^tunnel:" {APPDATA}/config.yml').split(':',1)[1].strip()
IX = 'https://172.16.3.10:443'
JNPM = 'https://172.16.3.20:18443'
KEEP = [
('azcomputerguru.com', IX),
('analytics.azcomputerguru.com', IX),
('community.azcomputerguru.com', IX),
('radio.azcomputerguru.com', IX),
('ix.azcomputerguru.com', IX),
('git.azcomputerguru.com', JNPM),
('plexrequest.azcomputerguru.com', JNPM),
('rmm.azcomputerguru.com', JNPM),
('rmm-api.azcomputerguru.com', JNPM),
('sync.azcomputerguru.com', JNPM),
]
config = f'tunnel: {UUID}\ncredentials-file: /home/nonroot/.cloudflared/{UUID}.json\ningress:\n'
for h, svc in KEEP:
config += f' - hostname: {h}\n service: {svc}\n originRequest:\n originServerName: {h}\n noTLSVerify: true\n'
config += ' - service: http_status:404\n'
jrun(f'cp {APPDATA}/config.yml {APPDATA}/config.yml.bak-$(date +%Y%m%d-%H%M%S)')
HD = "'EOF_CFG'"
jrun(f"cat > {APPDATA}/config.yml <<{HD}\n{config}\nEOF_CFG")
jrun(f'chown 65532:65532 {APPDATA}/config.yml')
print(f' 10 ingress hostnames kept (plex/rustdesk/secure removed)')
print('\n=== [2] revert DNS for 3 hosts ===')
for host, orig_ip in REVERT.items():
r = cfapi('GET', f'/zones/{ZONE}/dns_records?name={host}')
if not r.get('success') or not r['result']:
print(f' [{host}] no record, skipping'); continue
rec = r['result'][0]
print(f' [{host}] current: type={rec["type"]} content={rec["content"]}')
d = cfapi('DELETE', f'/zones/{ZONE}/dns_records/{rec["id"]}')
if not d.get('success'):
print(f' [FAIL delete] {d.get("errors")}'); continue
body = {'type':'A','name':host,'content':orig_ip,'proxied':True,'ttl':1}
cr = cfapi('POST', f'/zones/{ZONE}/dns_records', body)
if cr.get('success'):
print(f' [OK] restored A {orig_ip} proxied')
else:
print(f' [FAIL create] {cr.get("errors")}')
print('\n=== [3] restart cloudflared ===')
print(jrun('docker restart cloudflared').rstrip())
print('\n=== [4] wait for reconnect ===')
for i in range(20):
time.sleep(3)
logs = jrun('docker logs cloudflared 2>&1 | tail -30')
conns = logs.count('Registered tunnel connection')
if conns >= 4:
print(f' [try {i+1}] {conns} connections')
break
finally:
j.close()
print('\n=== [5] external probe all 10 tunneled hostnames ===')
import urllib.request
for h in [k[0] for k in [
('azcomputerguru.com',),('analytics.azcomputerguru.com',),('community.azcomputerguru.com',),
('radio.azcomputerguru.com',),('ix.azcomputerguru.com',),('git.azcomputerguru.com',),
('plexrequest.azcomputerguru.com',),('rmm.azcomputerguru.com',),('rmm-api.azcomputerguru.com',),
('sync.azcomputerguru.com',),
]]:
try:
req = urllib.request.Request(f'https://{h}/', method='HEAD',
headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0'})
with urllib.request.urlopen(req, timeout=15) as r:
print(f' {h:42} HTTP {r.status} {r.headers.get("Server","-")}')
except urllib.error.HTTPError as e:
print(f' {h:42} HTTP {e.code}')
except Exception as e:
print(f' {h:42} ERR {str(e)[:40]}')