Prepare chaves (em ambos os lados):
[ -f ~ /.ssh/id_ed25519 ] && [ -f ~ /.ssh/id_ed25519.pub ] || ssh-keygen -t ed25519
scp ~ /.ssh/id_ed25519.pub remote:from_remote_side/
Criptografado io.ReadWriteCloser
, fácil:
// 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 )
Configure o receptor:
session . SetHandlerFuncs ( secureio . MessageTypeChannel ( 0 ), func ( payload [] byte ) {
fmt . Println ( "I received a payload:" , payload )
}, func ( err error ) {
panic ( err )
})
Envie uma mensagem de forma síncrona:
_ , err := session . WriteMessage ( secureio . MessageTypeChannel ( 0 ), payload )
OU
Envie uma mensagem de forma assíncrona:
// 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 ()
Um MessageType para um canal personalizado pode ser criado por meio da função MessageTypeChannel(channelID uint32)
. channelID
é um número personalizado para identificar qual fluxo é (para conectar um remetente ao receptor apropriado no lado remoto). Nos exemplos acima foi usado 0
como valor channelID
, mas pode ser qualquer valor no intervalo: 0 <= x <= 2**31
.
Também há um MessageType especial MessageTypeReadWrite
usado para Read()
/ Write()
padrão. Mas você pode redirecionar esse fluxo para um manipulador personalizado.
SessionOptions.MaxPayloadSize
.SessionOptions.SendDelay
como &[]time.Duration{0}[0]
. O benchmark foi realizado com comunicação através de um soquete 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
Este pacote foi projetado para ser assíncrono, então basicamente Write
é um wrapper estúpido em torno do código de WriteMessageAsync
. Para obter mais rendimento, ele mescla todas as suas mensagens coletadas em 50 microssegundos em uma só, envia e depois as divide de volta. Permite reduzir a quantidade de syscalls e outras despesas gerais. Portanto, para atingir 1,86 MiB/s em mensagens de 1 byte, você precisa enviar muitas delas de forma assíncrona (umas das outras), para que sejam mescladas durante o envio/recebimento por meio da conexão de back-end.
Além disso, esses 800MiB/s são mais sobre o caso localhost. E o caso de rede mais realista (se tivermos MTU ~= 1400) é:
BenchmarkSessionWriteMessageAsyncRead1300_max1400-8 117862 10277 ns/op 126.49 MB/s 267 B/op 10 allocs/op
O lado remoto é autenticado por uma assinatura ED25519 (da mensagem de troca de chaves).
A troca de chaves é realizada via ECDH com X25519. Se um PSK for definido, então ele será concatenado com um valor salt constante, hash com blake3.Sum256
e sha3.Sum256
e usado para XOR a chave (trocada).
O valor resultante é usado como chave de criptografia para XChaCha20. Essa chave é chamada cipherKey
dentro do código.
A chave (recebida via ECDH) é atualizada a cada minuto. Então, por sua vez, o cipherKey
também é atualizado a cada minuto.
Um SessionID
é trocado pelas primeiras mensagens de troca de chaves. SessionID
é uma combinação de UnixNano (de quando a sessão foi inicializada) e um número inteiro aleatório de 64 bits.
Além disso, cada pacote começa com um PacketID
de texto simples exclusivo (para uma sessão) (na verdade, se o PSK estiver definido, PacketID
será criptografado com uma chave derivada como um hash de um PSK salgado).
A combinação de PacketID
e SessionID
é usada como IV/NONCE para XChaCha20 e cipherKey
é usada como chave.
Vale ressaltar que PacketID
deve ser exclusivo apenas dentro de uma sessão. Portanto, começa com zero e aumenta 1 para cada mensagem seguinte. Portanto, se o relógio do sistema estiver quebrado, a exclusividade de NONCE será garantida pelo valor aleatório de 64 bits de SessionID
.
A autenticação de mensagens é feita usando Poly1305. Como chave para Poly1305 usei um hash blake3.Sum256 de:
PacketID
e cipherKey
com XOR por um valor constante. Supõe-se que PacketID
esteja aumentando apenas. Os PacketID recebidos são lembrados em uma janela limitada de valores. Se foi recebido um pacote com o mesmo PacketID
(como já era) ou com um PackerID
menor (que o valor mínimo possível na janela) então o pacote é simplesmente ignorado.
Uma sessão bem-sucedida com as configurações padrão passa por estados/fases/estágios:
Este é o estágio onde todas as opções são analisadas e todas as goroutines necessárias são inicializadas.
Depois disso *Session
será alterada para o estado "Troca de Chaves".
Nesta fase, ambas as partes (a local e a remota) estão trocando chaves públicas ECDH para obter uma chave compartilhada simétrica (ver "Projeto de Segurança").
Além disso, se uma identidade remota for definida, verificamos se ela corresponde e ignoramos quaisquer mensagens de troca de chaves com quaisquer outras chaves públicas ED25519 (não confunda com a chave pública ECDH): os pares de chaves ED25519 são estáticos para cada parte (e geralmente pré- definido), enquanto os pares de chaves ECDH são gerados para cada troca de chaves.
Além disso, cada chave de troca de chaves é verificada pela chave pública (passada com a mensagem).
Com as configurações padrão, cada parte também envia mensagens de confirmação para verificar se as mensagens foram recebidas e percebidas. E (com as configurações padrão) cada parte aguarda uma mensagem de confirmação do lado remoto (consulte também SessionOptions.AnswersMode
).
Se tudo der certo aqui a *Session
passa para a fase de “Negociação”. Mas o processo de troca de chaves ainda é realizado periodicamente em segundo plano.
Neste estágio, tentamos determinar qual tamanho de pacote o io.Writer
subjacente pode manipular. Então tentamos enviar pacotes de 3 tamanhos diferentes e ver qual deles vai conseguir fazer uma viagem de ida e volta. Em seguida, repita o procedimento em um intervalo menor. E assim por diante até 4 vezes.
Este comportamento pode ser habilitado ou desabilitado por meio de SessionOptions.NegotiatorOptions.Enable
.
Quando este procedimento for finalizado (ou se estiver desabilitado), então a *Session
passa para o estado "Estabelecida".
Este é o estado em que a *Session
está operando normalmente, portanto você pode enviar e receber mensagens através dela. E as mensagens que tentarem ser enviadas antes de atingir este estágio serão enviadas assim que *Session
atingir este estágio.
Closing
é um estado de transição antes de se tornar Closed
. Se o estado Closed
for alcançado, significa que a sessão está inoperante e nada acontecerá mais com ela.
Async
para gravações sincronizadas.notewakeup
em vez de Cond.Wait