mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-04-28 13:19:29 +02:00
Rearrange daemonization/syslog stuff and make it more resilient.
Rename --background to -D/--daemon, to match other programs (like smbd). You can now have --syslog even without --daemon. Avoid using atexit(); try/finally is better. Don't just close stderr; we'll end up eating error output from ssh! Instead, redirect stderr to a 'logger' subprocess that will send to syslog. Delay redirecting stderr until after we know we're daemonizing, so handy error messages can go to stderr instead of syslog. Make pidfile stuff more resilient: support already-existing files, files with strict permissions, outdated files containing an already-dead pid. Add a --pidfile option to let you specify the pidfile path. chdir("/") while daemonizing, so that the filesystem containing $PWD can later be unmounted without killing the daemon. fw.done() can't wait on the firewall subprocess on exit when daemonized; we no longer are the parent of that process.
This commit is contained in:
parent
2ef3a301fb
commit
8a5ae1a40a
163
client.py
163
client.py
@ -1,10 +1,97 @@
|
|||||||
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
|
||||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
import os, sys, atexit, signal, syslog
|
|
||||||
|
_loggerp = None
|
||||||
|
def start_syslog():
|
||||||
|
global _loggerp
|
||||||
|
_loggerp = ssubprocess.Popen(['logger',
|
||||||
|
'-p', 'daemon.info',
|
||||||
|
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
|
||||||
|
|
||||||
|
|
||||||
|
def stderr_to_syslog():
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os.dup2(_loggerp.stdin.fileno(), 1)
|
||||||
|
os.dup2(_loggerp.stdin.fileno(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
si.close()
|
||||||
|
|
||||||
|
stderr_to_syslog()
|
||||||
|
log('daemonizing (%s).\n' % _pidname)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
@ -98,11 +185,9 @@ class FirewallClient:
|
|||||||
if rv:
|
if rv:
|
||||||
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
||||||
|
|
||||||
def exit_cleanup():
|
|
||||||
debug1('exit cleanup\n')
|
|
||||||
os.unlink('sshuttle.pid')
|
|
||||||
|
|
||||||
def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, background):
|
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 : '
|
||||||
@ -110,30 +195,9 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, back
|
|||||||
helpers.logprefix = 'client: '
|
helpers.logprefix = 'client: '
|
||||||
debug1('connecting to server...\n')
|
debug1('connecting to server...\n')
|
||||||
|
|
||||||
if background:
|
|
||||||
helpers.do_syslog = True
|
|
||||||
syslog.openlog('sshuttle')
|
|
||||||
|
|
||||||
# we're redirecting the standard outputs here early so that
|
|
||||||
# the stderr debug message of ssh subprocess would be
|
|
||||||
# redirected properly
|
|
||||||
|
|
||||||
# TODO: redirecting stderr of ssh to syslog
|
|
||||||
|
|
||||||
sys.stdout.flush()
|
|
||||||
sys.stderr.flush()
|
|
||||||
si = file('/dev/null', 'r')
|
|
||||||
so = file('/dev/null', 'a+')
|
|
||||||
se = file('/dev/null', 'a+', 0)
|
|
||||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
|
||||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
|
||||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
|
||||||
si.close()
|
|
||||||
so.close()
|
|
||||||
se.close()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python)
|
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
|
||||||
|
stderr=_loggerp.stdin)
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
if e.errno == errno.EPIPE:
|
if e.errno == errno.EPIPE:
|
||||||
raise Fatal("failed to establish ssh session")
|
raise Fatal("failed to establish ssh session")
|
||||||
@ -153,22 +217,10 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, back
|
|||||||
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 background:
|
if daemon:
|
||||||
debug1('daemonizing\n')
|
daemonize()
|
||||||
if os.fork():
|
elif syslog:
|
||||||
os._exit(0)
|
stderr_to_syslog()
|
||||||
os.setsid()
|
|
||||||
if os.fork():
|
|
||||||
os._exit(0)
|
|
||||||
|
|
||||||
outfd = os.open('sshuttle.pid',
|
|
||||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL)
|
|
||||||
os.write(outfd, '%i' % os.getpid())
|
|
||||||
os.close(outfd)
|
|
||||||
|
|
||||||
atexit.register(exit_cleanup)
|
|
||||||
# Normal exit when killed, or atexit won't work
|
|
||||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
|
||||||
|
|
||||||
def onroutes(routestr):
|
def onroutes(routestr):
|
||||||
if auto_nets:
|
if auto_nets:
|
||||||
@ -225,7 +277,15 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, back
|
|||||||
|
|
||||||
|
|
||||||
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, background):
|
subnets_include, subnets_exclude, syslog, daemon, pidfile):
|
||||||
|
if syslog:
|
||||||
|
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)
|
||||||
@ -256,6 +316,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, background)
|
python, seed_hosts, auto_nets, syslog, daemon)
|
||||||
finally:
|
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()
|
||||||
|
12
helpers.py
12
helpers.py
@ -1,17 +1,13 @@
|
|||||||
import sys, os, syslog
|
import sys, os
|
||||||
|
|
||||||
logprefix = ''
|
logprefix = ''
|
||||||
verbose = 0
|
verbose = 0
|
||||||
do_syslog = False
|
|
||||||
|
|
||||||
def log(s):
|
def log(s):
|
||||||
try:
|
try:
|
||||||
if do_syslog:
|
sys.stdout.flush()
|
||||||
syslog.syslog(logprefix + s)
|
sys.stderr.write(logprefix + s)
|
||||||
else:
|
sys.stderr.flush()
|
||||||
sys.stdout.flush()
|
|
||||||
sys.stderr.write(logprefix + s)
|
|
||||||
sys.stderr.flush()
|
|
||||||
except IOError:
|
except IOError:
|
||||||
# this could happen if stderr gets forcibly disconnected, eg. because
|
# this could happen if stderr gets forcibly disconnected, eg. because
|
||||||
# our tty closes. That sucks, but it's no reason to abort the program.
|
# our tty closes. That sucks, but it's no reason to abort the program.
|
||||||
|
14
main.py
14
main.py
@ -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 *
|
||||||
|
|
||||||
|
|
||||||
@ -45,11 +46,11 @@ def parse_ipport(s):
|
|||||||
|
|
||||||
|
|
||||||
optspec = """
|
optspec = """
|
||||||
sshuttle [-b] [-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
|
||||||
--
|
--
|
||||||
b,background run in background as daemon
|
|
||||||
l,listen= transproxy to this ip address and port number [0.0.0.0:0]
|
l,listen= transproxy to this ip address and port number [0.0.0.0: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
|
||||||
N,auto-nets automatically determine subnets to route
|
N,auto-nets automatically determine subnets to route
|
||||||
@ -59,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)
|
||||||
@ -66,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:
|
||||||
@ -106,7 +112,7 @@ try:
|
|||||||
opt.auto_nets,
|
opt.auto_nets,
|
||||||
parse_subnets(includes),
|
parse_subnets(includes),
|
||||||
parse_subnets(excludes),
|
parse_subnets(excludes),
|
||||||
opt.background))
|
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)
|
||||||
|
@ -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
4
ssh.py
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user