Подготовьте ключи (с обеих сторон):
[ -f ~ /.ssh/id_ed25519 ] && [ -f ~ /.ssh/id_ed25519.pub ] || ssh-keygen -t ed25519
scp ~ /.ssh/id_ed25519.pub remote:from_remote_side/
Зашифрованный io.ReadWriteCloser
, просто:
// Generate (if not exists) and read ED25519 keys
identity , err := secureio . NewIdentity ( `/home/user/.ssh` )
// Read remote identity
remoteIdentity , err := secureio . NewRemoteIdentityFromPublicKey ( `/home/user/from_remote_side/id_ed25519.pub` )
// Create a connection
conn , err := net . Dial ( "udp" , "10.0.0.2:1234" )
// Create an encrypted connection (and exchange keys using ECDH and verify remote side by ED25519 signature).
session := identity . NewSession ( remoteIdentity , conn , nil , nil )
session . Start ( context . Background ())
// Use it!
// Write to it
_ , err = session . Write ( someData )
// Or/and read from it
_ , err = session . Read ( someData )
Настройте ресивер:
session . SetHandlerFuncs ( secureio . MessageTypeChannel ( 0 ), func ( payload [] byte ) {
fmt . Println ( "I received a payload:" , payload )
}, func ( err error ) {
panic ( err )
})
Отправьте сообщение синхронно:
_ , err := session . WriteMessage ( secureio . MessageTypeChannel ( 0 ), payload )
ИЛИ
Отправьте сообщение асинхронно:
// Schedule the sending of the payload
sendInfo := session . WriteMessageAsync ( secureio . MessageTypeChannel ( 0 ), payload )
[.. your another stuff here if you want ..]
// Wait until the real sending
<- sendInfo . Done ()
// Here you get the error if any:
err := sendInfo. Err
// It's not necessary, but helps to reduce the pressure on GC (so to optimize CPU and RAM utilization)
sendInfo . Release ()
MessageType для пользовательского канала можно создать с помощью функции MessageTypeChannel(channelID uint32)
. channelID
— это специальный номер, позволяющий определить, какой это поток (чтобы соединить отправителя с соответствующим получателем на удаленной стороне). В приведенных выше примерах в качестве значения channelID
использовался 0
, но это может быть любое значение в диапазоне: 0 <= x <= 2**31
.
Также существует специальный MessageType MessageTypeReadWrite
, используемый по умолчанию для Read()
/ Write()
. Но вы можете перенаправить этот поток на собственный обработчик.
SessionOptions.MaxPayloadSize
.SessionOptions.SendDelay
значение &[]time.Duration{0}[0]
. Тестирование проводилось при обмене данными через UNIX-сокет.
BenchmarkSessionWriteRead1-8 10000 118153 ns/op 0.01 MB/s 468 B/op 8 allocs/op
BenchmarkSessionWriteRead16-8 10000 118019 ns/op 0.14 MB/s 455 B/op 8 allocs/op
BenchmarkSessionWriteRead1024-8 9710 119238 ns/op 8.59 MB/s 441 B/op 8 allocs/op
BenchmarkSessionWriteRead32000-8 6980 173441 ns/op 184.50 MB/s 488 B/op 9 allocs/op
BenchmarkSessionWriteRead64000-8 3994 310038 ns/op 206.43 MB/s 629 B/op 9 allocs/op
BenchmarkSessionWriteMessageAsyncRead1-8 2285032 539 ns/op 1.86 MB/s 0 B/op 0 allocs/op
BenchmarkSessionWriteMessageAsyncRead16-8 2109264 572 ns/op 27.99 MB/s 2 B/op 0 allocs/op
BenchmarkSessionWriteMessageAsyncRead1024-8 480385 2404 ns/op 425.87 MB/s 15 B/op 0 allocs/op
BenchmarkSessionWriteMessageAsyncRead32000-8 30163 39131 ns/op 817.76 MB/s 162 B/op 5 allocs/op
BenchmarkSessionWriteMessageAsyncRead64000-8 15435 77898 ns/op 821.59 MB/s 317 B/op 10 allocs/op
Этот пакет предназначен для асинхронной работы, поэтому по сути Write
— это глупая оболочка кода WriteMessageAsync
. Чтобы повысить пропускную способность, он объединяет все ваши сообщения, собранные за 50 микросекунд, в одно, отправляет, а затем разделяет их обратно. Это позволяет уменьшить количество системных вызовов и других накладных расходов. Таким образом, чтобы достичь скорости 1,86 МБ/с для 1-байтовых сообщений, вам необходимо отправлять множество из них асинхронно (друг от друга), чтобы они были объединены при отправке/получении через внутреннее соединение.
Кроме того, эти 800 МБ/с больше связаны с локальным хостом. И более реалистичный сетевой случай (если у нас MTU ~= 1400):
BenchmarkSessionWriteMessageAsyncRead1300_max1400-8 117862 10277 ns/op 126.49 MB/s 267 B/op 10 allocs/op
Удаленная сторона аутентифицируется подписью ED25519 (сообщения обмена ключами).
Обмен ключами осуществляется через ECDH с X25519. Если установлен PSK, то это PSK, объединенная с постоянным значением соли, хешированная с помощью blake3.Sum256
и sha3.Sum256
и используемая для XOR (обмененного) ключа.
Полученное значение используется в качестве ключа шифрования для XChaCha20. Этот ключ в коде называется cipherKey
.
Ключ (полученный через ECDH) обновляется каждую минуту. Таким образом, cipherKey
в свою очередь, также обновляется каждую минуту.
Обмен SessionID
осуществляется в самых первых сообщениях обмена ключами. SessionID
— это комбинация UnixNano (когда была инициализирована сессия) и случайного 64-битного целого числа.
Кроме того, каждый пакет начинается с уникального (для сеанса) открытого текстового PacketID
(фактически, если установлен PSK, то PacketID
зашифрован с помощью ключа, полученного как хэш соленого PSK).
Комбинация PacketID
и SessionID
используется как IV/NONCE для XChaCha20, а cipherKey
используется в качестве ключа.
Стоит отметить, что PacketID
должен быть уникальным только в пределах сеанса. Таким образом, оно начинается с нуля, а затем увеличивается на 1 для каждого следующего сообщения. Поэтому, если системные часы сломаны, уникальность NONCE гарантируется 64-битным случайным значением SessionID
.
Аутентификация сообщения осуществляется с помощью Poly1305. В качестве ключа для Poly1305 использовался хэш blake3.Sum256:
PacketID
и cipherKey
с использованием XOR с постоянным значением. PacketID
должен только увеличиваться. Полученный PacketID запоминается в ограниченном окне значений. Если был получен пакет с тем же PacketID
(как он уже был) или с меньшим PackerID
(чем минимально возможное значение в окне), то пакет просто игнорируется.
Успешный сеанс с настройками по умолчанию проходит через состояния/фазы/этапы:
На этом этапе анализируются все параметры и инициализируются все необходимые горутины.
После этого *Session
перейдет в состояние «Обмен ключами».
На этом этапе обе стороны (локальная и удаленная) обмениваются открытыми ключами ECDH, чтобы получить симметричный общий ключ (см. «Проектирование безопасности»).
Кроме того, если установлен удаленный идентификатор, мы проверяем, соответствует ли он, и игнорируем любые сообщения об обмене ключами с любыми другими открытыми ключами ED25519 (не путать с открытым ключом ECDH): пары ключей ED25519 являются статическими для каждой стороны (и обычно предварительно определено), а пары ключей ECDH генерируются для каждого обмена ключами.
Также каждый ключ обмена ключами проверяется открытым ключом (передаваемым вместе с сообщением).
При настройках по умолчанию каждая сторона также отправляет подтверждающие сообщения, чтобы проверить, были ли сообщения получены и восприняты. И (с настройками по умолчанию) каждая сторона ожидает подтверждающего сообщения от удаленной стороны (см. также SessionOptions.AnswersMode
).
Если здесь все успешно, то *Session
переходит в фазу «Переговоры». Но процесс обмена ключами по-прежнему периодически выполняется в фоновом режиме.
На этом этапе мы пытаемся определить, какой размер пакетов может обрабатывать базовый io.Writer
. Итак, мы пытаемся отправить пакеты трех разных размеров и посмотреть, какой из них сможет совершить путешествие туда и обратно. Затем повторите процедуру через более короткий интервал. И так до 4 раз.
Это поведение можно включить или отключить с помощью SessionOptions.NegotiatorOptions.Enable
.
Когда эта процедура будет завершена (или если она отключена), то *Session
перейдет в состояние «Установлено».
Это состояние, в котором *Session
работает нормально, поэтому вы можете отправлять и получать сообщения через него. И сообщения, которые попытались отправить до достижения этой стадии, будут отправлены, как только *Session
достигнет этой стадии.
Closing
— это переходное состояние перед Closed
. Если достигнуто состояние Closed
это означает, что сессия мертва и с ней больше ничего не произойдет.
Async
для синхронной записи.notewakeup
вместо Cond.Wait