Préparez les clés (des deux côtés) :
[ -f ~ /.ssh/id_ed25519 ] && [ -f ~ /.ssh/id_ed25519.pub ] || ssh-keygen -t ed25519
scp ~ /.ssh/id_ed25519.pub remote:from_remote_side/
io.ReadWriteCloser
crypté, facile :
// 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 )
Configurez le récepteur :
session . SetHandlerFuncs ( secureio . MessageTypeChannel ( 0 ), func ( payload [] byte ) {
fmt . Println ( "I received a payload:" , payload )
}, func ( err error ) {
panic ( err )
})
Envoyez un message de manière synchrone :
_ , err := session . WriteMessage ( secureio . MessageTypeChannel ( 0 ), payload )
OU
Envoyer un message de manière asynchrone :
// 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 ()
Un MessageType pour un canal personnalisé peut être créé via la fonction MessageTypeChannel(channelID uint32)
. channelID
est un numéro personnalisé permettant d'identifier de quel flux il s'agit (pour connecter un expéditeur au récepteur approprié du côté distant). Dans les exemples ci-dessus, la valeur 0
a été utilisée comme valeur channelID
, mais il peut s'agir de n'importe quelle valeur comprise dans la plage : 0 <= x <= 2**31
.
Il existe également un MessageType spécial MessageTypeReadWrite
qui est utilisé par défaut Read()
/ Write()
. Mais vous pouvez rediriger ce flux vers un gestionnaire personnalisé.
SessionOptions.MaxPayloadSize
.SessionOptions.SendDelay
sur &[]time.Duration{0}[0]
. Le benchmark a été réalisé avec une communication via un socket 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
Ce package est conçu pour être asynchrone, donc fondamentalement Write
est un stupide wrapper autour du code de WriteMessageAsync
. Pour obtenir plus de débit, il fusionne tous vos messages collectés en 50 microsecondes en un seul, les envoie, puis les divise. Cela permet de réduire le nombre d’appels système et autres frais généraux. Ainsi, pour atteindre 1,86 Mo/s sur des messages de 1 octet, vous devez en envoyer un grand nombre de manière asynchrone (les uns des autres), de sorte qu'ils seront fusionnés lors de l'envoi/réception via la connexion backend.
De plus, ces 800 Mio/s concernent davantage le cas de l'hôte local. Et un cas de réseau plus réaliste (si nous avons MTU ~= 1400) est :
BenchmarkSessionWriteMessageAsyncRead1300_max1400-8 117862 10277 ns/op 126.49 MB/s 267 B/op 10 allocs/op
Le côté distant est authentifié par une signature ED25519 (du message d'échange de clé).
L'échange de clés s'effectue via ECDH avec X25519. Si un PSK est défini, il s'agit alors d'un PSK concaténé avec une valeur de sel constante, haché avec blake3.Sum256
et sha3.Sum256
et utilisé pour XOR la clé (échangée).
La valeur résultante est utilisée comme clé de cryptage pour XChaCha20. Cette clé est appelée cipherKey
dans le code.
La clé (reçue via ECDH) est mise à jour toutes les minutes. Ainsi, la cipherKey
est également mise à jour toutes les minutes.
Un SessionID
est échangé par les tout premiers messages d'échange de clés. SessionID
est une combinaison d'UnixNano (du moment où la session a été initialisée) et d'un entier aléatoire de 64 bits.
De plus, chaque paquet commence par un PacketID
unique (pour une session) en texte brut (en fait, si PSK est défini, PacketID
est crypté avec une clé dérivée d'un hachage d'un PSK salé).
La combinaison de PacketID
et SessionID
est utilisée comme IV/NONCE pour XChaCha20 et cipherKey
est utilisée comme clé.
Il convient de mentionner que PacketID
est destiné à être unique uniquement au sein d'une session. Donc, cela commence simplement par zéro, puis augmente de 1 pour chaque message suivant. Par conséquent, si l'horloge système est cassée, l'unicité de NONCE est garantie par la valeur aléatoire de 64 bits de SessionID
.
L'authentification des messages se fait à l'aide de Poly1305. Comme clé pour Poly1305, un hachage blake3.Sum256 de :
PacketID
et cipherKey
XOR-ed par une valeur constante. PacketID
est censé augmenter uniquement. Les PacketID reçus sont mémorisés dans une fenêtre limitée de valeurs. S'il a reçu un paquet avec le même PacketID
(comme il l'était déjà) ou avec un PackerID
inférieur (à la valeur minimale possible dans la fenêtre), alors le paquet est simplement ignoré.
Une session réussie avec les paramètres par défaut passe par des états/phases/étapes :
C'est l'étape où toutes les options sont analysées et toutes les goroutines requises sont initialisées.
Après cela, *Session
passera à l'état « Échange de clés ».
A ce stade, les deux parties (la locale et la distante) échangent des clés publiques ECDH pour obtenir une clé partagée symétrique (voir « Conception de sécurité »).
De plus, si une identité distante est définie, nous vérifions si elle correspond et ignorons tout message d'échange de clé avec d'autres clés publiques ED25519 (à ne pas confondre avec la clé publique ECDH) : les paires de clés ED25519 sont statiques pour chaque partie (et généralement pré- défini), tandis que les paires de clés ECDH sont générées pour chaque échange de clés.
De plus, chaque clé d'échange de clé est vérifiée par la clé publique (transmise avec le message).
Avec les paramètres par défaut, chaque partie envoie également des messages d'accusé de réception pour vérifier si les messages ont été reçus et perçus. Et (avec les paramètres par défaut), chaque partie attend un message d'accusé de réception du côté distant (voir aussi SessionOptions.AnswersMode
).
Si tout réussit ici alors la *Session
passe à la phase « Négociation ». Mais le processus d’échange de clés est toujours effectué périodiquement en arrière-plan.
À ce stade, nous essayons de déterminer quelle taille de paquets le io.Writer
sous-jacent peut gérer. Nous essayons donc d'envoyer des paquets de 3 tailles différentes et regardons lequel d'entre eux pourra faire un aller-retour. Répétez ensuite la procédure à un intervalle plus court. Et ainsi de suite jusqu'à 4 fois.
Ce comportement peut être activé ou désactivé via SessionOptions.NegotiatorOptions.Enable
.
Lorsque cette procédure sera terminée (ou si elle est désactivée), alors la *Session
passe à l'état "Établie".
Il s'agit de l'état dans lequel la *Session
fonctionne normalement, vous pouvez donc envoyer et recevoir des messages via celle-ci. Et les messages tentés d'être envoyés avant d'atteindre cette étape seront envoyés dès que *Session
atteindra cette étape.
Closing
est un état de transition avant de devenir Closed
. Si l'état Closed
est atteint, cela signifie que la session est morte et que plus rien ne lui arrivera.
Async
pour les écritures synchronisées.notewakeup
au lieu de Cond.Wait