Merge branch 'daemon'

* daemon:
  daemonization: make sure the firewall subproc sends to syslog too.
  Rearrange daemonization/syslog stuff and make it more resilient.
  run in background (daemon) and option
This commit is contained in:
Avery Pennarun 2011-01-01 00:22:43 -08:00
commit e6d7c44e27
6 changed files with 142 additions and 14 deletions

111
client.py
View File

@ -1,6 +1,6 @@
import struct, socket, select, errno, re import struct, socket, select, errno, re, signal
import compat.ssubprocess as ssubprocess import compat.ssubprocess as ssubprocess
import helpers, ssnet, ssh import helpers, ssnet, ssh, ssyslog
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import * from helpers import *
@ -21,6 +21,79 @@ def _islocal(ip):
return True # it's a local IP, or there would have been an error 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): def original_dst(sock):
try: try:
SO_ORIGINAL_DST = 80 SO_ORIGINAL_DST = 80
@ -46,6 +119,8 @@ class FirewallClient:
argvbase = ([sys.argv[0]] + argvbase = ([sys.argv[0]] +
['-v'] * (helpers.verbose or 0) + ['-v'] * (helpers.verbose or 0) +
['--firewall', str(port)]) ['--firewall', str(port)])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [ argv_tries = [
['sudo', '-p', '[local sudo] Password: '] + argvbase, ['sudo', '-p', '[local sudo] Password: '] + argvbase,
['su', '-c', ' '.join(argvbase)], ['su', '-c', ' '.join(argvbase)],
@ -114,15 +189,18 @@ class FirewallClient:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) 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 = [] handlers = []
if helpers.verbose >= 1: if helpers.verbose >= 1:
helpers.logprefix = 'c : ' helpers.logprefix = 'c : '
else: else:
helpers.logprefix = 'client: ' helpers.logprefix = 'client: '
debug1('connecting to server...\n') debug1('connecting to server...\n')
try: 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: except socket.error, e:
if e.args[0] == errno.EPIPE: if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)") 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' raise Fatal('expected server init string %r; got %r'
% (expected, initstring)) % (expected, initstring))
debug1('connected.\n') 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): def onroutes(routestr):
if auto_nets: 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, 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') debug1('Starting sshuttle proxy.\n')
listener = socket.socket() listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 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: try:
return _main(listener, fw, ssh_cmd, remotename, return _main(listener, fw, ssh_cmd, remotename,
python, seed_hosts, auto_nets) python, seed_hosts, auto_nets, syslog, daemon)
finally: finally:
try:
if daemon:
# it's not our child anymore; can't waitpid
fw.p.returncode = 0
fw.done() fw.done()
finally:
if daemon:
daemon_cleanup()

View File

@ -1,6 +1,6 @@
import re, errno import re, errno
import compat.ssubprocess as ssubprocess import compat.ssubprocess as ssubprocess
import helpers import helpers, ssyslog
from helpers import * 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 # 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 # supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless. # are hopefully harmless.
def main(port): def main(port, syslog):
assert(port > 0) assert(port > 0)
assert(port <= 65535) assert(port <= 65535)
@ -235,6 +235,10 @@ def main(port):
# can read from it. # can read from it.
os.dup2(1, 0) os.dup2(1, 0)
if syslog:
ssyslog.start_syslog()
ssyslog.stderr_to_syslog()
debug1('firewall manager ready.\n') debug1('firewall manager ready.\n')
sys.stdout.write('READY\n') sys.stdout.write('READY\n')
sys.stdout.flush() sys.stdout.flush()

14
main.py
View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys, os, re import sys, os, re
import helpers, options, client, server, firewall, hostwatch import helpers, options, client, server, firewall, hostwatch
import compat.ssubprocess as ssubprocess
from helpers import * from helpers import *
@ -46,8 +47,9 @@ def parse_ipport(s):
optspec = """ optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...> sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --firewall <port> <subnets...>
sshuttle --server sshuttle --server
sshuttle --firewall <port> <subnets...>
sshuttle --hostwatch
-- --
l,listen= transproxy to this ip address and port number [127.0.0.1:0] 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 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 v,verbose increase debug message verbosity
e,ssh-cmd= the command to use to connect to the remote [ssh] 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) 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) server (internal use only)
firewall (internal use only) firewall (internal use only)
hostwatch (internal use only) hostwatch (internal use only)
@ -65,6 +70,8 @@ hostwatch (internal use only)
o = options.Options('sshuttle', optspec) o = options.Options('sshuttle', optspec)
(opt, flags, extra) = o.parse(sys.argv[1:]) (opt, flags, extra) = o.parse(sys.argv[1:])
if opt.daemon:
opt.syslog = 1
helpers.verbose = opt.verbose helpers.verbose = opt.verbose
try: try:
@ -75,7 +82,7 @@ try:
elif opt.firewall: elif opt.firewall:
if len(extra) != 1: if len(extra) != 1:
o.fatal('exactly one argument expected') 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: elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra)) sys.exit(hostwatch.hw_main(extra))
else: else:
@ -104,7 +111,8 @@ try:
sh, sh,
opt.auto_nets, opt.auto_nets,
parse_subnets(includes), parse_subnets(includes),
parse_subnets(excludes))) parse_subnets(excludes),
opt.syslog, opt.daemon, opt.pidfile))
except Fatal, e: except Fatal, e:
log('fatal: %s\n' % e) log('fatal: %s\n' % e)
sys.exit(99) sys.exit(99)

View File

@ -166,6 +166,7 @@ def main():
while mux.ok: while mux.ok:
if hw.pid: if hw.pid:
assert(hw.pid > 0)
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG) (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid: if rpid:
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)

4
ssh.py
View File

@ -21,7 +21,7 @@ def empackage(z, filename):
return '%s\n%d\n%s' % (basename,len(content), content) 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] main_exe = sys.argv[0]
portl = [] portl = []
@ -87,7 +87,7 @@ def connect(ssh_cmd, rhostport, python):
s1.close() s1.close()
debug2('executing: %r\n' % argv) debug2('executing: %r\n' % argv)
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True) close_fds=True, stderr=stderr)
os.close(s1a) os.close(s1a)
os.close(s1b) os.close(s1b)
s2.sendall(content) s2.sendall(content)

16
ssyslog.py Normal file
View File

@ -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)