Jak jsem napojil Deepseek na TOR
Jednou jsem zjistil, že DeepSeek zvládne víc, než bych čekal - tak jsem ho vzal do Toru. Tohle je příběh o tom, jak jsem si na vlastním VPS postavil AI asistenta, který vyhledává anonymizovaně: co je Tor, proč nestačí VPN a jak se na něj DeepSeek napojí. S kódem.
Začalo to jednou otázkou
Nejdřív jsem chtěl zjistit, jestli DeepSeek vůbec narazí na bezpečnostní opatření. Nenarazil. Ale ne proto, že by je prolomil - obešel jsem je sám. Žádný hack, jen cesta, kterou jsem si našel. A pak mě napadlo: co se stane, když DeepSeekovi skryju úplně všechno - IP, polohu, identitu?
Povolí mi provoz z Toru, nebo mě odpojí ve chvíli, kdy uvidí IP výstupního uzlu? To se mi nechtělo zjišťovat ručně v prohlížeči. Chtěl jsem z toho rovnou něco funkčního - AI asistenta, který vyhledává a odpovídá anonymizovaně, běží 24/7 a nezávisí na tom, jestli mám zrovna otevřený Tor Browser.
Tak jsem si ho postavil. Na vlastním VPS, s vlastními klíči, s vlastním přepínačem Tor režimu v UI. A odpověď na tu otázku z úvodu vás možná překvapí.
Co je Tor (a proč jsem neskočil na VPN)
Tor (The Onion Router) je decentralizovaná anonymizační síť. Provoz se v ní prohání přes tři a více dobrovolnických uzlů (relay), z nichž každý zná jen předchozí a následující hop - žádný z nich nezná celou cestu. Odtud ten název: data se obalují ve vrstvách jako cibule (onion) a každý uzel jednu vrstvu „sloupne".
Výsledek: cílový server vidí IP posledního uzlu (exit node), ne tvoji. A provozovatel uzlu nevidí, odkud dotaz přišel.
Pár pojmů, které budu používat dál:
- SOCKS5 proxy - rozhraní, přes které aplikace posílá provoz do Toru. Tor standardně poslouchá na
127.0.0.1:9050. - socks5h:// - varianta, kdy DNS řeší Tor sám, takže neunikne ani adresa dotazovaného serveru.
- Exit node - poslední uzel cesty; cílový server vidí jeho IP.
Proč na vlastním VPS
Tor proxy mi běží přímo na VPS - na stejném serveru jako aplikace. Žádný cizí hosting, žádný prostředník mezi mnou a anonymizační sítí. Dotaz neopustí moji infrastrukturu jinak než přes Tor tunel. Klíče (DeepSeek, Brave) žijí v .env na serveru a nikdy se necommitnou do gitu.
U cizího hostingu nemáš kontrolu nad tím, kdo se k datům dostane, a anonymizační proxy na sdíleném serveru by musela běžet mimo tvoji infrastrukturu. Tady mám všechno na jednom místě: aplikaci, klíče i Tor. Provozovatel VPS sice vidí, že se z IP připojuji na Tor uzly, ale nevidí obsah šifrovaných dotazů - a žádná cizí firma nevidí, na co se ptám.
- Vlastní kontrola - nad procesy, sítí i daty rozhoduji já.
- Žádný prostředník - mezi mnou a anonymizační sítí nikdo nestojí.
- 24/7 dostupnost - funguje z jakéhokoli zařízení, kdykoli.
- Klíče na místě -
.envna serveru, ne u třetí strany.
Jak se DeepSeek volá přes Tor
Celý trik projektu je vlastně banální: k libovolnému cURL požadavku přidám Tor proxy, když je torMode zapnutý. Stejný vzor používám u DeepSeek API, web_search i fetch_url. Třída asistenta dostane tři věci - DeepSeek klíč, Brave klíč a Tor proxy - a přepínač se nastaví před tool-calling smyčkou:
/** Přepínač Tor režimu — nastaví chat() před tool-calling smyčkou. */
private bool $torMode = false;
public function __construct(
private readonly string $apiKey,
private readonly string $braveApiKey = '',
private readonly string $torProxy = '',
) {
}
.env přes DI (services.neon). Když je TOR_PROXY prázdný, Tor režim prostě není dostupný a zbytek asistenta funguje dál. Nastavuje se jednou, na jednom místě, ne tvrdě v kódu.Samotné volání DeepSeek API pak vypadá úplně stejně jako bez Toru - až na dva řádky na konci:
// DeepSeek API (api.deepseek.com) — plné volání
$ch = curl_init('https://api.deepseek.com/chat/completions');
$opts = [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 240,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
],
];
// ── TOR: celý požadavek projde přes Tor SOCKS5 proxy ──
if ($this->torMode && $this->torProxy !== '') {
$opts[CURLOPT_PROXY] = $this->torProxy;
$opts[CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5_HOSTNAME;
}
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
$err = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $err || $httpCode !== 200) {
throw new \RuntimeException('Chyba při volání AI: ' . ($err ?: "HTTP {$httpCode}"));
}
CURLPROXY_SOCKS5_HOSTNAME řeší DNS Tor sám, takže nikam neunikne skutečná adresa serveru, na který se ptám.K tomu si model ve system promptu přidá notifikaci, že má v Tor režimu hledat anonymizovaně:
$systemPrompt = self::SYSTEM_PROMPT
. ($planMode ? self::PLAN_NOTICE : '')
. ($webSearch ? self::TOOLS_NOTICE : '')
. ($torMode ? self::TOR_NOTICE : '') // ← režim Tor
. ($researchMode ? self::RESEARCH_NOTICE : '')
. $memoryContext;
$this->torMode = $torMode;
$content = $this->chatStream($systemPrompt, $messages, self::MAX_TOKENS, 0.7, $webSearch, $onDelta, $onHeartbeat);
Vyhledávání přes Tor
Dotaz do AI je jedna věc, ale asistent taky potřebuje hledat na webu. Vyhledávání přes Tor volá malý Python skript, který jede přes stejnou Tor proxy. PHP mu předá proxy a Brave klíč přes env:
private function torSearch(string $query, ?callable $onHeartbeat = null): string
{
if ($query === '') {
return 'Prázdný dotaz.';
}
if ($this->torProxy === '') {
return 'Tor režim není nakonfigurovaný (chybí TOR_PROXY v .env).';
}
if ($this->braveApiKey === '') {
return 'Tor search není nakonfigurovaný (chybí BRAVE_SEARCH_API_KEY).';
}
$pythonExe = getenv('PYTHON_EXE') ?: 'python3';
$script = __DIR__ . '/../../../../tor-search/tor_search.py';
if (!is_file($script)) {
return 'Tor search skript nebyl nalezen.';
}
// Předá Tor proxy a Brave klíč Python skriptu přes env.
putenv('TOR_PROXY=' . $this->torProxy);
putenv('BRAVE_SEARCH_API_KEY=' . $this->braveApiKey);
$cmd = sprintf(
'%s %s %s 2>&1',
escapeshellarg($pythonExe),
escapeshellarg($script),
escapeshellarg($query),
);
exec($cmd, $lines, $exitCode);
$output = implode("\n", $lines);
$data = json_decode($output, true);
if (!is_array($data) || !$data) {
return 'Tor search selhal: ' . ($output !== '' ? $output : 'prázdný výstup');
}
if (isset($data['error'])) {
return 'Tor search: ' . $data['error'];
}
$lines = [];
foreach (array_slice($data, 0, 10) as $r) {
$title = trim((string) ($r['title'] ?? ''));
$url = trim((string) ($r['url'] ?? ''));
$desc = trim((string) ($r['description'] ?? ''));
if ($title === '' && $url === '') {
continue;
}
$lines[] = ($title ? $title . "\n" : '') . $url . ($desc ? "\n" . $desc : '');
}
return $lines ? implode("\n\n", $lines) : 'Nic nenalezeno.';
}
Celý skript je krátký a čitelný - pojďme si ho projít celý, ať vidíš, kde je ten trik:
#!/usr/bin/env python3
"""Anonymní vyhledávání přes Tor proxy. Volá Brave Search API přes
Tor SOCKS5 proxy. Na stdout vrací JSON pole výsledků
(title, url, description). Tor proxy se předává přes TOR_PROXY."""
import os
import sys
import json
try:
import requests
except ImportError:
print(json.dumps({"error": "Chybí knihovna requests."}, ensure_ascii=False))
sys.exit(1)
TOR_PROXY = os.environ.get("TOR_PROXY", "")
BRAVE_API_KEY = os.environ.get("BRAVE_SEARCH_API_KEY", "")
def brave_search(query: str, limit: int = 10) -> list[dict]:
"""Vyhledá přes Brave Search API skrz Tor proxy."""
if not BRAVE_API_KEY:
raise RuntimeError("Chybí BRAVE_SEARCH_API_KEY")
proxies = {}
if TOR_PROXY:
proxies = {"http": TOR_PROXY, "https": TOR_PROXY}
url = "https://api.search.brave.com/res/v1/web/search"
params = {"q": query, "count": limit}
resp = requests.get(
url,
params=params,
proxies=proxies if proxies else None,
timeout=30,
headers={
"Accept": "application/json",
"X-Subscription-Token": BRAVE_API_KEY,
},
)
resp.raise_for_status()
data = resp.json()
results = data.get("web", {}).get("results", [])
if not results:
return []
out = []
for r in results[:limit]:
title = (r.get("title") or "").strip()
url = (r.get("url") or "").strip()
desc = (r.get("description") or "").strip()
if not title and not url:
continue
out.append({"title": title, "url": url, "description": desc})
return out
def main() -> None:
query = sys.argv[1].strip() if len(sys.argv) > 1 else ""
if not query:
print(json.dumps({"error": "Prázdný dotaz."}, ensure_ascii=False))
sys.exit(1)
try:
results = brave_search(query)
except Exception as e:
print(json.dumps({"error": f"Tor search selhal: {e}"}, ensure_ascii=False))
sys.exit(1)
if not results:
print(json.dumps({"error": "Nic nenalezeno."}, ensure_ascii=False))
return
print(json.dumps(results, ensure_ascii=False, default=str))
if __name__ == "__main__":
main()
Když to celé zapnu, dostanu AI asistenta, který u citlivých dotazů neukazuje moji IP ani nezávisí na cizí infrastruktuře. Všechno běží na mém serveru, klíče mám ve vlastních rukou a vyhledávání jede anonymizovaně. A hlavně: funguje to 24/7, ne jen když mám zapnutý prohlížeč.
Instalace a konfigurace
Na VPS je instalace Toru otázka dvou příkazů:
sudo apt update
sudo apt install tor
Tor se spustí jako služba a poslouchá na SOCKS 127.0.0.1:9050. Ověřit se to dá takhle:
sudo ss -tlnp | grep 9050
# očekávaný výstup (podobný):
# LISTEN 0 ... 127.0.0.1:9050 ... (tor)
V Dockeru spouštím Tor přímo v entrypointu, aby sdílel localhost s aplikací:
mkdir -p /var/lib/tor-data
chown www-data:www-data /var/lib/tor-data
su -s /bin/sh -c 'tor --SocksPort 127.0.0.1:9050 --DataDirectory /var/lib/tor-data --RunAsDaemon 0 &' www-data
127.0.0.1 a nemusím řešit propojení sítí mezi kontejnery. Stačí TOR_PROXY=socks5h://127.0.0.1:9050 a hotovo. A Tor běží jako neprivilegovaný www-data, takže nemůže sáhnout na nic root-owned.Do .env na serveru pak přijde tahle konfigurace:
# Tor SOCKS5 proxy — anonymizace web_search a fetch_url.
# Zapíná se přepínačem 🔰 Tor v UI. Prázdná hodnota = Tor mód není
# k dispozici.
TOR_PROXY=socks5h://127.0.0.1:9050
# DeepSeek API key — klíč k modelu.
DEEPSEEK_API_KEY=sk-xxxx
# Brave Search API key — pro vyhledávání (web_search / tor_search).
BRAVE_SEARCH_API_KEY=
# Cesta k Pythonu pro tor_search.py (na Windows absolutní cesta).
PYTHON_EXE=python3
Hodnota TOR_PROXY se z .env dostane do PHP služby a použije se v cURL, jak jsem ukázal výše.
Ověření, že to fakt jede přes Tor
Nejdřív otestuju samotný Tor, ať vím, že běží:
curl --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ip
Odpověď je JSON typu {"IsTor":true,"IP":"..."} - IP patří výstupnímu uzlu, ne tobě. Když tam uvidíš svoji IP, proxy nefunguje. Tohle je kontrolní bod, na kterém ušetříš hodiny hledání chyby, protože když se proxy nepřipojí, všechno ostatní selže matoucím způsobem.
Stejně se dá otestovat i Python skript:
# přejdi do složky se skriptem tor_search.py
cd tor-search
TOR_PROXY=socks5h://127.0.0.1:9050 BRAVE_SEARCH_API_KEY=xxxx \
python3 tor_search.py "test dotaz"
Až bude Tor běžet, přepni asistenta do Tor režimu a dotaz projde anonymizovaně přes síť. Přepínač 🔰 v UI je přesně ten moment, kdy se z „obyčejného chatu s proxy" stane anonymní vyhledávání.
Když prompt rozhoduje o všem
Teď to nejdůležitější - a tady jsem si málem rozbil celý projekt. Technika sama ti anonymizaci zajistí, ale to, jak dobře ti asistent vyhledá a odpoví, stojí a padá na promptu. Když model nedostane správně vyladěné instrukce (že má v Tor režimu hledat hned, že nesmí jen „oznamovat záměr" bez volání nástroje, že nesmí odmítnout dotaz), tak to nebude fungovat dobře - model se může zaseknout, couvat, nebo vyhledávat naprázdno.
Já jsem si tím prošel na vlastní kůži: stačila změna promptu a všechna pravidla přestala platit. Přesně o tom je můj článek o promptu, protože tahle zkušenost si zaslouží vlastní text.
Na co si dát pozor
Než se do toho pustíš, pár věcí, které jsem zjistil až za běhu:
- Tor je pomalejší - každý hop přidává latenci. Když potřebuješ odpověď za sekundu, Tor režim nezapínej.
- Některé servery Tor blokují - CloudFlare a podobní umí provoz z exit nodů zahazovat. Asistent to musí umět říct a pokračovat dál.
- Provozovatel VPS vidí, že se z IP připojuješ na Tor uzly - anonymita končí u poskytovatele infrastruktury. Počítej s tím.
- Anonymizovaný je odchod do sítě, ne tvůj přihlášený účet v aplikaci. Přihlášený uživatel je dohledatelný i přes Tor.
- .onion weby můžou obsahovat nelegální obsah - hledej jen to, co je legální a eticky v pořádku.
- Bez správně vyladěného promptu to nebude fungovat dobře - viz předchozí sekce.
A ještě jedna věc, na kterou narazíš při registraci: Brave Search API zrušil pro nové uživatele volný plán s 2000 dotazy měsíčně a místo toho dává jednorázový kredit zhruba na 1000 dotazů. Starší účty si původní free tier drží. Než to budeš řešit, zkontroluj si v dashboardu svůj plán, ať tě první vyúčtování nepřekvapí.
Co se týče DeepSeek API - endpoint je pořád stejný (api.deepseek.com/chat/completions), jen se obměňují názvy modelů. Aktuálně to jsou modely řady V4 (Flash a Pro) a ceny jsou pořád v řádu desetin dolaru za milion tokenů - pořád hluboko pod tím, co stojí západní konkurence. Přesný ceník se mění, tak si ho radši zkontroluj na oficiální stránce, než začneš počítat rozpočet.
A co legalita?
Anonymizace není o tom něco nelegálního skrývat. Tor používám na legitimní věci: ověřování faktů, citlivá témata (zdraví, finance, soukromí), výzkum a to, abych neukazoval svou IP tam, kde to není potřeba. Právě proto je v předchozí sekci zmínka, že .onion weby můžou obsahovat nelegální obsah - a já ho nevyhledávám.
Tor je legální nástroj. Stejně jako klíč od domu se dá použít k legálním i nelegálním věcem - rozdíl dělá to, jak ho použiješ ty. Tenhle článek popisuje techniku (jak napojit API na anonymizační síť), ne jak něco dělat protizákonně. Za to, na co nástroj použiješ, neseš odpovědnost vždy ty.
Co si z toho odnést
Anonymita není o tom, že máš co skrývat. Je o tom, že si vybíráš, komu ukážeš, na co se ptáš. Tenhle asistent mi to umožňuje - a DeepSeek mě mimochodem nikdy neodpojil. Přes Tor, s cizí IP, s dotazy, u kterých jsem nechtěl, aby se spojovaly s mým jménem.
Když to celé shrnu: nejtěžší na tom nebylo technické napojení - to byly dva řádky v cURL. Nejtěžší bylo uvědomit si, že anonymizace je vlastnost celého systému, ne jednoho přepínače. A že i ten nejlepší nástroj stojí a padá na tom, jak ho model pochopí.