mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-02-10 07:31:54 +01:00
feat: switch to a network namespace on Linux
* Add support to run inside Linux namespace **Motivation:** In a specific use case, we use sshuttle to provide access to private networks from multiple sites to a specific host. The sites may contain networks that overlap each other, so each site is accessed inside a different namespace that provides process-level network isolation and prevents network overlap. **Objective:** This commit just adds a convenient way of spawning multiple sshuttle instances inside different namespaces from a single process, by passing the namespace's name though the variable --namespace. The result is the same as calling `ip netns exec $NAMESPACE sshuttle ...` * Add the argument --namespace-pid The argument '--namespace-pid' allows sshuttle to attach to the same net namespace used by a running process. * PEP-8 compliance * Add comment * Make --namespace and --namespace-pid mutually exclusive. * Prevent UnicodeDecodeError parsing iptables rule with comments If one or more iptables rule contains a comment with a non-unicode character, an UnicodeDecodeError would be raised.
This commit is contained in:
parent
cbe3d1e402
commit
8a123d9762
@ -11,6 +11,7 @@ import sshuttle.ssyslog as ssyslog
|
||||
from sshuttle.options import parser, parse_ipport
|
||||
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
||||
from sshuttle.sudoers import sudoers
|
||||
from sshuttle.namespace import enter_namespace
|
||||
|
||||
|
||||
def main():
|
||||
@ -37,6 +38,16 @@ def main():
|
||||
helpers.verbose = opt.verbose
|
||||
|
||||
try:
|
||||
# Since namespace and namespace-pid options are only available
|
||||
# in linux, we must check if it exists with getattr
|
||||
namespace = getattr(opt, 'namespace', None)
|
||||
namespace_pid = getattr(opt, 'namespace_pid', None)
|
||||
if namespace or namespace_pid:
|
||||
prefix = helpers.logprefix
|
||||
helpers.logprefix = 'ns: '
|
||||
enter_namespace(namespace, namespace_pid)
|
||||
helpers.logprefix = prefix
|
||||
|
||||
if opt.firewall:
|
||||
if opt.subnets or opt.subnets_file:
|
||||
parser.error('exactly zero arguments expected')
|
||||
|
40
sshuttle/namespace.py
Normal file
40
sshuttle/namespace.py
Normal file
@ -0,0 +1,40 @@
|
||||
import os
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
|
||||
from sshuttle.helpers import Fatal, debug1, debug2
|
||||
|
||||
|
||||
CLONE_NEWNET = 0x40000000
|
||||
NETNS_RUN_DIR = "/var/run/netns"
|
||||
|
||||
|
||||
def enter_namespace(namespace, namespace_pid):
|
||||
if namespace:
|
||||
namespace_dir = f'{NETNS_RUN_DIR}/{namespace}'
|
||||
else:
|
||||
namespace_dir = f'/proc/{namespace_pid}/ns/net'
|
||||
|
||||
if not os.path.exists(namespace_dir):
|
||||
raise Fatal('The namespace %r does not exists.' % namespace_dir)
|
||||
|
||||
debug2('loading libc')
|
||||
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
|
||||
|
||||
default_errcheck = libc.setns.errcheck
|
||||
|
||||
def errcheck(ret, *args):
|
||||
if ret == -1:
|
||||
e = ctypes.get_errno()
|
||||
raise Fatal(e, os.strerror(e))
|
||||
if default_errcheck:
|
||||
return default_errcheck(ret, *args)
|
||||
|
||||
libc.setns.errcheck = errcheck # type: ignore
|
||||
|
||||
debug1('Entering namespace %r' % namespace_dir)
|
||||
|
||||
with open(namespace_dir) as fd:
|
||||
libc.setns(fd.fileno(), CLONE_NEWNET)
|
||||
|
||||
debug1('Namespace %r successfully set' % namespace_dir)
|
@ -137,6 +137,15 @@ def parse_list(lst):
|
||||
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
||||
|
||||
|
||||
def parse_namespace(namespace):
|
||||
try:
|
||||
assert re.fullmatch(
|
||||
r'(@?[a-z_A-Z]\w+(?:\.@?[a-z_A-Z]\w+)*)', namespace)
|
||||
return namespace
|
||||
except AssertionError:
|
||||
raise Fatal("%r is not a valid namespace name." % namespace)
|
||||
|
||||
|
||||
class Concat(Action):
|
||||
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||
if nargs is not None:
|
||||
@ -460,3 +469,20 @@ parser.add_argument(
|
||||
hexadecimal (default '0x01')
|
||||
"""
|
||||
)
|
||||
|
||||
if sys.platform == 'linux':
|
||||
net_ns_group = parser.add_mutually_exclusive_group(
|
||||
required=False)
|
||||
|
||||
net_ns_group.add_argument(
|
||||
'--namespace',
|
||||
type=parse_namespace,
|
||||
help="Run inside of a net namespace with the given name."
|
||||
)
|
||||
net_ns_group.add_argument(
|
||||
'--namespace-pid',
|
||||
type=int,
|
||||
help="""
|
||||
Run inside the net namespace used by the process with
|
||||
the given pid."""
|
||||
)
|
||||
|
@ -176,3 +176,33 @@ def test_parse_subnetport_host_with_port(mock_getaddrinfo):
|
||||
(socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 90),
|
||||
(socket.AF_INET, '142.251.42.129', 32, 80, 90),
|
||||
])
|
||||
|
||||
|
||||
def test_parse_namespace():
|
||||
valid_namespaces = [
|
||||
'my_namespace',
|
||||
'my.namespace',
|
||||
'my_namespace_with_underscore',
|
||||
'MyNamespace',
|
||||
'@my_namespace',
|
||||
'my.long_namespace.with.multiple.dots',
|
||||
'@my.long_namespace.with.multiple.dots',
|
||||
'my.Namespace.With.Mixed.Case',
|
||||
]
|
||||
|
||||
for namespace in valid_namespaces:
|
||||
assert sshuttle.options.parse_namespace(namespace) == namespace
|
||||
|
||||
invalid_namespaces = [
|
||||
'',
|
||||
'123namespace',
|
||||
'my-namespace',
|
||||
'my_namespace!',
|
||||
'.my_namespace',
|
||||
'my_namespace.',
|
||||
'my..namespace',
|
||||
]
|
||||
|
||||
for namespace in invalid_namespaces:
|
||||
with pytest.raises(Fatal, match="'.*' is not a valid namespace name."):
|
||||
sshuttle.options.parse_namespace(namespace)
|
||||
|
Loading…
Reference in New Issue
Block a user