Sunil Mohan Adapa 038fec97a6
tor: Ability to run regular relays
- Allow users to run regular relays.  In addition to that users will be
  able to turn them into bridge relays.  Like before, by default,
  relaying is enabled and the relay type bridge relay.

- Show obfs3/4 transport ports as needing firewall port forwarding only
  if bridge relay is enabled.

- Remove pluggable transports configuration from configuration when
  bridge rely is disabled.

- Improve description message for relays and bridge relays.
2016-07-16 13:43:29 -04:00

418 lines
12 KiB
Python
Executable File

#!/usr/bin/python3
# -*- mode: python -*-
#
# This file is part of Plinth.
#
# 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/>.
#
"""
Configuration helper for the Tor service
"""
import argparse
import augeas
import codecs
import json
import os
import re
import socket
import time
from plinth import action_utils
from plinth.modules.tor.utils import get_real_apt_uri_path, iter_apt_uris, \
get_augeas, is_running, is_enabled, \
APT_TOR_PREFIX
SERVICE_FILE = '/etc/firewalld/services/tor-{0}.xml'
TOR_CONFIG = '/files/etc/tor/torrc'
TOR_STATE_FILE = '/var/lib/tor/state'
TOR_AUTH_COOKIE = '/var/run/tor/control.authcookie'
def parse_arguments():
"""Return parsed command line arguments as dictionary"""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('setup', help='Setup Tor configuration')
subparsers.add_parser('get-status',
help='Get Tor status in JSON format')
configure = subparsers.add_parser('configure', help='Configure Tor')
configure.add_argument('--service', choices=['enable', 'disable'],
help='Configure Tor service')
configure.add_argument('--relay', choices=['enable', 'disable'],
help='Configure relay')
configure.add_argument('--bridge-relay', choices=['enable', 'disable'],
help='Configure bridge relay')
configure.add_argument('--hidden-service', choices=['enable', 'disable'],
help='Configure hidden service')
configure.add_argument('--apt-transport-tor',
choices=['enable', 'disable'],
help='Configure package download over Tor')
return parser.parse_args()
def subcommand_setup(_):
"""Setup Tor configuration after installing it."""
aug = augeas_load()
aug.set(TOR_CONFIG + '/#comment[last() + 1]',
'Run as non-exit bridge relay')
aug.set(TOR_CONFIG + '/SocksPort[1]', '[::]:9050')
aug.set(TOR_CONFIG + '/SocksPort[2]', '0.0.0.0:9050')
aug.set(TOR_CONFIG + '/ControlPort', '9051')
_enable_relay(relay='enable', bridge='enable', restart=False, aug=aug)
aug.set(TOR_CONFIG + '/ExitPolicy[1]', 'reject *:*')
aug.set(TOR_CONFIG + '/ExitPolicy[2]', 'reject6 *:*')
aug.set(TOR_CONFIG + '/#comment[last() + 1]', 'Enable transparent proxy')
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/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.save()
action_utils.service_restart('tor')
_update_ports()
# wait until hidden service information is available
tries = 0
while not _get_hidden_service()['enabled']:
tries += 1
if tries >= 12:
return
time.sleep(10)
def subcommand_get_status(_):
"""Get Tor status in JSON format."""
print(json.dumps(get_status()))
def subcommand_configure(arguments):
"""Configure Tor."""
if arguments.service == 'disable':
_disable()
restart = arguments.service is None and arguments.hidden_service is None
_enable_relay(arguments.relay, arguments.bridge_relay, restart=restart)
restart = arguments.service is None
if arguments.hidden_service == 'enable':
_enable_hs(restart=restart)
elif arguments.hidden_service == 'disable':
_disable_hs(restart=restart)
if arguments.service == 'enable':
_enable()
if arguments.apt_transport_tor == 'enable':
_enable_apt_transport_tor()
elif arguments.apt_transport_tor == 'disable':
_disable_apt_transport_tor()
def get_status():
"""Return dict with Tor status."""
aug = augeas_load()
return {'relay_enabled': _is_relay_enabled(aug),
'bridge_relay_enabled': _is_bridge_relay_enabled(aug),
'ports': _get_ports(),
'hidden_service': _get_hidden_service(aug)}
def _is_relay_enabled(aug):
"""Return whether a relay is enabled."""
orport = aug.get(TOR_CONFIG + '/ORPort')
return bool(orport) and orport != '0'
def _is_bridge_relay_enabled(aug):
"""Return whether bridge relay is enabled."""
bridge = aug.get(TOR_CONFIG + '/BridgeRelay')
return bridge == '1'
def _get_ports():
"""Return dict mapping port names to numbers."""
ports = {}
try:
ports['orport'] = _get_orport()
except Exception:
pass
try:
with open(TOR_STATE_FILE, 'r') as state_file:
for line in state_file:
matches = re.match(
r'^\s*TransportProxy\s+(\S*)\s+\S+:(\d+)\s*$', line)
if matches:
ports[matches.group(1)] = matches.group(2)
except FileNotFoundError:
pass
return ports
def _get_orport():
"""Return the ORPort by querying running instance."""
cookie = open(TOR_AUTH_COOKIE, 'rb').read()
cookie = codecs.encode(cookie, 'hex').decode()
commands = '''AUTHENTICATE {cookie}
GETINFO net/listeners/or
QUIT
'''.format(cookie=cookie)
tor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tor_socket.connect(('localhost', 9051))
tor_socket.send(commands.encode())
response = tor_socket.recv(1024)
tor_socket.close()
line = response.split(b'\r\n')[1].decode()
matches = re.match(r'.*="[^:]+:(\d+)"', line)
return matches.group(1)
def _get_hidden_service(aug=None):
"""Return a string with configured Tor hidden service information"""
hs_enabled = False
hs_status = 'Ok'
hs_hostname = None
hs_ports = []
if not aug:
aug = augeas_load()
hs_dir = aug.get(TOR_CONFIG + '/HiddenServiceDir')
hs_port_paths = aug.match(TOR_CONFIG + '/HiddenServicePort')
for hs_port_path in hs_port_paths:
port_info = aug.get(hs_port_path).split()
hs_ports.append({'virtport': port_info[0],
'target': port_info[1]})
if not hs_dir:
hs_status = 'Not Configured'
else:
try:
with open(os.path.join(hs_dir, 'hostname'), 'r') as conf_file:
hs_hostname = conf_file.read().strip()
hs_enabled = True
except Exception:
hs_status = 'Not available (Run Tor at least once)'
return {'enabled': hs_enabled, 'status': hs_status,
'hostname': hs_hostname, 'ports': hs_ports}
def _enable():
"""Enable and start the service."""
action_utils.service_enable('tor')
_update_ports()
def _disable():
"""Disable and stop the service."""
_disable_apt_transport_tor()
action_utils.service_disable('tor')
def _enable_relay(relay=None, bridge=None, restart=True, aug=None):
"""Enable Tor bridge relay."""
if relay is None and bridge is None:
return
if not aug:
aug = augeas_load()
if relay == 'enable':
aug.set(TOR_CONFIG + '/ORPort', 'auto')
elif relay == 'disable':
aug.remove(TOR_CONFIG + '/ORPort')
if bridge == 'enable':
aug.set(TOR_CONFIG + '/BridgeRelay', '1')
aug.set(TOR_CONFIG + '/ServerTransportPlugin',
'obfs3,obfs4 exec /usr/bin/obfs4proxy')
aug.set(TOR_CONFIG + '/ExtORPort', 'auto')
elif bridge == 'disable':
aug.remove(TOR_CONFIG + '/BridgeRelay')
aug.remove(TOR_CONFIG + '/ServerTransportPlugin')
aug.remove(TOR_CONFIG + '/ExtORPort')
aug.save()
if restart:
if is_enabled() and is_running():
action_utils.service_restart('tor')
def _enable_hs(restart=True):
"""Enable Tor hidden service"""
aug = augeas_load()
if _get_hidden_service(aug)['enabled']:
return
aug.set(TOR_CONFIG + '/HiddenServiceDir',
'/var/lib/tor/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.save()
if restart:
if is_enabled() and is_running():
action_utils.service_restart('tor')
# wait until hidden service information is available
tries = 0
while not _get_hidden_service()['enabled']:
tries += 1
if tries >= 12:
return
time.sleep(10)
def _disable_hs(restart=True):
"""Disable Tor hidden service"""
aug = augeas_load()
if not _get_hidden_service(aug)['enabled']:
return
aug.remove(TOR_CONFIG + '/HiddenServiceDir')
aug.remove(TOR_CONFIG + '/HiddenServicePort')
aug.save()
if restart:
if is_enabled() and is_running():
action_utils.service_restart('tor')
def _enable_apt_transport_tor():
"""Enable package download over Tor."""
try:
aug = get_augeas()
except Exception:
# If there was an error, don't proceed
print('Error: Unable to understand sources format.')
exit(1)
for uri_path in iter_apt_uris(aug):
uri_path = get_real_apt_uri_path(aug, uri_path)
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_path = get_real_apt_uri_path(aug, uri_path)
uri = aug.get(uri_path)
if uri.startswith(APT_TOR_PREFIX):
aug.set(uri_path, uri[len(APT_TOR_PREFIX):])
aug.save()
def _update_ports():
"""Update firewall service information."""
ready = False
tries = 0
# port information may not be available immediately after Tor started
while not ready:
ports = _get_ports()
ready = 'orport' in ports and 'obfs3' in ports and 'obfs4' in ports
if ready:
break
tries += 1
if tries >= 12:
return
time.sleep(10)
lines = """<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Tor - {0}</short>
<port protocol="tcp" port="{1}"/>
</service>
"""
for name, number in ports.items():
try:
with open(SERVICE_FILE.format(name), 'w') as service_file:
service_file.writelines(lines.format(name, number))
except FileNotFoundError:
return
# XXX: We should ideally do firewalld reload instead. However,
# firewalld seems to fail to successfully reload sometimes.
action_utils.service_restart('firewalld')
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]', '/etc/tor/torrc')
aug.load()
return aug
def main():
"""Parse arguments and perform all duties"""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()