"""Complete the tunnel setup in one pass after cert.pem is in place. Steps: 1. Stop cf-login container 2. Create tunnel 'acg-origin', capture UUID 3. Write config.yml 4. Flip DNS: A (proxied, 72.194.62.5) -> CNAME (proxied, .cfargotunnel.com) for 4 hostnames 5. Start persistent container 'cloudflared' 6. Wait for 4 tunnel connections to register 7. Verify site returns 200 externally """ import json, os, re, socket, subprocess, time, urllib.request import paramiko HOST, USER = "172.16.3.20", "root" import subprocess as _sp, yaml as _y PWD = _y.safe_load(_sp.run(["sops","-d","D:/vault/infrastructure/jupiter-unraid-primary.sops.yaml"],capture_output=True,text=True,timeout=30,check=True).stdout)["credentials"]["password"] APPDATA = '/mnt/cache/appdata/cloudflared' import os as _os CF_TOKEN = _os.environ.get('CF_API_TOKEN_FULL_DNS', '') if not CF_TOKEN: raise SystemExit('[FAIL] set CF_API_TOKEN_FULL_DNS env var (token lives in 1Password)') ZONE = '1beb9917c22b54be32e5215df2c227ce' HOSTNAMES = ['azcomputerguru.com','analytics.azcomputerguru.com','community.azcomputerguru.com','radio.azcomputerguru.com'] ORIGIN = 'http://172.16.3.10:80' socket.setdefaulttimeout(60) c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(HOST, username=USER, password=PWD, timeout=30, look_for_keys=False, allow_agent=False) def run(cmd, to=120): _, o, e = c.exec_command(cmd, timeout=to) out = o.read().decode('utf-8','replace') err = e.read().decode('utf-8','replace') rc = o.channel.recv_exit_status() return out, err, rc 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)}]} try: print('=== [1] stop cf-login ===', flush=True) out, _, _ = run('docker rm -f cf-login 2>&1') print(out.rstrip()) print('\n=== [2] create tunnel acg-origin ===', flush=True) CREATE = ( f'docker run --rm ' f'-v {APPDATA}:/home/nonroot/.cloudflared ' f'cloudflare/cloudflared:latest tunnel create acg-origin' ) out, err, rc = run(CREATE) print(out.rstrip()) if err.strip(): print(f'[stderr] {err.rstrip()}') m = re.search(r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', out) if not m: raise SystemExit(f'[FAIL] no UUID in output; rc={rc}') UUID = m.group(1) print(f'[OK] tunnel UUID: {UUID}') print('\n=== [3] write config.yml ===', flush=True) config = f'''tunnel: {UUID} credentials-file: /home/nonroot/.cloudflared/{UUID}.json ingress: ''' for h in HOSTNAMES: config += f' - hostname: {h}\n service: {ORIGIN}\n' config += ' - service: http_status:404\n' # Write via heredoc HERE = "'EOF_CONFIG'" out, err, rc = run(f"cat > {APPDATA}/config.yml <<{HERE}\n{config}\nEOF_CONFIG") run(f'chown 65532:65532 {APPDATA}/config.yml') out, _, _ = run(f'cat {APPDATA}/config.yml') print(out.rstrip()) print('\n=== [4] DNS cutover (A -> CNAME) ===', flush=True) tunnel_target = f'{UUID}.cfargotunnel.com' for h in HOSTNAMES: # Find existing record r = cfapi('GET', f'/zones/{ZONE}/dns_records?name={h}') if not r.get('success') or not r['result']: print(f' [SKIP] {h}: no record found') continue rec = r['result'][0] print(f' [{h}] current: type={rec["type"]} content={rec["content"]} proxied={rec["proxied"]} id={rec["id"]}') if rec['type']=='CNAME' and rec['content']==tunnel_target: print(f' already pointing at tunnel, skipping') continue # Delete d = cfapi('DELETE', f'/zones/{ZONE}/dns_records/{rec["id"]}') if not d.get('success'): print(f' [FAIL delete] {d.get("errors")}') continue # Create CNAME body = {'type':'CNAME','name':h,'content':tunnel_target,'proxied':True,'ttl':1} cr = cfapi('POST', f'/zones/{ZONE}/dns_records', body) if cr.get('success'): print(f' [OK] -> CNAME {tunnel_target} proxied') else: print(f' [FAIL create] {cr.get("errors")}') print('\n=== [5] start persistent cloudflared ===', flush=True) run('docker rm -f cloudflared 2>&1') START = ( 'docker run -d --name cloudflared --restart=unless-stopped ' f'-v {APPDATA}:/home/nonroot/.cloudflared ' 'cloudflare/cloudflared:latest ' 'tunnel --config /home/nonroot/.cloudflared/config.yml run' ) out, err, rc = run(START) print(out.rstrip()) if err.strip(): print(f'[stderr] {err.rstrip()}') print('\n=== [6] wait for tunnel connections ===', flush=True) for i in range(20): time.sleep(3) out, _, _ = run('docker logs cloudflared 2>&1 | tail -30') conns = out.count('Registered tunnel connection') print(f' [try {i+1}] connections registered: {conns}') if conns >= 4: print(out.rstrip()[-800:]) break print('\n=== [7] verify externally ===', flush=True) finally: c.close() # Run external curl from this workstation print('\n[EXTERNAL CHECK]', flush=True) for h in HOSTNAMES: 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}: HTTP {r.status}') except urllib.error.HTTPError as e: print(f' {h}: HTTP {e.code}') except Exception as e: print(f' {h}: ERR {e}') print(f'\n[DONE] tunnel UUID: {UUID}') print(f'[DONE] config: {APPDATA}/config.yml') print(f'[DONE] persistent container: cloudflared')