Written by
HK
At
Sun Apr 28 2024
Clients
- Python: https://github.com/magic-wormhole/magic-wormhole (official, original)
- Rust: https://github.com/magic-wormhole/magic-wormhole.rs (official)
- Golang: https://github.com/psanford/wormhole-william.git (non-official)
- Golang + Fyne GUI Client: https://github.com/Jacalz/rymdport
- Rust + Tauri GUI Client: https://github.com/HuakunShen/wormhole-gui
Documentation
- https://github.com/magic-wormhole/magic-wormhole-protocols
- https://magic-wormhole.readthedocs.io/en/latest/
Performance
magic-wormhole can almost always eat the full bandwidth of the network. It's very fast. However, I have observed performance issue on Mac (M1 pro) during sending (not receiving).
See update on issue https://github.com/magic-wormhole/magic-wormhole.rs/issues/224
Sender Computer | Sender Client | Receiver Computer | Receiver Client | Speed |
---|---|---|---|---|
M1 pro Mac | python | Ubuntu i7 13700K | python | 112MB/s |
M1 pro Mac | rust | Ubuntu i7 13700K | python | 73MB/s |
M1 pro Mac | golang | Ubuntu i7 13700K | python | 117MB/s |
Ubuntu i7 13700K | python | M1 pro Mac | python | 115MB/s |
Ubuntu i7 13700K | rust | M1 pro Mac | python | 116MB/s |
Ubuntu i7 13700K | golang | M1 pro Mac | python | 117MB/s |
Ubuntu i7 13700K | python | Kali VM (on Mac) | python | 119MB/s |
Kali VM (on Mac) | python | Ubuntu i7 13700K | python | 30MB/s |
Ubuntu i7 11800H | rust | Ubuntu i7 13700K | python | 116MB/s |
Ubuntu i7 13700K | rust | Ubuntu i7 11800H | python | 116MB/s |
It seems like there is some performance issue with the rust implementation on the sender side.
Workflow
I read the client source code written in Python, Golang and Rust. The Python code is unreadable to me. Some packages like automat
and twisted
are used. I am not familiar with them and they make the code hard to read or follow. It even took me ~20-30 minutes to find the main function and get the debugger running. The code is not well-organized. It's hard to follow the workflow.
Rust is known for its complexity. It's async
and await
makes debugger jump everywhere. Variables allocated in heap are hard to track with debugger. Usually only a pointer address is shown.
The Golang version (although non-official) is the easiest to follow. Project structure is clear and simple. Goland's debugger works well. So let's follow the Golang version.
-
After command arguments parsing, everything starts here
sendFile(args[0])
-
A
wormhole.Client
is createdc := newClient()
-
The
code
is retrieved fromcode, status, err := c.SendFile(ctx, filepath.Base(filename), f, args...)
status
is a channel (var status chan wormhole.SendResult
) that waits for the result of sending file.-
-
Here is Wormhole Client's
SendFile()
method-
-
offer
contains the file name and size.
-
-
Let's go into
sendFileDirectory()
method here. Everything happens here.-
sideId
: RandSideID returns a string appropate for use as the Side ID for a client.NewClient returns a Rendezvous client. URL is the websocket url of Rendezvous server.
SideID
is the id for the client to use to distinguish messages in a mailbox from the other client. AppID is the application identity string of the client.Two clients can only communicate if they have the same AppID.
-
Then a nameplate is generated
If users provides the code, the mailbox is attached to the code. Otherwise, a new mailbox is created. A mailbox is a channel for communication between two clients. The sender creates a mailbox and sends the code (address of mailbox + key) to the receiver. The receiver uses the code to open the mailbox.
-
Then a
clientProto
is createdappID
is a constant string.sideID
is a random string.sideID := crypto.RandSideID()
RandSideID returns a string appropate for use as the Side ID for a client.Let's see how
newClientProtocol
works. -
Then enter a go routing (transfer happens here)
-
clinetProto.ReadPake(ctx)
: block and waiting for receiver to connect (wait for receiver to enter the code)ReadPake
callsreadPlainText
to read the event from the mailbox.pake
's body is a string of length 66.otherSidesMsg
is[]uint8
bytes of length 33.Then
sharedKey
is generated by callingcc.spake.Finish(otherSidesMsg)
.spake
is a SPAKE2 object.sharedKey
is a 32-byte long byte array.So what is
pake
message read from the mailbox?TODO
-
err = collector.waitFor(&answer)
: Wait for receiver to enter Y to confirm. The answer contains a OK message -
A cryptor (type=
transportCryptor
) is created.conn
is anet.TCPConn
TCP connection.A
readKey
andwriteKey
are generated with hkdf (HMAC-based Extract-and-Expand Key Derivation Function) fromtransitKey
and two strings innewTransportCryptor
.transitKey
is derived fromclientProto.sharedKey
andappID
.sharedKey
is a 32-byte long key generated byclientProto
(apake.Client
).recordSize
is 16384 byte (16kb), used to read file in chunks.hasher
is compute file hash while reading file. -
In the following loop, file is read and sent in chunks.
r
has typeio.Reader
. Every time 16KB is read.cryptor.writeRecord
encrypts the bytes and send the bytes.Let's see how
writeRecord
works.package secretbox ("golang.org/x/crypto/nacl/secretbox")
is used to encrypt data.d.conn.Write
sends the encrypted data out.
-
-
How is this guide?