1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
|
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to configure Tor Proxy."""
import json
import logging
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_noop
from plinth import app as app_module
from plinth import cfg, frontpage, kvstore, menu
from plinth.daemon import Daemon
from plinth.diagnostic_check import DiagnosticCheck
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.privileged import service as service_privileged
from plinth.utils import format_lazy
from . import manifest, privileged
logger = logging.getLogger(__name__)
PREINSTALL_CONFIG_KEY = 'torproxy_preinstall_config'
_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(
_('This app provides a web proxy on your {box_name} for internal '
'networks on TCP port 9050 using the SOCKS protocol. This can be '
'used by various apps to access the internet via the Tor network. '
'ISP censorship can be circumvented using upstream bridges.'),
box_name=_(cfg.box_name))
]
class TorProxyApp(app_module.App):
"""FreedomBox app for Tor Proxy."""
app_id = 'torproxy'
_version = 2
def __init__(self) -> None:
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=self._version,
name=_('Tor Proxy'), icon_filename='torproxy',
description=_description,
manual_page='TorProxy',
clients=manifest.clients, tags=manifest.tags,
donation_url='https://donate.torproject.org/')
self.add(info)
menu_item = menu.Menu('menu-torproxy', info.name, info.icon_filename,
info.tags, 'torproxy:index',
parent_url_name='apps')
self.add(menu_item)
shortcut = frontpage.Shortcut(
'shortcut-torproxy', info.name, icon=info.icon_filename,
description=info.description, manual_page=info.manual_page,
configure_url=reverse_lazy('torproxy:index'), tags=info.tags,
login_required=True)
self.add(shortcut)
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 enable(self):
"""Enable app."""
service_privileged.unmask('tor@fbxproxy')
super().enable()
def disable(self):
"""Disable APT use of Tor before disabling."""
privileged.configure(apt_transport_tor=False)
super().disable()
service_privileged.mask('tor@fbxproxy')
def diagnose(self) -> list[DiagnosticCheck]:
"""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()
if not old_version:
logger.info('Enabling apt-transport-tor')
config = kvstore.get_default(PREINSTALL_CONFIG_KEY, '{}')
config = json.loads(config)
config = {
'use_upstream_bridges': config.get('use_upstream_bridges'),
'upstream_bridges': config.get('upstream_bridges'),
'apt_transport_tor': config.get('apt_transport_tor', True),
}
privileged.configure(**config)
logger.info('Enabling Tor Proxy app')
self.enable()
kvstore.delete(PREINSTALL_CONFIG_KEY, ignore_missing=True)
# Version 2 masks the tor@fbproxy service when disabling the app to
# prevent the service starting by the Tor master service after system
# reboot.
if old_version and old_version < 2 and not self.is_enabled():
self.disable()
def uninstall(self):
"""De-configure and uninstall the app."""
super().uninstall()
privileged.uninstall()
def _diagnose_url_via_tor(url: str,
kind: str | None = None) -> DiagnosticCheck:
"""Diagnose whether a URL is reachable via Tor."""
result = diagnose_url(url, kind=kind, wrapper='torsocks')
result.check_id = 'torproxy-url'
result.description = gettext_noop('Access URL {url} on tcp{kind} via Tor')
return result
def _diagnose_tor_use(url: str, kind: str | None = None) -> DiagnosticCheck:
"""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.check_id = 'torproxy-using-tor'
result.description = gettext_noop(
'Confirm Tor usage at {url} on tcp{kind}')
return result
|