Skip to content

Commit

Permalink
Prepare port forwarding
Browse files Browse the repository at this point in the history
  • Loading branch information
blechschmidt committed Nov 19, 2023
1 parent b304cdb commit 07ea3be
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 41 deletions.
78 changes: 78 additions & 0 deletions pallium/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Not in use yet
import dataclasses
import ipaddress

import typing

from pallium import typeinfo
from pallium.exceptions import ConfigurationError


class FromJSON:
@classmethod
def from_json(cls, obj):
pass


class LocalPortForwarding:
protocol: str # Either "tcp" or "udp"
host: (typeinfo.IPAddress, int)
guest: (typeinfo.IPAddress, int)

def __init__(self, spec):
scheme, rest = spec.split('://', 1)
self.protocol = scheme.lower()

if self.protocol not in {'udp', 'tcp'}:
raise ConfigurationError('The scheme of the port forwarding %s is not either tcp or udp' % scheme)

components = rest.split(':')
if len(components) != 4:
raise ConfigurationError('Port forwarding expected to have the following scheme: '
'<udp|tcp>://<bind_host_ip>:<bind_host_port>:<guest_ip>:<guest_port>')

host_ip = ipaddress.ip_address(components[0])
host_port = int(components[1])
self.host = (host_ip, host_port)

# Requirement from slirpnetstack. See slirp.py.
guest_ip = '10.0.2.100'
guest_ip = ipaddress.ip_address(components[2])
guest_port = int(components[3])
self.guest = (guest_ip, guest_port)

def __str__(self):
# noinspection PyStringFormat
return '%s://%s:%s:%s:%s' % (self.protocol, *self.host, *self.guest)

@classmethod
def from_json(cls, obj):
if not isinstance(obj, str):
raise ConfigurationError('Local forwarding must be of type string')
return cls(obj)


@dataclasses.dataclass
class PortForwarding:
local: typing.List[LocalPortForwarding] = dataclasses.field(default_factory=list)

@classmethod
def from_json(cls, obj):
if not isinstance(obj, dict):
raise ConfigurationError('Port forwarding configuration must be an object')
local = obj.get('local', [])
if not isinstance(local, list):
raise ConfigurationError('Local port forwardings must be a list')
local = list(map(LocalPortForwarding.from_json, local))
result = cls(local)
return result


@dataclasses.dataclass
class Networking:
port_forwarding: PortForwarding = dataclasses.field(default_factory=PortForwarding)


@dataclasses.dataclass
class Configuration:
networking: Networking = dataclasses.field(default_factory=Networking)
6 changes: 3 additions & 3 deletions pallium/hops/hop.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from pyroute2.iproute import IPRoute

from .. import resolvconf, security, runtime
from .. import resolvconf, security, runtime, typeinfo
from .. import sysutil
from .. import util
from ..netns import NetworkNamespace
Expand Down Expand Up @@ -80,7 +80,7 @@ def __init__(self, nameservers):
self.nameservers = nameservers


class PortForward:
class InternalPortForwarding:
def __init__(self, nft_rule, ttl=1):
self.nft_rule = nft_rule
self.ttl = ttl
Expand Down Expand Up @@ -306,7 +306,7 @@ def setup_dns_servers(self, hop_info: HopInfo):
self.log_debug('Previous hop did not expose DNS servers.')

@property
def port_forwards(self) -> List[PortForward]:
def port_forwards(self) -> List[InternalPortForwarding]:
return []

@staticmethod
Expand Down
8 changes: 4 additions & 4 deletions pallium/hops/tor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import time
from typing import Optional, List

from .hop import PortForward
from .hop import InternalPortForwarding
from .. import nftables, security
from pyroute2.iproute import IPRoute

Expand Down Expand Up @@ -147,8 +147,8 @@ def connect(self):
nftables.dnat(to=(bind_ip, 1053)),
))
fw = [
PortForward((nftables.ip(daddr=bind_ip), nftables.udp(dport=53), nftables.accept())),
PortForward((nftables.ip(saddr=bind_ip), nftables.udp(sport=53), nftables.accept()))
InternalPortForwarding((nftables.ip(daddr=bind_ip), nftables.udp(dport=53), nftables.accept())),
InternalPortForwarding((nftables.ip(saddr=bind_ip), nftables.udp(sport=53), nftables.accept()))
]
self._port_forwards.extend(fw)
return self._port_forwards
Expand Down Expand Up @@ -193,5 +193,5 @@ def kill_switch_device(self) -> Optional[str]:
return None # Kill switching happens in the SocksHop

@property
def port_forwards(self) -> List[PortForward]:
def port_forwards(self) -> List[InternalPortForwarding]:
return self._port_forwards
102 changes: 76 additions & 26 deletions pallium/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,26 @@
import shutil
import signal
import socket
import stat
import subprocess
import sys
import traceback
from typing import List, Union, Optional, Callable

from pyroute2.iproute import IPRoute
from pyroute2.ethtool import Ethtool
from pyroute2.netlink.rtnl.ifaddrmsg import IFA_F_NODAD, IFA_F_NOPREFIXROUTE
from pyroute2.netlink.exceptions import NetlinkError

from .nftables import NFTables

from . import audio, debugging, resolvconf, runtime
from . import config
from . import dhcp as dhcpd
from . import hops
from . import nftables
from . import onexit
from . import security
from . import slirp
from . import sysutil
from . import typeinfo
from . import util
from .sandbox import Sandbox
from .graphics import enable_gui_access
Expand Down Expand Up @@ -69,20 +68,6 @@ def from_json(cls, obj):
return cls(**obj)


class LocalPortForwarding:
host: (typeinfo.IPAddress, int)
guest: (Optional[typeinfo.IPAddress], int)

def __init__(self):
pass

def __str__(self):
pass

def from_json(self, obj):
pass


class Bridge:
def __init__(self, name: Optional[str] = None,
routes: List[Union[ipaddress.ip_network, str]] = None,
Expand Down Expand Up @@ -144,7 +129,8 @@ def __init__(self, chain: List[hops.Hop], *, # The API is not stable yet. Preve
kill_switch: bool = True,
mounts: Optional[List[MountInstruction]] = None,
sandbox: Sandbox = None,
command: Union[List[str], str, None] = None):
command: Union[List[str], str, None] = None,
configuration: config.Configuration = None):
"""
Initialize a pallium profile.
Expand Down Expand Up @@ -198,6 +184,10 @@ def __init__(self, chain: List[hops.Hop], *, # The API is not stable yet. Preve
if sandbox is not None:
self._mounts.extend(self.sandbox.get_mounts())

if configuration is None:
configuration = config.Configuration()
self.config = configuration

# noinspection PyRedeclaration
@property
def quiet(self):
Expand Down Expand Up @@ -253,6 +243,16 @@ def from_config(cls, settings: dict) -> 'Profile':
sandbox = Sandbox()
profile_args['sandbox'] = sandbox

# This is (partially) how the configuration should be built.
# TODO: Build the configuration like this for all object properties.
# When complete, from_config should simply call config.Configuration.from_json.
port_forwarding = settings.get('networking', {}).get('port_forwarding', {})
profile_args['configuration'] = config.Configuration(
networking=config.Networking(
port_forwarding=config.PortForwarding.from_json(port_forwarding)
)
)

defaults = {}
for k in settings:
if not k.startswith('default_'):
Expand Down Expand Up @@ -401,6 +401,12 @@ def run(self, multisession: bool = False) -> 'OwnedSession':
self._postexec_fn.append(result)

self.start_networks = [util.find_unused_private_network(4), util.find_unused_private_network(6)]

self.netpool.add_used([
ipaddress.ip_network('10.0.2.0/24'),
ipaddress.ip_network('fd00::/64')
]) # Slirpnetstack, cf. slirp.py

self.netpool.add_used(self.start_networks)
# TODO: Deal with custom defined networks.
self.netinfo = self.start_networks.copy()
Expand Down Expand Up @@ -670,9 +676,9 @@ def create_filter_chain(nft, chain_name, hook, policy=0):
nft.chain('add', table='pallium', name=chain_name, type='filter', hook=hook, priority=0, policy=policy)


def masquerade(iface, netinfo: Union[ipaddress.IPv4Network, ipaddress.IPv6Network],
chain_name='POSTROUTING',
device=None):
def masquerade_interface_networks(iface, netinfo: Union[ipaddress.IPv4Network, ipaddress.IPv6Network],
chain_name='POSTROUTING',
device=None):
with IPRoute() as ipr:
ipr.addr('add', index=iface, address=str(netinfo.network_address + 1),
prefixlen=netinfo.prefixlen)
Expand Down Expand Up @@ -883,7 +889,7 @@ def revert():

sysutil.ip_forward(version, True)

def _filter_traffic(self, chain_prefix, hop_info):
def _setup_filter_rule(self, chain_prefix, hop_info):
oifname = hop_info.previous.indev
iifname = hop_info.outdev

Expand Down Expand Up @@ -990,7 +996,7 @@ def revert_netns_creation():
return

if runtime.use_slirp4netns() and is_first_hop:
slirp_app = slirp.available_slirp_class()(hop_info, self.profile.quiet)
slirp_app = slirp.available_slirp_class()(self.profile.config, hop_info, self.profile.quiet)

hop_info.netns.run(slirp_app.prepare)

Expand All @@ -1013,13 +1019,57 @@ def revert_first_hop():

def add_nft_rules():
for netinfo in hop_info.netinfo:
masquerade(outfd, netinfo, nft_postrouting_nat_chain, hop_info.outdev)
masquerade_interface_networks(outfd, netinfo, nft_postrouting_nat_chain, hop_info.outdev)

# if not is_first_hop and self.profile.kill_switch and hop_info.previous.hop.kill_switch_device is not None:
self._filter_traffic(nft_filter_prefix, hop_info)
self._setup_filter_rule(nft_filter_prefix, hop_info)

hop_info.previous.netns.run(add_nft_rules)

def local_port_forwarding_setup():
# TODO: In filter chain, do allow traffic.
# TODO: route_localnet for last namespace (https://serverfault.com/a/1022269).

with IPRoute() as ip:
for netinfo in hop_info.netinfo:
outdev = ip.link_lookup(ifname=hop_info.outdev)[0]
indev = ip.link_lookup(ifname=hop_info.previous.indev)[0]
dst = {
4: '10.0.2.101/32',
6: 'fd00::101/128'
}
ip.route('add', dst=dst[netinfo.version], oifd=outdev, gateway=str(netinfo.network_address + 2))

# We don't want to have this route in the namespace which slirp4netstack is in because
# the address is part of the subnet assigned to the slirp interface.
# 0 is the default namespace, 1 is the namespace which slirp4netstack is connected to,
# 2 is the following namespace. Since we run that in the previous namespace, we are at >= 3.
if hop_info.index > 2:
for netinfo in hop_info.previous.netinfo:
src = {
4: '10.0.2.2/32',
6: 'fd00::2/128'
}
ip.route('add', dst=src[netinfo.version], oifd=indev, gateway=str(netinfo.network_address + 1))

local_fwds = self.profile.config.networking.port_forwarding.local
if len(local_fwds) > 0:
with NFTables(nfgen_family=NFPROTO_INET) as nft:
nft.table('add', name='pallium')
nft.chain('add', table='pallium', name='prerouting', type='nat', hook='prerouting',
priority=-100, policy=nftables.NF_ACCEPT)

for i, local_fwd in enumerate(local_fwds):
port = i + 1
nft.rule('add', table='pallium', chain='prerouting', expressions=(
nftables.ip(daddr='10.0.2.100'),
# TODO: Also support UDP.
nftables.tcp(dport=port),
nftables.dnat(to=('10.0.2.101', 1))
))

hop_info.previous.netns.run(local_port_forwarding_setup)

def run_in_new_netns():
with IPRoute() as ip:
for netinfo in hop_info.netinfo:
Expand Down Expand Up @@ -1054,7 +1104,7 @@ def _build_main_bridge(self, hop_info: hops.HopInfo,
def run_in_hop_netns():
logging.debug('Setup bridge masquerading: %s' % str(nets))
for n in nets:
masquerade(outfd, n, bridge_name_in, bridge_name_in)
masquerade_interface_networks(outfd, n, bridge_name_in, bridge_name_in)

if self._bridge.dhcp:
# TODO: Transform for fork support.
Expand Down
Loading

0 comments on commit 07ea3be

Please sign in to comment.