Mixnet client

Clients

The Nym Client was built in the Installation section. If you haven't yet built the Nym Mixnet and want to run the code on this page, go there first.

From inside the nym-mixnet directory, the nym-mixnet-client binary got built to the build directory, so you can run it by invoking ./build/nym-mixnet-client:

nym-mixnet$ ./build/nym-mixnet-client
Usage: nym-mixnet-client COMMAND [OPTIONS]



  _ __  _   _ _ __ ___  
 | '_ \| | | | '_ \ _ \
 | | | | |_| | | | | | |
 |_| |_|\__, |_| |_| |_|
        |___/  

         (mixnet-client)


Commands:

    init        Initialise a Nym Mixnet client
    run         Run a persistent Nym Mixnet client process
    socket      Run a background Nym Mixnet client listening on a specified socket

Run "nym-mixnet-client help <command>" for more info on a specific command.

As you can see, there are three commands you can issue to the client.

  1. init - initialize a new client instance. Takes an optional --id clientname parameter. Otherwise it generates a random id.
  2. run - run a mixnet client in the foreground. Takes --id clientname as a parameter
  3. socket - run, and also listen on a socket for input messages

For the socket command, there are some required arguments:

  1. --id is the id of a generated clientname (see below)
  2. --socket specifies whether the client should listen on a tcp socket or a websocket. Allowable values are tcp or websocket.
  3. --port port on which the client is going to be listening

Let's try it out. First, you need to initialize a new client.

nym-mixnet$ ./build/nym-mixnet-client init --id alice
Saved generated private key to /home/you/.nym/clients/alice/config/private_key.pem
Saved generated public key to /home/you/.nym/clients/alice/config/public_key.pem
Saved generated config to /home/you/.nym/clients/alice/config/config.toml

Have a look at the generated files if you'd like - they contain clientname, public/private keypairs, etc.

You can run the client with user alice by doing this:

./build/nym-mixnet-client run --id alice
Our Public Key is: z-OQECd8VgC1BeVi6HsHMUbn3REnqZq1uXcyy9j7Hxc=

It doesn't look like much happened, it just sits there. But in fact, when you run() the client, it immediately starts generating (fake) cover traffic and sending it to the Nym Mixnet.

Congratulations, you have just contributed a tiny bit of privacy to the world! <CTRL-C> to stop the client.

If you want to see slightly more detail about what the client is doing, take a look at the log file at /tmp/nym_alice.log. You can change the file by modifying the client's config at /home/you/.nym/clients/alice/config/config.toml. If you change the logging file to an empty value, everything will be printed directly to STDOUT.

Try stopping and starting the client a few times. If you're interested, you should see your traffic reflected in the network traffic sent and received metrics at the Nym Dashboard. Have a look on the right hand side:

dashboard

Understanding the client

A large proportion of the Nym Mixnet's functionality is implemented client-side, including:

  1. determining network topology
  2. registering with mixnet providers
  3. fetching stored messages from the providers
  4. sending a constant stream of Sphinx packet cover traffic messages
  5. sending Sphinx packets with real messages
  6. sending Sphinx packet cover traffic when no real messages are being sent

Determining network topology

The first thing to understand is that it's the local client which picks the path that each packet will take through the mixnet topology.

When you first run your client, the client needs to figure what mixnodes exist, which layers they're in, and their public keys.

The client asks the Nym directory for the current mixnet topology. The client handles all this automatically, but in order to understand what's happening, you can try it yourself:

curl -X GET "https://directory.nymtech.net/api/presence/topology" -H  "accept: application/json" | jq

This returns a JSON-formatted list of MixNodes and MixProviderNodes, among other things:

"MixNodes": [
  {
    "host": "52.56.99.196:1789",
    "pubKey": "_ObRUsYnHDJOPDHXyfq5bnIoSbdn3BsSRcrLl-FCY1c=",
    "layer": 2,
    "lastSeen": 1572258570490299400
  },
  {
    "host": "18.130.86.190:1789",
    "pubKey": "dMtoH6vWDBfwjrU0EzPd-fhZDOGJazELsTp2qLyt72U=",
    "layer": 1,
    "lastSeen": 1572258571193777000
  },
  {
    "host": "3.10.22.152:1789",
    "pubKey": "03FFZ5RgfeBPmVVERenJOCLb-yXlOUtstc6izYc-wFs=",
    "layer": 1,
    "lastSeen": 1572258570994450200
  },
  {
    "host": "35.176.155.107:1789",
    "pubKey": "oaEqLzA5nUxMAqfg6yW5pneWC342uDMfVsSHxyNQ-DE=",
    "layer": 3,
    "lastSeen": 1572258571709773800
  },
  {
    "host": "3.9.12.238:1789",
    "pubKey": "ALf35HwBzXZXxaS6V55W7cLsx4a26AaRefinwwJHrg4=",
    "layer": 3,
    "lastSeen": 1572258571616835600
  },
  {
    "host": "35.178.213.77:1789",
    "pubKey": "KlfEn07FzcN93nMzzlsgq3wN5O1ID6O3Pd4DbezHEWo=",
    "layer": 2,
    "lastSeen": 1572258570492776400
  }
],
"MixProviderNodes": [
  {
    "host": "35.178.212.193:1789",
    "pubKey": "R_rGKmwelVAVRpicMwMIJwsHvdYHMNfcItPwNipu5GQ=",
    "registeredClients": [
      {
        "pubKey": "u7UTjC3UNXdz0HsjMKoxozzbvXyi3KrEvl8BxNNPcAM="
      },
     ...
    ],
    "lastSeen": 1572258572070089000
  },
  {
    "host": "3.8.176.11:1789",
    "pubKey": "XiVE6xA10xFkAwfIQuBDc_JRXWerL0Pcqi7DipEUeTE=",
    "registeredClients": [
      {
        "pubKey": "HGTg5XPWe4eiluFKTnC958PuGUSipjLcIeFdLi6zsww="
      },
      ...
    ],
    "lastSeen": 1572258571843881700
  }
],
...

The client does this when it starts. Each mixnode reports what layer it's in, its public key, and its IP address. Provider nodes do the same.

The client now has all the information needed to pick a path through the mixnet for each Sphinx packet, and do packet encryption.

Registering at a provider

When the client is first started, it sends a registration request to one of the available providers, (see MixProviderNode list in the topology). This returns a unique token that the client attaches to every subsequent request to the provider.

This is required as mixnet clients cannot receive messages directly from other clients as this would have required them to reveal their actual IP address which is not a desirable requirement. Instead the providers act as a sort of proxy between the client and the mixnet. So whenever client receives a message, it is stored by the specified provider until the recipient fetches it.

Fetching stored messages

Upon completing the provider registration, the client starts a separate message stream that periodically fetches all the client's stored messages on the provider. The rate at which this happens is set in the client config files.

Note that once the message is pulled, currently the provider immediately deletes it from its own storage.

Sending messages

Since it now understands the topology of the mixnet, the client can start sending traffic immediately. But what should it send?

If there's a real message to send (because you called client.SendMessage() or poked something down the client's socket connection), then the client will send a real message. Otherwise, the client will send cover traffic, at a rate determined in the client config file in ~/.nym/clients/<client-id>/config.toml

Real messages and cover traffic are both encrypted using the Sphinx packet format.

Sphinx packet creation

Clients create Sphinx packets. These packets are a bit complicated, but for now all you need to know is that they have the following characteristics:

  1. they consist of a header and a body
  2. the header contains unencrypted routing data
  3. bodies are encrypted
  4. bodies are padded so that they're all the same size
  5. observers can't tell anything about what's inside the encrypted body
  6. the body is layer-encrypted - it may contain either another sphinx packet or a payload message

Now let's build the Nym Mixnode and see what happens when a Sphinx packet hits a mixnode.

Integrating the mixnet client in your applications

Depending on what language you're using, you can fire up the client in one of two ways.

In Go

If you're a Gopher, you can compile the client code into your own application in the normal Go fashion. This will give you access to all public methods and attributes of the mixnet client. Most notably:

  • client.Start()
  • client.GetReceivedMessages()
  • client.SendMessage(message []byte, recipient config.ClientConfig)

client.Start() performs all of the aforementioned required setup, i.e. obtains network topology, registers at a provider, and starts all the traffic streams. In fact just calling client.Start() and not doing anything more is equivalent to the ./build/nym-mixnet-client run --id alice command.

You can decide at which particular provider should the client register by modifying the Provider attribute in the client struct before calling the .Start() method.

When the client fetches valid messages from its provider, they are stored in a local buffer until client.GetReceivedMessages() is called. This method returns all those messages and resets the buffer.

client.SendMessage(message []byte, recipient config.ClientConfig) as the name suggests, provides the core functionality of the mixnet by allowing sending arbitrary messages to specified recipients through the Nym Mixnet. It uses the current network topology to generate a path through the mixnet and automatically packs the content into an appropriate Sphinx packet.

The recipient argument is defined as a ClientConfig protobuf message thus allowing for better cross-language compatibility. In the case of the Go implementation, it is compiled down to the language. The protobuf message is defined as follows:

message ClientConfig {
    string Id = 1;
    string Host = 2;
    string Port = 3;
    bytes PubKey = 4;
    MixConfig Provider = 5;
}

where

message MixConfig {
    string Id = 1;
    string Host = 2;
    string Port = 3;
    bytes PubKey = 4;
    uint64 Layer = 5;
}

For the client, at this point of time, only the PubKey and Provider fields are relevant. Host and Port are no longer used in any meaningful way and will be removed later on. The Id equivalent to the encoding of the PubKey using the alternate URL base64 encoding as defined in RFC 4648. The similar is true for the MixConfig for the Provider - the Id is the base64 encodin of the public key. However, the Host and Port fields are actually vital here in order to generate routing information. As for the Layer, it is irrelevant in the context of a Provider. It is only required for Mix nodes.

In other languages

If you're not a Gopher (go coder), don't despair. You can run the client in socket mode instead, and use either websockets or TCP sockets to get equivalent functionality.

Using TCP Socket

In an effort to achieve relatively high-level cross-language compatibility, all messages exchanged between socket client and whatever entity is communicating with it, are defined as protobuf messages:

message Request {
    oneof value {
        RequestSendMessage send = 2;
        RequestFetchMessages fetch = 3;
        RequestGetClients clients = 4;
        RequestOwnDetails details = 5;
        RequestFlush flush = 6;
    }
}

// please refer to the current sourcecode for the current content of each request / response

message Response {
    oneof value {
        ResponseException exception = 1;
        ResponseSendMessage send = 2;
        ResponseFetchMessages fetch = 3;
        ResponseGetClients clients = 4;
        ResponseOwnDetails details = 5;
        ResponseFlush flush = 6;
    }
}

Currently the socket messages allow for the following:

  1. Send arbitrary bytes message to selected recipient
  2. Fetch all received messages from the client's buffer
  3. Get the list of all possible clients on the network
  4. Get ClientConfig message describing own Mixnet configuration
  5. Force the socket client to flush its writer buffer

Note that when the message is being written into the socket, additional information encoding the length of the message is included. This needs to be handled when reading and writing to the socket.

Proto-encoded messages are prepended with a 10-byte varint containing the length of the encoding. Please refer to the sample implementation

For example, you could start a TCP socket client with ./build/nym-mixnet-client socket --id alice --socket tcp --port 9001.

To send to oneself, and then fetch received messages using the client's TCP socket, one could do as follows. The example is in Go but the same basic approach should work in every language that speaks TCP:

package main

import (
	"encoding/binary"
	"fmt"
	"github.com/golang/protobuf/proto"
	"io"
	"net"
	"time"

	"github.com/nymtech/nym-mixnet/client/rpc/types"
	"github.com/nymtech/nym-mixnet/client/rpc/utils"
	"github.com/nymtech/nym-mixnet/config"
)

func main() {
	fmt.Println("Send and retrieve through mixnet demo")

	conn, err := net.Dial("tcp", "127.0.0.1:9001")
	if err != nil {
		panic(err)
	}

	myDetails := getOwnDetails(conn)

	fmt.Printf("myDetails: %+v\n\n", myDetails)

	sendMessage("foomp", myDetails, conn)

	fmt.Printf("We sent: %+v\n\n", "foomp")

	time.Sleep(time.Second * 1) // give it some time to send to the mixnet

	messages := fetchMessages(conn)

	fmt.Printf("We got back these bytes: %+v\n\n", messages)
	fmt.Printf("We got back this string: %+v\n\n", string(messages[0]))

}

func getOwnDetails(conn net.Conn) *config.ClientConfig {
	me := &types.Request{
		Value: &types.Request_Details{Details: &types.RequestOwnDetails{}},
	}

	flushRequest := &types.Request{
		Value: &types.Request_Flush{
			Flush: &types.RequestFlush{},
		},
	}

	err := utils.WriteProtoMessage(me, conn)
	if err != nil {
		panic(err)
	}

	err = utils.WriteProtoMessage(flushRequest, conn)
	if err != nil {
		panic(err)
	}

	res := &types.Response{}
	err = utils.ReadProtoMessage(res, conn)
	if err != nil {
		panic(err)
	}

	return res.Value.(*types.Response_Details).Details.Details

}

func sendMessage(msg string, recipient *config.ClientConfig, conn net.Conn) {

	msgBytes := []byte(msg)

	flushRequest := &types.Request{
		Value: &types.Request_Flush{
			Flush: &types.RequestFlush{},
		},
	}

	sendRequest := &types.Request{
		Value: &types.Request_Send{Send: &types.RequestSendMessage{
			Message:   msgBytes,
			Recipient: recipient,
		}},
	}

	err := utils.WriteProtoMessage(sendRequest, conn)
	if err != nil {
		panic(err)
	}

	err = utils.WriteProtoMessage(flushRequest, conn)
	if err != nil {
		panic(err)
	}

	res := &types.Response{}
	err = utils.ReadProtoMessage(res, conn)
	if err != nil {
		panic(err)
	}
}

func fetchMessages(conn net.Conn) [][]byte {
	flushRequest := &types.Request{
		Value: &types.Request_Flush{
			Flush: &types.RequestFlush{},
		},
	}

	fetchRequest := &types.Request{
		Value: &types.Request_Fetch{
			Fetch: &types.RequestFetchMessages{},
		},
	}

	err := writeProtoMessage(fetchRequest, conn)
	if err != nil {
		panic(err)
	}

	err = utils.WriteProtoMessage(flushRequest, conn)
	if err != nil {
		panic(err)
	}

	res2 := &types.Response{}
	err = utils.ReadProtoMessage(res2, conn)
	if err != nil {
		panic(err)
	}
	return res2.Value.(*types.Response_Fetch).Fetch.Messages

}

func writeProtoMessage(msg proto.Message, w io.Writer) error {
	b, err := proto.Marshal(msg)
	if err != nil {
		return err
	}

	return encodeByteSlice(w, b)
}

func encodeByteSlice(w io.Writer, bz []byte) (err error) {
	err = encodeBigEndianLen(w, uint64(len(bz)))
	if err != nil {
		return
	}
	_, err = w.Write(bz)
	return
}

func encodeBigEndianLen(w io.Writer, i uint64) (err error) {
	var buf = make([]byte, 8)
	binary.BigEndian.PutUint64(buf, i)
	_, err = w.Write(buf)
	return
}

Using the Websocket

Using the websocket is very similar to the way TCP socket is used. In fact it is actually simpler due to HTTP handling few aspects of it for us. For example the encoding of the lengths of messages exchanged or the buffer flushing.

Note that the websocket will only accept requests from the loopback address.

The identical set of request/responses is available for the Websocket as it was the case with the TCP socket, with the exception of RequestFlush, which does not exist. So for example having started the client with: ./build/nym-mixnet-client socket --id alice --socket websocket --port 9001, you could do the following to write a fetch request to a Websocket in Typescript:

const fetchMsg = JSON.stringify({
  fetch: {},
});

const conn = new WebSocket(`ws://localhost:9001/mix`);
conn.onmessage = (ev: MessageEvent): void => {
  const fetchData = JSON.parse(ev.data);
  const fetchedMesages = fetchData.fetch.messages;
  console.log(fetchedMessages);
}
conn.send(fetchMsg);

You can see a sample Electron application communicating with the Websocket client here.

It's also possible to write binary data to the websocket. Here's an example in Go, but the same technique will work in any language that has a byte type and supports protobufs:

import (
  "net/url"
  "github.com/golang/protobuf/proto"
  "github.com/gorilla/websocket"
  "github.com/nymtech/nym-mixnet/client/rpc/types"
)

fetchRequest := &types.Request{
  Value: &types.Request_Fetch{
    Fetch: &types.RequestFetchMessages{},
  },
}

u := url.URL{
  Scheme: "ws",
  Host:   "127.0.0.1:9000",
  Path:   "/mix",
}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
  panic(err)
}

defer c.Close()

fetchRequestBytes, err := proto.Marshal(fetchRequest)
if err != nil {
  panic(err)
}

err = c.WriteMessage(websocket.BinaryMessage, fetchRequestBytes)
if err != nil {
  panic(err)
}

time.Sleep(time.Second)

_, resB, err := c.ReadMessage()
if err != nil {
  panic(err)
}

res := &types.Response{}
err = proto.Unmarshal(resB, res)
if err != nil {
  panic(err)
}

fmt.Printf("%v", res)