feat(cred): harden client onboarding with Instagram linking portal, age verification bypass, and fleet runbook

This commit is contained in:
operator
2026-10-04 21:43:32 +00:00
parent 7902622852
commit 178ddc5dbf
10 changed files with 898 additions and 41 deletions
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
tailscale-verify-portal.py: Tailscale verification portal for Human-in-the-Loop age verification.
Serves on Tailscale IP (100.123.153.75:8765) and/or localhost.
Endpoints:
GET /status - JSON status of nodes awaiting verification
GET /verify/<node> - Generates fresh Instagram OAuth linking URL from the node's browser and 302 redirects
GET /check/<node> - Polls node to see if age gate has cleared into active chat session
"""
import http.server
import json
import os
import re
import socketserver
import subprocess
import sys
import time
import urllib.parse
PORT = 8765
BIND_HOST = "0.0.0.0"
NETVM_DIR = "/home/super/Projects/NetVM"
def get_tailscale_ip():
try:
out = subprocess.check_output(["tailscale", "ip", "-4"], text=True).strip().splitlines()
for line in out:
line = line.strip()
if re.match(r"^\d+\.\d+\.\d+\.\d+$", line):
return line
except Exception:
pass
return "100.123.153.75"
def get_tailscale_dns():
try:
out = subprocess.check_output(["tailscale", "status", "--json"], text=True)
data = json.loads(out)
self_dns = data.get("Self", {}).get("DNSName")
if self_dns:
return self_dns.rstrip(".")
except Exception:
pass
return "bl.tailfb5960.ts.net"
def fetch_node_ig_link(node):
"""Query CDP within node netns to get fresh Meta Accounts Center OAuth URL."""
script = """
import json, websocket, sys, urllib.request
try:
tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9450/json").read())
except Exception as e:
print(json.dumps({"error": f"CDP unreachable: {e}"}))
sys.exit(0)
muse_tab = next((t for t in tabs if "muse.ai" in t.get("url", "") and t.get("type") == "page"), None)
if not muse_tab:
print(json.dumps({"error": "No muse.ai tab open"}))
sys.exit(0)
try:
ws = websocket.create_connection(muse_tab["webSocketDebuggerUrl"], timeout=5)
expr = '''(async()=>{
try {
const r = await fetch('/api/hatch/age-confirmation/linking-web-auth?account_type=instagram', {
headers: {'Accept': 'application/json'}
});
return await r.json();
} catch(e) { return {error: String(e)}; }
})()'''
ws.send(json.dumps({"id": 1, "method": "Runtime.evaluate", "params": {"expression": expr, "awaitPromise": True, "returnByValue": True}}))
while True:
msg = json.loads(ws.recv())
if msg.get("id") == 1:
val = msg.get("result", {}).get("result", {}).get("value", {})
print(json.dumps(val))
break
ws.close()
except Exception as e:
print(json.dumps({"error": f"CDP evaluate failed: {e}"}))
"""
cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, "-c", script]
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
lines = res.stdout.strip().splitlines()
if lines:
data = json.loads(lines[-1])
return data
except Exception as e:
return {"error": str(e)}
return {"error": "No response from node"}
def check_node_cleared(node):
"""Check if node has transitioned past access/verification into active chat."""
checker = os.path.join(NETVM_DIR, "bin", "accounts-health.py")
# Lookup port from registry or default to 9450 for def
port = 9450
cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, checker, str(port)]
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
lines = res.stdout.strip().splitlines()
if lines:
data = json.loads(lines[-1])
return data
except Exception as e:
return {"error": str(e)}
return {"error": "No response from checker"}
class VerifyHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
parts = [p for p in parsed.path.split("/") if p]
if not parts or parts[0] == "":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
html = """<!DOCTYPE html>
<html>
<head><title>NetVM Operator Portal</title>
<style>body{font-family:system-ui,sans-serif;padding:2rem;background:#111;color:#eee;}a{color:#4ea8de;font-size:1.2rem;text-decoration:none;display:inline-block;margin:1rem 0;padding:0.75rem 1.5rem;background:#222;border-radius:8px;}a:hover{background:#333;}</style>
</head>
<body>
<h1>NetVM Client Verification Portal</h1>
<p>Nodes pending verification:</p>
<p><a href="/verify/def">Tap to Link Instagram for Node 'def'</a></p>
</body></html>"""
self.wfile.write(html.encode("utf-8"))
return
if parts[0] == "verify" and len(parts) >= 2:
node = parts[1]
data = fetch_node_ig_link(node)
url = data.get("url")
if url:
# 302 redirect directly to Instagram OAuth login
self.send_response(302)
self.send_header("Location", url)
self.end_headers()
return
else:
self.send_response(500)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": "Failed to get Instagram link", "detail": data}).encode("utf-8"))
return
if parts[0] == "check" and len(parts) >= 2:
node = parts[1]
st = check_node_cleared(node)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(st, indent=2).encode("utf-8"))
return
if parts[0] == "status":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"portal": "online", "ts_ip": get_tailscale_ip(), "ts_dns": get_tailscale_dns()}).encode("utf-8"))
return
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
# Concise logging
sys.stderr.write(f"[PORTAL] {self.address_string()} - {format % args}\n")
def run():
with socketserver.TCPServer((BIND_HOST, PORT), VerifyHandler) as httpd:
print(f"Tailscale Verification Portal serving on http://{get_tailscale_ip()}:{PORT}/")
print(f"Tailscale DNS: http://{get_tailscale_dns()}:{PORT}/")
sys.stdout.flush()
httpd.serve_forever()
if __name__ == "__main__":
run()