Запуск узла Hidden Lake на языке Go

Введение

Анонимная сеть Hidden Lake является в своей области уникальным и достаточно своеобразным проектом, т.к. базируется на совершенно иных методах и подходах анонимизации трафика, чем большинство ныне нам известных сетей. Из-за того, что сеть является относительно новой — она часто дополняется и совершенствуется. Одним из таковых дополнений стал новый способ запуска узла HL.

Немного о сети Hidden Lake

719994fe56de33f91e946d51b0dcdfe2.png

Анонимная сеть Hidden Lake (HL) — это децентрализованная F2F (friend-to-friend) анонимная сеть с теоретической доказуемостью. В отличие от известных анонимных сетей, подобия Tor, I2P, Mixminion, Crowds и т.п., сеть HL способна противостоять атакам глобального наблюдателя. Сети Hidden Lake для анонимизации своего трафика не важны такие критерии как: 1) уровень сетевой централизации, 2) количество узлов, 3) расположение узлов и 4) связь между узлами в сети.

Теоретическая доказуемость сети HL сводится к QB-задаче (задаче на базе очередей). Данная задача может быть описана следующим списком действий:

  1. Каждое сообщение m шифруется ключом получателя k: c = Ek (m),

  2. Сообщение c отправляется в период = T всем участникам сети,

  3. Период T одного участника независим от периодов T1, T2, …, Tn других участников,

  4. Если на период T сообщения не существует, то в сеть отправляется ложное сообщение v без получателя (со случайным ключом r): c = Er (v),

  5. Каждый участник пытается расшифровать принятое им сообщение из сети: m = Dk©.

    QB-сеть с тремя узлами A, B, C

    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)

e4e027b5b994a6a8c2a37ef2187ec6f3.png

Чтобы успешно связаться с другим узлом, посредством примитивов пакета 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)

cda059fe36d6d57a4332f1b3573e788d.png

С высокоуровневым подходом всё куда проще — нужно лишь установить 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 сервисов.

© Habrahabr.ru