/// /// /// /// // TODO(ldixon): reorganize the utransformers and rename uproxy-obfuscators. // Ideal: // import Transformer = require('uproxy-obfuscators/transformer'); // Current: /// // TODO(ldixon): re-enable FTE and regex2dfa. But this time, start with a pre- // computed set of DFAs because the regex2dfa.js library is 4MB in size. Also // experiment with uglify and zip to see if that size drops significantly. // // import regex2dfa = require('regex2dfa'); import arraybuffers = require('../../../third_party/uproxy-lib/arraybuffers/arraybuffers'); import churn_pipe_types = require('../churn-pipe/freedom-module.interface'); import churn_types = require('./churn.types'); import handler = require('../../../third_party/uproxy-lib/handler/queue'); import ipaddr = require('ipaddr.js'); import logging = require('../../../third_party/uproxy-lib/logging/logging'); import net = require('../net/net.types'); import peerconnection = require('../../../third_party/uproxy-lib/webrtc/peerconnection'); import random = require('../../../third_party/uproxy-lib/crypto/random'); import signals = require('../../../third_party/uproxy-lib/webrtc/signals'); import ChurnSignallingMessage = churn_types.ChurnSignallingMessage; import ChurnPipe = churn_pipe_types.freedom_ChurnPipe; var log :logging.Log = new logging.Log('churn'); export var filterCandidatesFromSdp = (sdp:string) : string => { return sdp.split('\n').filter((s) => { return s.indexOf('a=candidate') != 0; }).join('\n'); } var splitCandidateLine_ = (candidate:string) : string[] => { var lines = candidate.split(' '); if (lines.length < 8 || lines[6] != 'typ') { throw new Error('cannot parse candidate line: ' + candidate); } return lines; } var splitHostCandidateLine_ = (candidate:string) : string[] => { var lines = splitCandidateLine_(candidate) var typ = lines[7]; if (typ != 'host') { throw new Error('not a host candidate line: ' + candidate); } return lines; } export var extractEndpointFromCandidateLine = ( candidate:string) : net.Endpoint => { var lines = splitHostCandidateLine_(candidate); var address = lines[4]; var port = parseInt(lines[5]); if (port != port) { // Check for NaN. throw new Error('invalid port in candidate line: ' + candidate); } return { address: address, port: port }; } export var setCandidateLineEndpoint = ( candidate:string, endpoint:net.Endpoint) : string => { var lines = splitHostCandidateLine_(candidate); lines[4] = endpoint.address; lines[5] = endpoint.port.toString(); return lines.join(' '); } export interface NatPair { internal: net.Endpoint; external: net.Endpoint; } // This function implements a heuristic to select the single candidate // that is most likely to work for this connection. The heuristic // expresses a preference ordering: // Most preferred: public IP address bound as a host candidate // - rare outside of servers, but offers the very best connectivity // Next best: server-reflexive IP address // - most common // Worst: private IP address in a host candidate // - indicates that STUN has failed. Connection is still possible // if the other side is directly routable. // If none of these are present, the function will throw an exception. // TODO: Allow selecting more than one public address. This would help // when there are multiple interfaces or IPv6 and IPv4. export var selectPublicAddress = (candidates:freedom_RTCPeerConnection.RTCIceCandidate[]) : NatPair => { // TODO: Note that we cannot currently support IPv6 addresses: // https://github.com/uProxy/uproxy/issues/1107 var publicHostCandidates :net.Endpoint[] = []; var srflxCandidates :NatPair[] = []; var privateHostCandidates :net.Endpoint[] = []; for (var i = 0; i < candidates.length; ++i) { var line = candidates[i].candidate; var tokens = splitCandidateLine_(line); if (tokens[2].toLowerCase() != 'udp') { // Skip non-UDP candidates continue; } var typ = tokens[7]; if (typ === 'srflx') { var srflxAddress = tokens[4]; if (ipaddr.process(srflxAddress).kind() === 'ipv6') { continue; } var port = parseInt(tokens[5]); if (tokens[8] != 'raddr') { throw new Error('no raddr in candidate line: ' + line); } var raddr = tokens[9]; if (ipaddr.process(raddr).kind() === 'ipv6') { continue; } if (tokens[10] != 'rport') { throw new Error('no rport in candidate line: ' + line); } var rport = parseInt(tokens[11]); srflxCandidates.push({ external: { address: srflxAddress, port: port }, internal: { address: raddr, port: rport } }); } else if (typ === 'host') { var hostAddress = ipaddr.process(tokens[4]); // Store the host address in case no srflx candidates are found. if (hostAddress.kind() !== 'ipv6') { var endpoint :net.Endpoint = { address: tokens[4], port: parseInt(tokens[5]) }; if (hostAddress.range() === 'unicast') { publicHostCandidates.push(endpoint); } else { privateHostCandidates.push(endpoint); } } } } if (publicHostCandidates.length > 0) { return { internal: publicHostCandidates[0], external: publicHostCandidates[0] } } else if (srflxCandidates.length > 0) { return srflxCandidates[0]; } else if (privateHostCandidates.length > 0) { return { internal: privateHostCandidates[0], external: privateHostCandidates[0] } } throw new Error('no srflx or host candidate found'); }; // Returns a key for use with mirrorPipes_. var makeEndpointKey_ = (endpoint:net.Endpoint) : string => { return endpoint.address + ':' + endpoint.port; }; // Retry an async function with exponential backoff for up to 2 seconds // before failing. var retry_ = (func:() => Promise, delayMs?:number) : Promise => { delayMs = delayMs || 10; return func().catch((err) => { delayMs *= 2; if (delayMs > 2000) { return Promise.reject(err); } return new Promise((F, R) => { setTimeout(() => { this.retry_(func, delayMs).then(F, R); }, delayMs); }); }); } /** * A uproxypeerconnection-like Freedom module which establishes obfuscated * connections. * * DTLS packets are intercepted by pointing WebRTC at a local "forwarding" * port; connectivity to the remote host is achieved with the help of * another preceding, short-lived, peer-to-peer connection. * * This is mostly a thin wrapper over uproxypeerconnection except for the * magic required during setup. * * TODO: Give the uproxypeerconnections name, to help debugging. * TODO: Allow obfuscation parameters be configured. */ export class Connection implements peerconnection.PeerConnection { public pcState :peerconnection.State; public dataChannels :{[channelLabel:string] : peerconnection.DataChannel}; public peerOpenedChannelQueue :handler.QueueHandler; public signalForPeerQueue :handler.Queue; public peerName :string; public onceConnecting :Promise; public onceConnected :Promise; public onceDisconnected :Promise; // A short-lived connection used to determine network addresses on which // we might be able to communicate with the remote host. private probeConnection_ :peerconnection.PeerConnection; // The list of all candidates returned by the probe connection. private probeCandidates_ :freedom_RTCPeerConnection.RTCIceCandidate[] = []; // Fulfills once we have collected all candidates from the probe connection. private probingComplete_ :(endpoints:NatPair) => void; private onceProbingComplete_ = new Promise((F, R) => { this.probingComplete_ = F; }); // The obfuscated connection. private obfuscatedConnection_ :peerconnection.PeerConnection; // Fulfills once we know on which port the local obfuscated RTCPeerConnection // is listening. private haveWebRtcEndpoint_ :(endpoint:net.Endpoint) => void; private onceHaveWebRtcEndpoint_ = new Promise((F, R) => { this.haveWebRtcEndpoint_ = F; }); // Fulfills once we know on which port the remote CHURN pipe is listening. private haveRemoteEndpoint_ :(endpoint:net.Endpoint) => void; private onceHaveRemoteEndpoint_ = new Promise((F, R) => { this.haveRemoteEndpoint_ = F; }); // Fulfills once we've successfully allocated the mirror pipe representing the // remote peer's signalled transport address. // At that point, we can inject its address into candidate messages destined // for the local RTCPeerConnection. private haveForwardingSocketEndpoint_ :(endpoint:net.Endpoint) => void; private onceHaveForwardingSocketEndpoint_ = new Promise((F, R) => { this.haveForwardingSocketEndpoint_ = F; }); // A map from remote transport addresses to local pipes that represent them. private mirrorPipes_ : { [k: string]: ChurnPipe } = {}; private static internalConnectionId_ = 0; constructor(probeRtcPc:freedom_RTCPeerConnection.RTCPeerConnection, peerName?:string) { this.peerName = peerName || 'churn-connection-' + (++Connection.internalConnectionId_); this.signalForPeerQueue = new handler.Queue(); // Configure the probe connection. Once it completes, inform the remote // peer which public endpoint we will be using. this.onceProbingComplete_.then((endpoints:NatPair) => { this.signalForPeerQueue.handle({ publicEndpoint: endpoints.external }); }); // Start the obfuscated connection. this.configureObfuscatedConnection_(); // Once the obfuscated connection's local endpoint is known, the remote // peer has sent us its public endpoint, and probing is complete, we can // configure the obfuscating pipe and allow traffic to flow. this.configureProbeConnection_(probeRtcPc); Promise.all([this.onceHaveWebRtcEndpoint_, this.onceHaveRemoteEndpoint_, this.onceProbingComplete_]).then((answers:any[]) => { this.configurePipes_(answers[0], answers[1], answers[2]); }); // Handle |pcState| and related promises. this.pcState = peerconnection.State.WAITING; this.onceConnecting = this.obfuscatedConnection_.onceConnecting.then( () => { this.pcState = peerconnection.State.CONNECTING; }); this.onceConnected = this.obfuscatedConnection_.onceConnected.then(() => { this.pcState = peerconnection.State.CONNECTED; }); this.onceDisconnected = this.obfuscatedConnection_.onceDisconnected.then( () => { this.pcState = peerconnection.State.DISCONNECTED; }); // Debugging. this.onceProbingComplete_.then((endpoint:NatPair) => { log.debug('%1: NAT endpoints of probe connection are %2', this.peerName, JSON.stringify(endpoint)); }); this.onceHaveWebRtcEndpoint_.then((endpoint:net.Endpoint) => { log.debug('%1: obfuscated connection is bound to %2', this.peerName, JSON.stringify(endpoint)); }); this.onceHaveRemoteEndpoint_.then((endpoint:net.Endpoint) => { log.debug('%1: remote peer is contactable at %2', this.peerName, JSON.stringify(endpoint)); }); } private configureProbeConnection_ = ( freedomPc:freedom_RTCPeerConnection.RTCPeerConnection) => { var probePeerName = this.peerName + '-probe'; this.probeConnection_ = new peerconnection.PeerConnectionClass( freedomPc, probePeerName); this.probeConnection_.signalForPeerQueue.setSyncHandler( (message:signals.Message) => { if (message.type === signals.Type.CANDIDATE) { this.probeCandidates_.push(message.candidate); } else if (message.type === signals.Type.NO_MORE_CANDIDATES) { this.probeConnection_.close().then(() => { this.probingComplete_(selectPublicAddress(this.probeCandidates_)); }); } }); this.probeConnection_.negotiateConnection(); } // Add and return a local pipe that represents a specific remote address // for both send and receive. private addLocalPipe_ = ( webRtcEndpoint:net.Endpoint, remoteEndpoint:net.Endpoint, publicPipe:ChurnPipe) : Promise => { log.info('%1: Adding local pipe between %2 and %3', this.peerName, webRtcEndpoint, remoteEndpoint); var key = makeEndpointKey_(remoteEndpoint); if (this.mirrorPipes_[key]) { log.warn('%1: Got redundant call to add local pipe for %2', this.peerName, key); // Return the pipe, but wait until it's ready. return this.mirrorPipes_[key].getLocalEndpoint().then( (ignored:net.Endpoint) => { return this.mirrorPipes_[key]; }); } var localPipe = freedom['churnPipe'](); this.mirrorPipes_[key] = localPipe; // Packets received by this pipe should be obfuscated and forwarded // to the corresponding remote endpoint. localPipe.on('message', (m:churn_pipe_types.Message) => { publicPipe.sendTo(m.data, remoteEndpoint); }); return localPipe.bind( '127.0.0.1', 0, webRtcEndpoint.address, webRtcEndpoint.port, 'none', // no need to obfuscate local-only traffic. undefined, undefined) .catch((e:Error) => { log.error('%1: error establishing local pipe: %2', this.peerName, e.message); }) .then(localPipe.getLocalEndpoint) .then((forwardingSocketEndpoint:net.Endpoint) => { log.info('%1: configured local pipe between %2 and %3', this.peerName, JSON.stringify(forwardingSocketEndpoint), JSON.stringify(webRtcEndpoint)); return localPipe; }); } private bindPublicPipe_ = (publicPipe:ChurnPipe, local:net.Endpoint, remote:net.Endpoint) : Promise => { return publicPipe.bind( local.address, local.port, remote.address, remote.port, 'caesar', new Uint8Array([13]).buffer, '{}'); }; // Establishes the two pipes required to sustain the obfuscated // connection: // - a non-obfuscated, local only, between WebRTC and a new, // automatically allocated, port // - remote, obfuscated, port private configurePipes_ = ( webRtcEndpoint:net.Endpoint, remoteEndpoint:net.Endpoint, natEndpoints:NatPair) : void => { log.debug('%1: configuring pipes...', this.peerName); var publicPipe = freedom['churnPipe'](); // This retry is needed because the browser releases the UDP port // asynchronously after we call close() on the RTCPeerConnection, so // this.bindPublicPipe_ may initially fail, until the port is released. retry_(() => { return this.bindPublicPipe_( publicPipe, natEndpoints.internal, remoteEndpoint); }) // TODO(ldixon): renable FTE support instead of caesar cipher. // publicPipe.bind( // natEndpoints.internal.address, // natEndpoints.internal.port, // remoteEndpoint.address, // remoteEndpoint.port, // 'fte', // arraybuffers.stringToArrayBuffer('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), // JSON.stringify({ // 'plaintext_dfa': regex2dfa('^.*$'), // 'plaintext_max_len': 1400, // // This is equivalent to Rabbit cipher. // 'ciphertext_dfa': regex2dfa('^.*$'), // 'ciphertext_max_len': 1450 // })) .then(() => { log.info('%1: configured obfuscating pipe between %2 and %3', this.peerName, JSON.stringify(natEndpoints.internal), JSON.stringify(remoteEndpoint)); }, (e:Error) => { log.error('%1: error establishing public pipe between %2 and %3: %4', this.peerName, makeEndpointKey_(natEndpoints.internal), makeEndpointKey_(remoteEndpoint), e.message); }); publicPipe.on('message', (m:churn_pipe_types.Message) => { // This is the particular local pipe associated with this sender. var localPipe = this.mirrorPipes_[makeEndpointKey_(m.source)]; if (localPipe) { // Note: due to asynchronous setup, it's possible that this pipe // has not yet been bound. Hopefully, the send call will be // queued behind the bind call. If not, the packet may just be // dropped (which should be acceptable for a brief period). localPipe.send(m.data); } else if (this.pcState == peerconnection.State.WAITING || this.pcState == peerconnection.State.CONNECTING) { log.info('%1: Got packet from new source; peer has symmetric NAT?', this.peerName); this.addLocalPipe_(webRtcEndpoint, m.source, publicPipe).then( (localPipe:ChurnPipe) => { // Don't drop the first packet. localPipe.send(m.data); }); } else { log.warn('%1: Received unexpected packet of length %2 from %3' + ' while in state %4', this.peerName, m.data.byteLength, makeEndpointKey_(m.source), this.pcState); } }); this.addLocalPipe_(webRtcEndpoint, remoteEndpoint, publicPipe).then( (pipe:ChurnPipe) => { return pipe.getLocalEndpoint(); }).then(this.haveForwardingSocketEndpoint_); } private configureObfuscatedConnection_ = () => { // We use an empty configuration to ensure that no STUN servers are pinged. var obfConfig :freedom_RTCPeerConnection.RTCConfiguration = { iceServers: [] }; var obfPeerName = this.peerName + '-obfuscated'; var freedomPc = freedom['core.rtcpeerconnection'](obfConfig); this.obfuscatedConnection_ = new peerconnection.PeerConnectionClass( freedomPc, obfPeerName); this.obfuscatedConnection_.signalForPeerQueue.setSyncHandler( (message:signals.Message) => { // Super-paranoid check: remove candidates from SDP messages. // This can happen if a connection is re-negotiated. // TODO: We can safely remove this once we can reliably interrogate // peerconnection endpoints. if (message.type === signals.Type.OFFER || message.type === signals.Type.ANSWER) { message.description.sdp = filterCandidatesFromSdp(message.description.sdp); } if (message.type === signals.Type.CANDIDATE) { // This will tell us on which port webrtc is operating. // Record it and inject a fake endpoint, to be sure the remote // side never knows the real address (can be an issue when both // hosts are on the same network). try { if (!message.candidate || !message.candidate.candidate) { throw new Error('no candidate line'); } var address = extractEndpointFromCandidateLine( message.candidate.candidate); // TODO: We cannot currently support IPv6 addresses: // https://github.com/uProxy/uproxy/issues/1107 if (ipaddr.process(address.address).kind() === 'ipv6') { throw new Error('ipv6 unsupported'); } this.haveWebRtcEndpoint_(address); message.candidate.candidate = setCandidateLineEndpoint( message.candidate.candidate, { address: '0.0.0.0', port: 0 }); } catch (e) { log.debug('%1: ignoring candidate line %2: %3', this.peerName, JSON.stringify(message), e.message); } } var churnSignal :ChurnSignallingMessage = { webrtcMessage: message }; this.signalForPeerQueue.handle(churnSignal); }); // NOTE: Replacing |this.dataChannels| in this way breaks recursive nesting. // If the caller or |obfuscatedConnection_| applies the same approach, // the code will break in hard-to-debug fashion. This could be // addressed by using a javascript "getter", or by changing the // peerconnection.PeerConnection API. this.dataChannels = this.obfuscatedConnection_.dataChannels; this.peerOpenedChannelQueue = this.obfuscatedConnection_.peerOpenedChannelQueue; } public negotiateConnection = () : Promise => { return this.obfuscatedConnection_.negotiateConnection(); } // Forward the message to the relevant stage: churn-pipe or obfuscated. // In the case of obfuscated signalling channel messages, we inject our // local forwarding socket's endpoint. public handleSignalMessage = ( churnMessage:ChurnSignallingMessage) : void => { if (churnMessage.publicEndpoint !== undefined) { this.haveRemoteEndpoint_(churnMessage.publicEndpoint); } if (churnMessage.webrtcMessage) { var message = churnMessage.webrtcMessage; if (message.type === signals.Type.CANDIDATE) { this.onceHaveForwardingSocketEndpoint_.then( (forwardingSocketEndpoint:net.Endpoint) => { message.candidate.candidate = setCandidateLineEndpoint( message.candidate.candidate, forwardingSocketEndpoint); this.obfuscatedConnection_.handleSignalMessage(message); }); } else if (message.type == signals.Type.OFFER || message.type == signals.Type.ANSWER) { // Remove candidates from the SDP. This is redundant, but ensures // that a bug in the remote client won't cause us to send // unobfuscated traffic. message.description.sdp = filterCandidatesFromSdp(message.description.sdp); this.obfuscatedConnection_.handleSignalMessage(message); } } } public openDataChannel = (channelLabel:string, options?:freedom_RTCPeerConnection.RTCDataChannelInit) : Promise => { return this.obfuscatedConnection_.openDataChannel(channelLabel); } public close = () : Promise => { return this.obfuscatedConnection_.close(); } public toString = () : string => { return this.obfuscatedConnection_.toString(); }; }