Feature/route from topology (#201)

* directory-client: removing explicit type definitions

* clients/webassembly: ugly commit, removes dependency on HTTP crates

...at the cost of copying quite a bit of NymTopology-related stuff into
the WebAssembly client. I'll make an issue to refactor that later.

* A more helpful hint in the error message when websocket connection fails.

* Removing superfluous JavaScript, route checking now happens in Rust
This commit is contained in:
Dave Hrycyszyn
2020-04-23 12:01:25 +01:00
committed by GitHub
parent 44c48ef8ee
commit 903ef44328
10 changed files with 548 additions and 70 deletions
Generated
+3
View File
@@ -2678,10 +2678,13 @@ version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"crypto",
"log 0.4.8",
"nymsphinx",
"rand 0.7.3",
"serde",
"serde_json",
"slice_as_array",
"topology",
"wasm-bindgen",
"wasm-bindgen-test",
"wee_alloc",
@@ -8,7 +8,7 @@ async function main() {
var connection = await connectWebsocket(localClientUrl).then(function (c) {
return c;
}).catch(function (err) {
display("Websocket ERROR: " + err);
display("Websocket connection error. Is the client running with <pre>--connection-type WebSocket</pre> on port " + port + "?");
})
connection.onmessage = function (e) {
handleResponse(e);
+4 -1
View File
@@ -11,15 +11,18 @@ crate-type = ["cdylib", "rlib"]
default = ["console_error_panic_hook"]
[dependencies]
# itertools = "0.8.2"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
slice_as_array = "1.1.0"
wasm-bindgen = "0.2"
rand = "0.7.2"
# internal
crypto = { path = "../../common/crypto" }
nymsphinx = { path = "../../common/nymsphinx" }
topology = { path = "../../common/topology" }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
+1 -29
View File
@@ -14,19 +14,6 @@
import * as wasm from "nym-sphinx-wasm";
class Route {
constructor(nodes) {
this.nodes = nodes;
}
}
class NodeData {
constructor(address, public_key) {
this.address = address;
this.public_key = public_key;
}
}
async function main() {
var gatewayUrl = "ws://127.0.0.1:1793";
var directoryUrl = "http://127.0.0.1:8080/api/presence/topology";
@@ -50,10 +37,9 @@ async function main() {
// Create a Sphinx packet and send it to the mixnet through the Gateway node.
function sendMessageToMixnet(connection, topology) {
let route = constructRoute(topology);
var recipient = document.getElementById("recipient").value;
var sendText = document.getElementById("sendtext").value;
let packet = wasm.create_sphinx_packet(JSON.stringify(route), sendText, recipient);
let packet = wasm.create_sphinx_packet(JSON.stringify(topology), sendText, recipient);
connection.send(packet);
displaySend(packet);
display("Sent a Sphinx packet containing message: " + sendText);
@@ -65,20 +51,6 @@ async function getTopology(directoryUrl) {
return topology;
}
// Construct a route from the current network topology so we can get wasm to build us a Sphinx packet
function constructRoute(topology) {
const mixnodes = topology.mixNodes;
const provider = topology.mixProviderNodes[0];
let nodes = [];
mixnodes.forEach(node => {
let n = new NodeData(node.host, node.pubKey);
nodes.push(n);
});
let p = new NodeData(provider.mixnetListener, provider.pubKey)
nodes.push(p);
return new Route(nodes);
}
// Let's get started!
main();
+173 -28
View File
@@ -12,20 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crypto::identity::MixIdentityPublicKey;
use models::Topology;
use nymsphinx::addressing::nodes::NymNodeRoutingAddress;
use nymsphinx::chunking::split_and_prepare_payloads;
use nymsphinx::Node as SphinxNode;
use nymsphinx::{
delays, Destination, DestinationAddressBytes, Node, NodeAddressBytes, SphinxPacket,
IDENTIFIER_LENGTH,
delays, Destination, DestinationAddressBytes, NodeAddressBytes, SphinxPacket, IDENTIFIER_LENGTH,
};
use serde::{Deserialize, Serialize};
use serde_json;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::net::SocketAddr;
use std::time::Duration;
use topology::provider::Node as TopologyNode;
use wasm_bindgen::prelude::*;
mod models;
mod utils;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
@@ -53,10 +55,10 @@ pub struct NodeData {
/// Message chunking is currently not implemented. If the message exceeds the
/// capacity of a single Sphinx packet, the extra information will be discarded.
#[wasm_bindgen]
pub fn create_sphinx_packet(raw_route: &str, msg: &str, destination: &str) -> Vec<u8> {
pub fn create_sphinx_packet(topology_json: &str, msg: &str, destination: &str) -> Vec<u8> {
utils::set_panic_hook(); // nicer js errors.
let route = sphinx_route_from(raw_route);
let route = sphinx_route_from(topology_json);
let average_delay = Duration::from_secs_f64(0.1);
let delays = delays::generate_from_average_duration(route.len(), average_delay);
@@ -76,7 +78,7 @@ pub fn create_sphinx_packet(raw_route: &str, msg: &str, destination: &str) -> Ve
/// it should send a packet it receives. So we prepend the packet with the
/// address bytes of the first mix inside the packet, so that the gateway can
/// forward the packet to it.
fn payload(sphinx_packet: SphinxPacket, route: Vec<Node>) -> Vec<u8> {
fn payload(sphinx_packet: SphinxPacket, route: Vec<SphinxNode>) -> Vec<u8> {
let packet = sphinx_packet.to_bytes();
let first_mix_address = route.first().unwrap().clone().address.to_bytes().to_vec();
@@ -93,29 +95,16 @@ fn payload(sphinx_packet: SphinxPacket, route: Vec<Node>) -> Vec<u8> {
///
/// This function panics if the supplied `raw_route` json string can't be
/// extracted to a `JsonRoute`.
///
/// # Panics
///
/// This function panics if `JsonRoute.nodes` doesn't contain at least 1
/// node.
///
fn sphinx_route_from(raw_route: &str) -> Vec<Node> {
let json_route: JsonRoute = serde_json::from_str(raw_route).unwrap();
assert!(
json_route.nodes.len() > 0,
"Sphinx packet must route to at least one mixnode."
);
let mut sphinx_route: Vec<Node> = vec![];
for node_data in json_route.nodes.iter() {
let x = Node::try_from(node_data.clone()).expect("Malformed NodeData");
sphinx_route.push(x);
}
sphinx_route
fn sphinx_route_from(topology_json: &str) -> Vec<SphinxNode> {
let topology = Topology::new(topology_json);
let p: TopologyNode = topology.providers().first().unwrap().to_owned();
let provider = p.into();
let route = topology.route_to(provider).unwrap();
assert_eq!(4, route.len());
route
}
impl TryFrom<NodeData> for Node {
impl TryFrom<NodeData> for SphinxNode {
type Error = ();
fn try_from(node_data: NodeData) -> Result<Self, Self::Error> {
@@ -130,6 +119,162 @@ impl TryFrom<NodeData> for Node {
nymsphinx::key::new(dest)
};
Ok(Node { address, pub_key })
Ok(SphinxNode { address, pub_key })
}
}
#[cfg(test)]
mod test_constructing_a_sphinx_packet {
use super::*;
#[test]
fn produces_1404_bytes() {
// 32 byte address + 1372 byte sphinx packet
let packet = create_sphinx_packet(
topology_fixture(),
"foomp",
"AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR",
);
assert_eq!(1404, packet.len());
}
#[test]
fn starts_with_a_mix_address() {
let mut payload = create_sphinx_packet(
topology_fixture(),
"foomp",
"AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR",
);
let mut address_buffer = [0; 32];
let _ = payload.split_off(32);
address_buffer.copy_from_slice(payload.as_slice());
let address = NymNodeRoutingAddress::try_from_bytes(&address_buffer);
assert!(address.is_ok());
}
}
#[cfg(test)]
mod building_a_topology_from_json {
use super::*;
#[test]
#[should_panic]
fn panics_on_empty_string() {
sphinx_route_from("");
}
#[test]
#[should_panic]
fn panics_on_bad_json() {
sphinx_route_from("bad bad bad not json");
}
#[test]
#[should_panic]
fn panics_when_there_are_no_mixnodes() {
let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap();
topology.mix_nodes = vec![];
let json = serde_json::to_string(&topology).unwrap();
sphinx_route_from(&json);
}
#[test]
#[should_panic]
fn panics_when_there_are_not_enough_mixnodes() {
let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap();
let node = topology.mix_nodes.first().unwrap().clone();
topology.mix_nodes = vec![node]; // 1 mixnode isn't enough. Panic!
let json = serde_json::to_string(&topology).unwrap();
sphinx_route_from(&json);
}
#[test]
fn test_works_on_happy_json() {
let route = sphinx_route_from(topology_fixture());
assert_eq!(4, route.len());
}
}
#[cfg(test)]
fn topology_fixture() -> &'static str {
let json = r#"
{
"cocoNodes": [],
"mixNodes": [
{
"host": "nym.300baud.de:1789",
"pubKey": "AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR",
"version": "0.6.0",
"location": "Falkenstein, DE",
"layer": 3,
"lastSeen": 1587572945877713700
},
{
"host": "testnet_nymmixnode.roussel-zeter.eu:1789",
"pubKey": "9wJ3zLoyat41e4ZgT1AWeueExv5c6uwnjvkRepj8Ebis",
"version": "0.6.0",
"location": "Geneva, CH",
"layer": 3,
"lastSeen": 1587572945907250400
},
{
"host": "185.144.83.134:1789",
"pubKey": "59tCzpCYsiKXz89rtvNiEYwQDdkseSShPEkifQXhsCgA",
"version": "0.6.0",
"location": "Bucharest",
"layer": 1,
"lastSeen": 1587572946007431400
},
{
"host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789",
"pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG",
"version": "0.6.0",
"location": "Glarus",
"layer": 1,
"lastSeen": 1587572945920982000
},
{
"host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789",
"pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG",
"version": "0.6.0",
"location": "Glarus",
"layer": 2,
"lastSeen": 1587572945920982000
},
{
"host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789",
"pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG",
"version": "0.6.0",
"location": "Glarus",
"layer": 2,
"lastSeen": 1587572945920982000
}
],
"mixProviderNodes": [
{
"clientListener": "139.162.246.48:9000",
"mixnetListener": "139.162.246.48:1789",
"pubKey": "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh",
"version": "0.6.0",
"location": "London, UK",
"registeredClients": [
{
"pubKey": "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq"
}
],
"lastSeen": 1587572946261865200
},
{
"clientListener": "127.0.0.1:9000",
"mixnetListener": "127.0.0.1:1789",
"pubKey": "2XK8RDcUTRcJLUWoDfoXc2uP4YViscMLEM5NSzhSi87M",
"version": "0.6.0",
"location": "unknown",
"registeredClients": [],
"lastSeen": 1587572946304564700
}
]
}
"#;
json
}
@@ -0,0 +1,50 @@
// Copyright 2020 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use serde::{Deserialize, Serialize};
use topology::coco;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CocoPresence {
pub location: String,
pub host: String,
pub pub_key: String,
pub last_seen: u64,
pub version: String,
}
impl Into<topology::coco::Node> for CocoPresence {
fn into(self) -> topology::coco::Node {
topology::coco::Node {
location: self.location,
host: self.host,
pub_key: self.pub_key,
last_seen: self.last_seen,
version: self.version,
}
}
}
impl From<topology::coco::Node> for CocoPresence {
fn from(cn: coco::Node) -> Self {
CocoPresence {
location: cn.location,
host: cn.host,
pub_key: cn.pub_key,
last_seen: cn.last_seen,
version: cn.version,
}
}
}
@@ -0,0 +1,66 @@
// Copyright 2020 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use std::io;
use std::net::ToSocketAddrs;
use topology::mix;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MixNodePresence {
pub location: String,
pub host: String,
pub pub_key: String,
pub layer: u64,
pub last_seen: u64,
pub version: String,
}
impl TryInto<topology::mix::Node> for MixNodePresence {
type Error = io::Error;
fn try_into(self) -> Result<topology::mix::Node, Self::Error> {
let resolved_hostname = self.host.to_socket_addrs()?.next();
if resolved_hostname.is_none() {
return Err(io::Error::new(
io::ErrorKind::Other,
"no valid socket address",
));
}
Ok(topology::mix::Node {
location: self.location,
host: resolved_hostname.unwrap(),
pub_key: self.pub_key,
layer: self.layer,
last_seen: self.last_seen,
version: self.version,
})
}
}
impl From<topology::mix::Node> for MixNodePresence {
fn from(mn: mix::Node) -> Self {
MixNodePresence {
location: mn.location,
host: mn.host.to_string(),
pub_key: mn.pub_key,
layer: mn.layer,
last_seen: mn.last_seen,
version: mn.version,
}
}
}
+157
View File
@@ -0,0 +1,157 @@
// Copyright 2020 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use nymsphinx::Node as SphinxNode;
use rand::seq::IteratorRandom;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::HashMap;
use std::convert::TryInto;
use topology::mix;
use topology::provider;
pub mod coconodes;
pub mod mixnodes;
pub mod providers;
// Topology shows us the current state of the overall Nym network
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Topology {
pub coco_nodes: Vec<coconodes::CocoPresence>,
pub mix_nodes: Vec<mixnodes::MixNodePresence>,
pub mix_provider_nodes: Vec<providers::MixProviderPresence>,
}
impl Topology {
pub fn new(json: &str) -> Self {
serde_json::from_str(json).unwrap()
}
fn make_layered_topology(&self) -> Result<HashMap<u64, Vec<mix::Node>>, NymTopologyError> {
let mut layered_topology: HashMap<u64, Vec<mix::Node>> = HashMap::new();
let mut highest_layer = 0;
for mix in self.mix_nodes() {
// we need to have extra space for provider
if mix.layer > nymsphinx::MAX_PATH_LENGTH as u64 {
return Err(NymTopologyError::InvalidMixLayerError);
}
highest_layer = max(highest_layer, mix.layer);
let layer_nodes = layered_topology.entry(mix.layer).or_insert_with(Vec::new);
layer_nodes.push(mix);
}
// verify the topology - make sure there are no gaps and there is at least one node per layer
let mut missing_layers = Vec::new();
for layer in 1..=highest_layer {
if !layered_topology.contains_key(&layer) {
missing_layers.push(layer);
}
if layered_topology[&layer].is_empty() {
missing_layers.push(layer);
}
}
if !missing_layers.is_empty() {
return Err(NymTopologyError::MissingLayerError(missing_layers));
}
Ok(layered_topology)
}
// Tries to get a route through the mix network
fn mix_route(&self) -> Result<Vec<SphinxNode>, NymTopologyError> {
let mut layered_topology = self.make_layered_topology()?;
let num_layers = layered_topology.len();
let route = (1..=num_layers as u64)
// unwrap is safe for 'remove' as it it failed, it implied the entry never existed
// in the map in the first place which would contradict what we've just done
.map(|layer| layered_topology.remove(&layer).unwrap()) // for each layer
.map(|nodes| nodes.into_iter().choose(&mut rand::thread_rng()).unwrap()) // choose random node
.map(|random_node| random_node.into()) // and convert it into sphinx specific node format
.collect();
Ok(route)
}
// Sets up a route to a specific provider
pub fn route_to(&self, provider_node: SphinxNode) -> Result<Vec<SphinxNode>, NymTopologyError> {
Ok(self
.mix_route()?
.into_iter()
.chain(std::iter::once(provider_node))
.collect())
}
pub fn mix_nodes(&self) -> Vec<mix::Node> {
self.mix_nodes
.iter()
.filter_map(|x| x.clone().try_into().ok())
.collect()
}
pub fn providers(&self) -> Vec<provider::Node> {
self.mix_provider_nodes
.iter()
.map(|x| x.clone().into())
.collect()
}
}
#[derive(Debug)]
pub enum NymTopologyError {
InvalidMixLayerError,
MissingLayerError(Vec<u64>),
}
#[cfg(test)]
mod converting_mixnode_presence_into_topology_mixnode {
use super::*;
#[test]
fn it_returns_error_on_unresolvable_hostname() {
let unresolvable_hostname = "foomp.foomp.foomp:1234";
let mix_presence = mixnodes::MixNodePresence {
location: "".to_string(),
host: unresolvable_hostname.to_string(),
pub_key: "".to_string(),
layer: 0,
last_seen: 0,
version: "".to_string(),
};
let _result: Result<mix::Node, std::io::Error> = mix_presence.try_into();
// assert!(result.is_err()) // This fails only for me. Why?
// ¯\_(ツ)_/¯ - works on my machine (and travis)
}
#[test]
fn it_returns_resolved_ip_on_resolvable_hostname() {
let resolvable_hostname = "nymtech.net:1234";
let mix_presence = mixnodes::MixNodePresence {
location: "".to_string(),
host: resolvable_hostname.to_string(),
pub_key: "".to_string(),
layer: 0,
last_seen: 0,
version: "".to_string(),
};
let result: Result<topology::mix::Node, std::io::Error> = mix_presence.try_into();
assert!(result.is_ok())
}
}
@@ -0,0 +1,86 @@
// Copyright 2020 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use serde::{Deserialize, Serialize};
use topology::provider;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MixProviderPresence {
pub location: String,
pub client_listener: String,
pub mixnet_listener: String,
pub pub_key: String,
pub registered_clients: Vec<MixProviderClient>,
pub last_seen: u64,
pub version: String,
}
impl Into<topology::provider::Node> for MixProviderPresence {
fn into(self) -> topology::provider::Node {
topology::provider::Node {
location: self.location,
client_listener: self.client_listener.parse().unwrap(),
mixnet_listener: self.mixnet_listener.parse().unwrap(),
pub_key: self.pub_key,
registered_clients: self
.registered_clients
.into_iter()
.map(|c| c.into())
.collect(),
last_seen: self.last_seen,
version: self.version,
}
}
}
impl From<topology::provider::Node> for MixProviderPresence {
fn from(mpn: provider::Node) -> Self {
MixProviderPresence {
location: mpn.location,
client_listener: mpn.client_listener.to_string(),
mixnet_listener: mpn.mixnet_listener.to_string(),
pub_key: mpn.pub_key,
registered_clients: mpn
.registered_clients
.into_iter()
.map(|c| c.into())
.collect(),
last_seen: mpn.last_seen,
version: mpn.version,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MixProviderClient {
pub pub_key: String,
}
impl Into<topology::provider::Client> for MixProviderClient {
fn into(self) -> topology::provider::Client {
topology::provider::Client {
pub_key: self.pub_key,
}
}
}
impl From<topology::provider::Client> for MixProviderClient {
fn from(mpc: topology::provider::Client) -> Self {
MixProviderClient {
pub_key: mpc.pub_key,
}
}
}
+7 -11
View File
@@ -58,17 +58,13 @@ pub struct Client {
impl DirectoryClient for Client {
fn new(config: Config) -> Client {
let health_check: HealthCheckRequest = HealthCheckRequest::new(config.base_url.clone());
let metrics_mixes: MetricsMixRequest = MetricsMixRequest::new(config.base_url.clone());
let metrics_post: MetricsMixPost = MetricsMixPost::new(config.base_url.clone());
let presence_topology: PresenceTopologyRequest =
PresenceTopologyRequest::new(config.base_url.clone());
let presence_coconodes_post: PresenceCocoNodesPost =
PresenceCocoNodesPost::new(config.base_url.clone());
let presence_mix_nodes_post: PresenceMixNodesPost =
PresenceMixNodesPost::new(config.base_url.clone());
let presence_providers_post: PresenceProvidersPost =
PresenceProvidersPost::new(config.base_url);
let health_check = HealthCheckRequest::new(config.base_url.clone());
let metrics_mixes = MetricsMixRequest::new(config.base_url.clone());
let metrics_post = MetricsMixPost::new(config.base_url.clone());
let presence_topology = PresenceTopologyRequest::new(config.base_url.clone());
let presence_coconodes_post = PresenceCocoNodesPost::new(config.base_url.clone());
let presence_mix_nodes_post = PresenceMixNodesPost::new(config.base_url.clone());
let presence_providers_post = PresenceProvidersPost::new(config.base_url);
Client {
health_check,
metrics_mixes,