diff --git a/sshuttle/client.py b/sshuttle/client.py index 49bb7e2..25b3440 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -21,6 +21,10 @@ try: from pwd import getpwnam except ImportError: getpwnam = None +try: + from grp import getgrnam +except ImportError: + getgrnam = None import socket @@ -726,7 +730,7 @@ def main(listenip_v6, listenip_v4, latency_buffer_size, dns, nslist, method_name, seed_hosts, auto_hosts, auto_nets, subnets_include, subnets_exclude, daemon, to_nameserver, pidfile, - user, sudo_pythonpath, tmark): + user, group, sudo_pythonpath, tmark): if not remotename: raise Fatal("You must use -r/--remote to specify a remote " @@ -829,6 +833,15 @@ def main(listenip_v6, listenip_v4, raise Fatal("User %s does not exist." % user) required.user = False if user is None else True + if group is not None: + if getgrnam is None: + raise Fatal("Routing by group not available on this system.") + try: + group = getgrnam(group).gr_gid + except KeyError: + raise Fatal("User %s does not exist." % user) + required.group = False if group is None else True + if not required.ipv6 and len(subnets_v6) > 0: print("WARNING: IPv6 subnets were ignored because IPv6 is disabled " "in sshuttle.") @@ -1058,7 +1071,7 @@ def main(listenip_v6, listenip_v4, # start the firewall fw.setup(subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, - required.udp, user, tmark) + required.udp, user, group, tmark) # start the client process try: diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index b7ea43f..eaca961 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -104,6 +104,7 @@ def main(): opt.to_ns, opt.pidfile, opt.user, + opt.group, opt.sudo_pythonpath, opt.tmark) diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 9aaf62e..4a1abe6 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -50,6 +50,7 @@ class BaseMethod(object): result.udp = False result.dns = True result.user = False + result.group = False return result @staticmethod diff --git a/sshuttle/methods/ipfw.py b/sshuttle/methods/ipfw.py index 74fd9f7..053ddf3 100644 --- a/sshuttle/methods/ipfw.py +++ b/sshuttle/methods/ipfw.py @@ -156,7 +156,7 @@ class Method(BaseMethod): # udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): # IPv6 not supported if family not in [socket.AF_INET]: raise Exception( @@ -207,7 +207,7 @@ class Method(BaseMethod): else: ipfw('table', '126', 'add', '%s/%s' % (snet, swidth)) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if family not in [socket.AF_INET]: raise Exception( 'Address family "%s" unsupported by ipfw method' diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index 076d880..de95008 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -13,7 +13,7 @@ class Method(BaseMethod): # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if family != socket.AF_INET and family != socket.AF_INET6: raise Exception( 'Address family "%s" unsupported by nat method_name' @@ -35,9 +35,14 @@ class Method(BaseMethod): _ipt('-N', chain) _ipt('-F', chain) - if user is not None: - _ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user), - '-j', 'MARK', '--set-mark', str(port)) + if user is not None or group is not None: + margs = ['-I', 'OUTPUT', '1', '-m', 'owner'] + if user is not None: + margs.append('--uid-owner', str(user)) + if group is not None: + margs.append('--gid-owner', str(group)) + margs = args.append('-j', 'MARK', '--set-mark', str(port)) + nonfatal(_ipm, *margs) args = '-m', 'mark', '--mark', str(port), '-j', chain else: args = '-j', chain @@ -75,7 +80,7 @@ class Method(BaseMethod): '--dest', '%s/%s' % (snet, swidth), *(tcp_ports + ('--to-ports', str(port)))) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): # only ipv4 supported with NAT if family != socket.AF_INET and family != socket.AF_INET6: raise Exception( @@ -96,9 +101,15 @@ class Method(BaseMethod): # basic cleanup/setup of chains if ipt_chain_exists(family, table, chain): - if user is not None: - nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner', - str(user), '-j', 'MARK', '--set-mark', str(port)) + if user is not None or group is not None: + margs = ['-D', 'OUTPUT', '-m', 'owner'] + if user is not None: + margs.append('--uid-owner', str(user)) + if group is not None: + margs.append('--gid-owner', str(group)) + margs = args.append('-j', 'MARK', '--set-mark', str(port)) + nonfatal(_ipm, *margs) + args = '-m', 'mark', '--mark', str(port), '-j', chain else: args = '-j', chain @@ -111,6 +122,7 @@ class Method(BaseMethod): result = super(Method, self).get_supported_features() result.user = True result.ipv6 = True + result.group = True return result def is_supported(self): diff --git a/sshuttle/methods/nft.py b/sshuttle/methods/nft.py index 64ab3a6..59b6310 100644 --- a/sshuttle/methods/nft.py +++ b/sshuttle/methods/nft.py @@ -13,7 +13,7 @@ class Method(BaseMethod): # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if udp: raise Exception("UDP not supported by nft") @@ -87,7 +87,7 @@ class Method(BaseMethod): ip_version, 'daddr %s/%s' % (snet, swidth), ('redirect to :' + str(port))))) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if udp: raise Exception("UDP not supported by nft method_name") diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py index ed56c51..d5ed06a 100644 --- a/sshuttle/methods/pf.py +++ b/sshuttle/methods/pf.py @@ -448,7 +448,7 @@ class Method(BaseMethod): return sock.getsockname() def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by pf method_name' @@ -473,7 +473,7 @@ class Method(BaseMethod): pf.add_rules(anchor, includes, port, dnsport, nslist, family) pf.enable() - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by pf method_name' diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index 1d2ae29..e12943c 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -114,7 +114,7 @@ class Method(BaseMethod): udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by tproxy method' @@ -228,7 +228,7 @@ class Method(BaseMethod): '-m', 'udp', *(udp_ports + ('--on-port', str(port)))) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by tproxy method' diff --git a/sshuttle/options.py b/sshuttle/options.py index 0ac7690..acd46da 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -382,6 +382,12 @@ parser.add_argument( apply all the rules only to this linux user """ ) +parser.add_argument( + "--group", + help=""" + apply all the rules only to this linux group + """ +) parser.add_argument( "--firewall", action="store_true",