Files
claudetools/clients/internal-infrastructure/scripts/cloudflared-tunnel-setup/cf_analytics.py
Mike Swanson a78fb96f95 Session log: Cloudflare Tunnel for azcomputerguru + Cox BGP diagnosis
Diagnosed azcomputerguru.com 521 errors: Cox's BGP route to specific
Cloudflare origin-pull prefixes (162.158.0.0/16, 172.64.0.0/13,
173.245.48.0/20, 141.101.64.0/18) is broken from 72.194.62.0/29.
Confirmed by TCP probe matrix from pfSense WAN, traceroute latency
comparison, and state-table showing 0 inbound CF connections while
direct-internet traffic still reached origin.

Deployed Cloudflare Tunnel 'acg-origin' on Jupiter Unraid as a
Docker container. Routes 4 proxied hostnames (azcomputerguru.com,
analytics., community., radio.) through the tunnel with HTTPS
backend to IX 172.16.3.10:443 with per-ingress SNI matching. All
4 hostnames return 200 OK through CF edge after the cutover.

Repo hygiene:
- Merged clients/ix-server/ into clients/internal-infrastructure/
  (IX is internal infra, not a paying-client account). Git detected
  the session-log files as renames so history is preserved. Updated
  4 stale path references in 2 files.
- Moved cox-bgp ticket draft out of projects/dataforth-dos/ (wrong
  project) to clients/internal-infrastructure/vendor-tickets/.
- Relocated tunnel-setup helper scripts from
  projects/dataforth-dos/datasheet-pipeline/implementation/ to
  clients/internal-infrastructure/scripts/cloudflared-tunnel-setup/.
  Deleted superseded/abandoned login attempts. Sanitized hardcoded
  Jupiter/pfSense SSH passwords to pull from SOPS vault at runtime;
  Cloudflare token reads from env var (tokens still in 1Password,
  vault entry is metadata-only).

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

59 lines
2.2 KiB
Python

"""Pull CF Analytics via GraphQL to see origin-status per CF PoP."""
import json, os, sys, urllib.request
from datetime import datetime, timezone, timedelta
ZONE = '1beb9917c22b54be32e5215df2c227ce'
# CF API tokens live in 1Password (vault entry services/cloudflare.sops.yaml
# currently holds metadata only). Provide via env vars before running.
TOKENS = {
'full-dns': os.environ.get('CF_API_TOKEN_FULL_DNS', ''),
'legacy': os.environ.get('CF_API_TOKEN_LEGACY', ''),
}
since_30 = (datetime.now(timezone.utc) - timedelta(minutes=30)).strftime('%Y-%m-%dT%H:%M:%SZ')
QUERY = '''
query($zone:String!, $since:Time!){
viewer {
zones(filter:{zoneTag:$zone}){
httpRequestsAdaptiveGroups(limit:50, filter:{datetime_geq:$since}, orderBy:[count_DESC]){
count
dimensions { coloCode edgeResponseStatus originResponseStatus clientRequestHTTPHost }
}
}
}
}
'''
def gql(token, query, vars):
req = urllib.request.Request(
'https://api.cloudflare.com/client/v4/graphql',
data=json.dumps({'query': query, 'variables': vars}).encode(),
headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'},
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
for name, tok in TOKENS.items():
print(f'\n===== Trying {name} token =====')
try:
r = gql(tok, QUERY, {'zone': ZONE, 'since': since_30})
if r.get('errors'):
print('errors:', json.dumps(r['errors'], indent=2)[:600])
else:
zones = r.get('data', {}).get('viewer', {}).get('zones', [])
if not zones:
print('no zones returned')
continue
groups = zones[0].get('httpRequestsAdaptiveGroups', [])
print(f'{len(groups)} groups returned')
print(f'{"count":>6} {"colo":5} {"edge":5} {"origin":6} host')
for g in groups:
d = g['dimensions']
print(f"{g['count']:>6} {d.get('coloCode','-'):5} "
f"{str(d.get('edgeResponseStatus','-')):5} "
f"{str(d.get('originResponseStatus','-')):6} "
f"{d.get('clientRequestHTTPHost','-')}")
except Exception as e:
print(f'FAIL: {e}')