tunnel.py 4.95 KB
Newer Older
Daniel Stan's avatar
Daniel Stan committed
1 2 3
#!/usr/bin/env python
# -*- coding: utf-8 -*-

Daniel Stan's avatar
Daniel Stan committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17
"""
Lance un tunnel ssh pour un site ou une plage IP, via une connexion ssh,
les connexions vers ce site sont automatiquement redirigées (iptables et
redsocks) pour l'utilisateur courant.

Exemple :
sudo ./tunnel.py monip.org zamok.crans.org
# Une connexion sur http://monip.org affiche désormais zamok

sudo ./tunnel.py 10.231.136.0/24 vo.crans.org
# Toute connexion vers adm est tunnelée

"""

Daniel Stan's avatar
Daniel Stan committed
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
import sys
import os
import socket
import subprocess
import getpass
import tempfile
import atexit


REDSOCKS_TPL = """
base {
    // debug: connection progress & client list on SIGUSR1
    log_debug = off;

    // info: start and end of client session
    log_info = on;

    /* possible `log' values are:
     *   stderr
     *   "file:/path/to/file"
     *   syslog:FACILITY  facility is any of "daemon", "local0"..."local7"
     */
    log = "syslog:daemon";

    // detach from console
    daemon = off;

    /* Change uid, gid and root directory, these options require root
     * privilegies on startup.
     * Note, your chroot may requre /etc/localtime if you write log to syslog.
     * Log is opened before chroot & uid changing.
     */
    user = %(user)s;
    group = redsocks;
    // chroot = "/var/chroot";

    /* possible `redirector' values are:
     *   iptables   - for Linux
     *   ipf        - for FreeBSD
     *   pf         - for OpenBSD
     *   generic    - some generic redirector that MAY work
     */
    redirector = iptables;
}

redsocks {
    /* `local_ip' defaults to 127.0.0.1 for security reasons,
     * use 0.0.0.0 if you want to listen on every interface.
     * `local_*' are used as port to redirect to.
     */
    local_ip = 127.0.0.1;
    local_port = %(local_port)d;

    // `ip' and `port' are IP and tcp-port of proxy-server
    // You can also use hostname instead of IP, only one (random)
    // address of multihomed host will be used.
    ip = 127.0.0.1;
    port = %(port)d;


    // known types: socks4, socks5, http-connect, http-relay
    type = socks5;

    // login = "foobar";
    // password = "baz";
}
"""

def _fresh_localhost_port():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('',0))
    p = s.getsockname()[1]
    s.close()
    return p

class Tunnel(object):
    
    user = None
    rules = None
    redsocks_proc = None

99
    def __init__(self):
Daniel Stan's avatar
Daniel Stan committed
100 101 102
        self.user = os.getenv("SUDO_USER") or getpass.getuser() 
        self.rules = []
        self.local_port = _fresh_localhost_port()
103
        self.port = _fresh_localhost_port()
Daniel Stan's avatar
Daniel Stan committed
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

    def forward_ip(self, ip):
        rule=(
          'nat',
          'OUTPUT',
          [
            '-p', 'tcp',
            '--dst', ip,
            '-m', 'owner',
            '--uid', self.user,
            '-j', 'REDIRECT',
            '--to-port', str(self.local_port),
          ])
        self.iptables(rule)

    def iptables(self, rule, delete=False):
        """Rajoute ou supprime une règle donnée"""

        cmd = ['/sbin/iptables']
        cmd += ['-t', rule[0]]
        cmd.append('-D' if delete else '-I')
        cmd.append(rule[1])
        cmd += rule[2]
127 128 129 130 131
        try:
            subprocess.check_call(cmd)
        except subprocess.CalledProcessError:
            if not delete:
                raise
Daniel Stan's avatar
Daniel Stan committed
132 133 134 135 136 137
        if delete:
            self.rules.remove(rule)
        else:
            self.rules.append(rule)

    def run_redsocks(self):
138 139 140 141 142 143 144 145 146 147
        self.iptables(('filter',
            'OUTPUT',
            [
                '-m', 'owner',
                '!', '--uid', self.user,
                '-p', 'tcp',
                '--dst', '127.0.0.1',
                '--dport', str(self.port),
                '-j', 'REJECT',
            ]))
Daniel Stan's avatar
Daniel Stan committed
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        infos = {
            'local_port': self.local_port,
            'port': self.port,
            'user': self.user,
        }
        config_file = tempfile.NamedTemporaryFile(suffix='.cfg')
        atexit.register(config_file.close)
        config_file.write(REDSOCKS_TPL % infos)
        config_file.flush()
        self.config_file = config_file
        cmd = ['/usr/sbin/redsocks', '-c', config_file.name]
        self.redsocks_proc = subprocess.Popen(cmd)

    def flush(self):
        """Vide les règles insérées depuis le début du script"""
163
        for rule in list(self.rules):
Daniel Stan's avatar
Daniel Stan committed
164 165 166 167 168 169 170
            self.iptables(rule, delete=True)

    def run_ssh(self, host):
        cmd = ['/usr/bin/ssh', host]
        cmd = ['sudo', '-u', self.user] + cmd
        cmd += ['-D', str(self.port)]
        proc = subprocess.Popen(cmd)
171 172 173 174
        try:
            os.waitpid(proc.pid, 0)
        except KeyboardInterrupt, subprocess.CalledProcessError:
            pass
Daniel Stan's avatar
Daniel Stan committed
175 176 177 178 179 180 181 182 183

    def clean_up(self):
        self.flush()
        if self.redsocks_proc:
            self.redsocks_proc.kill()


if __name__ == '__main__':
    if len(sys.argv) <= 2:
184
        print "Usage: tunnel.py $IP $SSH_SERVER"
Daniel Stan's avatar
Daniel Stan committed
185
        exit(1)
186
    tunnel = Tunnel()
Daniel Stan's avatar
Daniel Stan committed
187 188
    tunnel.forward_ip(sys.argv[1])
    tunnel.run_redsocks()
189
    tunnel.run_ssh(sys.argv[2])
Daniel Stan's avatar
Daniel Stan committed
190 191
    tunnel.clean_up()