← Zpět na články

AI

Jak jsem napojil Deepseek na TOR

Petr Novák 13 min čtení

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.

Proč zrovna Tor a ne VPN? VPN znamená, že jedné firmě (poskytovateli VPN) věříš celou cestu - ta firma vidí, kdo jsi a na co se ptáš. Tor je naopak decentralizovaný: žádný jediný uzel nezná celou cestu. Pro anonymizaci dotazů do vyhledávače je to lepší volba. A navíc je Tor zadarmo a open source, takže nemusím platit další předplatné k tomu, co už mám.

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č socks5h a ne socks5? U klasického socks5 by DNS řešil tvůj systém a do sítě by unikla adresa, na kterou se ptáš (i když ne tvoje IP). U socks5h:// řeší DNS rovnou Tor - neunikne ani dotazovaná doména. U citlivých dotazů je to přesně to, co chci.

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ě - .env na 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 = '',
) {
}
Proč je Tor proxy parametrem konstruktoru? Protože hodnota přichází z .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}"));
}
Proč takhle jednoduše? Když je torMode zapnutý, do cURL nastavím proxy a hotovo - PHP za mě řeší celou komunikaci přes SOCKS5. Není potřeba žádná extra knihovna ani proxy proces, stačí dva řádky. A díky 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.';
}
Proč Python a ne přímo v PHP? Protože requests v Pythonu má čistou podporu SOCKS5 proxy a je to jen pár řádků. PHP skript prostě zavolám přes exec a Python udělá request výhradně přes Tor. Navíc je to samostatný malý nástroj, který se dá testovat i ručně z terminálu (viz sekce o ověření níže).

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
Proč Tor přímo v kontejneru? Když Tor a aplikace sdílí stejný kontejner, oba vidí 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.

Co to znamená v praxi? Když zapneš Tor režim, model musí vědět: 1) všechna vyhledávání teď jdou přes Tor, 2) tor_search se používá pro citlivé věci, 3) když nástroj selže (CloudFlare apod.), má to normálně říct a odhadnout z dostupných informací. Bez těchto instrukcí v system promptu je to jen obyčejný chat s proxy - a to nestačí.

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í.

#AI #deepseek

Ohodnoť článek

Přihlas se a dej mu hvězdičky — zabere to pár vteřin.

Přihlásit se

Diskuze 0

Zatím tu nikdo nediskutuje. Buď první.

Přidej se do diskuze

Komentovat můžou přihlášení čtenáři — účet zabere minutu.

Přihlásit se