torproxy: Add separate app for Tor Proxy

- Includes SocksPort and "Download software packages over Tor" feature,
  as well as setting upstream bridges.

- "Download software packages over Tor" option is enabled by default.

- When upgrading, if Tor app was enabled and "Download software
  packages over Tor" was enabled, then Tor Proxy will be installed.

- The default tor instance is now called tor@default. The "tor" service
  is an multi-instance master that has Wants relation all instances.

Tests:

- Tests for Tor and Tor Proxy passed.

- Enable Tor, and run the tests for Tor Proxy. Afterwards, Tor is still
  enabled and running.

- Enable Tor Proxy, and run the tests for Tor. Afterwards, Tor Proxy is
  still enabled and running.

- Test setting upstream bridges for Tor and Tor Proxy.

- Install FreedomBox 23.11 in a VM and install Tor with default
  settings. Install new FreedomBox version with Tor Proxy. After
  install, both Tor and Tor Proxy apps are installed and running.
  /etc/tor/instances/{plinth,fbxproxy}/torrc both have expected content.

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
James Valleroy 2023-06-09 15:37:21 -04:00 committed by Sunil Mohan Adapa
parent 64d6356c2f
commit b0c75b7849
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
23 changed files with 999 additions and 248 deletions

View File

@ -1,24 +1,29 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to configure Tor."""
import logging
from django.utils.translation import gettext_lazy as _
from plinth import action_utils
from plinth import app as app_module
from plinth import cfg, menu
from plinth import menu
from plinth import setup as setup_module
from plinth.daemon import (Daemon, app_is_running, diagnose_netcat,
diagnose_port_listening)
from plinth.modules.apache.components import Webserver, diagnose_url
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import Firewall
from plinth.modules.names.components import DomainType
from plinth.modules.torproxy.utils import is_apt_transport_tor_enabled
from plinth.modules.users.components import UsersAndGroups
from plinth.package import Packages
from plinth.signals import domain_added, domain_removed
from plinth.utils import format_lazy
from . import manifest, privileged, utils
logger = logging.getLogger(__name__)
_description = [
_('Tor is an anonymous communication system. You can learn more '
'about it from the <a href="https://www.torproject.org/">Tor '
@ -26,9 +31,6 @@ _description = [
'Tor Project recommends that you use the '
'<a href="https://www.torproject.org/download/download-easy.html.en">'
'Tor Browser</a>.'),
format_lazy(
_('A Tor SOCKS port is available on your {box_name} for internal '
'networks on TCP port 9050.'), box_name=_(cfg.box_name))
]
@ -37,7 +39,7 @@ class TorApp(app_module.App):
app_id = 'tor'
_version = 6
_version = 7
def __init__(self):
"""Create components for the app."""
@ -57,28 +59,20 @@ class TorApp(app_module.App):
parent_url_name='apps')
self.add(menu_item)
packages = Packages('packages-tor', [
'tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy', 'apt-transport-tor'
])
packages = Packages('packages-tor',
['tor', 'tor-geoipdb', 'obfs4proxy'])
self.add(packages)
domain_type = DomainType('domain-type-tor', _('Tor Onion Service'),
'tor:index', can_have_certificate=False)
self.add(domain_type)
firewall = Firewall('firewall-tor-socks', _('Tor Socks Proxy'),
ports=['tor-socks'], is_external=False)
self.add(firewall)
firewall = Firewall('firewall-tor-relay', _('Tor Bridge Relay'),
ports=['tor-orport', 'tor-obfs3',
'tor-obfs4'], is_external=True)
self.add(firewall)
daemon = Daemon(
'daemon-tor', 'tor@plinth', strict_check=True,
listen_ports=[(9050, 'tcp4'), (9050, 'tcp6'), (9040, 'tcp4'),
(9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')])
daemon = Daemon('daemon-tor', 'tor@plinth', strict_check=True)
self.add(daemon)
webserver = Webserver('webserver-onion-location',
@ -113,8 +107,7 @@ class TorApp(app_module.App):
update_hidden_service_domain()
def disable(self):
"""Disable APT use of Tor before disabling."""
privileged.configure(apt_transport_tor=False)
"""Disable the app and remove HS domain."""
super().disable()
update_hidden_service_domain()
@ -166,21 +159,12 @@ class TorApp(app_module.App):
'passed' if len(hs_hostname) == 56 else 'failed'
])
results.append(_diagnose_url_via_tor('http://www.debian.org', '4'))
results.append(_diagnose_url_via_tor('http://www.debian.org', '6'))
results.append(_diagnose_tor_use('https://check.torproject.org', '4'))
results.append(_diagnose_tor_use('https://check.torproject.org', '6'))
return results
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
privileged.setup(old_version)
if not old_version:
privileged.configure(apt_transport_tor=True)
update_hidden_service_domain(utils.get_status())
# Enable/disable Onion-Location component based on app status.
@ -189,11 +173,26 @@ class TorApp(app_module.App):
daemon_component = self.get_component('daemon-tor')
component = self.get_component('webserver-onion-location')
if daemon_component.is_enabled():
logger.info('Enabling Onion-Location component')
component.enable()
else:
logger.info('Disabling Onion-Location component')
component.disable()
# The SOCKS proxy and "Download software packages using Tor" features
# were moved into a new app, Tor Proxy, in version 7. If the "Download
# software packages using Tor" option was enabled, then install and
# enable Tor Proxy, to avoid any issues for apt.
if old_version and old_version < 7:
if self.is_enabled() and is_apt_transport_tor_enabled():
logger.info(
'Tor Proxy app will be installed for apt-transport-tor')
# This creates the operation, which will run after the current
# operation (Tor setup) is completed.
setup_module.run_setup_on_app('torproxy')
if not old_version:
logger.info('Enabling Tor app')
self.enable()
def uninstall(self):
@ -234,23 +233,3 @@ def _diagnose_control_port():
negate=negate))
return results
def _diagnose_url_via_tor(url, kind=None):
"""Diagnose whether a URL is reachable via Tor."""
result = diagnose_url(url, kind=kind, wrapper='torsocks')
result[0] = _('Access URL {url} on tcp{kind} via Tor') \
.format(url=url, kind=kind)
return result
def _diagnose_tor_use(url, kind=None):
"""Diagnose whether webpage at URL reports that we are using Tor."""
expected_output = 'Congratulations. This browser is configured to use Tor.'
result = diagnose_url(url, kind=kind, wrapper='torsocks',
expected_output=expected_output)
result[0] = _('Confirm Tor usage at {url} on tcp{kind}') \
.format(url=url, kind=kind)
return result

View File

@ -70,8 +70,8 @@ def bridges_validator(bridges):
raise validation_error
class TorForm(forms.Form): # pylint: disable=W0232
"""Tor configuration form."""
class TorCommonForm(forms.Form):
"""Tor common configuration form."""
use_upstream_bridges = forms.BooleanField(
label=_('Use upstream bridges to connect to Tor network'),
required=False, help_text=_(
@ -87,6 +87,10 @@ class TorForm(forms.Form): # pylint: disable=W0232
'https://bridges.torproject.org/</a> and copy/paste the bridge '
'information here. Currently supported transports are none, '
'obfs3, obfs4 and scamblesuit.'), validators=[bridges_validator])
class TorForm(TorCommonForm):
"""Tor configuration form."""
relay_enabled = forms.BooleanField(
label=_('Enable Tor relay'), required=False, help_text=format_lazy(
_('When enabled, your {box_name} will run a Tor relay and donate '
@ -107,12 +111,6 @@ class TorForm(forms.Form): # pylint: disable=W0232
'services (such as wiki or chat) without revealing its '
'location. Do not use this for strong anonymity yet.'),
box_name=_(cfg.box_name)))
apt_transport_tor_enabled = forms.BooleanField(
label=_('Download software packages over Tor'), required=False,
help_text=_('When enabled, software will be downloaded over the Tor '
'network for installations and upgrades. This adds a '
'degree of privacy and security during software '
'downloads.'))
def clean(self):
"""Validate the form for cross-field integrity."""

View File

@ -44,11 +44,11 @@ clients = [{
backup = {
'config': {
'directories': ['/etc/tor/'],
'directories': ['/etc/tor/instances/plinth/'],
'files': [str(privileged.TOR_APACHE_SITE)]
},
'secrets': {
'directories': ['/var/lib/tor/', '/var/lib/tor-instances/']
'directories': ['/var/lib/tor-instances/plinth/']
},
'services': ['tor@plinth']
}

View File

@ -2,6 +2,7 @@
"""Configure Tor service."""
import codecs
import logging
import os
import pathlib
import re
@ -15,20 +16,29 @@ import augeas
from plinth import action_utils
from plinth.actions import privileged
from plinth.modules.tor.utils import APT_TOR_PREFIX, get_augeas, iter_apt_uris
INSTANCE_NAME = 'plinth'
SERVICE_FILE = '/etc/firewalld/services/tor-{0}.xml'
TOR_CONFIG = '/files/etc/tor/instances/plinth/torrc'
TOR_STATE_FILE = '/var/lib/tor-instances/plinth/state'
TOR_AUTH_COOKIE = '/var/run/tor-instances/plinth/control.authcookie'
SERVICE_NAME = f'tor@{INSTANCE_NAME}'
TOR_CONFIG = f'/etc/tor/instances/{INSTANCE_NAME}/torrc'
TOR_CONFIG_AUG = f'/files/{TOR_CONFIG}'
TOR_STATE_FILE = f'/var/lib/tor-instances/{INSTANCE_NAME}/state'
TOR_AUTH_COOKIE = f'/var/run/tor-instances/{INSTANCE_NAME}/control.authcookie'
TOR_APACHE_SITE = '/etc/apache2/conf-available/onion-location-freedombox.conf'
logger = logging.getLogger(__name__)
@privileged
def setup(old_version: int):
"""Setup Tor configuration after installing it."""
if old_version and old_version <= 4:
_upgrade_orport_value()
if old_version:
if old_version <= 4:
_upgrade_orport_value()
if old_version <= 6:
_remove_proxy()
return
_first_time_setup()
@ -37,50 +47,39 @@ def setup(old_version: int):
def _first_time_setup():
"""Setup Tor configuration for the first time setting defaults."""
logger.info('Performing first time setup for Tor')
# Disable default tor service. We will use tor@plinth instance
# instead.
_disable_apt_transport_tor()
action_utils.service_disable('tor')
action_utils.service_disable('tor@default')
subprocess.run(['tor-instance-create', 'plinth'], check=True)
subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True)
# Remove line starting with +SocksPort, since our augeas lens
# doesn't handle it correctly.
with open('/etc/tor/instances/plinth/torrc', 'r',
encoding='utf-8') as torrc:
with open(TOR_CONFIG, 'r', encoding='utf-8') as torrc:
torrc_lines = torrc.readlines()
with open('/etc/tor/instances/plinth/torrc', 'w',
encoding='utf-8') as torrc:
with open(TOR_CONFIG, 'w', encoding='utf-8') as torrc:
for line in torrc_lines:
if not line.startswith('+'):
torrc.write(line)
aug = augeas_load()
aug.set(TOR_CONFIG + '/SocksPort[1]', '[::]:9050')
aug.set(TOR_CONFIG + '/SocksPort[2]', '0.0.0.0:9050')
aug.set(TOR_CONFIG + '/ControlPort', '9051')
aug.set(TOR_CONFIG_AUG + '/ControlPort', '9051')
_enable_relay(relay=True, bridge=True, aug=aug)
aug.set(TOR_CONFIG + '/ExitPolicy[1]', 'reject *:*')
aug.set(TOR_CONFIG + '/ExitPolicy[2]', 'reject6 *:*')
aug.set(TOR_CONFIG_AUG + '/ExitPolicy[1]', 'reject *:*')
aug.set(TOR_CONFIG_AUG + '/ExitPolicy[2]', 'reject6 *:*')
aug.set(TOR_CONFIG + '/VirtualAddrNetworkIPv4', '10.192.0.0/10')
aug.set(TOR_CONFIG + '/AutomapHostsOnResolve', '1')
aug.set(TOR_CONFIG + '/TransPort[1]', '127.0.0.1:9040')
aug.set(TOR_CONFIG + '/TransPort[2]', '[::1]:9040')
aug.set(TOR_CONFIG + '/DNSPort[1]', '127.0.0.1:9053')
aug.set(TOR_CONFIG + '/DNSPort[2]', '[::1]:9053')
aug.set(TOR_CONFIG + '/HiddenServiceDir',
'/var/lib/tor-instances/plinth/hidden_service')
aug.set(TOR_CONFIG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
aug.set(TOR_CONFIG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
aug.set(TOR_CONFIG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
aug.set(TOR_CONFIG_AUG + '/HiddenServiceDir',
f'/var/lib/tor-instances/{INSTANCE_NAME}/hidden_service')
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
aug.save()
action_utils.service_enable('tor@plinth')
action_utils.service_restart('tor@plinth')
action_utils.service_enable(SERVICE_NAME)
action_utils.service_restart(SERVICE_NAME)
_update_ports()
# wait until hidden service information is available
@ -110,28 +109,45 @@ def _upgrade_orport_value():
443 is not possible in FreedomBox due it is use for other purposes.
"""
logger.info('Upgrading ORPort value for Tor')
aug = augeas_load()
if _is_relay_enabled(aug):
aug.set(TOR_CONFIG + '/ORPort[1]', '9001')
aug.set(TOR_CONFIG + '/ORPort[2]', '[::]:9001')
aug.set(TOR_CONFIG_AUG + '/ORPort[1]', '9001')
aug.set(TOR_CONFIG_AUG + '/ORPort[2]', '[::]:9001')
aug.save()
action_utils.service_try_restart('tor@plinth')
action_utils.service_try_restart(SERVICE_NAME)
# Tor may not be running, don't try to read/update all ports
_update_port('orport', 9001)
action_utils.service_restart('firewalld')
def _remove_proxy():
"""Remove SocksProxy from configuration.
This functionality was split off to a separate app, Tor Proxy.
"""
logger.info('Removing SocksProxy from Tor configuration')
aug = augeas_load()
for config in [
'SocksPort', 'VirtualAddrNetworkIPv4', 'AutomapHostsOnResolve',
'TransPort', 'DNSPort'
]:
aug.remove(TOR_CONFIG_AUG + '/' + config)
aug.save()
action_utils.service_try_restart(SERVICE_NAME)
@privileged
def configure(use_upstream_bridges: Optional[bool] = None,
upstream_bridges: Optional[str] = None,
relay: Optional[bool] = None,
bridge_relay: Optional[bool] = None,
hidden_service: Optional[bool] = None,
apt_transport_tor: Optional[bool] = None):
hidden_service: Optional[bool] = None):
"""Configure Tor."""
aug = augeas_load()
@ -151,11 +167,6 @@ def configure(use_upstream_bridges: Optional[bool] = None,
elif hidden_service is not None:
_disable_hs(aug=aug)
if apt_transport_tor:
_enable_apt_transport_tor()
elif apt_transport_tor is not None:
_disable_apt_transport_tor()
@privileged
def update_ports():
@ -166,12 +177,12 @@ def update_ports():
@privileged
def restart():
"""Restart Tor."""
if (action_utils.service_is_enabled('tor@plinth', strict_check=True)
and action_utils.service_is_running('tor@plinth')):
action_utils.service_restart('tor@plinth')
if (action_utils.service_is_enabled(SERVICE_NAME, strict_check=True)
and action_utils.service_is_running(SERVICE_NAME)):
action_utils.service_restart(SERVICE_NAME)
aug = augeas_load()
if aug.get(TOR_CONFIG + '/HiddenServiceDir'):
if aug.get(TOR_CONFIG_AUG + '/HiddenServiceDir'):
# wait until hidden service information is available
tries = 0
while not _get_hidden_service()['enabled']:
@ -197,26 +208,26 @@ def get_status() -> dict[str, Union[bool, str, dict[str, Any]]]:
def _are_upstream_bridges_enabled(aug) -> bool:
"""Return whether upstream bridges are being used."""
use_bridges = aug.get(TOR_CONFIG + '/UseBridges')
use_bridges = aug.get(TOR_CONFIG_AUG + '/UseBridges')
return use_bridges == '1'
def _get_upstream_bridges(aug) -> str:
"""Return upstream bridges separated by newlines."""
matches = aug.match(TOR_CONFIG + '/Bridge')
matches = aug.match(TOR_CONFIG_AUG + '/Bridge')
bridges = [aug.get(match) for match in matches]
return '\n'.join(bridges)
def _is_relay_enabled(aug) -> bool:
"""Return whether a relay is enabled."""
orport = aug.get(TOR_CONFIG + '/ORPort[1]')
orport = aug.get(TOR_CONFIG_AUG + '/ORPort[1]')
return bool(orport) and orport != '0'
def _is_bridge_relay_enabled(aug) -> bool:
"""Return whether bridge relay is enabled."""
bridge = aug.get(TOR_CONFIG + '/BridgeRelay')
bridge = aug.get(TOR_CONFIG_AUG + '/BridgeRelay')
return bridge == '1'
@ -272,8 +283,8 @@ def _get_hidden_service(aug=None) -> dict[str, Any]:
if not aug:
aug = augeas_load()
hs_dir = aug.get(TOR_CONFIG + '/HiddenServiceDir')
hs_port_paths = aug.match(TOR_CONFIG + '/HiddenServicePort')
hs_dir = aug.get(TOR_CONFIG_AUG + '/HiddenServiceDir')
hs_port_paths = aug.match(TOR_CONFIG_AUG + '/HiddenServicePort')
for hs_port_path in hs_port_paths:
port_info = aug.get(hs_port_path).split()
@ -300,14 +311,13 @@ def _get_hidden_service(aug=None) -> dict[str, Any]:
def _enable():
"""Enable and start the service."""
action_utils.service_enable('tor@plinth')
action_utils.service_enable(SERVICE_NAME)
_update_ports()
def _disable():
"""Disable and stop the service."""
_disable_apt_transport_tor()
action_utils.service_disable('tor@plinth')
action_utils.service_disable(SERVICE_NAME)
def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None,
@ -320,9 +330,9 @@ def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None,
aug = augeas_load()
if use_upstream_bridges:
aug.set(TOR_CONFIG + '/UseBridges', '1')
aug.set(TOR_CONFIG_AUG + '/UseBridges', '1')
else:
aug.set(TOR_CONFIG + '/UseBridges', '0')
aug.set(TOR_CONFIG_AUG + '/UseBridges', '0')
aug.save()
@ -335,16 +345,16 @@ def _set_upstream_bridges(upstream_bridges=None, aug=None):
if not aug:
aug = augeas_load()
aug.remove(TOR_CONFIG + '/Bridge')
aug.remove(TOR_CONFIG_AUG + '/Bridge')
if upstream_bridges:
bridges = [bridge.strip() for bridge in upstream_bridges.split('\n')]
bridges = [bridge for bridge in bridges if bridge]
for bridge in bridges:
parts = [part for part in bridge.split() if part]
bridge = ' '.join(parts)
aug.set(TOR_CONFIG + '/Bridge[last() + 1]', bridge.strip())
aug.set(TOR_CONFIG_AUG + '/Bridge[last() + 1]', bridge.strip())
aug.set(TOR_CONFIG + '/ClientTransportPlugin',
aug.set(TOR_CONFIG_AUG + '/ClientTransportPlugin',
'obfs3,scramblesuit,obfs4 exec /usr/bin/obfs4proxy')
aug.save()
@ -362,20 +372,20 @@ def _enable_relay(relay: Optional[bool], bridge: Optional[bool],
use_upstream_bridges = _are_upstream_bridges_enabled(aug)
if relay and not use_upstream_bridges:
aug.set(TOR_CONFIG + '/ORPort[1]', '9001')
aug.set(TOR_CONFIG + '/ORPort[2]', '[::]:9001')
aug.set(TOR_CONFIG_AUG + '/ORPort[1]', '9001')
aug.set(TOR_CONFIG_AUG + '/ORPort[2]', '[::]:9001')
elif relay is not None:
aug.remove(TOR_CONFIG + '/ORPort')
aug.remove(TOR_CONFIG_AUG + '/ORPort')
if bridge and not use_upstream_bridges:
aug.set(TOR_CONFIG + '/BridgeRelay', '1')
aug.set(TOR_CONFIG + '/ServerTransportPlugin',
aug.set(TOR_CONFIG_AUG + '/BridgeRelay', '1')
aug.set(TOR_CONFIG_AUG + '/ServerTransportPlugin',
'obfs3,obfs4 exec /usr/bin/obfs4proxy')
aug.set(TOR_CONFIG + '/ExtORPort', 'auto')
aug.set(TOR_CONFIG_AUG + '/ExtORPort', 'auto')
elif bridge is not None:
aug.remove(TOR_CONFIG + '/BridgeRelay')
aug.remove(TOR_CONFIG + '/ServerTransportPlugin')
aug.remove(TOR_CONFIG + '/ExtORPort')
aug.remove(TOR_CONFIG_AUG + '/BridgeRelay')
aug.remove(TOR_CONFIG_AUG + '/ServerTransportPlugin')
aug.remove(TOR_CONFIG_AUG + '/ExtORPort')
aug.save()
@ -388,11 +398,11 @@ def _enable_hs(aug=None):
if _get_hidden_service(aug)['enabled']:
return
aug.set(TOR_CONFIG + '/HiddenServiceDir',
'/var/lib/tor-instances/plinth/hidden_service')
aug.set(TOR_CONFIG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
aug.set(TOR_CONFIG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
aug.set(TOR_CONFIG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
aug.set(TOR_CONFIG_AUG + '/HiddenServiceDir',
f'/var/lib/tor-instances/{INSTANCE_NAME}/hidden_service')
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[1]', '22 127.0.0.1:22')
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[2]', '80 127.0.0.1:80')
aug.set(TOR_CONFIG_AUG + '/HiddenServicePort[3]', '443 127.0.0.1:443')
aug.save()
_set_onion_header(_get_hidden_service(aug))
@ -405,39 +415,12 @@ def _disable_hs(aug=None):
if not _get_hidden_service(aug)['enabled']:
return
aug.remove(TOR_CONFIG + '/HiddenServiceDir')
aug.remove(TOR_CONFIG + '/HiddenServicePort')
aug.remove(TOR_CONFIG_AUG + '/HiddenServiceDir')
aug.remove(TOR_CONFIG_AUG + '/HiddenServicePort')
aug.save()
_set_onion_header(None)
def _enable_apt_transport_tor():
"""Enable package download over Tor."""
aug = get_augeas()
for uri_path in iter_apt_uris(aug):
uri = aug.get(uri_path)
if uri.startswith('http://') or uri.startswith('https://'):
aug.set(uri_path, APT_TOR_PREFIX + uri)
aug.save()
def _disable_apt_transport_tor():
"""Disable package download over Tor."""
try:
aug = get_augeas()
except Exception:
# Disable what we can, so APT is not unusable.
pass
for uri_path in iter_apt_uris(aug):
uri = aug.get(uri_path)
if uri.startswith(APT_TOR_PREFIX):
aug.set(uri_path, uri[len(APT_TOR_PREFIX):])
aug.save()
def _update_port(name, number):
"""Update firewall service information for single port."""
lines = """<?xml version="1.0" encoding="utf-8"?>
@ -485,14 +468,14 @@ def augeas_load():
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Tor/lens', 'Tor.lns')
aug.set('/augeas/load/Tor/incl[last() + 1]',
'/etc/tor/instances/plinth/torrc')
aug.set('/augeas/load/Tor/incl[last() + 1]', TOR_CONFIG)
aug.load()
return aug
def _set_onion_header(hidden_service):
"""Set Apache configuration for the Onion-Location header."""
logger.info('Setting Onion-Location header for Apache')
config_file = pathlib.Path(TOR_APACHE_SITE)
if hidden_service and hidden_service['enabled']:
# https://community.torproject.org/onion-services/advanced/onion-location/
@ -512,10 +495,13 @@ def _set_onion_header(hidden_service):
@privileged
def uninstall():
"""Remove create instances."""
"""Remove plinth instance."""
directories = [
'/etc/tor/instances/', '/var/lib/tor-instances/',
'/var/run/tor-instances/'
f'/etc/tor/instances/{INSTANCE_NAME}/',
f'/var/lib/tor-instances/{INSTANCE_NAME}/',
f'/var/run/tor-instances/{INSTANCE_NAME}/'
]
for directory in directories:
shutil.rmtree(directory, ignore_errors=True)
os.unlink(f'/var/run/tor-instances/{INSTANCE_NAME}.defaults')

View File

@ -11,7 +11,6 @@ _TOR_FEATURE_TO_ELEMENT = {
'relay': 'tor-relay_enabled',
'bridge-relay': 'tor-bridge_relay_enabled',
'hidden-services': 'tor-hs_enabled',
'software': 'tor-apt_transport_tor_enabled'
}
pytestmark = [pytest.mark.apps, pytest.mark.domain, pytest.mark.tor]
@ -19,11 +18,8 @@ pytestmark = [pytest.mark.apps, pytest.mark.domain, pytest.mark.tor]
class TestTorApp(functional.BaseAppTests):
app_name = 'tor'
has_service = True
has_service = False
has_web = False
# TODO: Investigate why accessing IPv6 sites through Tor fails in
# container.
check_diagnostics = False
def test_set_tor_relay_configuration(self, session_browser):
"""Test setting Tor relay configuration."""
@ -52,13 +48,6 @@ class TestTorApp(functional.BaseAppTests):
enabled=True)
_assert_hidden_services(session_browser)
def test_set_download_software_packages_over_tor(self, session_browser):
"""Test setting download software packages over Tor."""
functional.app_enable(session_browser, 'tor')
_feature_enable(session_browser, 'software', should_enable=True)
_feature_enable(session_browser, 'software', should_enable=False)
_assert_feature_enabled(session_browser, 'software', enabled=False)
# TODO: Test more thoroughly by checking same hidden service is restored
# and by actually connecting using Tor.
@pytest.mark.backups

View File

@ -14,14 +14,6 @@ from plinth.modules.tor import forms, utils
class TestTor:
"""Test cases for testing the Tor module."""
@staticmethod
@pytest.mark.usefixtures('needs_root')
def test_is_apt_transport_tor_enabled():
"""Test that is_apt_transport_tor_enabled does not raise any unhandled
exceptions.
"""
utils.is_apt_transport_tor_enabled()
@staticmethod
@patch('plinth.app.App.get')
@pytest.mark.usefixtures('needs_root', 'load_cfg')
@ -29,7 +21,7 @@ class TestTor:
"""Test that get_status does not raise any unhandled exceptions.
This should work regardless of whether tor is installed, or
/etc/tor/torrc exists.
/etc/tor/instances/plinth/torrc exists.
"""
utils.get_status()

View File

@ -1,21 +1,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Tor utility functions."""
import itertools
import augeas
from plinth import app as app_module
from plinth.daemon import app_is_running
from plinth.modules.names.components import DomainName
from . import privileged
APT_SOURCES_URI_PATHS = ('/files/etc/apt/sources.list/*/uri',
'/files/etc/apt/sources.list.d/*/*/uri',
'/files/etc/apt/sources.list.d/*/*/URIs/*')
APT_TOR_PREFIX = 'tor+'
def get_status(initialized=True):
"""Return current Tor status."""
@ -53,50 +44,4 @@ def get_status(initialized=True):
'hs_hostname': hs_info['hostname'],
'hs_ports': hs_info['ports'],
'hs_services': hs_services,
'apt_transport_tor_enabled': is_apt_transport_tor_enabled()
}
def iter_apt_uris(aug):
"""Iterate over all the APT source URIs."""
return itertools.chain.from_iterable(
[aug.match(path) for path in APT_SOURCES_URI_PATHS])
def get_augeas():
"""Return an instance of Augeaus for processing APT configuration."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Aptsources/lens', 'Aptsources.lns')
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
'/etc/apt/sources.list')
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
'/etc/apt/sources.list.d/*.list')
aug.set('/augeas/load/Aptsources822/lens', 'Aptsources822.lns')
aug.set('/augeas/load/Aptsources822/incl[last() + 1]',
'/etc/apt/sources.list.d/*.sources')
aug.load()
# Check for any errors in parsing sources lists.
if aug.match('/augeas/files/etc/apt/sources.list/error') or \
aug.match('/augeas/files/etc/apt/sources.list.d//error'):
raise Exception('Error parsing sources list')
return aug
def is_apt_transport_tor_enabled():
"""Return whether APT is set to download packages over Tor."""
try:
aug = get_augeas()
except Exception:
# If there was an error with parsing.
return False
for uri_path in iter_apt_uris(aug):
uri = aug.get(uri_path)
if not uri.startswith(APT_TOR_PREFIX) and \
(uri.startswith('http://') or uri.startswith('https://')):
return False
return True

View File

@ -98,11 +98,6 @@ def __apply_changes(old_status, new_status):
arguments['hidden_service'] = new_status['hs_enabled']
needs_restart = True
if old_status['apt_transport_tor_enabled'] != \
new_status['apt_transport_tor_enabled']:
arguments['apt_transport_tor'] = (
is_enabled and new_status['apt_transport_tor_enabled'])
if old_status['use_upstream_bridges'] != \
new_status['use_upstream_bridges']:
arguments['use_upstream_bridges'] = new_status['use_upstream_bridges']

View File

@ -0,0 +1,130 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to configure Tor Proxy."""
import logging
from django.utils.translation import gettext_lazy as _
from plinth import app as app_module
from plinth import cfg, menu
from plinth.daemon import Daemon
from plinth.modules.apache.components import diagnose_url
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import Firewall
from plinth.modules.users.components import UsersAndGroups
from plinth.package import Packages
from plinth.utils import format_lazy
from . import manifest, privileged
logger = logging.getLogger(__name__)
_description = [
_('Tor is an anonymous communication system. You can learn more '
'about it from the <a href="https://www.torproject.org/">Tor '
'Project</a> website. For best protection when web surfing, the '
'Tor Project recommends that you use the '
'<a href="https://www.torproject.org/download/download-easy.html.en">'
'Tor Browser</a>.'),
format_lazy(
_('A Tor SOCKS port is available on your {box_name} for internal '
'networks on TCP port 9050.'), box_name=_(cfg.box_name))
]
class TorProxyApp(app_module.App):
"""FreedomBox app for Tor Proxy."""
app_id = 'torproxy'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=self._version,
name=_('Tor Proxy'), icon_filename='tor',
short_description=_('Anonymity Network'),
description=_description,
manual_page='TorProxy',
clients=manifest.clients,
donation_url='https://donate.torproject.org/')
self.add(info)
menu_item = menu.Menu('menu-torproxy', info.name,
info.short_description, info.icon_filename,
'torproxy:index', parent_url_name='apps')
self.add(menu_item)
packages = Packages('packages-torproxy', [
'tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy', 'apt-transport-tor'
])
self.add(packages)
firewall = Firewall('firewall-torproxy-socks', _('Tor Socks Proxy'),
ports=['tor-socks'], is_external=False)
self.add(firewall)
daemon = Daemon(
'daemon-torproxy', 'tor@fbxproxy', strict_check=True,
listen_ports=[(9050, 'tcp4'), (9050, 'tcp6'), (9040, 'tcp4'),
(9040, 'tcp6'), (9053, 'udp4'), (9053, 'udp6')])
self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-torproxy',
reserved_usernames=['debian-tor'])
self.add(users_and_groups)
backup_restore = BackupRestore('backup-restore-torproxy',
**manifest.backup)
self.add(backup_restore)
def disable(self):
"""Disable APT use of Tor before disabling."""
privileged.configure(apt_transport_tor=False)
super().disable()
def diagnose(self):
"""Run diagnostics and return the results."""
results = super().diagnose()
results.append(_diagnose_url_via_tor('http://www.debian.org', '4'))
results.append(_diagnose_url_via_tor('http://www.debian.org', '6'))
results.append(_diagnose_tor_use('https://check.torproject.org', '4'))
results.append(_diagnose_tor_use('https://check.torproject.org', '6'))
return results
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
privileged.setup(old_version)
if not old_version:
logger.info('Enabling apt-transport-tor')
privileged.configure(apt_transport_tor=True)
logger.info('Enabling Tor Proxy app')
self.enable()
def uninstall(self):
"""De-configure and uninstall the app."""
super().uninstall()
privileged.uninstall()
def _diagnose_url_via_tor(url, kind=None):
"""Diagnose whether a URL is reachable via Tor."""
result = diagnose_url(url, kind=kind, wrapper='torsocks')
result[0] = _('Access URL {url} on tcp{kind} via Tor') \
.format(url=url, kind=kind)
return result
def _diagnose_tor_use(url, kind=None):
"""Diagnose whether webpage at URL reports that we are using Tor."""
expected_output = 'Congratulations. This browser is configured to use Tor.'
result = diagnose_url(url, kind=kind, wrapper='torsocks',
expected_output=expected_output)
result[0] = _('Confirm Tor usage at {url} on tcp{kind}') \
.format(url=url, kind=kind)
return result

View File

@ -0,0 +1 @@
plinth.modules.torproxy

View File

@ -0,0 +1,19 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Forms for configuring Tor Proxy.
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from plinth.modules.tor.forms import TorCommonForm
class TorProxyForm(TorCommonForm):
"""Tor Proxy configuration form."""
apt_transport_tor_enabled = forms.BooleanField(
label=_('Download software packages over Tor'), required=False,
help_text=_('When enabled, software will be downloaded over the Tor '
'network for installations and upgrades. This adds a '
'degree of privacy and security during software '
'downloads.'))

View File

@ -0,0 +1,52 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""App manifest for Tor Proxy."""
from django.utils.translation import gettext_lazy as _
from plinth.clients import store_url
_ORBOT_PACKAGE_ID = 'org.torproject.android'
_TOR_BROWSER_DOWNLOAD_URL = \
'https://www.torproject.org/download/download-easy.html'
clients = [{
'name':
_('Tor Browser'),
'platforms': [{
'type': 'download',
'os': 'windows',
'url': _TOR_BROWSER_DOWNLOAD_URL,
}, {
'type': 'download',
'os': 'gnu-linux',
'url': _TOR_BROWSER_DOWNLOAD_URL,
}, {
'type': 'download',
'os': 'macos',
'url': _TOR_BROWSER_DOWNLOAD_URL,
}]
}, {
'name':
_('Orbot: Proxy with Tor'),
'platforms': [{
'type': 'store',
'os': 'android',
'store_name': 'google-play',
'url': store_url('google-play', _ORBOT_PACKAGE_ID)
}, {
'type': 'store',
'os': 'android',
'store_name': 'f-droid',
'url': store_url('f-droid', _ORBOT_PACKAGE_ID)
}]
}]
backup = {
'config': {
'directories': ['/etc/tor/instances/fbxproxy/'],
},
'secrets': {
'directories': ['/var/lib/tor-instances/fbxproxy/']
},
'services': ['tor@fbxproxy']
}

View File

@ -0,0 +1,219 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure Tor Proxy service."""
import logging
import os
import shutil
import subprocess
from typing import Any, Optional, Union
import augeas
from plinth import action_utils
from plinth.actions import privileged
from plinth.modules.torproxy.utils import (APT_TOR_PREFIX, get_augeas,
iter_apt_uris)
logger = logging.getLogger(__name__)
INSTANCE_NAME = 'fbxproxy'
SERVICE_FILE = '/etc/firewalld/services/tor-{0}.xml'
SERVICE_NAME = f'tor@{INSTANCE_NAME}'
TORPROXY_CONFIG = f'/etc/tor/instances/{INSTANCE_NAME}/torrc'
TORPROXY_CONFIG_AUG = f'/files/{TORPROXY_CONFIG}'
@privileged
def setup(old_version: int):
"""Setup Tor configuration after installing it."""
_first_time_setup()
def _first_time_setup():
"""Setup Tor configuration for the first time setting defaults."""
logger.info('Performing first time setup for Tor Proxy')
# Disable default tor service. We will use tor@fbxproxy instance
# instead.
_disable_apt_transport_tor()
action_utils.service_disable('tor@default')
subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True)
# Remove line starting with +SocksPort, since our augeas lens
# doesn't handle it correctly.
with open(TORPROXY_CONFIG, 'r', encoding='utf-8') as torrc:
torrc_lines = torrc.readlines()
with open(TORPROXY_CONFIG, 'w', encoding='utf-8') as torrc:
for line in torrc_lines:
if not line.startswith('+'):
torrc.write(line)
aug = augeas_load()
aug.set(TORPROXY_CONFIG_AUG + '/SocksPort[1]', '[::]:9050')
aug.set(TORPROXY_CONFIG_AUG + '/SocksPort[2]', '0.0.0.0:9050')
aug.set(TORPROXY_CONFIG_AUG + '/ExitPolicy[1]', 'reject *:*')
aug.set(TORPROXY_CONFIG_AUG + '/ExitPolicy[2]', 'reject6 *:*')
aug.set(TORPROXY_CONFIG_AUG + '/VirtualAddrNetworkIPv4', '10.192.0.0/10')
aug.set(TORPROXY_CONFIG_AUG + '/AutomapHostsOnResolve', '1')
aug.set(TORPROXY_CONFIG_AUG + '/TransPort[1]', '127.0.0.1:9040')
aug.set(TORPROXY_CONFIG_AUG + '/TransPort[2]', '[::1]:9040')
aug.set(TORPROXY_CONFIG_AUG + '/DNSPort[1]', '127.0.0.1:9053')
aug.set(TORPROXY_CONFIG_AUG + '/DNSPort[2]', '[::1]:9053')
aug.save()
action_utils.service_enable(SERVICE_NAME)
action_utils.service_restart(SERVICE_NAME)
@privileged
def configure(use_upstream_bridges: Optional[bool] = None,
upstream_bridges: Optional[str] = None,
apt_transport_tor: Optional[bool] = None):
"""Configure Tor."""
aug = augeas_load()
_use_upstream_bridges(use_upstream_bridges, aug=aug)
if upstream_bridges:
_set_upstream_bridges(upstream_bridges, aug=aug)
if apt_transport_tor:
_enable_apt_transport_tor()
elif apt_transport_tor is not None:
_disable_apt_transport_tor()
@privileged
def restart():
"""Restart Tor."""
if (action_utils.service_is_enabled(SERVICE_NAME, strict_check=True)
and action_utils.service_is_running(SERVICE_NAME)):
action_utils.service_restart(SERVICE_NAME)
@privileged
def get_status() -> dict[str, Union[bool, str, dict[str, Any]]]:
"""Return dict with Tor Proxy status."""
aug = augeas_load()
return {
'use_upstream_bridges': _are_upstream_bridges_enabled(aug),
'upstream_bridges': _get_upstream_bridges(aug)
}
def _are_upstream_bridges_enabled(aug) -> bool:
"""Return whether upstream bridges are being used."""
use_bridges = aug.get(TORPROXY_CONFIG_AUG + '/UseBridges')
return use_bridges == '1'
def _get_upstream_bridges(aug) -> str:
"""Return upstream bridges separated by newlines."""
matches = aug.match(TORPROXY_CONFIG_AUG + '/Bridge')
bridges = [aug.get(match) for match in matches]
return '\n'.join(bridges)
def _enable():
"""Enable and start the service."""
action_utils.service_enable(SERVICE_NAME)
def _disable():
"""Disable and stop the service."""
_disable_apt_transport_tor()
action_utils.service_disable(SERVICE_NAME)
def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None,
aug=None):
"""Enable use of upstream bridges."""
if use_upstream_bridges is None:
return
if not aug:
aug = augeas_load()
if use_upstream_bridges:
aug.set(TORPROXY_CONFIG_AUG + '/UseBridges', '1')
else:
aug.set(TORPROXY_CONFIG_AUG + '/UseBridges', '0')
aug.save()
def _set_upstream_bridges(upstream_bridges=None, aug=None):
"""Set list of upstream bridges."""
if upstream_bridges is None:
return
if not aug:
aug = augeas_load()
aug.remove(TORPROXY_CONFIG_AUG + '/Bridge')
if upstream_bridges:
bridges = [bridge.strip() for bridge in upstream_bridges.split('\n')]
bridges = [bridge for bridge in bridges if bridge]
for bridge in bridges:
parts = [part for part in bridge.split() if part]
bridge = ' '.join(parts)
aug.set(TORPROXY_CONFIG_AUG + '/Bridge[last() + 1]',
bridge.strip())
aug.set(TORPROXY_CONFIG_AUG + '/ClientTransportPlugin',
'obfs3,scramblesuit,obfs4 exec /usr/bin/obfs4proxy')
aug.save()
def _enable_apt_transport_tor():
"""Enable package download over Tor."""
aug = get_augeas()
for uri_path in iter_apt_uris(aug):
uri = aug.get(uri_path)
if uri.startswith('http://') or uri.startswith('https://'):
aug.set(uri_path, APT_TOR_PREFIX + uri)
aug.save()
def _disable_apt_transport_tor():
"""Disable package download over Tor."""
try:
aug = get_augeas()
except Exception:
# Disable what we can, so APT is not unusable.
pass
for uri_path in iter_apt_uris(aug):
uri = aug.get(uri_path)
if uri.startswith(APT_TOR_PREFIX):
aug.set(uri_path, uri[len(APT_TOR_PREFIX):])
aug.save()
def augeas_load():
"""Initialize Augeas."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Tor/lens', 'Tor.lns')
aug.set('/augeas/load/Tor/incl[last() + 1]', TORPROXY_CONFIG)
aug.load()
return aug
@privileged
def uninstall():
"""Remove fbxproxy instance."""
directories = [
f'/etc/tor/instances/{INSTANCE_NAME}/',
f'/var/lib/tor-instances/{INSTANCE_NAME}/',
f'/var/run/tor-instances/{INSTANCE_NAME}/'
]
for directory in directories:
shutil.rmtree(directory, ignore_errors=True)
os.unlink(f'/var/run/tor-instances/{INSTANCE_NAME}.defaults')

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="512"
height="512"
id="图层_1"
class="mozwebext"
sodipodi:docname="tor.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata27696">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs27694" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1956"
inkscape:window-height="1329"
id="namedview27692"
showgrid="false"
inkscape:zoom="0.41594518"
inkscape:cx="-376.85156"
inkscape:cy="-302.70084"
inkscape:window-x="1463"
inkscape:window-y="369"
inkscape:window-maximized="0"
inkscape:current-layer="图层_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
transform="matrix(2.7708177,0,0,2.7708177,-452.25352,-177.84153)"
id="layer3"
style="display:inline">
<g
id="layer5">
<path
d="m 264.513,77.977773 -4.917,19.529001 c 6.965,-13.793001 18.027,-24.172001 30.729,-33.323001 -9.287,10.789 -17.754,21.579001 -22.944,32.368001 8.741,-12.292001 20.486,-19.120001 33.733,-23.627001 -17.618,15.706001 -31.60228,32.559277 -42.25528,49.494277 l -8.467,-3.687 c 1.501,-13.521 6.60928,-27.369276 14.12128,-40.754277 z"
id="path2554"
style="fill:#abcd03"
inkscape:connector-curvature="0" />
<path
d="m 241.90113,115.14152 16.116,6.68594 c 0,4.098 -0.33313,16.59703 2.22938,20.28403 26.80289,34.5191 22.29349,103.71329 -5.42951,105.48829 -42.21656,0 -58.317,-28.679 -58.317,-55.03801 0,-24.037 28.816,-40.016 46.025,-54.219 4.37,-3.824 3.61113,-12.27525 -0.62387,-23.20125 z"
id="path2534"
style="fill:#fffcdb"
inkscape:connector-curvature="0" />
<path
d="m 258.02197,121.58695 5.80803,2.96282 c -0.546,3.823 0.273,12.292 4.096,14.476 16.936,10.516 32.914,21.988 39.197,33.46 22.398,40.42601 -15.706,77.84601 -48.62,74.29501 17.891,-13.248 23.081,-40.42501 16.389,-70.06201 -2.731,-11.609 -6.966,-22.125 -14.478,-34.007 -3.25421,-5.83246 -2.11803,-13.06582 -2.39203,-21.12482 z"
id="path2536"
style="fill:#7d4698"
inkscape:connector-curvature="0" />
</g>
<g
id="layer4"
style="display:inline">
<path
d="m 255.226,120.58877 12.018,1.639 c -3.551,11.745 6.966,19.939 10.38,21.852 7.64801,4.234 15.02301,8.604 20.89601,13.93 11.063,10.106 17.345,24.31 17.345,39.333 0,14.886 -6.829,29.226 -18.301,38.786 -10.789,9.014 -25.67501,12.838 -40.15201,12.838 -9.014,0 -17.072,-0.409 -25.812,-3.278 -19.939,-6.692 -34.826,-23.763 -36.055,-44.25 -1.093,-15.979 2.458,-28.134 14.887,-40.835 6.418,-6.692 19.393,-14.34 28.271,-20.486 4.371,-3.005 9.014,-11.473 0.136,-27.451 l 1.776,-1.366 13.15659,8.81203 -11.10759,-4.57803 c 0.956,1.366 3.551,7.512 4.098,9.287 1.229,5.053 0.683,9.971 -0.41,12.155 -5.599,10.107 -15.159,12.838 -22.124,18.574 -12.292,10.106 -25.676,18.164 -24.174,45.888 0.683,13.657 11.336,30.319 27.314,38.104 9.014,4.371 19.394,6.146 29.91,6.692 9.423,0.41 27.45101,-5.19 37.28401,-13.384 10.516,-8.74 16.389,-21.988 16.389,-35.508 0,-13.658 -5.463,-26.632 -15.706,-35.783 -5.873,-5.326 -15.56901,-11.745 -21.57801,-15.16 -6.009,-3.414 -13.521,-12.974 -11.063,-22.124 z"
id="path2538"
inkscape:connector-curvature="0" />
<path
d="m 251.539,140.80177 c -1.229,6.283 -2.595,17.618 -8.058,21.852 -2.322,1.638 -4.644,3.278 -7.102,4.916 -9.833,6.693 -19.667,12.974 -24.173,29.09 -0.956,3.415 -0.136,7.102 0.684,10.516 2.458,9.833 9.423,20.486 14.886,26.769 0,0.273 1.093,0.956 1.093,1.229 4.507,5.327 5.873,6.829 22.944,10.652 l -0.41,1.913 c -10.243,-2.731 -18.71,-5.189 -24.037,-11.336 0,-0.136 -0.956,-1.093 -0.956,-1.093 -5.736,-6.556 -12.702,-17.481 -15.296,-27.724 -0.956,-4.098 -1.775,-7.238 -0.683,-11.473 4.643,-16.661 14.75,-23.217 24.993,-30.182 2.322,-1.502 5.053,-2.869 7.238,-4.644 4.233,-3.14 6.554,-12.701 8.877,-20.485 z"
id="path2540"
inkscape:connector-curvature="0" />
<path
d="m 255.90625,166.74951 c 0.137,7.102 -0.55625,10.66475 1.21875,15.71875 1.092,3.004 4.782,7.1015 5.875,11.0625 1.502,5.327 3.138,11.19901 3,14.75001 0,4.09799 -0.25625,11.74249 -2.03125,19.93749 -1.35362,6.77108 -4.47323,12.58153 -9.71875,15.875 -5.37327,-1.10644 -11.68224,-2.99521 -15.40625,-6.1875 -7.238,-6.282 -13.64875,-16.7865 -14.46875,-25.9375 -0.682,-7.51099 6.27275,-18.5885 15.96875,-24.1875 8.194,-4.78 10.1,-10.22775 11.875,-18.96875 -2.458,7.648 -4.7665,14.05925 -12.6875,18.15625 -11.472,6.009 -17.3585,16.09626 -16.8125,25.65625 0.819,12.291 5.7415,20.6195 15.4375,27.3125 4.097,2.868 11.75125,5.89875 16.53125,6.71875 v -0.625 c 3.62493,-0.67888 8.31818,-6.63267 10.65625,-14.6875 2.049,-7.238 2.85675,-16.502 2.71875,-22.37499 -0.137,-3.414 -1.643,-10.80801 -4.375,-17.50001 -1.502,-3.687 -3.8095,-7.37375 -5.3125,-9.96875 -1.637,-2.597 -1.64875,-8.195 -2.46875,-14.75 z"
id="path2542"
inkscape:connector-curvature="0" />
<path
d="m 255.09375,193.53076 c 0.136,4.78 2.056,10.90451 2.875,17.18751 0.684,4.64399 0.387,9.30824 0.25,13.40624 -0.13495,4.74323 -1.7152,13.24218 -3.875,17.375 -2.03673,-0.93403 -2.83294,-1.99922 -4.15625,-3.71875 -1.638,-2.322 -2.75075,-4.644 -3.84375,-7.375 -0.819,-2.049 -1.7765,-4.394 -2.1875,-7.125 -0.546,-4.097 -0.393,-10.5065 4.25,-17.06249 3.551,-5.19001 4.36475,-5.58476 5.59375,-11.59376 -1.64,5.326 -2.8625,5.869 -6.6875,10.37501 -4.233,4.917 -4.9375,12.15924 -4.9375,18.03124 0,2.459 0.9805,5.18725 1.9375,7.78125 1.092,2.732 2.02925,5.452 3.53125,7.5 2.25796,3.32082 5.14798,5.20922 6.5625,5.5625 0.009,0.002 0.022,-0.002 0.0312,0 0.0303,0.007 0.0649,0.0255 0.0937,0.0312 v -0.15625 c 2.64982,-2.95437 4.24444,-5.88934 4.78125,-8.84375 0.683,-3.551 0.84,-7.10975 1.25,-11.34375 0.409,-3.551 0.11225,-8.334 -0.84375,-13.24999 -1.365,-6.146 -3.669,-12.41226 -4.625,-16.78126 z"
id="path2544"
inkscape:connector-curvature="0" />
<path
d="m 255.499,135.06577 c 0.137,7.101 0.683,20.35 2.595,25.539 0.546,1.775 5.599,9.56 9.149,18.983 2.459,6.556 3.005,12.565 3.415,14.34 1.639,7.785 -0.41,20.896 -3.142,33.324 -1.365,6.692 -6.009,15.023 -11.335,18.301 l -1.092,1.912 c 3.005,-0.137 10.379,-7.375 12.974,-16.389 4.371,-15.296 6.146,-22.398 4.098,-39.333 -0.273,-1.64 -0.956,-7.238 -3.551,-13.248 -3.824,-9.151 -9.287,-17.891 -9.969,-19.667 -1.23,-2.867 -2.869,-15.295 -3.142,-23.762 z"
id="path2550"
inkscape:connector-curvature="0" />
<path
d="m 258.06151,125.35303 c -0.40515,7.29812 -0.51351,9.98574 0.85149,15.31174 1.502,5.873 9.151,14.34 12.292,24.037 6.009,18.574 4.507,42.884 0.136,61.867 -1.638,6.691 -9.424,16.389 -17.208,19.529 l 5.736,1.366 c 3.141,-0.137 11.198,-7.648 14.34,-16.252 5.052,-13.521 6.009,-29.636 3.96,-46.571 -0.137,-1.639 -2.869,-16.252 -5.463,-22.398 -3.688,-9.15 -10.244,-17.345 -10.926,-19.119 -1.228,-3.005 -3.92651,-9.24362 -3.71849,-17.77074 z"
id="path2552"
inkscape:connector-curvature="0" />
<rect
width="0.550412"
height="126.01891"
x="253.71959"
y="120.21686"
id="rect2556" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* @licstart The following is the entire license notice for the JavaScript
* code in this page.
*
* This file is part of FreedomBox.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice for the JavaScript code
* in this page.
*/
(function($) {
$('#id_torproxy-use_upstream_bridges').change(function() {
if ($('#id_torproxy-use_upstream_bridges').prop('checked')) {
$('#id_torproxy-upstream_bridges').parent().parent().show('slow');
} else {
$('#id_torproxy-upstream_bridges').parent().parent().hide('slow');
}
}).change();
})(jQuery);

View File

@ -0,0 +1,12 @@
{% extends "app.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block page_js %}
<script type="text/javascript" src="{% static 'torproxy/torproxy.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,64 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Functional, browser based tests for Tor Proxy app.
"""
import pytest
from plinth.tests import functional
_TOR_FEATURE_TO_ELEMENT = {'software': 'torproxy-apt_transport_tor_enabled'}
pytestmark = [pytest.mark.apps, pytest.mark.torproxy]
class TestTorProxyApp(functional.BaseAppTests):
"""Tests for the Tor Proxy app."""
app_name = 'torproxy'
has_service = True
has_web = False
# TODO: Investigate why accessing IPv6 sites through Tor fails in
# container.
check_diagnostics = False
def test_set_download_software_packages_over_tor(self, session_browser):
"""Test setting download software packages over Tor."""
functional.app_enable(session_browser, 'torproxy')
_feature_enable(session_browser, 'software', should_enable=True)
_feature_enable(session_browser, 'software', should_enable=False)
_assert_feature_enabled(session_browser, 'software', enabled=False)
@pytest.mark.backups
def test_backup_restore(self, session_browser):
"""Test backup and restore of configuration."""
functional.app_enable(session_browser, 'torproxy')
# TODO: Check that upstream bridges are restored.
functional.backup_create(session_browser, 'torproxy', 'test_torproxy')
functional.backup_restore(session_browser, 'torproxy', 'test_torproxy')
assert functional.service_is_running(session_browser, 'torproxy')
def _feature_enable(browser, feature, should_enable):
"""Enable/disable a Tor Proxy feature."""
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
functional.nav_to_module(browser, 'torproxy')
checkbox_element = browser.find_by_name(element_name).first
if should_enable == checkbox_element.checked:
return
if should_enable:
checkbox_element.check()
else:
checkbox_element.uncheck()
functional.submit(browser, form_class='form-configuration')
functional.wait_for_config_update(browser, 'torproxy')
def _assert_feature_enabled(browser, feature, enabled):
"""Assert whether Tor Proxy feature is enabled or disabled."""
element_name = _TOR_FEATURE_TO_ELEMENT[feature]
functional.nav_to_module(browser, 'torproxy')
assert browser.find_by_name(element_name).first.checked == enabled

View File

@ -0,0 +1,33 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Tests for Tor Proxy module.
"""
from unittest.mock import patch
import pytest
from plinth.modules.torproxy import utils
class TestTorProxy:
"""Test cases for testing the Tor Proxy module."""
@staticmethod
@pytest.mark.usefixtures('needs_root')
def test_is_apt_transport_tor_enabled():
"""Test that is_apt_transport_tor_enabled does not raise any unhandled
exceptions.
"""
utils.is_apt_transport_tor_enabled()
@staticmethod
@patch('plinth.app.App.get')
@pytest.mark.usefixtures('needs_root', 'load_cfg')
def test_get_status(_app_get):
"""Test that get_status does not raise any unhandled exceptions.
This should work regardless of whether tor is installed, or
/etc/tor/instances/fbxproxy/torrc exists.
"""
utils.get_status()

View File

@ -0,0 +1,13 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
URLs for the Tor Proxy module.
"""
from django.urls import re_path
from . import views
urlpatterns = [
re_path(r'^apps/torproxy/$', views.TorProxyAppView.as_view(),
name='index'),
]

View File

@ -0,0 +1,75 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Tor Proxy utility functions."""
import itertools
import augeas
from plinth import app as app_module
from plinth.daemon import app_is_running
from . import privileged
APT_SOURCES_URI_PATHS = ('/files/etc/apt/sources.list/*/uri',
'/files/etc/apt/sources.list.d/*/*/uri',
'/files/etc/apt/sources.list.d/*/*/URIs/*')
APT_TOR_PREFIX = 'tor+'
def get_status(initialized=True):
"""Return current Tor status."""
status = privileged.get_status()
app = app_module.App.get('torproxy')
return {
'enabled': app.is_enabled() if initialized else False,
'is_running': app_is_running(app) if initialized else False,
'use_upstream_bridges': status['use_upstream_bridges'],
'upstream_bridges': status['upstream_bridges'],
'apt_transport_tor_enabled': is_apt_transport_tor_enabled()
}
def iter_apt_uris(aug):
"""Iterate over all the APT source URIs."""
return itertools.chain.from_iterable(
[aug.match(path) for path in APT_SOURCES_URI_PATHS])
def get_augeas():
"""Return an instance of Augeaus for processing APT configuration."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Aptsources/lens', 'Aptsources.lns')
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
'/etc/apt/sources.list')
aug.set('/augeas/load/Aptsources/incl[last() + 1]',
'/etc/apt/sources.list.d/*.list')
aug.set('/augeas/load/Aptsources822/lens', 'Aptsources822.lns')
aug.set('/augeas/load/Aptsources822/incl[last() + 1]',
'/etc/apt/sources.list.d/*.sources')
aug.load()
# Check for any errors in parsing sources lists.
if aug.match('/augeas/files/etc/apt/sources.list/error') or \
aug.match('/augeas/files/etc/apt/sources.list.d//error'):
raise Exception('Error parsing sources list')
return aug
def is_apt_transport_tor_enabled():
"""Return whether APT is set to download packages over Tor."""
try:
aug = get_augeas()
except Exception:
# If there was an error with parsing.
return False
for uri_path in iter_apt_uris(aug):
uri = aug.get(uri_path)
if not uri.startswith(APT_TOR_PREFIX) and \
(uri.startswith('http://') or uri.startswith('https://')):
return False
return True

View File

@ -0,0 +1,104 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app for configuring Tor Proxy."""
import logging
from django.utils.translation import gettext_noop
from django.views.generic.edit import FormView
from plinth import app as app_module
from plinth import operation as operation_module
from plinth.views import AppView
from . import privileged
from . import utils as tor_utils
from .forms import TorProxyForm
logger = logging.getLogger(__name__)
class TorProxyAppView(AppView):
"""Show Tor Proxy app main page."""
app_id = 'torproxy'
template_name = 'torproxy.html'
form_class = TorProxyForm
prefix = 'torproxy'
status = None
def get_initial(self):
"""Return the values to fill in the form."""
if not self.status:
self.status = tor_utils.get_status()
initial = super().get_initial()
initial.update(self.status)
return initial
def get_context_data(self, *args, **kwargs):
"""Add additional context data for template."""
if not self.status:
self.status = tor_utils.get_status()
context = super().get_context_data(*args, **kwargs)
context['status'] = self.status
return context
def form_valid(self, form):
"""Configure tor app on successful form submission."""
operation_module.manager.new(self.app_id,
gettext_noop('Updating configuration'),
_apply_changes,
[form.initial, form.cleaned_data],
show_notification=False)
# Skip check for 'Settings unchanged' message by calling grandparent
return super(FormView, self).form_valid(form)
def _apply_changes(old_status, new_status):
"""Try to apply changes and handle errors."""
logger.info('torproxy: applying configuration changes')
exception_to_update = None
message = None
try:
__apply_changes(old_status, new_status)
except Exception as exception:
exception_to_update = exception
message = gettext_noop('Error configuring app: {error}').format(
error=exception)
else:
message = gettext_noop('Configuration updated.')
logger.info('torproxy: configuration changes completed')
operation = operation_module.Operation.get_operation()
operation.on_update(message, exception_to_update)
def __apply_changes(old_status, new_status):
"""Apply the changes."""
needs_restart = False
arguments = {}
app = app_module.App.get('torproxy')
is_enabled = app.is_enabled()
if old_status['apt_transport_tor_enabled'] != \
new_status['apt_transport_tor_enabled']:
arguments['apt_transport_tor'] = (
is_enabled and new_status['apt_transport_tor_enabled'])
if old_status['use_upstream_bridges'] != \
new_status['use_upstream_bridges']:
arguments['use_upstream_bridges'] = new_status['use_upstream_bridges']
needs_restart = True
if old_status['upstream_bridges'] != new_status['upstream_bridges']:
arguments['upstream_bridges'] = new_status['upstream_bridges']
needs_restart = True
if arguments:
privileged.configure(**arguments)
if needs_restart and is_enabled:
privileged.restart()