commit a0688866101db87c5e4692dd8523c3c338efbcb5 Author: Quinn Jarrell qjarrell@gosynapsify.com Date: Mon Jun 2 15:30:46 2014 -0400
Added a configuration class which reads in a fog-file. Added example fog-file. Added launching from a fog-file. --- example-fog-config | 13 ++++ obfs-flash-client | 206 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 178 insertions(+), 41 deletions(-)
diff --git a/example-fog-config b/example-fog-config new file mode 100644 index 0000000..746e5ad --- /dev/null +++ b/example-fog-config @@ -0,0 +1,13 @@ +#Based off of ticket #9744 +#Client transports are setup like so: +#ClientTransportPlugin name commandline +#For instance to launch obfs3, the client transport line should be this +#ClientTransportPlugin obfs3 obfsproxy managed +# +#For chaining transports together, an alias line is used. +#Alias chainname firsttransportname|secondtransportname +#tor expects alias to use underscores instead of pipes. So an alias links the tor version of a plugin chain to the actual plugins. See ticket #9580 + +ClientTransportPlugin dummy obfsproxy managed +ClientTransportPlugin b64 obfsproxy managed +Alias dummy_b64 dummy|b64 diff --git a/obfs-flash-client b/obfs-flash-client index 32c0c9c..cc45d40 100755 --- a/obfs-flash-client +++ b/obfs-flash-client @@ -25,7 +25,10 @@ from twisted.protocols.portforward import ProxyServer as _ProxyServer from twisted.python import log from txsocksx.client import SOCKS4ClientEndpoint
-PT_METHOD_NAME = "obfs3_flashproxy" +import shlex + +import logging +
def pt_child_env(managed_ver, env=os.environ): """ @@ -38,7 +41,7 @@ def pt_child_env(managed_ver, env=os.environ): cur_env.append(('TOR_PT_MANAGED_TRANSPORT_VER', ','.join(managed_ver))) return cur_env
-class CMethod(namedtuple('MethodSpec', 'name protocol addrport args opts')): +class MethodSpec(namedtuple('MethodSpec', 'name protocol addrport args opts')): @classmethod def fromLine(cls, line): args = line.rstrip('\n').split(' ') @@ -47,7 +50,7 @@ class CMethod(namedtuple('MethodSpec', 'name protocol addrport args opts')): addrport = parse_addr_spec(args[2]) args = args[3][-5:].split(',') if len(args) > 3 and args[3].startswith("ARGS=") else [] opts = args[4][-9:].split(',') if len(args) > 4 and args[4].startswith("OPT-ARGS=") else [] - return CMethod(name, protocol, addrport, args, opts) + return MethodSpec(name, protocol, addrport, args, opts)
def branch(parent): """ @@ -103,7 +106,7 @@ class ManagedTransportProtocolV1(LineReceiver): if version != self.protocol_version: self._abort(ValueError("child used unsupported managed transport version: %s" % version)) elif kw == "CMETHOD": - cmethod = CMethod.fromLine(args) + cmethod = MethodSpec.fromLine(args) self.cmethods[cmethod.name] = cmethod elif kw == "CMETHODS" and args == "DONE": self._fireCMethodsDone().callback(self.cmethods) @@ -218,99 +221,220 @@ if sys.platform == "win32":
self.proto.makeConnection(self)
-def pt_launch_child(reactor, client, methodnames, cmdline): +def pt_launch_child(reactor, client, methodnames, pt_method_name, cmdline): """Launch a child PT and ensure it has the right transport methods.""" cur_env = pt_child_env(ManagedTransportProtocolV1.protocol_version) + environment = dict(cur_env + { + "TOR_PT_CLIENT_TRANSPORTS": ",".join(methodnames), + }.items()) sub_proc = Popen(cmdline, stdout = PIPE, - env = dict(cur_env + { - "TOR_PT_CLIENT_TRANSPORTS": ",".join(methodnames), - }.items()), + env = environment, ) sub_protocol = ManagedTransportProtocolV1() # we ought to pass reactor=reactor in below, but this breaks Twisted 12 StandardIO(sub_protocol, stdin=sub_proc.stdout.fileno()) methoddefers = [sub_protocol.whenCMethodsDone().addCallback( - partial(pt_require_child, client, name)) + partial(pt_require_child, client, name, pt_method_name)) for name in methodnames] return sub_proc, sub_protocol, methoddefers
-def pt_require_child(client, childmethod, cmethods): +def pt_require_child(client, childmethod, pt_method_name, cmethods): """Callback for checking a child PT has the right transport methods.""" if childmethod not in cmethods: - client.reportMethodError(PT_METHOD_NAME, "failed to start required child transport: %s" % childmethod) + client.reportMethodError(pt_method_name, "failed to start required child transport: %s" % childmethod) raise ValueError() return cmethods[childmethod]
-def obfs3_flashproxy(reactor, client, obfs_out, fp_remote, fp_args=[], fp_local=0): +def pt_launch_pair(reactor, client, callback_port, configuration, pt_method_name): """ - Set up the obfs3_flashproxy combined PT. - + Launches a pair of pluggable transports. Currently only supports chaining two transports together. + The pair is set by pt_method_names :param twisted.internet.interfaces.IReactor reactor: Reactor to install this PT to. :param pyptlib.client.ClientTransportPlugin client: PT client API. - :param int obfs_out: Local listen port for obfsproxy to connect to. - :param str fp_remote: Listen address for remote flashproxy connections. - :param int fp_local: Local listen port for local flashproxy connections. + :param int callback_port: Local listen port for the first PT to connect to. + :param Config configuration: The configuration structure for this pair. + :param String pt_method_name: The name of the pt chain to launch. Ex: "obfs3_flashproxy" """ - ob_client = os.getenv("OBFSPROXY", "obfsproxy") - fp_client = os.getenv("FLASHPROXY_CLIENT", "flashproxy-client") + #TODO Modify pt_launch_pair to launch more than 2 transports + + if pt_method_name in configuration.alias_map: + pt_chain = configuration.alias_map[pt_method_name] + else: + logging.error('Pluggable Transport Combination %s not found in configuration alias map.' % pt_method_name) + raise KeyError()
- # start fp and ensure that it launches OK - _, _, fp_defer = pt_launch_child(reactor, client, - ["flashproxy"], [fp_client, "--transport", "obfs3|websocket"] + fp_args + - ['127.0.0.1:%s' % fp_local, fp_remote]) + defer_list = []
- # start ob and ensure that it launches OK - _, _, ob_defer = pt_launch_child(reactor, client, - ["obfs3"], [ob_client, "managed"]) + for pt in pt_chain: + if pt in configuration.transport_map: + pt_cmdline = configuration.transport_map[pt] + else: + logging.error("Pluggable transport %s not found in transport_map. Check your configuration file." % str(pt)) + raise ValueError() + _, _, defer = pt_launch_child(reactor, client, [pt], pt_method_name, pt_cmdline) + defer_list.extend(defer) + whenAllDone = DeferredList(defer_list, consumeErrors=False)
- whenAllDone = DeferredList(fp_defer + ob_defer, consumeErrors=True) def allDone(success_list): - success = all(r[0] for r in success_list) + """ + :param success_list: A list of tuples containing a launch status boolean, MethodSpec pairs. + Ex: [(True, MethodSpec(name='dummy', protocol='socks4', addrport=('127.0.0.1', 58982), args=[], opts=[])), + (True, MethodSpec(name='b64', protocol='socks4', addrport=('127.0.0.1', 58981), args=[], opts=[]))] + """ + + success = all(r[0] for r in success_list if r[1].name in pt_chain) # failure was already reported by pt_require_child, just return if not success: return - fp_result = [r[1] for r in success_list if r[1].name == "flashproxy"][0] - ob_result = [r[1] for r in success_list if r[1].name == "obfs3"][0]
- fp_port = fp_result.addrport[1] # if port was 0, read the actual port + result1 = [r[1] for r in success_list if r[1].name == pt_chain[0]][0] # Normally obfs3 result + result2 = [r[1] for r in success_list if r[1].name == pt_chain[1]][0] # Normally flashproxy result + destination_port = result2.addrport[1] # if port was 0, read the actual port # shim to wrap obfs data output to SOCKS input for flashproxy - reactor.listenTCP(interface='127.0.0.1', port=obfs_out, factory= - SOCKS4WrapperFactory('127.0.0.1', fp_port, '0.0.1.0', 1)) - + reactor.listenTCP(interface='127.0.0.1', port=callback_port, factory= + SOCKS4WrapperFactory('127.0.0.1', destination_port, '0.0.1.0', 1)) # now report success of the overall composed PT - client.reportMethodSuccess(PT_METHOD_NAME, ob_result.protocol, ob_result.addrport) + client.reportMethodSuccess(pt_method_name, result1.protocol, result1.addrport) client.reportMethodsEnd() whenAllDone.addCallback(allDone)
+class Config(): + # Transport map links a pluggable transport name to the a commandline to launch it. + # Ex: {'b64' : 'exec obfsproxy managed'} + transport_map = None + + #Alias map links a pluggable transport chain name to a list of individual pluggable transports + # Ex: {'dummy_b64_dummy2' : ['dummy''b64''dummy2']} + alias_map = None + + def __init__(self, transport_map, alias_map): + self.transport_map = transport_map + self.alias_map = alias_map + + def __repr__(self): + return "Config(%s, %s)" % (self.transport_map, self.alias_map) + + def __str__(self): + return "Config Object with transport_map: %s, and alias_map %s." % (self.transport_map, self.alias_map) + + @classmethod + def parse(cls, config_string): + """ + Reads a configuration string and returns an instance of configuration. Uses shlex to parse configuration lines. + :param String config_string: The string which will be parsed to populate the transport_map and alias_map hash tables. + See the file example-fog-config for format. + """ + # TODO Add possibility of reading a ClientTransportPlugin with multiple transport types + # Ex: ClientTransportPlugin obfs3,scramblesuit obfsclient --option=value + + line_counter = 0 + lines = config_string.split('\n') + transport_map = {} + alias_map = {} + + for line in lines: + line_counter += 1 + if len(line) > 0 and line[0] != '#' : # Check for empty lines and comment tags on the first + line = line.strip() + delimited_tokens = shlex.split(line) + if len(delimited_tokens) > 1: + config_line_type = delimited_tokens[0] # This can be either Alias or ClientTransportPlugin + if config_line_type == 'ClientTransportPlugin': + cls.parse_transport_line(transport_map, delimited_tokens, line_counter) + elif config_line_type == 'Alias': + cls.parse_alias_line(alias_map, transport_map, delimited_tokens, line_counter) + else: + logging.warn("Configuration file has unknown line %s: '%s'" % (line_counter, line)) + return cls(transport_map, alias_map) + + @classmethod + def parse_transport_line(cls, transport_map, delimited_tokens, line_counter): + transport_name = delimited_tokens[1] + transport_cmdline = delimited_tokens[2:] + if transport_name in transport_map: + raise ValueError('Configuration file has duplicate ClientTransportPlugin lines. Duplicate line is at line number %s' % line_counter) + transport_map[transport_name] = transport_cmdline + + @classmethod + def parse_alias_line(cls, alias_map, transport_map, delimited_tokens, line_counter): + alias_name = delimited_tokens[1] # Example: "obfs3_flashproxy" + alias_path = delimited_tokens[2].split('|') # Example: "obfs3|flashproxy" + if alias_name in alias_map: + raise ValueError('Configuration file has duplicate Alias lines. Duplicate line is at line number %s' % line_counter) + for pt_name in alias_path: + if pt_name not in transport_map: + raise KeyError('Transport map is missing pluggable transport %s needed for chain %s. Check your configuration file for a ClientTransportPlugin line can launch %s' % (pt_name, alias_name, pt_name)) + alias_map[alias_name] = alias_path + +def obfs3_flashproxy(reactor, client, callback_port, fp_remote, fp_args=[], fp_local=0): + """ + Set up the obfs3_flashproxy combined PT. + + :param twisted.internet.interfaces.IReactor reactor: Reactor to install this PT to. + :param pyptlib.client.ClientTransportPlugin client: PT client API. + :param int callback_port: Local listen port for obfsproxy to connect to. + :param str fp_remote: Listen address for remote flashproxy connections. + :param int fp_local: Local listen port for local flashproxy connections. + """ + + ob_client = os.getenv("OBFSPROXY", "obfsproxy") + fp_client = os.getenv("FLASHPROXY_CLIENT", "flashproxy-client") + + fp_cmdline = [fp_client, "--transport", '"obfs3|websocket"'] + fp_args + ['127.0.0.1:%s' % fp_local, fp_remote] + obfs_cmd_line = [ob_client, "managed"] + + transport_map = {'obfs3': obfs_cmd_line, 'flashproxy': fp_cmdline} + alias_map = {'obfs3_flashproxy': ['obfs3', 'flashproxy']} + + configuration = Config(transport_map, alias_map) + pt_launch_pair(reactor, client, callback_port, configuration, "obfs3_flashproxy") # Launch + def main(*args): parser = argparse.ArgumentParser() - parser.add_argument("obfs_out", help="local listen port for obfsproxy to " - "connect to. This must match the appropriate 'Bridge %s' line in your " - "torrc." % PT_METHOD_NAME, + parser.add_argument("callback_port", help="local listen port for obfsproxy to " + "connect to. This must match the appropriate bridge line in your " # TODO print bridge line + "torrc.", metavar='PORT', type=int) parser.add_argument("fp_remote", help="remote connections listen address " "for flashproxy, default %(default)s", metavar='REMOTE:PORT', nargs='?', default=":9000") parser.add_argument("--fp-arg", help="arguments for flashproxy-client", metavar='ARG', action='append') + parser.add_argument("-f", help="fog configuration file path", + metavar='FOGFILE', type=argparse.FileType('r')) + # TODO(infinity0): add an "external" mode, which would require us to run # obfsproxy in external mode too.
opts = parser.parse_args(args) + # ensure str address is valid _, _, = parse_addr_spec(opts.fp_remote, defhost="0.0.0.0") + configuration = None + + if opts.f: + file_contents = opts.f.read() + configuration = Config.parse(file_contents) + pt_method_names = configuration.alias_map.keys()
client = ClientTransportPlugin() - client.init([PT_METHOD_NAME]) + client.init(pt_method_names) # Initialize our possible methods to all the chains listed by the fog file and stored in alias map. if not client.getTransports(): - print >> sys.stderr, "no transports to serve" + logging.error("no transports to serve. pt_method_names may be invalid.") return 1
from twisted.internet import reactor auto_killall(1, cleanup=reactor.stop) - obfs3_flashproxy(reactor, client, opts.obfs_out, opts.fp_remote, opts.fp_arg or []) + #TODO Change from launching a single pair to launching multiple chains. + if configuration: + pt_launch_pair(reactor, client, opts.callback_port, configuration, pt_method_names[0]) + else: + logging.warn("No configuration file specified. Defaulting to launching obfs|flashproxy pair.") + obfs3_flashproxy(reactor, client, opts.callback_port, opts.fp_remote, opts.fp_arg or []) + reactor.run(installSignalHandlers=0) return 0
if __name__ == "__main__": sys.exit(main(*sys.argv[1:])) +
tor-commits@lists.torproject.org