///
///
///
///
///
///
///
///
// TODO: https://github.com/uProxy/uproxy-obfuscators/issues/35
var regex2dfa :any;
module Churn {
var log :Freedom_UproxyLogging.Log = freedom['core.log']('churn');
/**
* 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 Provider {
// A short-lived connection used to determine network addresses on which
// we can communicate with the remote host.
private surrogateConnection_ :freedom_UproxyPeerConnection.Pc;
// The obfuscated connection.
private obfuscatedConnection_ :freedom_UproxyPeerConnection.Pc;
// Fulfills once obfuscatedConnection_ has been configured.
// At that point, the negotiator can safely attempt to
// negotiate the obfuscated peerconnection.
private pc2Setup_ :() => void;
private oncePc2Setup_ = new Promise((F, R) => {
this.pc2Setup_ = F;
});
// Fulfills once we know on which port the RTCPeerConnection used to
// establish the obfuscated peerconnection is listening.
private haveWebRtcEndpoint_ :(endpoint:freedom_Pipe.Endpoint) => void;
private onceHaveWebRtcEndpoint_ = new Promise((F, R) => {
this.haveWebRtcEndpoint_ = F;
});
// Fulfills once we've successfully started an obfuscated peerconnection.
private churnSetup_ :() => void;
private onceChurnSetup_ = new Promise((F, R) => {
this.churnSetup_ = F;
});
// Fulfills once we've successfully allocated the forwarding socket.
// At that point, we can inject its address into candidate messages destined
// for the local RTCPeerConnection.
private haveForwardingSocketEndpoint_ :(endpoint:freedom_Pipe.Endpoint) => void;
private onceHaveForwardingSocketEndpoint_ = new Promise((F, R) => {
this.haveForwardingSocketEndpoint_ = F;
});
constructor(
private dispatchEvent_:(name:string, args:any) => void,
config:WebRtc.PeerConnectionConfig) {
// TODO: Remove when objects-for-constructors is fixed in Freedom:
// https://github.com/freedomjs/freedom/issues/87
if (Array.isArray(config)) {
// Extract the first element of this single element array.
config = ( config)[0];
}
// Configure the surrogate connection. Once it's been successfully
// established *and* we know on which port WebRTC is listening we have all
// the information we need in order to configure the pipes required to
// establish the obfuscated connection.
this.configureSurrogateConnection_(config);
Promise.all([this.onceHaveWebRtcEndpoint_,
this.surrogateConnection_.onceConnected()]).then((answers:any[]) => {
this.configurePipes_(answers[0], answers[1]);
});
}
private configureSurrogateConnection_ = (
config:WebRtc.PeerConnectionConfig) => {
log.debug('configuring surrogate connection...');
this.surrogateConnection_ = freedom['core.uproxypeerconnection'](config);
this.surrogateConnection_.on('signalForPeer',
(signal:WebRtc.SignallingMessage) => {
var churnSignal :Churn.ChurnSignallingMessage =
signal;
churnSignal.churnStage = 1;
this.dispatchEvent_('signalForPeer', churnSignal);
});
// Once the surrogate connection has been successfully established,
// we want to tear it down and setup the obfuscated connection.
this.surrogateConnection_.onceConnected().then(
(endpoints:WebRtc.ConnectionAddresses) => {
this.surrogateConnection_.close().then(() => {
this.configureObfuscatedConnection_(endpoints);
});
});
}
// 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:freedom_Pipe.Endpoint,
publicEndpoints:WebRtc.ConnectionAddresses) : void => {
log.debug('configuring pipes...');
var localPipe = freedom.pipe();
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('error setting up local pipe: ' + e.message);
})
.then(localPipe.getLocalEndpoint)
.then((forwardingSocketEndpoint:freedom_Pipe.Endpoint) => {
this.haveForwardingSocketEndpoint_(forwardingSocketEndpoint);
log.info('configured local pipe between forwarding socket at ' +
forwardingSocketEndpoint.address + ':' +
forwardingSocketEndpoint.port + ' and webrtc at ' +
webRtcEndpoint.address + ':' + webRtcEndpoint.port);
var publicPipe = freedom.pipe();
publicPipe.bind(
publicEndpoints.local.address,
publicEndpoints.local.port,
publicEndpoints.remote.address,
publicEndpoints.remote.port,
'fte',
ArrayBuffers.stringToArrayBuffer('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'),
JSON.stringify({
'plaintext_dfa': regex2dfa('^.*$'),
'plaintext_max_len': 1400,
// TFTP read request for file with name "abc", by netascii.
// By default, Wireshark only looks for TFTP traffic if the packet's destination
// port is 69; you can change this in Preferences.
'ciphertext_dfa': regex2dfa('^\x00\x01\x61\x62\x63\x00netascii.*$'),
'ciphertext_max_len': 1450
}))
.then(() => {
log.info('configured obfuscating pipe: ' +
publicEndpoints.local.address + ':' +
publicEndpoints.local.port + ' <-> ' +
publicEndpoints.remote.address + ':' +
publicEndpoints.remote.port);
// Connect the local pipe to the remote, obfuscating, pipe.
localPipe.on('message', (m:freedom_Pipe.Message) => {
publicPipe.send(m.data);
});
publicPipe.on('message', (m:freedom_Pipe.Message) => {
localPipe.send(m.data);
});
})
.catch((e:Error) => {
log.error('error setting up obfuscated pipe: ' + e.message);
});
});
}
private configureObfuscatedConnection_ = (
endpoints:WebRtc.ConnectionAddresses) => {
log.debug('configuring obfuscated connection...');
// TODO: It may be safe to re-use the config supplied to the constructor.
var config :WebRtc.PeerConnectionConfig = {
webrtcPcConfig: {
iceServers: []
},
webrtcMediaConstraints: {
optional: [{DtlsSrtpKeyAgreement: true}]
}
};
this.obfuscatedConnection_ = freedom['core.uproxypeerconnection'](config);
this.obfuscatedConnection_.on('signalForPeer',
(signal:WebRtc.SignallingMessage) => {
// 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 (signal.type === WebRtc.SignalType.OFFER ||
signal.type === WebRtc.SignalType.ANSWER) {
signal.description.sdp =
Provider.filterCandidatesFromSdp(signal.description.sdp);
}
if (signal.type === WebRtc.SignalType.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).
this.haveWebRtcEndpoint_(
Churn.Provider.extractEndpointFromCandidateLine(
signal.candidate.candidate));
signal.candidate.candidate =
Churn.Provider.setCandidateLineEndpoint(
signal.candidate.candidate, {
address: '0.0.0.0',
port: 0
});
}
var churnSignal :Churn.ChurnSignallingMessage =
signal;
churnSignal.churnStage = 2;
this.dispatchEvent_('signalForPeer', churnSignal);
});
this.obfuscatedConnection_.onceConnected().then(
(endpoints:WebRtc.ConnectionAddresses) => {
this.obfuscatedConnection_.on('dataFromPeer',
this.dispatchEvent_.bind(null, 'dataFromPeer'));
this.obfuscatedConnection_.on('peerOpenedChannel',
this.dispatchEvent_.bind(null, 'peerOpenedChannel'));
this.churnSetup_();
});
this.pc2Setup_();
}
public negotiateConnection = () : Promise => {
// TODO: propagate errors.
log.debug('negotiating initial connection...');
this.surrogateConnection_.negotiateConnection();
return this.oncePc2Setup_.then(() => {
log.debug('negotiating obfuscated connection...');
return this.obfuscatedConnection_.negotiateConnection();
});
}
// Forward the message to the relevant stage: surrogate or obfuscated.
// In the case of obfuscated signalling channel messages, we inject our
// local forwarding socket's endpoint.
public handleSignalMessage = (
signal:Churn.ChurnSignallingMessage) : Promise => {
if (signal.churnStage == 1) {
return this.surrogateConnection_.handleSignalMessage(signal);
} else if (signal.churnStage == 2) {
if (signal.type === WebRtc.SignalType.CANDIDATE) {
return this.onceHaveForwardingSocketEndpoint_.then(
(forwardingSocketEndpoint:freedom_Pipe.Endpoint) => {
signal.candidate.candidate =
Churn.Provider.setCandidateLineEndpoint(
signal.candidate.candidate, forwardingSocketEndpoint);
return this.obfuscatedConnection_.handleSignalMessage(signal);
});
} else {
return this.obfuscatedConnection_.handleSignalMessage(signal);
}
} else {
// Should never happen. Incompatible remote version?
return Promise.reject(new Error(
'unknown churn stage in signalling channel message: ' +
signal.churnStage));
}
}
public openDataChannel = (channelLabel:string) : Promise => {
return this.obfuscatedConnection_.openDataChannel(channelLabel);
}
public closeDataChannel = (channelLabel:string) : Promise => {
return this.obfuscatedConnection_.closeDataChannel(channelLabel);
}
public onceDataChannelOpened = (channelLabel:string) : Promise => {
return this.obfuscatedConnection_.onceDataChannelOpened(channelLabel);
}
public onceDataChannelClosed = (channelLabel:string) : Promise => {
return this.obfuscatedConnection_.onceDataChannelClosed(channelLabel);
}
public send = (channelLabel:string, data:WebRtc.Data) : Promise => {
return this.obfuscatedConnection_.send(channelLabel, data);
}
public close = () : Promise => {
return this.obfuscatedConnection_.close();
}
public onceConnected = () : Promise => {
// obfuscatedConnection_ doesn't exist until onceChurnSetup_ fulfills.
return this.onceChurnSetup_.then(() => {
return this.obfuscatedConnection_.onceConnected();
});
}
public onceConnecting = () : Promise => {
return this.surrogateConnection_.onceConnecting();
}
public onceDisconnected = () : Promise => {
// obfuscatedConnection_ doesn't exist until onceChurnSetup_ fulfills.
return this.onceChurnSetup_.then(() => {
return this.obfuscatedConnection_.onceDisconnected();
});
}
// Strips candidate lines from an SDP.
// In general, an SDP is a newline-delimited series of lines of the form:
// x=yyy
// where x is a single character and yyy arbitrary text.
//
// ICE candidate lines look like this:
// a=candidate:1297 1 udp 2122 192.168.1.5 4533 typ host generation 0
//
// For more information on SDP, see section 6 of the RFC:
// http://tools.ietf.org/html/rfc2327
public static filterCandidatesFromSdp = (sdp:string) : string => {
return sdp.split('\n').filter((s) => {
return s.indexOf('a=candidate') != 0;
}).join('\n');
}
private static isHostCandidateLine_ = (candidate:string) : string[] => {
var lines = candidate.split(' ');
if (lines.length != 10 || lines[6] != 'typ') {
throw new Error('cannot parse candidate line: ' + candidate);
}
var typ = lines[7];
if (typ != 'host') {
throw new Error('cannot parse candidate line: ' + candidate);
}
return lines;
}
// Extracts the endpoint from an SDP candidate line.
// Raises an exception if the supplied string is not a candidate line of
// type host or the endpoint cannot be parsed.
//
// ICE candidate lines look something like this:
// a=candidate:1297 1 udp 2122 192.168.1.5 4533 typ host generation 0
//
// For more information on candidate lines, see section 15.1 of the RFC:
// http://tools.ietf.org/html/rfc5245#section-15.1
public static extractEndpointFromCandidateLine = (
candidate:string) : freedom_Pipe.Endpoint => {
var lines = Churn.Provider.isHostCandidateLine_(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
}
}
// Extracts the endpoint from an SDP candidate line.
// Raises an exception if the supplied string is not a candidate line of
// type host.
//
// See #extractEndpointFromCandidateLine.
public static setCandidateLineEndpoint = (
candidate:string, endpoint:freedom_Pipe.Endpoint) : string => {
var lines = Churn.Provider.isHostCandidateLine_(candidate);
lines[4] = endpoint.address;
lines[5] = endpoint.port.toString();
return lines.join(' ');
}
}
if (typeof freedom !== 'undefined') {
freedom.churn().providePromises(Churn.Provider);
}
}