diff --git a/app.py b/app.py index 0502dfb..b23d734 100644 --- a/app.py +++ b/app.py @@ -357,5 +357,31 @@ def api_stop(): cfg = load_config(); cfg["stopped"] = True; save_config(cfg) return jsonify(ok=True) +# --- obfuscated remote access: token-gated listener on the tailnet IP only --- +TAILNET_IP = "100.120.108.13" +REMOTE_PORT = 47077 +try: + OPERATOR_TOKEN = open(os.path.join(APP_DIR, "operator_token.txt")).read().strip() +except Exception: + OPERATOR_TOKEN = "" + +@app.before_request +def _gate_remote(): + """Requests arriving on the tailnet listener must carry the token; localhost is free.""" + if request.host.startswith(f"{TAILNET_IP}:{REMOTE_PORT}") and OPERATOR_TOKEN: + auth = request.headers.get("X-Auth-Token", "") + # also accept ?t= for browser links + if not auth: + auth = request.args.get("t", "") + from hmac import compare_digest + if not compare_digest(auth, OPERATOR_TOKEN): + return ("not found", 404) + return None + +def _run_remote(): + app.run(host=TAILNET_IP, port=REMOTE_PORT, threaded=True, use_reloader=False) + if __name__ == "__main__": + import threading + threading.Thread(target=_run_remote, daemon=True).start() app.run(host="127.0.0.1", port=8899, threaded=True) diff --git a/engine.py b/engine.py index 4d64fc1..bfb71bb 100644 --- a/engine.py +++ b/engine.py @@ -18,7 +18,8 @@ log = logging.getLogger("mailer") # ------------------------------------------------------------------ config def load_config(): defaults = { - "ollama_url": "http://100.103.34.0:11440", # nightmare tailnet via non-chunked shim + "ollama_url": "http://100.103.34.0:28443", # obfuscated shim (token-gated, nonstandard port) + "ollama_token": "", # X-Auth-Token for the shim "ollama_model": "mgraffam/gemma4-heretic:12b", "ses_region": "us-east-1", "send_mode": "smtp", # smtp (works now) | ses (needs AWS keys) @@ -119,15 +120,22 @@ def init_db(): # ------------------------------------------------------------------ ollama class OllamaClient: - def __init__(self, url, model, timeout=300): + def __init__(self, url, model, timeout=300, token=""): self.url = url.rstrip("/") self.model = model self.timeout = timeout + self.token = token self._fail_until = 0 + def _headers(self): + h = {} + if self.token: + h["X-Auth-Token"] = self.token + return h + def healthy(self): try: - r = requests.get(f"{self.url}/api/tags", timeout=5) + r = requests.get(f"{self.url}/tags", headers=self._headers(), timeout=8) return r.status_code == 200 except Exception: return False @@ -136,7 +144,8 @@ class OllamaClient: if time.time() < self._fail_until: return None try: - r = requests.post(f"{self.url}/api/generate", + r = requests.post(f"{self.url}/gen", + headers=self._headers(), json={"model": self.model, "prompt": prompt, "stream": False, "options": {"num_predict": 350, "temperature": 0.8}, "keep_alive": "15m"}, timeout=self.timeout) @@ -326,7 +335,7 @@ class Engine: "total": len(rows), "started_at": datetime.now().isoformat(timespec='seconds')}) log.info(f"campaign {campaign_id} start: {len(rows)} recipients") - ollama = OllamaClient(cfg["ollama_url"], cfg["ollama_model"]) if cfg.get("personalize") else None + ollama = OllamaClient(cfg["ollama_url"], cfg["ollama_model"], token=cfg.get("ollama_token", "")) if cfg.get("personalize") else None if ollama: self.state["current"] = f"checking Ollama at {cfg['ollama_url']}..." if ollama.healthy(): diff --git a/ollama_shim_v2.py b/ollama_shim_v2.py new file mode 100644 index 0000000..197cd4b --- /dev/null +++ b/ollama_shim_v2.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Ollama shim v2 — obfuscated relay on nightmare (replaces v1, same systemd unit). +- Binds tailnet-only 100.103.34.0:28443 (nonstandard port, nothing fingerprints as Ollama) +- Bearer token required (X-Auth-Token); constant-time compare +- Generic 404 + fake nginx server header on anything else (no Ollama fingerprint) +- Proxies /gen -> localhost:11434/api/generate, returns ONE non-chunked body +""" +import json +import hmac +import http.server, socketserver, urllib.request + +LOCAL = "http://127.0.0.1:11434" +BIND = ("100.103.34.0", 28443) +TOKEN = open("/opt/ollama-shim/shared_secret.txt").read().strip() + +class H(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.0" + server_version = "nginx" + sys_version = "" + + def _authed(self): + got = self.headers.get("X-Auth-Token", "") + return hmac.compare_digest(got, TOKEN) + + def _deny(self): + body = b'{"error":"not found"}' + self.send_response(404) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if not self._authed(): + return self._deny() + if self.path == "/tags": + body = urllib.request.urlopen(LOCAL + "/api/tags", timeout=10).read() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self._deny() + + def do_POST(self): + if not self._authed(): + return self._deny() + if self.path != "/gen": + return self._deny() + n = int(self.headers.get("Content-Length", 0)) + payload = json.loads(self.rfile.read(n) or b"{}") + payload["stream"] = False + req = urllib.request.Request(LOCAL + "/api/generate", + data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=600) as r: + body = r.read() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *a): + pass + +class TS(socketserver.ThreadingTCPServer): + allow_reuse_address = True + daemon_threads = True + +if __name__ == "__main__": + TS(BIND, H).serve_forever() diff --git a/operator_token.txt b/operator_token.txt new file mode 100644 index 0000000..8b66841 --- /dev/null +++ b/operator_token.txt @@ -0,0 +1 @@ +postal_3bf0d0793c45aeed3d2a46bc \ No newline at end of file diff --git a/shared_secret.txt b/shared_secret.txt new file mode 100644 index 0000000..7b17363 --- /dev/null +++ b/shared_secret.txt @@ -0,0 +1 @@ +nx_6eff2d6e040c4db43464d8979e320e45 \ No newline at end of file