#!/usr/bin/env python # -*- coding: utf-8 -*- # # EtherWebSocket tunneling Server/Client # # depends on: # - python-2.7.2 # - python-pytun-0.2 # - websocket-client-0.7.0 # - tornado-2.2.1 # # todo: # - direct binary transmission support (to improve performance) # # =========================================================================== # Copyright (c) 2012, Atzm WATANABE # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # =========================================================================== # # $Id$ import os import sys import base64 import select import argparse import threading import pytun import websocket import tornado.httpserver import tornado.ioloop import tornado.web import tornado.websocket class TapListener(threading.Thread): daemon = True def __init__(self, dev, debug=False): super(TapListener, self).__init__() self._debug = debug self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI) self._tap_lock = threading.Lock() self._clients = [] self._clients_lock = threading.Lock() try: self._tap_lock.acquire() self._tap.up() finally: self._tap_lock.release() def register_client(self, client): try: self._clients_lock.acquire() self._clients.append(client) finally: self._clients_lock.release() def unregister_client(self, client): try: self._clients_lock.acquire() self._clients.remove(client) except ValueError: pass finally: self._clients_lock.release() def write(self, caller, message): if self._debug: sys.stderr.write('%s: %s\n' % (caller.__class__.__name__, message.encode('hex'))) try: self._clients_lock.acquire() clients = self._clients[:] finally: self._clients_lock.release() if caller is not self: clients.remove(caller) try: self._tap_lock.acquire() self._tap.write(message) finally: self._tap_lock.release() message = base64.b64encode(message) for c in clients: c.write_message(message) def run(self): epoll = select.epoll() try: self._tap_lock.acquire() epoll.register(self._tap.fileno(), select.EPOLLIN) finally: self._tap_lock.release() while True: evts = epoll.poll(1) for fileno, evt in evts: try: self._tap_lock.acquire() data = self._tap.read(self._tap.mtu) finally: self._tap_lock.release() self.write(self, data) class EtherWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, app, req, tap, debug=False): super(EtherWebSocket, self).__init__(app, req) self._tap = tap self._debug = debug def open(self): if self._debug: sys.stderr.write('[%s] opened\n' % self.request.remote_ip) self._tap.register_client(self) def on_message(self, message): if self._debug: sys.stderr.write('[%s] received\n' % self.request.remote_ip) self._tap.write(self, base64.b64decode(message)) def on_close(self): if self._debug: sys.stderr.write('[%s] closed\n' % self.request.remote_ip) self._tap.unregister_client(self) def daemonize(nochdir=False, noclose=False): if os.fork() > 0: sys.exit(0) os.setsid() if os.fork() > 0: sys.exit(0) if not nochdir: os.chdir('/') if not noclose: os.umask(0) sys.stdin.close() sys.stdout.close() sys.stderr.close() os.close(0) os.close(1) os.close(2) sys.stdin = open(os.devnull) sys.stdout = open(os.devnull, 'a') sys.stderr = open(os.devnull, 'a') def server_main(args): tap = TapListener(args.device, debug=args.debug) tap.start() app = tornado.web.Application([ (args.path, EtherWebSocket, {'tap': tap, 'debug': args.debug}), ]) server = tornado.httpserver.HTTPServer(app) server.listen(args.port, address=args.address) tornado.ioloop.IOLoop.instance().start() def client_main(args): if args.debug: websocket.enableTrace(True) tap = TapListener(args.device, debug=args.debug) client = websocket.WebSocketApp(args.uri) client.write_message = client.send client.on_message = lambda s, m: tap.write(client, base64.b64decode(m)) if args.debug: client.on_error = lambda s, e: sys.stderr.write(str(e) + '\n') client.on_close = lambda s: sys.stderr.write('closed\n') tap.register_client(client) tap.start() client.run_forever() def main(): parser = argparse.ArgumentParser() parser.add_argument('--device', action='store', default='ethws%d') parser.add_argument('--foreground', action='store_true', default=False) parser.add_argument('--debug', action='store_true', default=False) subparsers = parser.add_subparsers(dest='subcommand') parser_server = subparsers.add_parser('server') parser_server.add_argument('--address', action='store', default='') parser_server.add_argument('--port', action='store', type=int, default=80) parser_server.add_argument('--path', action='store', default='/') parser_client = subparsers.add_parser('client') parser_client.add_argument('--uri', action='store', required=True) args = parser.parse_args() if not args.foreground: daemonize() if args.subcommand == 'server': server_main(args) elif args.subcommand == 'client': client_main(args) if __name__ == '__main__': main()