diff --git a/client.py b/client.py index 092bc2a..daa1e6a 100644 --- a/client.py +++ b/client.py @@ -1,6 +1,6 @@ -import struct, socket, select, errno, re +import struct, socket, select, errno, re, signal import compat.ssubprocess as ssubprocess -import helpers, ssnet, ssh +import helpers, ssnet, ssh, ssyslog from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from helpers import * @@ -21,6 +21,79 @@ def _islocal(ip): return True # it's a local IP, or there would have been an error +def got_signal(signum, frame): + log('exiting on signal %d\n' % signum) + sys.exit(1) + + +_pidname = None +def check_daemon(pidfile): + global _pidname + _pidname = os.path.abspath(pidfile) + try: + oldpid = open(_pidname).read(1024) + except IOError, e: + if e.errno == errno.ENOENT: + return # no pidfile, ok + else: + raise Fatal("can't read %s: %s" % (_pidname, e)) + if not oldpid: + os.unlink(_pidname) + return # invalid pidfile, ok + oldpid = int(oldpid.strip() or 0) + if oldpid <= 0: + os.unlink(_pidname) + return # invalid pidfile, ok + try: + os.kill(oldpid, 0) + except OSError, e: + if e.errno == errno.ESRCH: + os.unlink(_pidname) + return # outdated pidfile, ok + elif e.errno == errno.EPERM: + pass + else: + raise + raise Fatal("%s: sshuttle is already running (pid=%d)" + % (_pidname, oldpid)) + + +def daemonize(): + if os.fork(): + os._exit(0) + os.setsid() + if os.fork(): + os._exit(0) + + outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666) + try: + os.write(outfd, '%d\n' % os.getpid()) + finally: + os.close(outfd) + os.chdir("/") + + # Normal exit when killed, or try/finally won't work and the pidfile won't + # be deleted. + signal.signal(signal.SIGTERM, got_signal) + + si = open('/dev/null', 'r+') + os.dup2(si.fileno(), 0) + os.dup2(si.fileno(), 1) + si.close() + + ssyslog.stderr_to_syslog() + + +def daemon_cleanup(): + try: + os.unlink(_pidname) + except OSError, e: + if e.errno == errno.ENOENT: + pass + else: + raise + + def original_dst(sock): try: SO_ORIGINAL_DST = 80 @@ -46,6 +119,8 @@ class FirewallClient: argvbase = ([sys.argv[0]] + ['-v'] * (helpers.verbose or 0) + ['--firewall', str(port)]) + if ssyslog._p: + argvbase += ['--syslog'] argv_tries = [ ['sudo', '-p', '[local sudo] Password: '] + argvbase, ['su', '-c', ' '.join(argvbase)], @@ -114,15 +189,18 @@ class FirewallClient: raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) -def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets): +def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, + syslog, daemon): handlers = [] if helpers.verbose >= 1: helpers.logprefix = 'c : ' else: helpers.logprefix = 'client: ' debug1('connecting to server...\n') + try: - (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python) + (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, + stderr=ssyslog._p and ssyslog._p.stdin) except socket.error, e: if e.args[0] == errno.EPIPE: raise Fatal("failed to establish ssh session (1)") @@ -148,6 +226,12 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets): raise Fatal('expected server init string %r; got %r' % (expected, initstring)) debug1('connected.\n') + if daemon: + daemonize() + log('daemonizing (%s).\n' % _pidname) + elif syslog: + debug1('switching to syslog.\n') + ssyslog.stderr_to_syslog() def onroutes(routestr): if auto_nets: @@ -219,7 +303,15 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets): def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets, - subnets_include, subnets_exclude): + subnets_include, subnets_exclude, syslog, daemon, pidfile): + if syslog: + ssyslog.start_syslog() + if daemon: + try: + check_daemon(pidfile) + except Fatal, e: + log("%s\n" % e) + return 5 debug1('Starting sshuttle proxy.\n') listener = socket.socket() listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -250,6 +342,13 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets, try: return _main(listener, fw, ssh_cmd, remotename, - python, seed_hosts, auto_nets) + python, seed_hosts, auto_nets, syslog, daemon) finally: - fw.done() + try: + if daemon: + # it's not our child anymore; can't waitpid + fw.p.returncode = 0 + fw.done() + finally: + if daemon: + daemon_cleanup() diff --git a/firewall.py b/firewall.py index 36ba768..044ac52 100644 --- a/firewall.py +++ b/firewall.py @@ -1,6 +1,6 @@ import re, errno import compat.ssubprocess as ssubprocess -import helpers +import helpers, ssyslog from helpers import * @@ -216,7 +216,7 @@ def restore_etc_hosts(port): # exit. In case that fails, it's not the end of the world; future runs will # supercede it in the transproxy list, at least, so the leftover rules # are hopefully harmless. -def main(port): +def main(port, syslog): assert(port > 0) assert(port <= 65535) @@ -235,6 +235,10 @@ def main(port): # can read from it. os.dup2(1, 0) + if syslog: + ssyslog.start_syslog() + ssyslog.stderr_to_syslog() + debug1('firewall manager ready.\n') sys.stdout.write('READY\n') sys.stdout.flush() diff --git a/main.py b/main.py index a3ac837..66954f8 100755 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import sys, os, re import helpers, options, client, server, firewall, hostwatch +import compat.ssubprocess as ssubprocess from helpers import * @@ -46,8 +47,9 @@ def parse_ipport(s): optspec = """ sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] -sshuttle --firewall sshuttle --server +sshuttle --firewall +sshuttle --hostwatch -- l,listen= transproxy to this ip address and port number [127.0.0.1:0] H,auto-hosts scan for remote hostnames and update local /etc/hosts @@ -58,6 +60,9 @@ x,exclude= exclude this subnet (can be used more than once) v,verbose increase debug message verbosity e,ssh-cmd= the command to use to connect to the remote [ssh] seed-hosts= with -H, use these hostnames for initial scan (comma-separated) +D,daemon run in the background as a daemon +syslog send log messages to syslog (default if you use --daemon) +pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] server (internal use only) firewall (internal use only) hostwatch (internal use only) @@ -65,6 +70,8 @@ hostwatch (internal use only) o = options.Options('sshuttle', optspec) (opt, flags, extra) = o.parse(sys.argv[1:]) +if opt.daemon: + opt.syslog = 1 helpers.verbose = opt.verbose try: @@ -75,7 +82,7 @@ try: elif opt.firewall: if len(extra) != 1: o.fatal('exactly one argument expected') - sys.exit(firewall.main(int(extra[0]))) + sys.exit(firewall.main(int(extra[0]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: @@ -104,7 +111,8 @@ try: sh, opt.auto_nets, parse_subnets(includes), - parse_subnets(excludes))) + parse_subnets(excludes), + opt.syslog, opt.daemon, opt.pidfile)) except Fatal, e: log('fatal: %s\n' % e) sys.exit(99) diff --git a/server.py b/server.py index 08af657..24dd462 100644 --- a/server.py +++ b/server.py @@ -166,6 +166,7 @@ def main(): while mux.ok: if hw.pid: + assert(hw.pid > 0) (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG) if rpid: raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) diff --git a/ssh.py b/ssh.py index dd9e042..ac7f411 100644 --- a/ssh.py +++ b/ssh.py @@ -21,7 +21,7 @@ def empackage(z, filename): return '%s\n%d\n%s' % (basename,len(content), content) -def connect(ssh_cmd, rhostport, python): +def connect(ssh_cmd, rhostport, python, stderr): main_exe = sys.argv[0] portl = [] @@ -87,7 +87,7 @@ def connect(ssh_cmd, rhostport, python): s1.close() debug2('executing: %r\n' % argv) p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, - close_fds=True) + close_fds=True, stderr=stderr) os.close(s1a) os.close(s1b) s2.sendall(content) diff --git a/ssyslog.py b/ssyslog.py new file mode 100644 index 0000000..9958c9d --- /dev/null +++ b/ssyslog.py @@ -0,0 +1,16 @@ +import sys, os +from compat import ssubprocess + + +_p = None +def start_syslog(): + global _p + _p = ssubprocess.Popen(['logger', + '-p', 'daemon.info', + '-t', 'sshuttle'], stdin=ssubprocess.PIPE) + + +def stderr_to_syslog(): + sys.stdout.flush() + sys.stderr.flush() + os.dup2(_p.stdin.fileno(), 2)