Запуск узла Hidden Lake на языке Go
Введение
Анонимная сеть Hidden Lake является в своей области уникальным и достаточно своеобразным проектом, т.к. базируется на совершенно иных методах и подходах анонимизации трафика, чем большинство ныне нам известных сетей. Из-за того, что сеть является относительно новой — она часто дополняется и совершенствуется. Одним из таковых дополнений стал новый способ запуска узла HL.
Немного о сети Hidden Lake
Анонимная сеть Hidden Lake (HL) — это децентрализованная F2F (friend-to-friend) анонимная сеть с теоретической доказуемостью. В отличие от известных анонимных сетей, подобия Tor, I2P, Mixminion, Crowds и т.п., сеть HL способна противостоять атакам глобального наблюдателя. Сети Hidden Lake для анонимизации своего трафика не важны такие критерии как: 1) уровень сетевой централизации, 2) количество узлов, 3) расположение узлов и 4) связь между узлами в сети.
Теоретическая доказуемость сети HL сводится к QB-задаче (задаче на базе очередей). Данная задача может быть описана следующим списком действий:
Каждое сообщение m шифруется ключом получателя k: c = Ek (m),
Сообщение c отправляется в период = T всем участникам сети,
Период T одного участника независим от периодов T1, T2, …, Tn других участников,
Если на период T сообщения не существует, то в сеть отправляется ложное сообщение v без получателя (со случайным ключом r): c = Er (v),
Каждый участник пытается расшифровать принятое им сообщение из сети: m = Dk©.
QB-сеть с тремя узлами A, B, C
При такой модели глобальный наблюдатель будет видеть лишь факт генерации шифртекстов C = {c1, c2, …, cn} в определённо заданные периоды времени = T без возможности дальнейшего различия истинности Ek (m) или ложности Er (v) выбираемых им шифртекстов.
Более подробный анализ анонимной сети Hidden Lake, а также QB-задачи, можно найти в исследовательской работе: Анонимная сеть «Hidden Lake».
Возвращаемся к проблеме
Ранее существовало два способа того, как можно поднять узел Hidden Lake — либо использовать низкоуровневые функции и методы из пакета go-peer, чтобы с нуля сконструировать полноценную ноду, либо использовать высокоуровневый подход и запускать готовое приложение HLS (сервис анонимизации трафика), которое будет общаться с прикладными сервисами.
Для большинства случаев, когда нужно просто использовать функциональность сети — второй вариант конечно является наиболее релевантным. Тем не менее, когда хочется что-либо запрограммировать, проверить или протестировать, так чтобы, это ещё и успешно работало с сетью, то использование связки сервисов во главе с HLS будет являться избыточным, особенно когда нужно запрограммировать что-либо на родном языке Hidden Lake — языке Go. С другой стороны, использование пакета go-peer для реконструкции уже существующего и полностью работающего механизма HLS является ещё куда более нежелательным и избыточным способом.
Таким образом, перенос функциональности HLS в экспортируемую область позволит решить данное противоречие. И в этой статье мы посмотрим как стало возможно работать с сетью Hidden Lake средствами языка Go после переноса этой самой функциональности.
Низкоуровневый подход (go-peer)
Чтобы успешно связаться с другим узлом, посредством примитивов пакета go-peer, требуется сконструировать собственный узел как из деталек лего. Так например, чтобы создать экземпляр полноценного узла Hidden Lake, требуется написать примерно следующее:
Конструктор узла
networkMask := uint32(0x5f67705f)
msgSizeBytes := uint64(8 << 10)
netMsgSettings := net_message.NewSettings(&net_message.SSettings{
FWorkSizeBits: 18,
FNetworkKey: "oi4r9NW9Le7fKF9d",
})
node := anonymity.NewNode(
anonymity.NewSettings(&anonymity.SSettings{
FNetworkMask: networkMask,
FServiceName: "service",
FFetchTimeout: time.Minute,
}),
logger.NewLogger(
logger.NewSettings(&logger.SSettings{
FInfo: os.Stdout,
FWarn: os.Stdout,
FErro: os.Stderr,
}),
func(pLogArg logger.ILogArg) string {
logFactory, ok := pLogArg.(anon_logger.ILogGetterFactory)
if !ok {
panic("got invalid log arg")
}
logGetter := logFactory.Get()
return encoding.HexEncode(logGetter.GetHash())
},
),
func() database.IKVDatabase {
db, err := database.NewKVDatabase(dbPath)
if err != nil {
panic(err)
}
return db
}(),
network.NewNode(
network.NewSettings(&network.SSettings{
FAddress: "localhost:9571",
FMaxConnects: 256,
FReadTimeout: 5*time.Second,
FWriteTimeout: 5*time.Second,
FConnSettings: conn.NewSettings(&conn.SSettings{
FMessageSettings: netMsgSettings,
FLimitMessageSizeBytes: msgSizeBytes,
FWaitReadTimeout: time.Hour,
FDialTimeout: 5*time.Second,
FReadTimeout: 5*time.Second,
FWriteTimeout: 5*time.Second,
}),
}),
cache.NewLRUCache(2 << 10),
),
queue.NewQBProblemProcessor(
queue.NewSettings(&queue.SSettings{
FMessageConstructSettings: net_message.NewConstructSettings(&net_message.SConstructSettings{
FSettings: netMsgSettings,
FParallel: 1,
}),
FNetworkMask: networkMask,
FQueuePeriod: 5*time.Second,
FPoolCapacity: [2]uint64{256, 32},
}),
func() client.IClient {
client := client.NewClient(asymmetric.NewPrivKey(), msgSizeBytes)
if client.GetPayloadLimit() <= encoding.CSizeUint64 {
panic(`client.GetPayloadLimit() <= encoding.CSizeUint64`)
}
return client
}(),
),
asymmetric.NewMapPubKeys(),
).HandleFunc(
0x5f686c5f,
func(
pCtx context.Context,
pNode anonymity.INode,
pSender asymmetric.IPubKey,
pReqBytes []byte,
) ([]byte, error) {
loadReq, err := request.LoadRequest(pReqBytes)
if err != nil {
return nil, errors.Join(ErrLoadRequest, err)
}
service, ok := servicesMap[loadReq.GetHost()]
if !ok {
return nil, ErrUndefinedService
}
pushReq, err := http.NewRequestWithContext(
pCtx,
loadReq.GetMethod(),
fmt.Sprintf("http://%s%s", service, loadReq.GetPath()),
bytes.NewReader(loadReq.GetBody()),
)
if err != nil {
return nil, errors.Join(ErrBuildRequest, err)
}
for key, val := range loadReq.GetHead() {
pushReq.Header.Set(key, val)
}
pushReq.Header.Set(hls_settings.CHeaderPublicKey, pSender.ToString())
httpClient := &http.Client{Timeout: time.Minute}
resp, err := httpClient.Do(pushReq)
if err != nil {
logger.PushWarn(logBuilder.WithType(internal_anon_logger.CLogWarnRequestToService))
return nil, errors.Join(ErrBadRequest, err)
}
defer resp.Body.Close()
respMode := resp.Header.Get(hls_settings.CHeaderResponseMode)
switch respMode {
case "", hls_settings.CHeaderResponseModeON:
return response.NewResponse(resp.StatusCode).
WithHead(getResponseHead(resp)).
WithBody(getResponseBody(resp)).
ToBytes(),
nil
case hls_settings.CHeaderResponseModeOFF:
return nil, nil
default:
return nil, ErrInvalidResponseMode
}
},
)
connKeeper := connkeeper.NewConnKeeper(
connkeeper.NewSettings(&connkeeper.SSettings{
FDuration: 10*time.Second,
FConnections: func() []string{
return getConnections()
},
}),
node.GetNetworkNode(),
)
...
Если человек не знаком с Hidden Lake, то такая простыня кода его скорее отпугнёт, чем заинтересует. Поэтому существует, как минимум, ещё один — высокоуровневый подход.
Высокоуровневый подход (HLS)
С высокоуровневым подходом всё куда проще — нужно лишь установить HLS (сервис анонимизации) и далее запустить его с ключом сети.
$ go install github.com/number571/hidden-lake/cmd/hls@latest
$ hls -network=oi4r9NW9Le7fKF9d
Данный подход работает потому как в репозитории проекта Hidden Lake по умолчанию вшит список работающих ретрансляторов. Даже если ретрансляторы в будущем будут скомпрометированы, то из-за применяемой QB-задачи ничего плохого не произойдёт — анонимизация трафика нарушена не будет.
Среднеуровневый подход
Новый подход позволяет избавиться от излишних подробностей низкоуровневого подхода (настроек и конструктов) и от необходимости использования сервисов в высокоуровневом подходе. Иными словами, данный подход идеален, когда: 1) нужно запрограммировать что-либо конкретно связанное с сетью Hidden Lake без использования микросервисной архитектуры, 2) нет сил, возможности или желания разбираться в низкоуровневых деталях работы. Получаем в итоге своеобразное API.
Конструктор узла
node := network.NewHiddenLakeNode(
const networkKey = "oi4r9NW9Le7fKF9d"
network.NewSettingsByNetworkKey(networkKey, nil),
asymmetric.NewPrivKey(),
func() database.IKVDatabase {
kv, err := database.NewKVDatabase(dbPath + ".db")
if err != nil {
panic(err)
}
return kv
}(),
func() []string {
network := hiddenlake.GNetworks[networkKey]
conns := make([]string, 0, len(network.FConnections))
for _, c := range network.FConnections {
conns = append(conns, fmt.Sprintf("%s:%d", c.FHost, c.FPort))
}
return conns
},
func(_ context.Context, _ asymmetric.IPubKey, r request.IRequest) (response.IResponse, error) {
rsp := []byte(fmt.Sprintf("echo: %s", string(r.GetBody())))
return response.NewResponse().WithBody(rsp), nil
},
)
...
Проверяем работу
Всё, что нам остаётся — это проверить работу нового подхода на практике. Для этого достаточно будет запустить два узла, которые будут между собой как-либо взаимодействовать. Один из наиболее простых способов взаимодействия — это реализация echo-сервиса, при которой один узел будет отправлять запрос, а другой соответственно отвечать.
package main
import (
...
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Запускаем два узла
var (
node1 = runNode(ctx, "node1")
node2 = runNode(ctx, "node2")
)
// Узлы обмениваются публичными ключами
_, pubKey := exchangeKeys(node1, node2)
for {
// Запрос с ожиданием ответа
rsp, err := node1.FetchRequest(
ctx,
pubKey,
request.NewRequest().WithBody([]byte("hello, world!")),
)
if err != nil {
fmt.Println("error:(%s)\n", err.Error())
continue
}
fmt.Printf("response:(%s)\n", string(rsp.GetBody()))
}
}
...
Функции runNode & exchangeKeys
func runNode(ctx context.Context, dbPath string) network.IHiddenLakeNode {
const networkKey = "oi4r9NW9Le7fKF9d"
node := network.NewHiddenLakeNode(
// Задаём настройки узла исходя из ключа сети
network.NewSettingsByNetworkKey(networkKey, nil),
// Приватный ключ - идентификатор узла
asymmetric.NewPrivKey(),
// База данных нужна для сохранения хешей принимаемых шифртекстов
func() database.IKVDatabase {
kv, err := database.NewKVDatabase(dbPath + ".db")
if err != nil {
panic(err)
}
return kv
}(),
// Подключаем все соединения из build/networks.yml по ключу сети
func() []string {
network := hiddenlake.GNetworks[networkKey]
conns := make([]string, 0, len(network.FConnections))
for _, c := range network.FConnections {
conns = append(conns, fmt.Sprintf("%s:%d", c.FHost, c.FPort))
}
return conns
},
// Пример запуска простого echo-сервиса
func(_ context.Context, _ asymmetric.IPubKey, r request.IRequest) (response.IResponse, error) {
rsp := []byte(fmt.Sprintf("echo: %s", string(r.GetBody())))
return response.NewResponse().WithBody(rsp), nil
},
)
// Запускаем узел на
// 1. генерацию шифрованного трафика
// 2. подключение к ретрансляторам
go func() { _ = node.Run(ctx) }()
return node
}
func exchangeKeys(hlNode1, hlNode2 network.IHiddenLakeNode) (asymmetric.IPubKey, asymmetric.IPubKey) {
node1 := hlNode1.GetOrigNode()
node2 := hlNode2.GetOrigNode()
pubKey1 := node1.GetMessageQueue().GetClient().GetPrivKey().GetPubKey()
pubKey2 := node2.GetMessageQueue().GetClient().GetPrivKey().GetPubKey()
node1.GetMapPubKeys().SetPubKey(pubKey2)
node2.GetMapPubKeys().SetPubKey(pubKey1)
return pubKey1, pubKey2
}
Запускаем и проверяем. Полный исходный код данного примера можно найти тут.
$ go run .
>
response:(echo: hello, world!)
response:(echo: hello, world!)
response:(echo: hello, world!)
response:(echo: hello, world!)
response:(echo: hello, world!)
response:(echo: hello, world!)
response:(echo: hello, world!)
Заключение
В результате, мы смогли запустить два полноценно работающих узла в сети Hidden Lake, обменивающихся между собой сообщениями без использования низкоуровневых примитивов пакета go-peer и высокоуровневого подхода связки HL сервисов.