Skip to content

Commit

Permalink
Improve rootless support
Browse files Browse the repository at this point in the history
  • Loading branch information
blechschmidt committed May 30, 2024
1 parent 0343bf9 commit 11a9d5d
Show file tree
Hide file tree
Showing 15 changed files with 284 additions and 329 deletions.
2 changes: 1 addition & 1 deletion pallium/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def opener(_, __):

profile = Profile.from_config(data)
new_session = True
elif config_json:
elif config_json is not None:
profile = Profile.from_config(config_json)
new_session = True
else:
Expand Down
10 changes: 8 additions & 2 deletions pallium/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Not in use yet
import dataclasses
import ipaddress
import os

import typing

Expand Down Expand Up @@ -163,10 +164,15 @@ def from_json(cls, obj):
return result


def default_command():
shell = os.environ.get('SHELL', '/usr/bin/sh')
return [shell]


@json_serializable
@dataclasses.dataclass
class Run:
command: typing.Optional[typing.List[str]] = dataclasses.field(default=None)
command: typing.Optional[typing.List[str]] = dataclasses.field(default_factory=default_command)
quiet: bool = dataclasses.field(default=False) # Whether to suppress status information of pallium and its helpers


Expand All @@ -184,5 +190,5 @@ class Networking:
@dataclasses.dataclass
class Configuration:
networking: Networking = dataclasses.field(default_factory=Networking)
sandbox: typing.Optional[Sandbox] = dataclasses.field(default=None)
sandbox: Sandbox = dataclasses.field(default_factory=Sandbox)
run: Run = dataclasses.field(default_factory=Run)
5 changes: 1 addition & 4 deletions pallium/dhcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,10 @@ def _get_conf(self):
return conf

def _run_server(self):
def preexec_fn():
sysutil.prctl(sysutil.PR_SET_PDEATHSIG, signal.SIGTERM)

self.cmd_args += ['-d' if self.debug else '-k']
self.dhcp_server = util.popen(['dnsmasq', '--conf-file=-', '--pid-file'] + self.cmd_args,
stdin=subprocess.PIPE,
preexec_fn=preexec_fn)
stdin=subprocess.PIPE)
sysutil.write_blocking(self.dhcp_server.stdin.fileno(), self._get_conf().encode('ascii'))
self.dhcp_server.stdin.close()

Expand Down
3 changes: 1 addition & 2 deletions pallium/dnsproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import socket
import struct
import os
import subprocess
import threading
import time

Expand Down Expand Up @@ -137,8 +138,6 @@ def f():
child = os.fork() if forked else None
if not forked or forked and child == 0:
if forked:
if security.is_sudo_or_root():
sysutil.prctl(sysutil.PR_SET_PDEATHSIG, signal.SIGKILL)
onexit.clear()
proxy.start(threaded=threaded)
if forked:
Expand Down
25 changes: 21 additions & 4 deletions pallium/hops/hop.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,36 @@ def __init__(self, quiet=None, dns=None, **kwargs):
self.required_routes = []

@classmethod
def from_json(cls, value: typing.Dict[str, typing.Any]) -> 'Hop':
value = dict(value)
def from_json(cls, obj: typing.Dict[str, typing.Any]) -> 'Hop':
# Do not modify the passed dict.
obj = dict(obj)

if 'dns' in obj:
proxied_addrs = []
non_proxied_addrs = []
for addr in obj['dns']:
if addr.startswith('tcp://'):
proxied_addrs.append(addr[6:])
else:
non_proxied_addrs.append(addr)
dns = non_proxied_addrs
if len(proxied_addrs) > 0:
dns.append(DnsTcpProxy(proxied_addrs))
obj['dns'] = dns

type2class = dict()
for hop_class in util.get_subclasses(cls):
class_name = hop_class.__name__
if hop_class.__name__.endswith('Hop'):
class_name = class_name[:-len('Hop')]
type2class[class_name.lower()] = hop_class

hop_type = value.pop('type')
hop_type = obj.pop('type')
hop_class = type2class.get(hop_type.lower())
if hop_class is None:
raise ""
return hop_class(**value)

return hop_class(**obj)

def popen(self, *args, **kwargs):
"""Popen wrapper that keeps track of the started processes and handles command output.
Expand Down Expand Up @@ -206,6 +222,7 @@ def connect(self):
def free(self):
self.log_debug('Free hop %s' % repr(self))
for pid in self.started_pids:
# TODO: Why do we have this check again?
if not security.is_sudo_or_root():
continue
self.log_debug('Kill process %d' % pid)
Expand Down
18 changes: 13 additions & 5 deletions pallium/hops/socksapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_tcp_connections():
yield local, remote


def wait_for_listener(addr, timeout: float = 30):
def wait_for_listener(addr, timeout: float = 30, exception_function=None):
start_time = time.perf_counter()
end_time = start_time + timeout if timeout is not None else None
addr = ipaddress.ip_address(addr[0]), addr[1]
Expand All @@ -44,15 +44,16 @@ def wait_for_listener(addr, timeout: float = 30):
return False
time.sleep(0.1)

if exception_function is not None:
exception_function()


class SocksAppHop(hop.Hop):
def __init__(self, user: str, cmd=None, timeout: float = 30, **kwargs):
super().__init__(**kwargs)
self._tun2socks = None
self._socks_endpoint = None
self._user = user
if self._user is None:
self._user = security.real_user()
self._timeout = timeout
self.cmd = cmd
self._proc_pid = None
Expand Down Expand Up @@ -83,14 +84,21 @@ def connect(self):
# kwargs = {'preexec_fn': netns.map_back_real}
kwargs = {}

if security.is_sudo_or_root():
if security.is_sudo_or_root() and self._user is not None:
kwargs = sysutil.privilege_drop_preexec(self._user, True)

process = self.popen(self.cmd, **kwargs)
self._proc_pid = process.pid
# Wait for the SOCKS listener to appear
self.log_debug('Waiting for SSH socks endpoint to appear at %s.' % str(self._socks_endpoint))
if not wait_for_listener(self._socks_endpoint):

def ssh_error():
returncode = process.poll()
if returncode is not None and returncode != 0:
# TODO: Include SSH output in exception.
raise ConnectionError('SSH exited with code %d' % returncode)

if not wait_for_listener(self._socks_endpoint, exception_function=ssh_error):
raise TimeoutError

def next_hop(self) -> Optional[hop.Hop]:
Expand Down
4 changes: 4 additions & 0 deletions pallium/hops/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ def __init__(self,
self._args = ssh_args if ssh_args is not None else []

def update_cmd(self, hop_info):

# The correct home directory needs to be figured out by OpenSSH.
# If the user is fakeroot, the home directory inferred from /etc/passwd will be wrong.
if not security.is_sudo_or_root():
sandbox.map_back_real()

self.cmd = ['ssh', '-N', '-D', '%s:%d' % self._socks_endpoint]
if self._args is not None:
self.cmd += self._args # Append custom user-provided arguments
Expand Down
5 changes: 3 additions & 2 deletions pallium/hops/tor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ipaddress
import os
import pwd
import shutil
import time
from typing import Optional, List
Expand Down Expand Up @@ -57,7 +58,7 @@ def __init__(self, *, timeout=300, circuit_build_timeout=60, builtin_dns=True, u
self._circuit_build_timeout = circuit_build_timeout
self._builtin_dns = builtin_dns
if user is None:
user = security.real_user()
user = security.least_privileged_user()
self._user = user
self._onion_support = onion_support
self.required_routes = [ipaddress.ip_network('0.0.0.0/0'), ipaddress.ip_network('::/0')]
Expand Down Expand Up @@ -97,7 +98,7 @@ def connect(self):
command += ['--Log', 'notice stderr']
kwargs = {'env': os.environ.copy()}

if security.is_sudo_or_root():
if security.is_sudo_or_root() and self._user is not None:
kwargs = sysutil.privilege_drop_preexec(self._user)

tor_env = self.get_tool_env('tor')
Expand Down
Loading

0 comments on commit 11a9d5d

Please sign in to comment.