mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 08:40:30 +02:00
Compare commits
14 Commits
sshuttle-0
...
sshuttle-0
Author | SHA1 | Date | |
---|---|---|---|
973d5a95a1 | |||
95ab6e7119 | |||
e6d7c44e27 | |||
5bf6e40682 | |||
8a5ae1a40a | |||
651b607361 | |||
dc9a5e63c7 | |||
33bc55be27 | |||
c3204d2728 | |||
b1edb226a5 | |||
7fa1c3c4e4 | |||
cca69eb496 | |||
91f65132be | |||
2ef3a301fb |
164
client.py
164
client.py
@ -1,9 +1,98 @@
|
||||
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 *
|
||||
|
||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||
|
||||
def _islocal(ip):
|
||||
sock = socket.socket()
|
||||
try:
|
||||
try:
|
||||
sock.bind((ip, 0))
|
||||
except socket.error, e:
|
||||
if e.args[0] == errno.EADDRNOTAVAIL:
|
||||
return False # not a local IP
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
sock.close()
|
||||
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:
|
||||
@ -30,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)],
|
||||
@ -98,25 +189,34 @@ 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.errno == errno.EPIPE:
|
||||
raise Fatal("failed to establish ssh session")
|
||||
if e.args[0] == errno.EPIPE:
|
||||
raise Fatal("failed to establish ssh session (1)")
|
||||
else:
|
||||
raise
|
||||
mux = Mux(serversock, serversock)
|
||||
handlers.append(mux)
|
||||
|
||||
expected = 'SSHUTTLE0001'
|
||||
initstring = serversock.recv(len(expected))
|
||||
try:
|
||||
initstring = serversock.recv(len(expected))
|
||||
except socket.error, e:
|
||||
if e.args[0] == errno.ECONNRESET:
|
||||
raise Fatal("failed to establish ssh session (2)")
|
||||
else:
|
||||
raise
|
||||
|
||||
rv = serverproc.poll()
|
||||
if rv:
|
||||
@ -126,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:
|
||||
@ -153,11 +259,26 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets):
|
||||
mux.got_host_list = onhostlist
|
||||
|
||||
def onaccept():
|
||||
sock,srcip = listener.accept()
|
||||
global _extra_fd
|
||||
try:
|
||||
sock,srcip = listener.accept()
|
||||
except socket.error, e:
|
||||
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
|
||||
debug1('Rejected incoming connection: too many open files!\n')
|
||||
# free up an fd so we can eat the connection
|
||||
os.close(_extra_fd)
|
||||
try:
|
||||
sock,srcip = listener.accept()
|
||||
sock.close()
|
||||
finally:
|
||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||
return
|
||||
else:
|
||||
raise
|
||||
dstip = original_dst(sock)
|
||||
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
|
||||
dstip[0],dstip[1]))
|
||||
if dstip == listener.getsockname():
|
||||
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
|
||||
dstip[0],dstip[1]))
|
||||
if dstip[1] == listener.getsockname()[1] and _islocal(dstip[0]):
|
||||
debug1("-- ignored: that's my address!\n")
|
||||
sock.close()
|
||||
return
|
||||
@ -182,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)
|
||||
@ -213,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()
|
||||
|
@ -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()
|
||||
|
18
main.py
18
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,18 +47,22 @@ def parse_ipport(s):
|
||||
|
||||
optspec = """
|
||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --server
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --hostwatch
|
||||
--
|
||||
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 [127.0.0.1:0]
|
||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||
N,auto-nets automatically determine subnets to route
|
||||
python= specify the name/path of the python interpreter on the remote server [python]
|
||||
python= path to python interpreter on the remote server [python]
|
||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
||||
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)
|
||||
|
@ -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)
|
||||
|
4
ssh.py
4
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)
|
||||
|
32
sshuttle.md
32
sshuttle.md
@ -1,6 +1,6 @@
|
||||
% sshuttle(8) Sshuttle 0.42
|
||||
% sshuttle(8) Sshuttle 0.44
|
||||
% Avery Pennarun <apenwarr@gmail.com>
|
||||
% 2010-11-09
|
||||
% 2010-12-31
|
||||
|
||||
# NAME
|
||||
|
||||
@ -41,7 +41,13 @@ entire subnet to the VPN.
|
||||
-l, --listen=*[ip:]port*
|
||||
: use this ip address and port number as the transparent
|
||||
proxy port. By default sshuttle finds an available
|
||||
port automatically, so you don't need to override it.
|
||||
port automatically and listens on IP 127.0.0.1
|
||||
(localhost), so you don't need to override it, and
|
||||
connections are only proxied from the local machine,
|
||||
not from outside machines. If you want to accept
|
||||
connections from other machines on your network (ie. to
|
||||
run sshuttle on a router) try enabling IP Forwarding in
|
||||
your kernel, then using `--listen 0.0.0.0:0`.
|
||||
|
||||
-H, --auto-hosts
|
||||
: scan for remote hostnames and update the local /etc/hosts
|
||||
@ -103,6 +109,20 @@ entire subnet to the VPN.
|
||||
if you use this option to give it a few names to start
|
||||
from.
|
||||
|
||||
-D, --daemon
|
||||
: automatically fork into the background after connecting
|
||||
to the remote server. Implies `--syslog`.
|
||||
|
||||
--syslog
|
||||
: after connecting, send all log messages to the
|
||||
`syslog`(3) service instead of stderr. This is
|
||||
implicit if you use `--daemon`.
|
||||
|
||||
--pidfile=*pidfilename*
|
||||
: when using `--daemon`, save sshuttle's pid to
|
||||
*pidfilename*. The default is `sshuttle.pid` in the
|
||||
current directory.
|
||||
|
||||
--server
|
||||
: (internal use only) run the sshuttle server on
|
||||
stdin/stdout. This is what the client runs on
|
||||
@ -139,8 +159,8 @@ Test locally by proxying all local connections, without using ssh:
|
||||
s: 192.168.42.0/24
|
||||
c : connected.
|
||||
firewall manager: starting transproxy.
|
||||
c : Accept: '192.168.42.106':50035 -> '192.168.42.121':139.
|
||||
c : Accept: '192.168.42.121':47523 -> '77.141.99.22':443.
|
||||
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
|
||||
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
|
||||
...etc...
|
||||
^C
|
||||
firewall manager: undoing changes.
|
||||
@ -166,7 +186,7 @@ and subnet guessing:
|
||||
hostwatch: Found: testbox1: 1.2.3.4
|
||||
hostwatch: Found: mytest2: 5.6.7.8
|
||||
hostwatch: Found: domaincontroller: 99.1.2.3
|
||||
c : Accept: '192.168.42.121':60554 -> '77.141.99.22':22.
|
||||
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
||||
^C
|
||||
firewall manager: undoing changes.
|
||||
c : Keyboard interrupt: exiting.
|
||||
|
15
ssnet.py
15
ssnet.py
@ -15,7 +15,7 @@ CMD_EXIT = 0x4200
|
||||
CMD_PING = 0x4201
|
||||
CMD_PONG = 0x4202
|
||||
CMD_CONNECT = 0x4203
|
||||
# CMD_CLOSE = 0x4204 # never used - removed
|
||||
CMD_STOP_SENDING = 0x4204
|
||||
CMD_EOF = 0x4205
|
||||
CMD_DATA = 0x4206
|
||||
CMD_ROUTES = 0x4207
|
||||
@ -27,6 +27,7 @@ cmd_to_name = {
|
||||
CMD_PING: 'PING',
|
||||
CMD_PONG: 'PONG',
|
||||
CMD_CONNECT: 'CONNECT',
|
||||
CMD_STOP_SENDING: 'STOP_SENDING',
|
||||
CMD_EOF: 'EOF',
|
||||
CMD_DATA: 'DATA',
|
||||
CMD_ROUTES: 'ROUTES',
|
||||
@ -106,6 +107,8 @@ class SockWrapper:
|
||||
def seterr(self, e):
|
||||
if not self.exc:
|
||||
self.exc = e
|
||||
self.nowrite()
|
||||
self.noread()
|
||||
|
||||
def try_connect(self):
|
||||
if self.connect_to and self.shut_write:
|
||||
@ -162,8 +165,6 @@ class SockWrapper:
|
||||
except OSError, e:
|
||||
# unexpected error... stream is dead
|
||||
self.seterr(e)
|
||||
self.nowrite()
|
||||
self.noread()
|
||||
return 0
|
||||
|
||||
def write(self, buf):
|
||||
@ -231,6 +232,11 @@ class Proxy(Handler):
|
||||
self.wrap2 = wrap2
|
||||
|
||||
def pre_select(self, r, w, x):
|
||||
if self.wrap1.shut_read: self.wrap2.nowrite()
|
||||
if self.wrap1.shut_write: self.wrap2.noread()
|
||||
if self.wrap2.shut_read: self.wrap1.nowrite()
|
||||
if self.wrap2.shut_write: self.wrap1.noread()
|
||||
|
||||
if self.wrap1.connect_to:
|
||||
_add(w, self.wrap1.rsock)
|
||||
elif self.wrap1.buf:
|
||||
@ -430,6 +436,7 @@ class MuxWrapper(SockWrapper):
|
||||
def noread(self):
|
||||
if not self.shut_read:
|
||||
self.shut_read = True
|
||||
self.mux.send(self.channel, CMD_STOP_SENDING, '')
|
||||
self.maybe_close()
|
||||
|
||||
def nowrite(self):
|
||||
@ -464,6 +471,8 @@ class MuxWrapper(SockWrapper):
|
||||
def got_packet(self, cmd, data):
|
||||
if cmd == CMD_EOF:
|
||||
self.noread()
|
||||
elif cmd == CMD_STOP_SENDING:
|
||||
self.nowrite()
|
||||
elif cmd == CMD_DATA:
|
||||
self.buf.append(data)
|
||||
else:
|
||||
|
16
ssyslog.py
Normal file
16
ssyslog.py
Normal 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.notice',
|
||||
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
|
||||
|
||||
|
||||
def stderr_to_syslog():
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os.dup2(_p.stdin.fileno(), 2)
|
Reference in New Issue
Block a user