Анонимная сеть в 200 строк кода на Go
Введение
Анонимность — великая штука. Высшее наслаждение. Это что-то, чего ты не можешь оценить до тех пор, пока не потеряешь.
(Билл Мюррей)
Реализации анонимных сетей всегда стремятся быть как можно проще, доступнее для понимания, как на теоретическом, так и на программном уровнях. Такие условия становятся необходимыми вследствие одного из основных принципов построения безопасных программ — чем проще объяснить, тем легче доказать. Но к сожалению теория часто может расходиться с практикой, и то, что легко объяснить в теории, может быть проблематично объяснять на коде.
Вследствие этого, можно сказать just-for-fun, у меня появился вопрос: можно ли реализовать анонимную сеть настолько малую, чтобы её программный код смог понять даже начинающий программист за короткое время?
Выбор ядра
Анонимные сети всегда базируются на каком-либо алгоритме запутывающей маршрутизации. Так например, Tor базируется на луковой маршрутизации, I2P на чесночной, Mixminion на перемешивании трафика, DC-сети (Herbivore, Dissent) на задаче обедающих криптографов, HIdden Lake на задаче очередей и т.д. Выбираемый алгоритм запутывающей маршрутизации не только может приводить к выстраиванию разных моделей угроз, но и к ограничению, либо расширению прикладного использования итоговой анонимной сети. Таким образом, можно говорить, что алгоритм запутывающей маршрутизации — это есть ядро анонимной сети.
Если нашей задачей является реализация анонимной сети с наименьшим количеством кода, то её ядро должно быть также минималистично, то есть не должно требовать много операций, условий, маршрутизаций и прочего, а также должно быть легко объяснимым. Наиболее подходящим ядром (алгоритмом запутывающей маршрутизации) в таком случае становится задача на базе очередей. В отличие от большинства других алгоритмов, она не требует многократных маршрутизаций, не требует создания нескольких слоёв шифрования, плюс к этому является теоретически доказуемой моделью для построения анонимных сетей.
Теоретически доказуемая анонимность
Анонимными сетями с теоретически доказуемой анонимностью принято считать замкнутые (полностью прослушиваемые) системы, в которых становится невозможным осуществление любых пассивных атак (в том числе и при существовании глобального наблюдателя) направленных на деанонимизацию отправителя и получателя с минимальными условностями по количеству узлов неподчинённых сговору. Говоря иначе, с точки зрения пассивного атакующего, апостериорные знания (полученные вследствие наблюдений) должны оставаться равными априорным (до наблюдений), тем самым сохраняя равновероятность деанонимизации по N-ому множеству субъектов сети.
Таковую задачу я уже приводил в большом количестве моих работ, как например тут (сервис анонимной сети), тут (анонимный мессенджер) тут (хранитель трафика), тут (тайные каналы связи) и здесь (абстрактные анонимные сети). Тем не менее, не поленюсь объяснить и здесь основной принцип функционирования. Те кто уже знаком с такой задачей, могут пропустить следующий раздел.
Задача на базе очередей
Предположим, что существует три участника {A, B, C}. Каждый из них соединён друг c другом (не является обязательным критерием, но данный случай я привёл исключительно для упрощения). Каждый субъект устанавливает время генерации информации = T. У каждого участника имеется своё внутренее хранилище по типу FIFO (первый пришёл — первый ушёл), можно сказать имеется структура «очередь».
Сеть на базе очередей при трёх участниках {A, B, C}
Предположим, что участник A хочет отправить некую информацию одному из участников {B, C}, так, чтобы другой участник (или внешний наблюдатель) не знал, что существует какой-либо факт отправления. Каждый участник в определённый период T генерирует сообщение. Такое сообщение может быть либо ложным (не имеющее никакого фактического содержания и никому по факту не отправляется, заполняясь случайными битами), либо истинным (запрос или ответ). Отправить раньше или позже положенного времени T никакой участник не может. Если скопилось несколько запросов одному и тому же участнику, тогда он их ложит в свою очередь сообщений и после периода T достаёт из очереди и отсылает в сеть.
Таким образом, внешний глобальный наблюдатель будет видеть лишь картину, при которой каждый участник в определённо заданный период времени T отправляет некое сообщение всем остальным узлам сети, что не даёт никакой информации о факте отправления, либо получения. Внутренние пассивные участники также неспособны узнать коммуниицирует ли один из участников в данный период времени с каким-либо другим, т.к. предполагается, что шифрованная информация не выдаёт никаких данных об отправителе и получателе непосредственно.
Особенности
Задача на базе очередей обладает рядом особенностей, которые так или иначе стоит учитывать:
Параллельность запросов невозможна. Эта проблема базируется на периоде T за пределы которого нельзя выйти, и пределы которого нельзя нарушать. Пользователь, чтобы оставаться анонимным, должен строго придерживаться своего расписания генерации.
Линейная нагрузка на систему. Чем больше пользователей будет находиться в системе, тем сильнее сама система будет нагружена. Связано это в первую очередь с тем, что каждому пользователю необходимо будет обрабатывать в период T: N-1 запросов, где N — количество пользователей в сети.
Абоненты связаны между собой. В отличие от множества других анонимных сетей (например, Tor или I2P), разграничивающих отправителя и получателя между собой, сети же на базе очередей не делают такого различия. Это приводит к тому, что абонент априори связывает ваш сетевой адрес (IP) с вашим уникальным идентификатором (ID) в сети, и как следствие, при общении с абонентом вы не можете быть анонимным к нему (а он к вам).
Данный случай никак не нарушает анонимность со стороны внешнего наблюдателя или внутреннего, он лишь свидетельствует о том, что вы знаете с кем общаетесь, ровно как и собеседник знает с кем он общается. Например, если один из абонентов становится активным атакующим, знающим лишь и только ID своего собеседника, то ему не составит труда получить также его IP за счёт сотрудничества с глобальным наблюдателем. Тем не менее, знание связи IP-ID даст ему только местоположение собеседника, но не информацию о его состоянии в сети (действие или бездействие). Поэтому и предполагается, что абонент должен быть заранее известен, чтобы исключить на корню полезность его сотрудничества с глобальным наблюдателем.
Проектирование
Для того, чтобы знать как реализовывать мы должны понять, а что мы собственно хотим сделать. Какой протокол брать в качестве основы для передачи данных? Как пользователи будут взаимодействовать друг с другом? Как будет происходить шифрование / расшифрование данных? Какого прикладного применения мы хотим достичь? Начнём пожалуй по порядку.
В качестве основы мы возьмём протокол HTTP.Т. к. в языке Go уже существует стандартная библиотека для работы с HTTP протоколом, то результирующего кода потребуется меньше, чем если бы мы писали все взаимодействия пользователей на уровне транспортного протокола (того же TCP).
Далее, для ещё большего сжатия кода, мы сведём одноранговые соединения к примитивному виду, а именно — каждый клиент должен быть также HTTP сервером. В конечном итоге, каждый пользователь станет узлом сети. Примитивность вида заключается в том факте, что чисто технически узел может существовать и функционировать в системе без поднятия сервера на своей стороне. Это возможно, если существует: 1) узел с поднятым сервером, 2) маршрутизация, позволяющая передавать сообщения через несколько узлов в сети. В нашем сценарии анонимной сети не будет существовать ни узлов без своего адреса, ни маршрутизации. Иными словами, пользователь при входе в анонимную сеть должен будет подключиться заранее ко всем узлам в сети.
Взаимодействие пользователей у нас будет формироваться по следующим правилам:
Все отправляемые данные будут проходить через очередь, в которой установлен период генерации пакетов. Иными словами, нельзя будет отправить данные вне механизма очереди. В таком случае, очередь становится производителем сообщений.
Все получаемые данные будут приходить на HTTP сервер. В таком случае, HTTP-сервер будет выступать исключительно потребителем сообщений получаемых от очередей.
И последнее, шифрование / расшифрование данных у нас будет происходить асимметричным шифром RSA. Это сделано по нескольким причинам:
За счёт асимметричности алгоритма мы можем сразу идентифицировать пользователей по их публичным ключам, вследствие чего не потребуется дополнительных протоколов с хендшейком.
Использование гибридных схем (симметричная + асимметричная криптография) привело бы к увеличению количества кода.
Размер результирующей шифрованной информации всегда будет статичен и равен длине ключа. По этой причине не нужны дополнительные меры/алгоритмы, скрывающие размерность информации.
Выбор исключительно шифра RSA также имеет и негативные последствия в лице невозможности шифровать большие размеры данных, ограничиваясь лишь и только длиной своего ключа. Из этого следует, что анонимная сеть должна представлять транспортировку малых объёмов данных. Для этого удачно могут подойти чаты/мессенджеры, коей и будет являться наша дальнейшая реализация.
Плюс к этому, если мы помимо шифрования будем также добавлять механизмы целостности (использование хеш-функций) и аутентификации (подписание данных), то количество нашего кода также будет сильно увеличиваться. Поэтому перед шифрованием открытых сообщений мы будем к ним добавлять просто ключ сети, который будет исполнять роль аутентификации отправляемых / принимаемых сообщений со стороны самой системы. Хеш-функции будут у нас присутствовать неявно в режиме шифрования OAEP. Данная схема также позволит нам скрывать однотипность шифрованных текстов за счёт внутреннего механизма случайности.
Реализация
Весь реализованный код у нас будет находиться в одном файле (для удобства копирования). Также при написании я использовал достаточно старую версию Go — v1.16, поэтому данный код не привередлив к новшествам по типу дженериков.
Начнём мы пожалуй с основной функции main
. Здесь всё достаточно просто. Мы должны при запуске указать наш никнейм, хост и порт на котором мы будем поднимать HTTP сервер. Далее, параллельно запускаем HTTP-сервер (на получение сообщений) и две очереди (на отправление сообщений), где одна очередь с реальными сообщениями (runQueue
), другая с ложными (runQueueVoid
). После этого уходим в бесконечный цикл чтения команд от клиента.
Таковых команд всего две: attach
и send
. Первая команда привязывает публичный ключ при помощи которого мы будем шифровать все дальнейшие сообщения. Вторая команда шифрует сообщение привязанным публичным ключом и отправляет результат шифрования в очередь.
func main() {
if len(os.Args) != 3 {
panic("example run: ./main [nickname] [host:port]")
}
go runService(os.Args[2])
go runQueueVoid()
go runQueue()
for {
cmd := readCmd(startInput)
if len(cmd) != 2 {
fmt.Println("len cmd != 2")
continue
}
switch cmd[0] {
case "attach":
if err := getPubKey(cmd[1], attach); err != nil {
fmt.Println("error:", err)
continue
}
fmt.Println("ok")
case "send":
msg := fmt.Sprintf("%s%s: %s", authBytes, os.Args[1], strings.TrimSpace(cmd[1]))
encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, attach, []byte(msg), nil)
if err != nil {
panic(err)
}
queue <- encBytes
}
}
}
Далее, давайте разберёмся с самой очередью. По факту это просто бесконечный цикл, в котором находится time.Sleep
на 5 секунд после чего происходит отправка зашифрованных данных по всем своим соединениям. Отдельная очередь с ложными сообщениями необходима для того, чтобы основная очередь не останавливалась в момент шифрования ложного сообщения. Такое событие привело бы к возможным атакам связанных с учитыванием времени генерации пакетов.
func runQueue() {
for {
time.Sleep(5 * time.Second)
encBytes := <-getQueue()
for _, conn := range connects {
go func(conn string) {
req, err := http.NewRequest(http.MethodPost, conn, bytes.NewBuffer(encBytes))
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 5 * time.Second}
_, _ = client.Do(req)
}(conn)
}
}
}
func getQueue() chan []byte {
if len(queue) == 0 {
return queueVoid
}
return queue
}
В другой очереди находится немного иная логика, а именно сам процесс шифрования. В отличие от основной очереди, которая начинает свою итерацию исполнения только спустя 5 секунд, очередь генерирующая ложные сообщения будет работать без периодов до тех пор, пока не заполнит всю очередь queueVoid
ложными сообщениями. Сообщения мы генерируем на себя, чтобы никто другой не мог получить хотя бы информацию об успешном факте расшифрования.
func runQueueVoid() {
for {
if len(queueVoid) == queueSize {
time.Sleep(time.Second)
continue
}
msg := ""
encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &privKey.PublicKey, []byte(msg), nil)
if err != nil {
panic(err)
}
queueVoid <- encBytes
}
}
Ну и сама серверная часть выглядит таким образом. Её основная цель — это получить запрос, далее попытаться его расшифровать, при успешном расшифровании проверить существование ключа сети в самом сообщении и если таковой есть, то напечатать полученное сообщение. В любом случае, кроме ошибки чтения данных мы должны всегда возвращать http.StatusOK = 200
, для того, чтобы исключить атаки ориентируемые на подбор authBytes
. Успешность кода не будет давать злоумышленнику дополнительной информации о состоянии обработки полученных данных.
func runService(addr string) {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
encBytes, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer w.WriteHeader(http.StatusOK)
msg, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey, encBytes, nil)
if err != nil {
return
}
if !bytes.HasPrefix(msg, []byte(authBytes)) {
return
}
msg = bytes.TrimPrefix(msg, []byte(authBytes))
fmt.Printf("\n%s\n%s", string(msg), startInput)
})
http.ListenAndServe(addr, nil)
}
Всё оставшееся является уже скорее удобством в использовании, чтобы получать authBytes
, privKey
, attach
, connects
и прочее. Поэтому внутренний механизм нашей анонимной сети вышел лишь в 100 строк кода, оставшаяся сотня представляет собой уже дополнительные функции.
Весь исходный код
package main
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
initDir = "_init/"
keysDir = "_keys/"
startInput = "> "
queueSize = 32
)
var (
authBytes = getAuthKey()
privKey = getPrivKey()
connects = getConnects()
queueVoid = make(chan []byte, queueSize)
queue = make(chan []byte, queueSize)
attach = &privKey.PublicKey
)
func main() {
if len(os.Args) != 3 {
panic("example run: ./main [nickname] [host:port]")
}
go runService(os.Args[2])
go runQueueVoid()
go runQueue()
for {
cmd := readCmd(startInput)
if len(cmd) != 2 {
fmt.Println("len cmd != 2")
continue
}
switch cmd[0] {
case "attach":
if err := getPubKey(cmd[1], attach); err != nil {
fmt.Println("error:", err)
continue
}
fmt.Println("ok")
case "send":
msg := fmt.Sprintf("%s%s: %s", authBytes, os.Args[1], strings.TrimSpace(cmd[1]))
encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, attach, []byte(msg), nil)
if err != nil {
panic(err)
}
queue <- encBytes
}
}
}
func runService(addr string) {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
encBytes, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer w.WriteHeader(http.StatusOK)
msg, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey, encBytes, nil)
if err != nil {
return
}
if !bytes.HasPrefix(msg, []byte(authBytes)) {
return
}
msg = bytes.TrimPrefix(msg, []byte(authBytes))
fmt.Printf("\n%s\n%s", string(msg), startInput)
})
http.ListenAndServe(addr, nil)
}
func runQueue() {
for {
time.Sleep(5 * time.Second)
encBytes := <-getQueue()
for _, conn := range connects {
go func(conn string) {
req, err := http.NewRequest(http.MethodPost, conn, bytes.NewBuffer(encBytes))
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 5 * time.Second}
_, _ = client.Do(req)
}(conn)
}
}
}
func runQueueVoid() {
for {
if len(queueVoid) == queueSize {
time.Sleep(time.Second)
continue
}
msg := ""
encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &privKey.PublicKey, []byte(msg), nil)
if err != nil {
panic(err)
}
queueVoid <- encBytes
}
}
func getQueue() chan []byte {
if len(queue) == 0 {
return queueVoid
}
return queue
}
func getPubKey(filename string, pubKey *rsa.PublicKey) error {
pubKeyBytes, err := os.ReadFile(keysDir + filename)
if err != nil {
return err
}
pubKeyBlock, _ := pem.Decode(pubKeyBytes)
if pubKeyBlock == nil || pubKeyBlock.Type != "PUBLIC KEY" {
panic("pem block is invalid")
}
pub, err := x509.ParsePKCS1PublicKey(pubKeyBlock.Bytes)
if err != nil {
panic(err)
}
*pubKey = *pub
return nil
}
func getAuthKey() string {
authKeyBytes, err := os.ReadFile(initDir + "auth.key")
if err != nil || len(authKeyBytes) == 0 {
panic(err)
}
return string(authKeyBytes)
}
func getPrivKey() *rsa.PrivateKey {
privKeyBytes, err := os.ReadFile(initDir + "priv.key")
if err != nil {
panic(err)
}
privateKeyBlock, _ := pem.Decode(privKeyBytes)
if privateKeyBlock == nil || privateKeyBlock.Type != "PRIVATE KEY" {
panic("pem block is invalid")
}
priv, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
if err != nil {
panic(err)
}
return priv
}
func getConnects() []string {
cFile, err := os.Open(initDir + "connects.txt")
if err != nil {
panic(err)
}
defer cFile.Close()
connects := make([]string, 0, 100)
scanner := bufio.NewScanner(cFile)
for scanner.Scan() {
conn := strings.TrimSpace(scanner.Text())
if conn == "" {
continue
}
connects = append(connects, conn)
}
return connects
}
func readCmd(s string) []string {
fmt.Print(s)
input, _, err := bufio.NewReader(os.Stdin).ReadLine()
if err != nil {
panic(err)
}
cmd := strings.Split(string(input), "$")
for i := range cmd {
cmd[i] = strings.TrimSpace(cmd[i])
}
return cmd
}
Использование
В первую очередь, чтобы воспользоваться анонимной сетью нам необходимо структурировать директорию в которой запускается наш гошный файл. В директории _init находятся наши настройки — ключ сети auth.key, приватный ключ priv.key и список соединений connects.txt. Публичный ключ pub.key здесь необязателен.
Структура проекта
В директории _keys находятся публичные ключи пользователей, с которыми мы хотим общаться. Его можно пополнять или убирать. В отличие от файлов в _init, названия которых имеют статичные значения, в _keys ключи вы можете называть как душе угодно. Все ключи сохраняются в формате PEM/X509.
Для того, чтобы не затруднять себя генерацией ключей, я написал небольшую программу.
Код для генерации ключей
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
)
func main() {
privKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
panic(err)
}
privateKeyBlock := &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
}
ioutil.WriteFile("priv.key", pem.EncodeToMemory(privateKeyBlock), 0644)
publicKeyBlock := &pem.Block{
Type: "PUBLIC KEY",
Bytes: x509.MarshalPKCS1PublicKey(&privKey.PublicKey),
}
ioutil.WriteFile("pub.key", pem.EncodeToMemory(publicKeyBlock), 0644)
}
Пример работоспособности итоговой анонимной сети можно посмотреть на следующей гифке.
Пример чата двух пользователей
Безопасность
Несмотря на свою простоту, получившаяся анонимная сеть обладает достаточно сильными характеристиками. Так например, не только внешние и внутренние пассивные наблюдатели не способны будут нарушить анонимность субъектов за счёт теоретической доказуемости, но также и ряд активных наблюдателей.
Предположим, что существует кооперация внешнего активного наблюдателя (глобального наблюдателя), способного менять маршрут пакетов, блокировать узлы и прочее, и внутреннего активного наблюдателя, способного генерировать запросы в сети, DDoS’ить узлы и подобное. Возьмём в качестве их цели деанонимизацию узла посредством определения его текущего состояния (общается ли он с кем-то или бездействует). Определить состояние было бы возможным при условии, что 1) активный внутренний наблюдатель находится уже внутри сети (знает ключ сети), 2) располагает публичным ключом жертвы, 3) любой узел в сети на каждый запрос создаёт свой ответ (что успешно получил сообщение). В таком случае, сам ответ бы ложился в основную очередь, тем самым её «перегружая». Если кто-то в определённый момент бы уже отправил запрос, то очередь будет не пуста, т.к. в ней будет находиться уже ответ. В таком случае, внутренний активный наблюдатель может раз в N-ое время отправлять запрос жертве и если ответ будет сгенерирован лишь спустя несколько периодом T (итераций), то это будет означать, что жертва с кем-то общается. Если количество узлов в сети ограничено, то злоумышленник может исходить из априорных вероятностей связи и на основе этого частично деанонимизировать факт отправления/получения информации.
Тем не менее, вся вышеописанная атака становится бессмысленной, если отсутствует пункт 3, направленный на обязательную генерацию ответа. Если ответ не будет генерироваться автоматически, то следовательно он не будет ложиться в очередь сообщений и как следствие, не будет давать никакой информации злоумышленнику. Тем не менее, мы разобрали лишь одну сторону медали: ответ со стороны жертвы, но что если жертва сама генерирует запросы (что в принципе логично)? То в таком случае, атака будет наиболее эффективна вследствие присутствия в очереди истинных сообщений. Чтобы избежать этого нападения, можно возложить обязанность на оставшиеся пункты: либо на первый (скрытие ключа сети), либо на второй (скрытие публичного ключа).
Как я считаю, наиболее подходящим будет соблюдение первого пункта, т.к. его отсутствие позволяет злоумышленнику уже проникнуть в сеть и как следствие, совершать не только атаки нацеленные на деанонимизацию субъектов, но и атаки направленные на генерацию большого количества сообщений, которые пользователь будет постоянно получать/сохранять. Второй пункт является наверное самым слабым из рассматриваемых потому как сама идентификация в виде публичного ключа должна как-то передаться всем остальным субъектам сети, чтобы они могли обращаться по нему и шифровать им все сообщения. Если атакующий кооперирует с другими наблюдателями — сервисами, на которых выкладывается данный публичный ключ, то всё дальнейшее сокрытие публичного ключа априори становится бессмысленным занятием.
И в итоге, у нас появляется одна базовая проблема — проблема передачи ключа сети безопасно по сети. Это можно сделать посредством следующих действий:
Необходимо, чтобы все пользователи обменялись своими публичными ключами. Данное обстоятельство также может быть проблемой, потому как публичный ключ может быть подменён извне. Из-за децентрализованного характера сети, не допускается использование центров сертификации (ЦС). Тем не менее, можно воспользоваться N-ым количеством централизованных сервисов явно не связанных между собой для безопасной передачи публичных ключей.
Сгенерировать ключ сети K и зашифровать его всеми полученными публичными ключами Pub1, Pub2, Pub3, … => EPub1(K), EPub2(K), EPub3(K), … После этого отправить шифрованные версии ключа всем абонентам. В таком случае, будет сгенерирован и распространён ключ сети.
Единственной оставшейся проблемой здесь является лишь возможность существования злоумышленника в кругу абонентов, которым передаётся ключ сети. Чтобы частично побороть данную проблему, можно внести в код элемент случайности, который будет с определённой долей вероятности выносить несколько зашифрованных сообщений из queueVoid
в queue
, тем самым нарушая однозначность связи хранения истинных сообщений в queue
. При данном сценарии лишь будет страдать производительность системы, где для успешного отправления потребуется теперь не 5 секунд времени, а 5n секунд, где n — количество перемещённых сообщений из queueVoid
в queue
.
Заключение
В результате всего вышеописанного, была реализована анонимная сеть, всего в 200 строк кода, представляющая собой чат между несколькими пользователями. При этом сама анонимная сеть базируется на теоретически доказуемой модели анонимизации сетевого трафика (на базе очередей), что защищает как от внешних, так и от внутренних пассивных наблюдателей. Также более подробно, с теорией анонимных сетей, и скрытых систем в общем, вы можете ознакомиться тут, тут и здесь. Со всеми исходными кодами вы также можете ознакомиться здесь и тут.