Skip to content
trevj edited this page Nov 28, 2016 · 22 revisions

Introduction

We'd like to run uProxy cloud servers on Node.js because it will be cheaper and simpler than running inside Firefox on a (headless) X server.

Background

freedom-for-node provides core.rtcpeerconnection with the help of something called wrtc. wrtc is a thin layer (as are several other WebRTC-on-Node.js-type NPMs) over something called build-webrtc which makes it possible to compile WebRTC for Node.js.

This has enabled uProxy to include two sample apps that run in Node.js:

Great, right? Unfortunately, there are two major problems:

  1. it's slow as hell: ~25% of Firefox's throughput
  2. it's affected much more by latency than either Chrome or Firefox - >66% slowdown when that latency rises to ~150ms

The latency issue is doubly puzzling because uProxy made changes to Chrome in 2015 to improve throughput in the presence of high latency between peers - and wrtc is built from the same source.

Status

Setup

Simple SOCKS

NOTE: This only works if wrtc supports your platform out of the box (some older Ubuntu distributions and versions of OSX). See below for how to make a custom wrtc build.

git clone https://github.com/uProxy/uproxy.git
cd uproxy
yarn
grunt simpleSocks
node build/src/lib/samples/simple-socks-node

Zork

Zork powers cloud servers - cool, we built a Docker image and now you can easily run a Node.js-based uProxy cloud server: (add -d localhost for testing on your workstation)

git clone https://github.com/uProxy/uproxy-docker.git
cd uproxy-docker/testing/run-scripts
./run_cloud.sh -z uproxy/node-stable

TODO: add -z to our installer

Custom wrtc builds

Here's how to make a custom wrtc build which will work on Linux and run on Docker:

TO LAUNCH A DOCKER SESSION IN WHICH TO BUILD WRTC:

docker run --rm -ti -v $PWD:/work -w /work ubuntu:yakkety

apt-get update
apt-get install -y curl
curl -sL https://deb.nodesource.com/setup_6.x | bash -
apt-get install -y nodejs
npm install -g yarn

apt-get install -y make git python binutils g++

ALSO SEEMS LIKE IF YOU ARE RUNNING ON DOCKER YOU NEED:

apt-get install -y alsa-base pulseaudio
##
## build-webrtc
##
git clone https://github.com/markandrus/build-webrtc.git
cd build-webrtc

# PATCH #1 (https://github.com/js-platform/node-webrtc/issues/281#issuecomment-242173952)
#   edit jakelib/environment.js and add:
#     'include_pulse_audio=0',
#     'include_internal_video_render=0'

##
## node-webrtc
##
git clone https://github.com/js-platform/node-webrtc.git
cd node-webrtc
export SKIP_DOWNLOAD=true
export BUILD_WEBRTC_DEPENDENCY=/work/build-webrtc/

# PATCH #2
#   git fetch origin pull/287/head:fake_audio_device
#   git checkout fake_audio_device
#   git merge develop

# IF YOU ARE RUNNING AS ROOT:
#   echo 'unsafe-perm = true' > /root/.npmrc

npm install

# cleanup:
rm -fR node_modules/build-webrtc third_party

Aaaaaaaaaaaaaaaaand finally:

node examples/ping-pong-test.js

(find more advice on custom builds at https://github.com/js-platform/node-webrtc/wiki/Building)

Tools

stress

This pumps 32MB of data through a datachannel. Copy it into examples/ in your node-webrtc clone.

'use strict';

const webrtc = require('..');

const MB = 32;

const pc1 = new webrtc.RTCPeerConnection();
const pc2 = new webrtc.RTCPeerConnection();

pc1.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log('pc1 candidate: ' + JSON.stringify(candidate));
    pc2.addIceCandidate(candidate.candidate);
  }
};

pc2.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log('pc2 candidate: ' + JSON.stringify(candidate));
    pc1.addIceCandidate(candidate.candidate);
  }
};

pc2.ondatachannel = (event) => {
  console.log('pc2: data channel open');
  const dc = event.channel;
  let r = 0;
  dc.onmessage = (event) => {
    r++;
    if (r >= 1024 * MB) {
      console.log('all received!');
      // TODO: signal that we're done
      pc1.close();
      pc2.close();
    }
  };
};

// NOTE: must create the datachannel before the offer
const dc = pc1.createDataChannel('test');
dc.onopen = () => {
  console.log('pc1: data channel open');
  for (let i = 0; i < 1024 * MB; i++) {
    const ab = new ArrayBuffer(1500);
    dc.send(ab);
  }
  console.log('all sent!');
};

pc1.createOffer((offer) => {
  console.log('offer: ' + JSON.stringify(offer));
  pc1.setLocalDescription(offer);
  pc2.setRemoteDescription(offer);
  pc2.createAnswer((answer) => {
    console.log('answer: ' + JSON.stringify(answer));
    pc2.setLocalDescription(answer);
    pc1.setRemoteDescription(answer);
  }, console.error);
}, console.error);

distributed stress

Again, copy these two files into examples/ in your node-webrtc clone. Run it like so:

mkfifo fifo
node stress-send.js < fifo | /usr/bin/time -f %e node stress-receive.js  > fifo

On Docker:

docker run --rm -i --name=stress-send -v ~/wrtc/node-webrtc:/work -w /work node:6 node examples/stress-send.js < fifo | \
/usr/bin/time -f %e \
docker run --rm -i --name=stress-receive -v ~/wrtc/node-webrtc:/work -w /work node:6 node examples/stress-receive.js > fifo

Simulating latency

It's a real pain to configure an individual container's network interface; try just configuring the docker0 virtual bridge:

tc qdisc change dev docker0 root netem delay 100ms
tc qdisc show docker0
tc qdisc del dev docker0 root

stress-send.js

'use strict';

const readline = require('readline');
const webrtc = require('.././');

const MB = 32;

const pc = new webrtc.RTCPeerConnection();
const reader = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

pc.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log(JSON.stringify(candidate));
  }
};

// NOTE: must create the datachannel before the offer
const dc = pc.createDataChannel('test');
dc.onopen = () => {
  console.error('sender: data channel open');
  for (let i = 0; i < 1024 * MB; i++) {
    const ab = new ArrayBuffer(1500);
    dc.send(ab);
  }
  console.error('all sent!');
  pc.close();
};

pc.createOffer((offer) => {
  console.log(JSON.stringify(offer));
  pc.setLocalDescription(offer);
}, console.error);

reader.on('line', function(line) {
  try {
    const yo = JSON.parse(line);
    if (yo.type === 'icecandidate') {
      pc.addIceCandidate(yo.candidate);
    } else {
      pc.setRemoteDescription(yo);
    }
  } catch (e) {
    console.error('could not parse line: ' + line);
  }
});

stress-receive.js

'use strict';

const readline = require('readline');
const webrtc = require('.././');

const MB = 32;

const pc = new webrtc.RTCPeerConnection();
const reader = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

pc.onicecandidate = (candidate) => {
  if (candidate.candidate) {
    console.log(JSON.stringify(candidate));
  }
};

pc.ondatachannel = (event) => {
  console.error('receiver: data channel open');
  const dc = event.channel;
  let r = 0;
  dc.onmessage = (event) => {
    r++;
    if (r >= 1024 * MB) {
      console.error('all received!');
      pc.close();
      process.exit();
    }
  };
};

reader.on('line', function(line) {
  try {
    const yo = JSON.parse(line);
    if (yo.type === 'icecandidate') {
      pc.addIceCandidate(yo.candidate);
    } else {
      pc.setRemoteDescription(yo);
      pc.createAnswer((answer) => {
        console.log(JSON.stringify(answer));
        pc.setLocalDescription(answer);
      }, console.error);
    }
  } catch (e) {
    console.error('could not parse line: ' + line);
  }
});
Clone this wiki locally