diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c45df77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/* +__pycache__/* +vanguards.state diff --git a/MANIFEST.in b/MANIFEST.in index 71d7e7b..e592e6c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -12,3 +12,4 @@ recursive-include tests *.mock recursive-include tests *.conf recursive-include tests *.py recursive-include tests cached-microdesc-consensus +recursive-include service * diff --git a/README.md b/README.md index d88248b..3c74392 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +# 0.4.1 + + # The Vanguards Onion Service Addon [![Build Status](https://travis-ci.org/mikeperry-tor/vanguards.png?branch=master)](https://travis-ci.org/mikeperry-tor/vanguards) [![Coverage Status](https://coveralls.io/repos/github/mikeperry-tor/vanguards/badge.png?branch=master)](https://coveralls.io/github/mikeperry-tor/vanguards?branch=master) diff --git a/build/lib/vanguards/NodeSelection.py b/build/lib/vanguards/NodeSelection.py new file mode 100644 index 0000000..1402e4a --- /dev/null +++ b/build/lib/vanguards/NodeSelection.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python + +import copy +import random + +from .logger import plog + +class RestrictionError(Exception): + "Error raised for issues with applying restrictions" + pass + +class NoNodesRemain(RestrictionError): + "Error raised for issues with applying restrictions" + pass + +class NodeRestriction: + "Interface for node restriction policies" + def r_is_ok(self, r): + "Returns true if Router 'r' is acceptable for this restriction" + return True + +class FlagsRestriction(NodeRestriction): + "Restriction for mandatory and forbidden router flags" + def __init__(self, mandatory, forbidden=[]): + """Constructor. 'mandatory' and 'forbidden' are both lists of router + flags as strings.""" + self.mandatory = mandatory + self.forbidden = forbidden + + def r_is_ok(self, router): + for m in self.mandatory: + if not m in router.flags: return False + for f in self.forbidden: + if f in router.flags: return False + return True + +class MetaNodeRestriction(NodeRestriction): + """Interface for a NodeRestriction that is an expression consisting of + multiple other NodeRestrictions""" + def next_rstr(self): raise NotImplemented() + +class NodeRestrictionList(MetaNodeRestriction): + "Class to manage a list of NodeRestrictions" + def __init__(self, restrictions): + "Constructor. 'restrictions' is a list of NodeRestriction instances" + self.restrictions = restrictions + + def r_is_ok(self, r): + "Returns true of Router 'r' passes all of the contained restrictions" + for rs in self.restrictions: + if not rs.r_is_ok(r): return False + return True + +class NodeGenerator: + "Interface for node generation" + def __init__(self, sorted_r, rstr_list): + """Constructor. Takes a bandwidth-sorted list of Routers 'sorted_r' + and a NodeRestrictionList 'rstr_list'""" + self.rstr_list = rstr_list + self.rebuild(sorted_r) + self.rewind() + + def rewind(self): + "Rewind the generator to the 'beginning'" + self.routers = copy.copy(self.rstr_routers) + if not self.routers: + plog("NOTICE", "No routers left after restrictions applied: "+str(self.rstr_list)) + raise NoNodesRemain(str(self.rstr_list)) + + def rebuild(self, sorted_r=None): + """ Extra step to be performed when new routers are added or when + the restrictions change. """ + if sorted_r != None: + self.sorted_r = sorted_r + self.rstr_routers = list(filter(lambda r: self.rstr_list.r_is_ok(r), + self.sorted_r)) + + if not self.rstr_routers: + plog("NOTICE", "No routers left after restrictions applied: "+str(self.rstr_list)) + raise NoNodesRemain(str(self.rstr_list)) + + def generate(self): + "Return a python generator that yields routers according to the policy" + raise NotImplemented() + +class BwWeightedGenerator(NodeGenerator): + POSITION_GUARD = 'g' + POSITION_MIDDLE = 'm' + POSITION_EXIT = 'e' + + def flag_to_weight(self, node): + if 'Guard' in node.flags and "Exit" in node.flags: + return self.bw_weights[u'W'+self.position+'d']/self.WEIGHT_SCALE + + if 'Exit' in node.flags: + return self.bw_weights[u'W'+self.position+'e']/self.WEIGHT_SCALE + + if "Guard" in node.flags: + return self.bw_weights[u'W'+self.position+'g']/self.WEIGHT_SCALE + + return self.bw_weights[u'Wmm']/self.WEIGHT_SCALE + + + # This function handles https://github.com/mikeperry-tor/vanguards/issues/24 + # + # Exit nodes got their weights set to 0 by the BwWeightedGenerator, + # because we told it we were selecting for middle. Since they can + # still be used as RPs in normal client cannibalized circuits, + # we need to set their upper bound to be their exit selection + # probability. + # + # Note that we deliberately *don't* re-normalize weight_total + # since we don't want to lower the upper bound of the rest + # of the nodes due to this madness. But we do want a separate + # exit_total for use with these Exit nodes (since it gives their + # selection probability for cannibalized circs). + def repair_exits(self): + oldpos = self.position + self.position = BwWeightedGenerator.POSITION_EXIT + self.exit_total = 0 + + i = 0 + rlen = len(self.rstr_routers) + while i < rlen: + r = self.rstr_routers[i] + if "Exit" in r.flags: + self.node_weights[i] = r.measured*self.flag_to_weight(r) + self.exit_total += self.node_weights[i] + + i+=1 + + self.position = oldpos + + def rebuild(self, sorted_r=None): + NodeGenerator.rebuild(self, sorted_r) + NodeGenerator.rewind(self) + # TODO: Use consensus param + self.WEIGHT_SCALE = 10000.0 + + self.node_weights = [] + for r in self.rstr_routers: + self.node_weights.append(r.measured*self.flag_to_weight(r)) + + self.weight_total = sum(self.node_weights) + + def __init__(self, sorted_r, rstr_list, bw_weights, position): + self.position = position + self.bw_weights = bw_weights + self.node_weights = [] + NodeGenerator.__init__(self, sorted_r, rstr_list) + + def generate(self): + while True: + choice_val = random.uniform(0, self.weight_total) + choose_total = 0 + choice_idx = 0 + while choose_total < choice_val: + choose_total += self.node_weights[choice_idx] + choice_idx += 1 + yield self.rstr_routers[choice_idx-1] + +# FIXME: FlagsRestriction: Uptime, capacity (NodeRestriction: always want) +# FIXME: Subnet16Restriction: Set restriction: at least one be different +# FIXME: FamilyRestriction: Set restriction: at least one must be different + + diff --git a/build/lib/vanguards/__init__.py b/build/lib/vanguards/__init__.py new file mode 100644 index 0000000..5a34dd8 --- /dev/null +++ b/build/lib/vanguards/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +__version__ = "0.4.1" +__author__ = "Mike Perry" +__license__ = "MIT/Expat" +__url__ = "https://github.com/mikeperry-tor/vanguards" diff --git a/build/lib/vanguards/bandguards.py b/build/lib/vanguards/bandguards.py new file mode 100644 index 0000000..ea0e2cb --- /dev/null +++ b/build/lib/vanguards/bandguards.py @@ -0,0 +1,524 @@ +""" Simple checks against bandwidth side channels """ +import time +import stem + +from . import control + +from .logger import plog + +############ BandGuard Options ################# + +# Kill a circuit if this many read+write bytes have been exceeded. +# Very loud application circuits could be used to introduce timing +# side channels. +# Warning: if your application has large resources that cannot be +# split up over multiple requests (such as large HTTP posts for eg: +# securedrop, or sharing large files via onionshare), you must set +# this high enough for those uploads not to get truncated! +CIRC_MAX_MEGABYTES = 0 + +# Kill circuits older than this many seconds. +# Really old circuits will continue to use old guards after the TLS connection +# has rotated, which means they will be alone on old TLS links. This lack +# of multiplexing may allow an adversary to use netflow records to determine +# the path through the Tor network to a hidden service. +CIRC_MAX_AGE_HOURS = 24 # 1 day + +# Maximum size for an hsdesc fetch (including setup+get+dropped cells) +CIRC_MAX_HSDESC_KILOBYTES = 30 + +# Maximum number of bytes allowed on a service intro circ before close +CIRC_MAX_SERV_INTRO_KILOBYTES = 0 + +# Warn if Tor can't build or use circuits for this many seconds +CIRC_MAX_DISCONNECTED_SECS = 30 + +# Warn if Tor has no connections for this many seconds +CONN_MAX_DISCONNECTED_SECS = 15 + +############ Constants ############### +_CELL_PAYLOAD_SIZE = 509 +_RELAY_HEADER_SIZE = 11 +_RELAY_PAYLOAD_SIZE = _CELL_PAYLOAD_SIZE - _RELAY_HEADER_SIZE +_CELL_DATA_RATE = (float(_RELAY_PAYLOAD_SIZE)/_CELL_PAYLOAD_SIZE) + +_SECS_PER_HOUR = 60*60 +_BYTES_PER_KB = 1024 +_BYTES_PER_MB = 1024*_BYTES_PER_KB + +# Because we have to map circuits to guard destroy events, we need. +# The event really should arrive in the same second, but let's +# give it until the next couple in case there is a scheduled events hiccup +_MAX_CIRC_DESTROY_LAG_SECS = 2 + +class BwCircuitStat: + def __init__(self, circ_id, is_hs): + self.circ_id = circ_id + self.is_hs = is_hs + self.is_service = 1 + self.is_hsdir = 0 + self.is_serv_intro = 0 + self.dropped_cells_allowed = 0 + self.purpose = None + self.hs_state = None + self.old_purpose = None + self.old_hs_state = None + self.in_use = 0 + self.built = 0 + self.created_at = time.time() + self.read_bytes = 0 + self.sent_bytes = 0 + self.delivered_read_bytes = 0 + self.delivered_sent_bytes = 0 + self.overhead_read_bytes = 0 + self.overhead_sent_bytes = 0 + self.guard_fp = None + self.possibly_destroyed_at = None + + def total_bytes(self): + return self.read_bytes + self.sent_bytes + + def dropped_read_cells(self): + return self.read_bytes/_CELL_PAYLOAD_SIZE - \ + (self.delivered_read_bytes+self.overhead_read_bytes)/_RELAY_PAYLOAD_SIZE + +class BwGuardStat: + def __init__(self, guard_fp): + self.to_guard = guard_fp + self.killed_conns = 0 + self.killed_conn_at = 0 + self.killed_conn_pending = False + self.conns_made = 0 + self.close_reasons = {} # key=reason val=count + +class BandwidthStats: + def __init__(self, controller): + self.controller = controller + self.circs = {} # key=circid val=BwCircStat + self.live_guard_conns = {} # key=connid val=BwGuardStat + self.guards = {} # key=guardfp val=BwGuardStat + self.circs_destroyed_total = 0 + self.no_conns_since = int(time.time()) + self.no_circs_since = None + self.network_down_since = None + self.max_fake_id = -1 + self.disconnected_circs = False + self.disconnected_conns = False + self._orconn_init(controller) + self._network_liveness_init(controller) + + def _network_liveness_init(self, controller): + if controller.get_info("network-liveness") != "up": + self.network_down_since = int(time.time()) + + # Load in our current orconns. orconn-status does not + # tell us IDs, so we have to fake it and keep track of fakes :/ + def _orconn_init(self, controller): + fake_id = 0 + for l in controller.get_info("orconn-status").split("\n"): + if len(l): + self.orconn_event( + stem.response.ControlMessage.from_str( + "650 ORCONN "+l+" ID="+str(fake_id)+"\r\n", "EVENT")) + fake_id += 1 + self.max_fake_id = fake_id - 1 + + # We need to scan for our fake_id conns here and fixup + # the event.id accordingly... + def _fixup_orconn_event(self, event): + guard_fp = event.endpoint_fingerprint + fake_id = self.max_fake_id + while fake_id >= 0: + if str(fake_id) in self.live_guard_conns and \ + self.live_guard_conns[str(fake_id)].to_guard == guard_fp: + event.id = str(fake_id) + fake_id -= 1 + + # We watch orconn events so that when one closes, we can mark + # the circuits that might have been alive on it and watch for + # their close messages later. We have to do this dance because + # the CIRC event doesn't tell us which hop killed the circuit. + # + # We also keep additional stats on the number of connections, to + # monitor overall guard use for debugging. + def orconn_event(self, event): + guard_fp = event.endpoint_fingerprint + if not event.endpoint_fingerprint in self.guards: + self.guards[guard_fp] = BwGuardStat(guard_fp) + + if event.status == "CONNECTED": + if self.disconnected_conns: + disconnected_secs = event.arrived_at - self.no_conns_since + plog("NOTICE", "Reconnected to the Tor network after %d seconds.", + disconnected_secs) + self.live_guard_conns[event.id] = self.guards[guard_fp] + self.guards[guard_fp].conns_made += 1 + self.no_conns_since = 0 + self.disconnected_conns = False + elif event.status == "CLOSED" or event.status == "FAILED": + if event.id not in self.live_guard_conns: + self._fixup_orconn_event(event) + + if event.id in self.live_guard_conns: + # Scan the circuit list for any circuits that might + # be using this guard and that are in use. This is to + # watch for their close later. + for c in self.circs.values(): + if c.in_use and c.guard_fp == guard_fp: + c.possibly_destroyed_at = event.arrived_at + self.live_guard_conns[event.id].killed_conn_at = event.arrived_at + plog("INFO", "Marking possibly destroyed circ %s at %d", + c.circ_id, event.arrived_at) + + del self.live_guard_conns[event.id] + if len(self.live_guard_conns) == 0 and \ + not self.no_conns_since: + self.no_conns_since = event.arrived_at + # Keep stats on CLOSED reasons. We don't do anything with these atm + if event.status == "CLOSED": + if not event.reason in self.guards[guard_fp].close_reasons: + self.guards[guard_fp].close_reasons[event.reason] = 0 + self.guards[guard_fp].close_reasons[event.reason] += 1 + plog("INFO", event.raw_content()) + + def circuit_destroyed(self, event): + self.circs_destroyed_total += 1 + guardfp = event.path[0][0] + if event.arrived_at - self.guards[guardfp].killed_conn_at \ + <= _MAX_CIRC_DESTROY_LAG_SECS: + self.guards[guardfp].killed_conn_at = 0 + self.guards[guardfp].killed_conns += 1 + # FIXME: Limit to warn after N killed connections? + plog("NOTICE", "The connection to guard "+guardfp+" was closed with "+\ + "a live circuit.") + + plog("INFO", "The connection to guard "+guardfp+" was closed with "+\ + "circuit "+event.id+" on it.") + + def any_circuits_pending(self, except_id=None): + for c in self.circs.values(): + if not c.built and c.circ_id != except_id: + return True + return False # All circuits in use + + def circ_event(self, event): + # Failed circuits mean the network could be down: + if event.status == stem.CircStatus.FAILED and \ + not self.no_circs_since and self.any_circuits_pending(event.id): + self.no_circs_since = event.arrived_at + + # Sometimes circuits get multiple FAILED+CLOSED events, + # so we must check that first... + if (event.status == stem.CircStatus.FAILED or \ + event.status == stem.CircStatus.CLOSED): + if event.id in self.circs: + # If the circuit was in use, and possibly closed due to a guard + # connection closure recently, and this event says it died due to + # a channel closure, then record that. + if self.circs[event.id].in_use and \ + self.circs[event.id].possibly_destroyed_at: + if event.arrived_at - self.circs[event.id].possibly_destroyed_at \ + <= _MAX_CIRC_DESTROY_LAG_SECS and \ + event.remote_reason == "CHANNEL_CLOSED": + self.circuit_destroyed(event) + else: + plog("INFO", + "Circuit %s possibly destroyed, but outside of the time window (%d - %d)", + event.id, event.arrived_at, self.circs[event.id].possibly_destroyed_at) + plog("DEBUG", "Closed hs circ for "+event.raw_content()) + del self.circs[event.id] + return + + if event.id not in self.circs: + self.circs[event.id] = BwCircuitStat(event.id, + event.hs_state or event.purpose[0:2] == "HS") + + # Handle direct build purpose settings + if event.purpose[0:9] == "HS_CLIENT": + self.circs[event.id].is_service = 0 + elif event.purpose[0:10] == "HS_SERVICE": + self.circs[event.id].is_service = 1 + if event.purpose == "HS_CLIENT_HSDIR" or \ + event.purpose == "HS_SERVICE_HSDIR": + self.circs[event.id].is_hsdir = 1 + elif event.purpose == "HS_SERVICE_INTRO": + self.circs[event.id].is_serv_intro = 1 + plog("DEBUG", "Added circ for "+event.raw_content()) + + # Debugging + self.circs[event.id].purpose = event.purpose + self.circs[event.id].hs_state = event.hs_state + + # Consider all BUILT circs that have a specific HS purpose + # to be "in_use". + if event.status == stem.CircStatus.BUILT or \ + event.status == "GUARD_WAIT": + self.circs[event.id].built = 1 + + if self.disconnected_circs: + disconnected_secs = event.arrived_at - self.no_circs_since + plog("NOTICE", "Circuit use resumed after %d seconds.", + disconnected_secs) + self.no_circs_since = None + self.disconnected_circs = False + + if event.purpose[0:9] == "HS_CLIENT" or \ + event.purpose[0:10] == "HS_SERVICE": + self.circs[event.id].in_use = 1 + self.circs[event.id].guard_fp = event.path[0][0] + plog("DEBUG", "Circ "+event.id+" now in-use. %d delivered bytes.", + self.circs[event.id].delivered_read_bytes) + # Extending a circuit means the network is OK + elif event.status == "EXTENDED": + if self.disconnected_circs: + disconnected_secs = event.arrived_at - self.no_circs_since + plog("NOTICE", "Circuit use resumed after %d seconds.", + disconnected_secs) + self.no_circs_since = None + self.disconnected_circs = False + + # We need CIRC_MINOR to determine client from service as well + # as recognize cannibalized HSDIR circs + def circ_minor_event(self, event): + if event.id not in self.circs: + return + + self.circs[event.id].purpose = event.purpose + self.circs[event.id].hs_state = event.hs_state + self.circs[event.id].old_purpose = event.old_purpose + self.circs[event.id].old_hs_state = event.old_hs_state + + if event.purpose[0:9] == "HS_CLIENT": + self.circs[event.id].is_service = 0 + elif event.purpose[0:10] == "HS_SERVICE": + self.circs[event.id].is_service = 1 + if event.purpose == "HS_CLIENT_HSDIR" or \ + event.purpose == "HS_SERVICE_HSDIR": + self.circs[event.id].is_hsdir = 1 + elif event.purpose == "HS_SERVICE_INTRO": + self.circs[event.id].is_serv_intro = 1 + + # PURPOSE_CHANGED from HS_VANGUARDS -> in_use + if event.event == stem.CircEvent.PURPOSE_CHANGED: + if event.old_purpose == "HS_VANGUARDS": + self.circs[event.id].in_use = 1 + self.circs[event.id].guard_fp = event.path[0][0] + plog("DEBUG", "Circ "+event.id+" now in-use. %d delivered bytes.", + self.circs[event.id].delivered_read_bytes) + + plog("DEBUG", event.raw_content()) + + def circbw_event(self, event): + # Circuit bandwidth means circuits are working + if self.disconnected_circs: + disconnected_secs = event.arrived_at - self.no_circs_since + plog("NOTICE", "Circuit use resumed after %d seconds.", + disconnected_secs) + self.no_circs_since = None + self.disconnected_circs = False + + if event.id in self.circs: + plog("DEBUG", event.raw_content()) + delivered_read = int(event.keyword_args["DELIVERED_READ"]) + delivered_written = int(event.keyword_args["DELIVERED_WRITTEN"]) + overhead_read = int(event.keyword_args["OVERHEAD_READ"]) + overhead_written = int(event.keyword_args["OVERHEAD_WRITTEN"]) + + if delivered_read + overhead_read > event.read*_CELL_DATA_RATE: + plog("ERROR", + "Application read data exceeds cell data:"+event.raw_content()); + if delivered_written + overhead_written > event.written*_CELL_DATA_RATE: + plog("ERROR", + "Application written data exceeds cell data:"+event.raw_content()); + + self.circs[event.id].read_bytes += event.read + self.circs[event.id].sent_bytes += event.written + + self.circs[event.id].delivered_read_bytes += delivered_read + self.circs[event.id].delivered_sent_bytes += delivered_written + + self.circs[event.id].overhead_read_bytes += overhead_read + self.circs[event.id].overhead_sent_bytes += overhead_written + + self.check_circuit_limits(self.circs[event.id]) + + def check_connectivity(self, now): + if self.no_conns_since: + disconnected_secs = int(now - self.no_conns_since) + + if CONN_MAX_DISCONNECTED_SECS > 0 and \ + disconnected_secs >= CONN_MAX_DISCONNECTED_SECS: + if not self.disconnected_conns or \ + disconnected_secs % CONN_MAX_DISCONNECTED_SECS == 0: + plog("WARN", "We've been disconnected from the Tor network for %d seconds!" + % disconnected_secs) + self.disconnected_conns = True + elif self.no_circs_since: + disconnected_secs = int(now - self.no_circs_since) + + if CIRC_MAX_DISCONNECTED_SECS > 0 and \ + disconnected_secs >= CIRC_MAX_DISCONNECTED_SECS and \ + self.any_circuits_pending(): + if not self.disconnected_circs or \ + disconnected_secs % CIRC_MAX_DISCONNECTED_SECS == 0: + if self.network_down_since: + plog("WARN", "Tor has been disconnected for %d seconds, "+\ + "and failing all circuits for %d seconds!", + now - self.network_down_since, + disconnected_secs) + else: + plog("NOTICE", "Tor has been failing all circuits for %d seconds!" + % disconnected_secs) + self.disconnected_circs = True + + def network_liveness_event(self, event): + plog("DEBUG", event.raw_content()) + if event.status == "UP": + self.network_down_since = None + elif event.status == "DOWN": + self.network_down_since = event.arrived_at + + def check_circ_ages(self, now): + if CIRC_MAX_AGE_HOURS <= 0: + return + + # Unused except to expire circuits -- 1x/sec + # FIXME: This is needless copying on python 2.. + kill_circs = list(filter( + lambda c: now - c.created_at > \ + CIRC_MAX_AGE_HOURS*_SECS_PER_HOUR, + self.circs.values())) + for circ in kill_circs: + control.try_close_circuit(self.controller, circ.circ_id) + self.limit_exceeded("NOTICE", "CIRC_MAX_AGE_HOURS", + circ.circ_id, + (now - circ.created_at)/_SECS_PER_HOUR, + CIRC_MAX_AGE_HOURS) + + # Used for 1x/sec heartbeat only + def bw_event(self, event): + now = time.time() + self.check_connectivity(event.arrived_at) + self.check_circ_ages(now) + + def check_circuit_limits(self, circ): + if circ.dropped_read_cells() > circ.dropped_cells_allowed: + # Workaround for Tor bug #29699 (see also + # https://github.com/mikeperry-tor/vanguards/issues/37). + # Case 2. Intro circs can get dupped cells on retries while in use. + # Demote log to notice, but still close them though, cause fuck it. + # They get rebuilt, and side channels are bad, mmmk. + if circ.purpose == "HS_SERVICE_INTRO" and \ + circ.hs_state == "HSSI_ESTABLISHED": + control.try_close_circuit(self.controller, circ.circ_id) + plog("INFO", "Tor bug #29699: Got %d dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", + circ.dropped_read_cells(), circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + elif circ.purpose == "CIRCUIT_PADDING" and \ + circ.old_purpose == "HS_CLIENT_INTRO" and \ + circ.old_hs_state == "HSCI_INTRO_SENT": + control.try_close_circuit(self.controller, circ.circ_id) + plog("INFO", "Tor bug #40359. Got %d dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s)", + circ.dropped_read_cells(), circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + # Workaround for Tor bug #29927 (see also + # https://github.com/mikeperry-tor/vanguards/issues/37). + # Class 4: Mysterious client-side cases of dropped cells + # and protocol errors. + # Always close now, since this is a failure anyway: + # https://github.com/mikeperry-tor/vanguards/issues/69 + elif circ.purpose == "HS_CLIENT_REND" or \ + (circ.purpose == "HS_CLIENT_INTRO" and \ + circ.hs_state == "HSCI_DONE"): + control.try_close_circuit(self.controller, circ.circ_id) + plog("INFO", "Tor bug #29927: Got %d dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", + circ.dropped_read_cells(), circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + # Workaround for Tor bug #29700 (see also + # https://github.com/mikeperry-tor/vanguards/issues/37). + # Class 3: Service rend circs can sometimes fail ntor handshake on extend + # Always close now, since this is a failure anyway: + # https://github.com/mikeperry-tor/vanguards/issues/69 + elif circ.purpose == "HS_SERVICE_REND" and \ + circ.hs_state == "HSSR_CONNECTING": + control.try_close_circuit(self.controller, circ.circ_id) + plog("INFO", "Tor bug #29700: Got %d dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", + circ.dropped_read_cells(), circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + elif circ.purpose == "PATH_BIAS_TESTING": + # Pathbias circs all seem to have cases of dropped cells. If we + # get any, it probably means the probe failed anyway. Worst case, + # it might even have been withheld until dropped cells can be + # injected. + control.try_close_circuit(self.controller, circ.circ_id) + plog("INFO", "Tor bug #29786: Got a dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + elif not circ.built: + plog("INFO", "Tor bug #29927: Got a dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + else: + control.try_close_circuit(self.controller, circ.circ_id) + plog("WARN", "Possible Tor bug, or possible attack if very frequent: "\ + +"Got %d dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s)", + circ.dropped_read_cells(), circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + elif circ.dropped_read_cells() > 0: + # Log workaround drop cell cases for completeness + if circ.purpose == "PATH_BIAS_TESTING": + plog("INFO", "Tor bug #29786: Got a dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + elif circ.purpose == "HS_SERVICE_REND" and \ + circ.hs_state == "HSSR_CONNECTING": + plog("INFO", "Tor bug #29700: Got a dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + else: + plog("INFO", "Tor bug #29927: Got a dropped cell on circ %s "\ + +"(in state %s %s; old state %s %s).", circ.circ_id, + str(circ.purpose), str(circ.hs_state), + str(circ.old_purpose), str(circ.old_hs_state)) + + if CIRC_MAX_MEGABYTES > 0 and \ + circ.total_bytes() > CIRC_MAX_MEGABYTES*_BYTES_PER_MB: + control.try_close_circuit(self.controller, circ.circ_id) + self.limit_exceeded("NOTICE", "CIRC_MAX_MEGABYTES", + circ.circ_id, + circ.total_bytes(), + CIRC_MAX_MEGABYTES*_BYTES_PER_MB) + if CIRC_MAX_HSDESC_KILOBYTES > 0 and \ + circ.is_hsdir and circ.total_bytes() > \ + CIRC_MAX_HSDESC_KILOBYTES*_BYTES_PER_KB: + control.try_close_circuit(self.controller, circ.circ_id) + self.limit_exceeded("WARN", "CIRC_MAX_HSDESC_KILOBYTES", + circ.circ_id, + circ.total_bytes(), + CIRC_MAX_HSDESC_KILOBYTES*_BYTES_PER_KB) + if CIRC_MAX_SERV_INTRO_KILOBYTES > 0 and \ + circ.is_serv_intro and circ.total_bytes() > \ + CIRC_MAX_SERV_INTRO_KILOBYTES*_BYTES_PER_KB: + control.try_close_circuit(self.controller, circ.circ_id) + self.limit_exceeded("WARN", "CIRC_MAX_SERV_INTRO_KILOBYTES", + circ.circ_id, + circ.total_bytes(), + CIRC_MAX_SERV_INTRO_KILOBYTES*_BYTES_PER_KB) + + def limit_exceeded(self, level, str_name, circ_id, cur_val, max_val, extra=""): + plog(level, "Circ "+str(circ_id)+" exceeded "+str_name+": "+str(cur_val)+ + " > "+str(max_val)+". "+extra) diff --git a/build/lib/vanguards/cbtverify.py b/build/lib/vanguards/cbtverify.py new file mode 100644 index 0000000..ae188b9 --- /dev/null +++ b/build/lib/vanguards/cbtverify.py @@ -0,0 +1,113 @@ +""" This code monitors the circuit build timeout. It is non-essential """ +from .logger import plog + +class CircuitStat: + def __init__(self, circ_id, is_hs): + self.circ_id = circ_id + self.is_hs = is_hs + +class TimeoutStats: + def __init__(self): + self.circuits = {} + self.zero_fields() + self.record_timeouts = True + + def zero_fields(self): + self.all_launched = 0 + self.all_built = 0 + self.all_timeout = 0 + self.hs_launched = 0 + self.hs_built = 0 + self.hs_timeout = 0 + + def circ_event(self, event): + is_hs = event.hs_state or event.purpose[0:2] == "HS" + + if is_hs and event.id in self.circuits and \ + self.circuits[event.id].is_hs != is_hs: + plog("ERROR", "Circuit "+event.id+" just changed from non-HS to HS: "\ + +event.raw_content()) + + # Do not record circuits built while we have no timeout + # (ie: after reset but before computed) + if not self.record_timeouts: + return + + # Stages of circuits: + # LAUNCHED -> BUILT + # BUILT -> EXTENDED -> FINISHED + # BUILT -> EXTENDED -> FAILED + # BUILT -> EXTENDED -> TIMEOUT + # LAUNCHED -> TIMEOUT + # TIMEOUT -> MEASURED + # MEASURED -> FINSHED + # MEASURED -> EXPIRED + # LAUNCHED -> FAILED + # LAUNCHED -> CLOSED + # FAILED -> CLOSED + # TIMEOUT -> CLOSED + if event.status == "LAUNCHED": + self.add_circuit(event.id, is_hs) + elif event.status == "BUILT": + self.built_circuit(event.id) + elif event.reason == "TIMEOUT": + self.timeout_circuit(event.id) + elif event.purpose != "MEASURE_TIMEOUT" and \ + (event.status == "CLOSED" or event.status == "FAILED"): + self.closed_circuit(event.id) + + def cbt_event(self, event): + # TODO: Check if this is too high... + plog("INFO", "CBT Timeout rate: "+str(event.timeout_rate)+"; Our measured timeout rate: "+str(self.timeout_rate_all())+"; Hidden service timeout rate: "+str(self.timeout_rate_hs())) + plog("INFO", event.raw_content()) + if event.set_type == "COMPUTED": + plog("INFO", "CBT Timeout computed: "+event.raw_content()) + self.record_timeouts = True + if event.set_type == "RESET": + plog("INFO", "CBT Timeout reset") + self.record_timeouts = False + self.zero_fields() + + + def add_circuit(self, circ_id, is_hs): + if circ_id in self.circuits: + plog("ERROR", "Circuit "+circ_id+" already exists in map!") + self.circuits[circ_id] = CircuitStat(circ_id, is_hs) + self.all_launched += 1 + if is_hs: self.hs_launched += 1 + + def built_circuit(self, circ_id): + if circ_id in self.circuits: + self.all_built += 1 + if self.circuits[circ_id].is_hs: + self.hs_built += 1 + del self.circuits[circ_id] + + def closed_circuit(self, circ_id): + # If we are closed but still in circuits, then we closed + # before being built or timing out. Don't count as a launched circ + if circ_id in self.circuits: + self.all_launched -= 1 + if self.circuits[circ_id].is_hs: + self.hs_launched -= 1 + del self.circuits[circ_id] + + def timeout_circuit(self, circ_id): + if circ_id in self.circuits: + self.all_timeout += 1 + if self.circuits[circ_id].is_hs: + self.hs_timeout += 1 + del self.circuits[circ_id] + + def timeout_rate_all(self): + if self.all_launched: + return float(self.all_timeout)/(self.all_launched) + else: return 0.0 + + def timeout_rate_hs(self): + if self.hs_launched: + return float(self.hs_timeout)/(self.hs_launched) + else: return 0.0 + + + diff --git a/build/lib/vanguards/config.py b/build/lib/vanguards/config.py new file mode 100644 index 0000000..8922269 --- /dev/null +++ b/build/lib/vanguards/config.py @@ -0,0 +1,261 @@ +""" This file contains configuration defaults, options parsing, and config + file code. +""" +import argparse +import ipaddress +import os +import socket +import sys +import tempfile + +from . import bandguards +from . import rendguard +from . import vanguards +from . import control + +from . import logger +from .logger import plog + +from configparser import ConfigParser, Error + +################# Global options ################## + +ENABLE_VANGUARDS=True + +ENABLE_RENDGUARD=True + +ENABLE_BANDGUARDS=True + +ENABLE_LOGGUARD=True + +ENABLE_CBTVERIFY=False + +ENABLE_PATHVERIFY=False + +# State file location +STATE_FILE = os.path.join(tempfile.gettempdir(), "vanguards.state") + +# Config file location +_CONFIG_FILE = os.path.join(tempfile.gettempdir(), "vanguards.conf") + +# Loglevel +LOGLEVEL = "NOTICE" + +# Log to file instead of stdout +LOGFILE = "" + +# If true, write/update vanguards to torrc and then exit +ONE_SHOT_VANGUARDS = False + +CLOSE_CIRCUITS = True + +CONTROL_IP = "127.0.0.1" +CONTROL_PORT = "" +CONTROL_SOCKET = "" +CONTROL_PASS = "" + +_RETRY_LIMIT = None + +DAEMONIZE = False + +def daemonize(): + pid = os.fork() + if pid > 0: + sys.exit(0) + os.setsid() + pid = os.fork() + if pid > 0: + sys.exit(0) + sys.stdout.flush() + sys.stderr.flush() + with open(os.devnull, "r") as f: + os.dup2(f.fileno(), sys.stdin.fileno()) + with open(os.devnull, "w") as f: + os.dup2(f.fileno(), sys.stdout.fileno()) + os.dup2(f.fileno(), sys.stderr.fileno()) + +def setup_options(): + global CONTROL_IP, CONTROL_PORT, CONTROL_SOCKET, CONTROL_PASS, STATE_FILE + global ENABLE_BANDGUARDS, ENABLE_RENDGUARD, ENABLE_LOGGUARD, ENABLE_CBTVERIFY + global ENABLE_PATHVERIFY + global LOGLEVEL, LOGFILE + global ONE_SHOT_VANGUARDS, ENABLE_VANGUARDS + global DAEMONIZE + + parser = argparse.ArgumentParser() + + parser.add_argument("--state", dest="state_file", + default=os.environ.get("VANGUARDS_STATE", STATE_FILE), + help="File to store vanguard state") + + parser.add_argument("--generate_config", dest="write_file", type=str, + help="Write config to a file after applying command args") + + parser.add_argument("--loglevel", dest="loglevel", type=str, + help="Log verbosity (DEBUG, INFO, NOTICE, WARN, or ERROR)") + + parser.add_argument("--logfile", dest="logfile", type=str, + help="Log to LOGFILE instead of stdout") + + parser.add_argument("--config", dest="config_file", + default=os.environ.get("VANGUARDS_CONFIG", _CONFIG_FILE), + help="Location of config file with more advanced settings") + + parser.add_argument("--control_ip", dest="control_ip", default=CONTROL_IP, + help="The IP address of the Tor Control Port to connect to (default: "+ + CONTROL_IP+")") + parser.add_argument("--control_port", type=str, dest="control_port", + default=CONTROL_PORT, + help="The Tor Control Port to connect to (default: "+ + "tries both 9050 and 9151)") + + parser.add_argument("--control_socket", dest="control_socket", + default=CONTROL_SOCKET, + help="The Tor Control Socket path to connect to "+ + "(default: try /run/tor/control, then control port)") + parser.add_argument("--control_pass", dest="control_pass", + default=CONTROL_PASS, + help="The Tor Control Port password (optional) ") + + parser.add_argument("--retry_limit", dest="retry_limit", + default=_RETRY_LIMIT, type=int, + help="Reconnect attempt limit on failure (default: Infinite)") + + parser.add_argument("--one_shot_vanguards", dest="one_shot_vanguards", + action="store_true", + help="Set and write layer2 and layer3 guards to Torrc and exit.") + + parser.add_argument("--disable_vanguards", dest="vanguards_enabled", + action="store_false", + help="Disable setting any layer2 and layer3 guards.") + parser.set_defaults(vanguards_enabled=ENABLE_VANGUARDS) + + parser.add_argument("--disable_bandguards", dest="bandguards_enabled", + action="store_false", + help="Disable circuit side channel checks (may help performance)") + parser.set_defaults(bandguards_eabled=ENABLE_BANDGUARDS) + + parser.add_argument("--disable_logguard", dest="logguard_enabled", + action="store_false", + help="Disable Tor log monitoring (may help performance)") + parser.set_defaults(logguard_enabled=ENABLE_LOGGUARD) + + parser.add_argument("--disable_rendguard", dest="rendguard_enabled", + action="store_false", + help="Disable rendezvous misuse checks (may help performance)") + parser.set_defaults(rendguard_enabled=ENABLE_RENDGUARD) + + parser.add_argument("--enable_cbtverify", dest="cbtverify_enabled", + action="store_true", + help="Enable Circuit Build Time monitoring") + parser.set_defaults(cbtverify_enabled=ENABLE_CBTVERIFY) + + parser.add_argument("--enable_pathverify", dest="pathverify_enabled", + action="store_true", + help="Enable path selection monitoring") + parser.set_defaults(pathverify_enabled=ENABLE_PATHVERIFY) + + parser.add_argument("--daemonize", "-d", dest="daemonize", + action="store_true", + help="Run vanguards as a background daemon") + + options = parser.parse_args() + + (STATE_FILE, CONTROL_IP, CONTROL_PORT, CONTROL_SOCKET, CONTROL_PASS, + ENABLE_BANDGUARDS, ENABLE_RENDGUARD, ENABLE_LOGGUARD, ENABLE_CBTVERIFY, + ENABLE_PATHVERIFY, ONE_SHOT_VANGUARDS, ENABLE_VANGUARDS, + DAEMONIZE) = \ + (options.state_file, options.control_ip, options.control_port, + options.control_socket, options.control_pass, + options.bandguards_enabled, options.rendguard_enabled, + options.logguard_enabled, + options.cbtverify_enabled, options.pathverify_enabled, + options.one_shot_vanguards, options.vanguards_enabled, + options.daemonize) + + if options.loglevel != None: + LOGLEVEL = options.loglevel + logger.set_loglevel(LOGLEVEL) + + if options.logfile != None: + LOGFILE = options.logfile + + if LOGFILE != "": + logger.set_logfile(LOGFILE) + + if options.write_file != None: + config = generate_config() + config.write(open(options.write_file, "w")) + plog("NOTICE", "Wrote config to "+options.write_file) + sys.exit(0) + + # If control_ip is a domain name, try to resolve it. + if options.control_ip != None: + try: + _ = ipaddress.ip_address(options.control_ip) + except ValueError: + try: + # We're fine with AF_INET, stem supports only IPv4 addresses anyway. + addr = socket.getaddrinfo(options.control_ip, None, socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + CONTROL_IP = addr[0][4][0] + except socket.gaierror: + plog("ERROR", "Failed to resolve hostname "+options.control_ip) + sys.exit(1) + + plog("DEBUG", "Applied command line options") + + return options + +# Avoid a big messy dict of defaults. We already have them. +def get_option(config, section, option, default): + try: + if type(default) == bool: + ret = config.get(section, option) == "True" + else: + ret = type(default)(config.get(section, option)) + except Error as e: + return default + return ret + +def get_options_for_module(config, module, section): + for param in dir(module): + if param.isupper() and param[0] != '_': + val = getattr(module, param) + setattr(module, param, + get_option(config, section, param.lower(), val)) + +def set_options_from_module(config, module, section): + config.add_section(section) + for param in dir(module): + if param.isupper() and param[0] != '_': + val = getattr(module, param) + config.set(section, param, str(val)) + +def generate_config(): + config = ConfigParser(allow_no_value=True) + set_options_from_module(config, sys.modules[__name__], "Global") + set_options_from_module(config, vanguards, "Vanguards") + set_options_from_module(config, bandguards, "Bandguards") + set_options_from_module(config, rendguard, "Rendguard") + set_options_from_module(config, rendguard, "Logguard") + + return config + +def apply_config(config_file): + config = ConfigParser(allow_no_value=True) + + config.read_file(open(config_file, "r")) + + get_options_for_module(config, sys.modules[__name__], "Global") + get_options_for_module(config, vanguards, "Vanguards") + get_options_for_module(config, bandguards, "Bandguards") + get_options_for_module(config, rendguard, "Rendguard") + get_options_for_module(config, rendguard, "Logguard") + + # Special cased CLOSE_CIRCUITS option has to be transfered + # to the control.py module + setattr(control, "_CLOSE_CIRCUITS", CLOSE_CIRCUITS) + + + plog("NOTICE", "Vanguards successfilly applied config options from "+ + config_file) diff --git a/build/lib/vanguards/control.py b/build/lib/vanguards/control.py new file mode 100644 index 0000000..b4c1f46 --- /dev/null +++ b/build/lib/vanguards/control.py @@ -0,0 +1,47 @@ +import stem +import sys +import getpass + +from .logger import plog + +from . import __version__ + +_CLOSE_CIRCUITS = True + +def authenticate_any(controller, passwd=""): + try: + controller.authenticate() + except stem.connection.MissingPassword: + if passwd == "": + passwd = getpass.getpass("Controller password: ") + + try: + controller.authenticate(password=passwd) + except stem.connection.PasswordAuthFailed: + print("Unable to authenticate, password is incorrect") + sys.exit(1) + except stem.connection.AuthenticationFailure as exc: + print("Unable to authenticate: %s" % exc) + sys.exit(1) + + plog("NOTICE", "Vanguards %s connected to Tor %s using stem %s", + __version__, controller.get_version(), stem.__version__) + +def get_consensus_weights(consensus_filename): + parsed_consensus = next(stem.descriptor.parse_file(consensus_filename, + document_handler = + stem.descriptor.DocumentHandler.BARE_DOCUMENT)) + + assert(parsed_consensus.is_consensus) + return parsed_consensus.bandwidth_weights + +def try_close_circuit(controller, circ_id): + if controller._logguard: + controller._logguard.dump_log_queue(circ_id, "Pre") + + if _CLOSE_CIRCUITS: + try: + controller.close_circuit(circ_id) + plog("INFO", "We force-closed circuit "+str(circ_id)) + except stem.InvalidRequest as e: + plog("INFO", "Failed to close circuit "+str(circ_id)+": "+str(e.message)) diff --git a/build/lib/vanguards/logger.py b/build/lib/vanguards/logger.py new file mode 100644 index 0000000..66dbdbb --- /dev/null +++ b/build/lib/vanguards/logger.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python + +import logging.handlers +import sys +import os.path + +logger = None +loglevel = "DEBUG" +logfile = None + +loglevels = { "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "NOTICE": logging.INFO + 5, + "WARN": logging.WARN, + "ERROR": logging.ERROR, + "ERR" : logging.ERROR, + "NONE": logging.ERROR + 5 } + +def set_loglevel(level): + global loglevel + if level not in loglevels: + plog("ERROR", "Invalid loglevel: "+str(level)) + sys.exit(1) + loglevel = level + +def set_logfile(filename): + global logfile + try: + if filename == ":syslog:": + logfile = ":syslog:" + else: + logfile = open(filename, "a") + logger_init() + except Exception as e: + plog("ERROR", "Can't open log file "+str(filename)+": "+str(e)) + sys.exit(1) + +def logger_init(): + global logger, logfile + + # Default init = old TorCtl format + default behavior + # Default behavior = log to stdout if TorUtil.logfile is None, + # or to the open file specified otherwise. + logger = logging.getLogger("TorCtl") + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + if logfile == ":syslog:": + if os.path.exists("/dev/log"): + ch = logging.handlers.SysLogHandler("/dev/log") + elif os.path.exists("/var/run/syslog"): + ch = logging.handlers.SysLogHandler("/var/run/syslog") + else: + ch = logging.handlers.SysLogHandler() + + formatter = logging.Formatter("vanguards %(levelname)s: %(message)s") + else: + formatter = logging.Formatter("%(levelname)s[%(asctime)s]: %(message)s", + "%a %b %d %H:%M:%S %Y") + if not logfile: + logfile = sys.stdout + ch = logging.StreamHandler(logfile) + else: + ch = logging.StreamHandler(logfile) + + ch.setFormatter(formatter) + logger.addHandler(ch) + logger.setLevel(loglevels[loglevel]) + + +def plog(level, msg, *args): + if not logger: + logger_init() + + logger.log(loglevels[level], msg.strip(), *args) + + diff --git a/build/lib/vanguards/logguard.py b/build/lib/vanguards/logguard.py new file mode 100644 index 0000000..d358c31 --- /dev/null +++ b/build/lib/vanguards/logguard.py @@ -0,0 +1,88 @@ +""" Log monitoring for attacks, protocol issues, and debugging """ +import stem +import time +import functools + +from . import control + +from .logger import plog +from .logger import loglevels + +# XXX: Off by default for now +LOG_PROTOCOL_WARNS = True + +LOG_DUMP_LIMIT = 25 + +LOG_DUMP_LEVEL = "NOTICE" + +class LogGuard: + def __init__(self, controller): + self.controller = controller + self.log_buffer = list() + self.log_level = LOG_DUMP_LEVEL.lower() + self.log_limit = LOG_DUMP_LIMIT + + # Upgrade to ProtocolWarns if set. Otherwise, leave whatever torrc set + if LOG_PROTOCOL_WARNS: + controller.set_conf("ProtocolWarnings", "1") + + self._init_logbuffer(controller) + + # Register log events depending on log dump level + def _init_logbuffer(self, controller): + dump_level = loglevels[LOG_DUMP_LEVEL] + + if dump_level <= loglevels["DEBUG"]: + controller.add_event_listener( + functools.partial(LogGuard.log_all_event, self), + stem.control.EventType.DEBUG) + + if dump_level <= loglevels["INFO"]: + controller.add_event_listener( + functools.partial(LogGuard.log_all_event, self), + stem.control.EventType.INFO) + + + if dump_level <= loglevels["NOTICE"]: + controller.add_event_listener( + functools.partial(LogGuard.log_all_event, self), + stem.control.EventType.NOTICE) + + + if dump_level <= loglevels["WARN"]: + controller.add_event_listener( + functools.partial(LogGuard.log_all_event, self), + stem.control.EventType.WARN) + + + if dump_level <= loglevels["ERROR"]: + controller.add_event_listener( + functools.partial(LogGuard.log_all_event, self), + stem.control.EventType.ERR) + + + def log_warn_event(self, event): + plog("NOTICE", "Tor log warn: "+event.message) + + def log_all_event(self, event): + self.log_buffer.append(event) + + # XXX: This might not be efficient, idk + # XXX: This might also be a memleak/memfrag, depending on how list() + # decides to handle this underneath :/ + while len(self.log_buffer) > self.log_limit: + self.log_buffer.pop(0) + + # This is called before and after circuit close. The "when" argument is + # "Pre" before we close a circuit in controller.try_close_circuit(), and "Post" after. + def dump_log_queue(self, circ_id, when): + while len(self.log_buffer): + event = self.log_buffer.pop(0) + plog("NOTICE", when+"-close CIRC ID="+circ_id+" Tor log: TOR_"+event.runlevel+"["+time.ctime(event.arrived_at)+"]: "+event.message) + + # This lets us emit post-close logs that may be relevant (more ProtocolWarns, etc) + def circ_event(self, event): + if (event.status == "CLOSED" or event.status == "FAILED") \ + and event.reason == "REQUESTED": + self.dump_log_queue(event.id, "Post") + diff --git a/build/lib/vanguards/main.py b/build/lib/vanguards/main.py new file mode 100644 index 0000000..f2297db --- /dev/null +++ b/build/lib/vanguards/main.py @@ -0,0 +1,264 @@ +import functools +import os +import stem +import tempfile +import time +import sys + +import stem.response.events + +from . import control +from . import rendguard +from . import vanguards +from . import bandguards +from . import logguard +from . import cbtverify +from . import pathverify + +from . import config + +from .logger import plog +from . import logger + +_MIN_TOR_VERSION_FOR_BW = stem.version.Version("0.3.4.10") + +def main(): + try: + run_main() + except KeyboardInterrupt as e: + plog("NOTICE", "Got CTRL+C. Exiting.") + +def run_main(): + try: + config.apply_config(config._CONFIG_FILE) + except: + pass # Default config can be absent. + options = config.setup_options() + + # If the user specifies a config file, any values there should override + # any previous config file options, but not options on the command line. + if options.config_file != config._CONFIG_FILE: + try: + config.apply_config(options.config_file) + except Exception as e: + plog("ERROR", + "Specified config file "+options.config_file+\ + " can't be read: "+str(e)) + sys.exit(1) + options = config.setup_options() + + if config.DAEMONIZE: + if config.LOGFILE == "": + logpath = os.path.join(tempfile.gettempdir(), "vanguards.log") + config.LOGFILE = logpath + logger.set_logfile(logpath) + config.daemonize() + + try: + # TODO: Use tor's data directory.. or our own + state = vanguards.VanguardState.read_from_file(config.STATE_FILE) + plog("INFO", "Current layer2 guards: "+state.layer2_guardset()) + plog("INFO", "Current layer3 guards: "+state.layer3_guardset()) + except Exception as e: + plog("NOTICE", "Creating new vanguard state file at: "+config.STATE_FILE) + state = vanguards.VanguardState(config.STATE_FILE) + + state.enable_vanguards = config.ENABLE_VANGUARDS + stem.response.events.PARSE_NEWCONSENSUS_EVENTS = False + + reconnects = 0 + last_connected_at = None + connected = False + while options.retry_limit == None or reconnects < options.retry_limit: + ret = control_loop(state) + if not last_connected_at: + last_connected_at = time.time() + + if ret == "closed": + connected = True + # Try once per second but only tell the user every 10 seconds + if ret == "closed" or reconnects % 10 == 0: + if time.time() - last_connected_at > \ + bandguards.CONN_MAX_DISCONNECTED_SECS: + plog("WARN", "Tor daemon connection "+ret+". Trying again...") + else: + plog("NOTICE", "Tor daemon connection "+ret+". Trying again...") + reconnects += 1 + time.sleep(1) + + if not connected: + sys.exit(1) + +def control_loop(state): + if not config.CONTROL_SOCKET and not config.CONTROL_PORT: + try: + controller = \ + stem.control.Controller.from_socket_file("/run/tor/control") + plog("NOTICE", "Connected to Tor via /run/tor/control socket") + except stem.SocketError as e: + try: + controller = stem.control.Controller.from_port(config.CONTROL_IP) + except stem.SocketError as e: + return "failed: "+str(e) + plog("NOTICE", "Connected to Tor via "+config.CONTROL_IP+" control port") + else: + try: + if config.CONTROL_SOCKET != "": + controller = \ + stem.control.Controller.from_socket_file(config.CONTROL_SOCKET) + plog("NOTICE", "Connected to Tor via socket "+config.CONTROL_SOCKET) + else: + if not config.CONTROL_PORT or config.CONTROL_PORT == "default": + controller = stem.control.Controller.from_port(config.CONTROL_IP) + plog("NOTICE", "Connected to Tor via "+config.CONTROL_IP+ + " control port") + else: + controller = stem.control.Controller.from_port(config.CONTROL_IP, + int(config.CONTROL_PORT)) + plog("NOTICE", "Connected to Tor via control port "+ + config.CONTROL_IP+ ":"+config.CONTROL_PORT) + except ValueError as e: + plog("ERROR", "Control port must be an integer or 'default'. Got "+ + str(config.CONTROL_PORT)) + sys.exit(1) + except stem.SocketError as e: + return "failed: "+str(e) + + control.authenticate_any(controller, config.CONTROL_PASS) + + # The new_consensus_event must still get called even if vanguards + # is "disabled", because we also have to parse the consensus to + # update rendguard counts + if config.ENABLE_VANGUARDS or config.ENABLE_RENDGUARD: + try: + state.new_consensus_event(controller, None) + except stem.DescriptorUnavailable as e: + controller.close() + plog("NOTICE", "Tor needs descriptors: "+str(e)+". Trying again...") + return "failed: "+str(e) + + if config.ONE_SHOT_VANGUARDS: + try: + controller.save_conf() + except stem.OperationFailed as e: + plog("NOTICE", "Tor can't save its own config file: "+str(e)) + sys.exit(1) + plog("NOTICE", "Updated vanguards in torrc. Exiting.") + sys.exit(0) + + # Thread-safety: state, timeouts, and bandwidths are effectively + # transferred to the event thread here. They must not be used in + # our thread anymore. + + if config.ENABLE_RENDGUARD: + controller.add_event_listener( + functools.partial(rendguard.RendGuard.circ_event, + state.rendguard, controller), + stem.control.EventType.CIRC) + + # Ok, little low on fucks here. But this is fine. This will work. + # We check for None in control.try_close_circuit() + controller._logguard = None + + if config.ENABLE_LOGGUARD: + logs = logguard.LogGuard(controller) # Also registeres logbuffer events + + # Make the log object available later for log dumping + controller._logguard = logs + + # Always log warns + controller.add_event_listener( + functools.partial(logguard.LogGuard.log_warn_event, + logs), + stem.control.EventType.WARN) + + # For post-close logs + controller.add_event_listener( + functools.partial(logguard.LogGuard.circ_event, logs), + stem.control.EventType.CIRC) + + + if config.ENABLE_BANDGUARDS: + bandwidths = bandguards.BandwidthStats(controller) + + controller.add_event_listener( + functools.partial(bandguards.BandwidthStats.circ_event, bandwidths), + stem.control.EventType.CIRC) + controller.add_event_listener( + functools.partial(bandguards.BandwidthStats.bw_event, bandwidths), + stem.control.EventType.BW) + controller.add_event_listener( + functools.partial(bandguards.BandwidthStats.orconn_event, bandwidths), + stem.control.EventType.ORCONN) + controller.add_event_listener( + functools.partial(bandguards.BandwidthStats.network_liveness_event, + bandwidths), + stem.control.EventType.NETWORK_LIVENESS) + + if controller.get_version() >= _MIN_TOR_VERSION_FOR_BW: + controller.add_event_listener( + functools.partial(bandguards.BandwidthStats.circbw_event, bandwidths), + stem.control.EventType.CIRC_BW) + controller.add_event_listener( + functools.partial(bandguards.BandwidthStats.circ_minor_event, bandwidths), + stem.control.EventType.CIRC_MINOR) + else: + plog("NOTICE", "In order for bandwidth-based protections to be "+ + "enabled, you must use Tor 0.3.4.10 or newer.") + + + if config.ENABLE_CBTVERIFY: + timeouts = cbtverify.TimeoutStats() + + controller.add_event_listener( + functools.partial(cbtverify.TimeoutStats.circ_event, timeouts), + stem.control.EventType.CIRC) + controller.add_event_listener( + functools.partial(cbtverify.TimeoutStats.cbt_event, timeouts), + stem.control.EventType.BUILDTIMEOUT_SET) + + if config.ENABLE_PATHVERIFY: + paths = pathverify.PathVerify(controller, + config.ENABLE_VANGUARDS, + vanguards.NUM_LAYER1_GUARDS, + vanguards.NUM_LAYER2_GUARDS, + vanguards.NUM_LAYER3_GUARDS) + + controller.add_event_listener( + functools.partial(pathverify.PathVerify.circ_event, paths), + stem.control.EventType.CIRC) + controller.add_event_listener( + functools.partial(pathverify.PathVerify.circ_minor_event, paths), + stem.control.EventType.CIRC_MINOR) + controller.add_event_listener( + functools.partial(pathverify.PathVerify.orconn_event, paths), + stem.control.EventType.ORCONN) + controller.add_event_listener( + functools.partial(pathverify.PathVerify.guard_event, paths), + stem.control.EventType.GUARD) + controller.add_event_listener( + functools.partial(pathverify.PathVerify.conf_changed_event, + paths), + stem.control.EventType.CONF_CHANGED) + # After launch pathverify, we send a NEWNYM to get fresh circs and + # vg-lite guards + controller.signal("NEWNYM") + + + # Thread-safety: We're effectively transferring controller to the event + # thread here. + if config.ENABLE_VANGUARDS or config.ENABLE_RENDGUARD: + controller.add_event_listener( + functools.partial(vanguards.VanguardState.new_consensus_event, + state, controller), + stem.control.EventType.NEWCONSENSUS) + controller.add_event_listener( + functools.partial(vanguards.VanguardState.signal_event, + state, controller), + stem.control.EventType.SIGNAL) + + # Blah... + while controller.is_alive(): + time.sleep(1) + + return "closed" diff --git a/build/lib/vanguards/pathverify.py b/build/lib/vanguards/pathverify.py new file mode 100644 index 0000000..e5783cb --- /dev/null +++ b/build/lib/vanguards/pathverify.py @@ -0,0 +1,265 @@ +""" Simple checks against bandwidth side channels """ +import stem + +from . import control + +from .logger import plog + +_ROUTELEN_FOR_PURPOSE = { + "HS_VANGUARDS" : 4, + "HS_CLIENT_HSDIR" : 5, + "HS_CLIENT_INTRO" : 5, + "HS_CLIENT_REND" : 4, + "HS_SERVICE_HSDIR" : 4, + "HS_SERVICE_INTRO" : 4, + "HS_SERVICE_REND" : 5 + } + +# XXX: Hrmm +_ROUTELEN_FOR_PURPOSE_LITE = { + "HS_VANGUARDS" : 3, + "HS_CLIENT_HSDIR" : 4, + "HS_CLIENT_INTRO" : 4, + "HS_CLIENT_REND" : 3, + "HS_SERVICE_HSDIR" : 4, + "HS_SERVICE_INTRO" : 4, + "HS_SERVICE_REND" : 4 + } + +class Layer1Stats: + def __init__(self): + self.use_count = 0 + self.conn_count = 1 + +class Layer1Guards: + def __init__(self, num_layer1): + self.guards = {} + self.num_layer1 = num_layer1 + + def add_conn(self, guard_fp): + if guard_fp in self.guards: + self.guards[guard_fp].conn_count += 1 + else: + self.guards[guard_fp] = Layer1Stats() + + def del_conn(self, guard_fp): + if guard_fp in self.guards: + if self.guards[guard_fp].conn_count > 1: + self.guards[guard_fp].conn_count -= 1 + else: + del self.guards[guard_fp] + + # Returns -1 when fewer than expected, 0 when correct, +1 when too many + # (Retval used only by tests) + def check_conn_counts(self): + ret = 0 + + if len(self.guards) < self.num_layer1: + plog("NOTICE", "Fewer guard connections than configured. Connected to: "+ \ + str(self.guards.keys())) + ret = -1 + elif len(self.guards) > self.num_layer1: + plog("NOTICE", "More guard connections than configured. Connected to: "+ \ + str(self.guards.keys())) + ret = 1 + + for g in self.guards.keys(): + if self.guards[g].conn_count > 1: + plog("NOTICE", "Extra connections to guard "+g+": "+\ + str(self.guards[g].conn_count)) + ret = 1 + return ret + + def add_use_count(self, guard_fp): + if not guard_fp in self.guards: + plog("WARN", "Guard "+guard_fp+" not in "+ \ + str(self.guards.keys())) + else: + self.guards[guard_fp].use_count += 1 + + # Returns -1 when fewer than expected, 0 when correct, +1 when too many + # (Retval used only by tests) + def check_use_counts(self): + ret = 0 + layer1_in_use = list(filter(lambda x: self.guards[x].use_count, + self.guards.keys())) + layer1_counts = list(map(lambda x: + x+": "+str(self.guards[x].use_count), + layer1_in_use)) + + if len(layer1_in_use) > self.num_layer1: + plog("WARN", "Circuits are being used on more guards " + \ + "than configured. Current guard use: "+str(layer1_counts)) + ret = 1 + elif len(layer1_in_use) < self.num_layer1: + plog("NOTICE", "Circuits are being used on fewer guards " + \ + "than configured. Current guard use: "+str(layer1_counts)) + ret = -1 + return ret + +class PathVerify: + def __init__(self, controller, full_vanguards, num_layer1, num_layer2, num_layer3): + self.controller = controller + self.full_vanguards = full_vanguards + self.layer2 = set() + self.layer3 = set() + self.num_layer1 = num_layer1 + self.num_layer2 = num_layer2 + self.num_layer3 = num_layer3 + self._layers_init(controller) + self.layer1 = Layer1Guards(self.num_layer1) + self._orconn_init(controller) + + def _orconn_init(self, controller): + for l in controller.get_info("orconn-status").split("\n"): + if len(l): + guard_fp = l.split("~")[0][1:] + self.layer1.add_conn(guard_fp) + + self.layer1.check_conn_counts() + + def _layers_init(self, controller): + layer2 = controller.get_conf("HSLayer2Nodes", None) + layer3 = controller.get_conf("HSLayer3Nodes", None) + + # These may be empty at startup + if layer2: + self.layer2 = set(layer2.split(",")) + self.full_vanguards = True + + if layer3: + self.layer3 = set(layer3.split(",")) + self.full_vanguards = True + + # If they are empty, and vanguards is disabled by the addon, + # then we're just verifying vg-lite in C-Tor. + if not layer2 and not layer3 and not self.full_vanguards: + plog("NOTICE", "Monitoring vanguards-lite with pathverify.") + # Update our num layer params because they now depend on vg-lite + self.num_layer1 = 1 + self.num_layer2 = 4 + self.num_layer3 = 0 + else: + plog("NOTICE", "Monitoring vanguards with pathverify.") + + self._check_layer_counts() + + def conf_changed_event(self, event): + if "HSLayer2Nodes" in event.changed: + self.layer2 = set(event.changed["HSLayer2Nodes"][0].split(",")) + self.full_vanguards = True + + if "HSLayer3Nodes" in event.changed: + self.layer3 = set(event.changed["HSLayer3Nodes"][0].split(",")) + self.full_vanguards = True + + self._check_layer_counts() + + plog("DEBUG", event.raw_content()) + + # Returns True when right number, False otherwise + def _check_layer_counts(self): + ret = False + # These can become empty briefly on sighup and startup. Aka set(['']) + if len(self.layer2) > 1: + if len(self.layer2) != self.num_layer2: + plog("NOTICE", "Wrong number of layer2 guards. " + \ + str(self.num_layer2)+" vs: "+str(self.layer2)) + ret = False + else: + ret = True + + if len(self.layer3) > 1: + if len(self.layer3) != self.num_layer3: + plog("NOTICE", "Wrong number of layer3 guards. " + \ + str(self.num_layer3)+" vs: "+str(self.layer3)) + ret = False + else: + ret = True + return ret + + def orconn_event(self, event): + if event.status == "CONNECTED": + self.layer1.add_conn(event.endpoint_fingerprint) + elif event.status == "CLOSED" or event.status == "FAILED": + self.layer1.del_conn(event.endpoint_fingerprint) + + self.layer1.check_conn_counts() + + def guard_event(self, event): + if event.status == "GOOD_L2": + self.layer2.add(event.endpoint_fingerprint) + elif event.status == "BAD_L2": + self.layer2.discard(event.endpoint_fingerprint) + + plog("DEBUG", event.raw_content()) + + def routelen_for_purpose(self, purpose): + if self.full_vanguards: + return _ROUTELEN_FOR_PURPOSE[purpose] + else: + return _ROUTELEN_FOR_PURPOSE_LITE[purpose] + + def circ_event(self, event): + if event.purpose[0:3] == "HS_" and (event.status == stem.CircStatus.BUILT or \ + event.status == "GUARD_WAIT"): + if len(event.path) != self.routelen_for_purpose(event.purpose): + if (event.purpose == "HS_SERVICE_HSDIR" and \ + event.hs_state == "HSSI_CONNECTING") or \ + (event.purpose == "HS_CLIENT_INTRO" and \ + event.hs_state == "HSCI_CONNECTING"): + # This can happen when HS_VANGUARDS are cannibalized.. + # XXX: Is that a bug? + # It can also happen if client intros fail and are retried with a + # new hop. That case is not a bug. + plog("INFO", "Tor made a "+str(len(event.path))+ "-hop path, but I wanted a " + \ + str(self.routelen_for_purpose(event.purpose))+ "-hop path for purpose " + \ + event.purpose +":"+str(event.hs_state)+" + " + \ + event.raw_content()) + else: + plog("NOTICE", "Tor made a "+str(len(event.path))+ "-hop path, but I wanted a " + \ + str(self.routelen_for_purpose(event.purpose))+ "-hop path for purpose " + \ + event.purpose +":"+str(event.hs_state)+" + " + \ + event.raw_content()) + + self.layer1.add_use_count(event.path[0][0]) + self.layer1.check_use_counts() + + if not event.path[1][0] in self.layer2: + plog("WARN", "Layer2 "+event.path[1][0]+" not in "+ \ + str(self.layer2)) + + if self.num_layer3 and not event.path[2][0] in self.layer3: + plog("WARN", "Layer3 "+event.path[1][0]+" not in "+ \ + str(self.layer3)) + + if len(self.layer2) != self.num_layer2: + plog("WARN", "Circuit built with different number of layer2 nodes " + \ + "than configured. Currently using: " + str(self.layer2)) + + if len(self.layer3) != self.num_layer3: + plog("WARN", "Circuit built with different number of layer3 nodes " + \ + "than configured. Currently using: " + str(self.layer3)) + + def circ_minor_event(self, event): + if event.purpose[0:3] == "HS_" and event.old_purpose[0:3] != "HS_": + plog("WARN", "Purpose switched from non-hs to hs: "+ \ + str(event.raw_content())) + elif event.purpose[0:3] != "HS_" and event.old_purpose[0:3] == "HS_": + if event.purpose != "CIRCUIT_PADDING" and \ + event.purpose != "MEASURE_TIMEOUT" and \ + event.purpose != "PATH_BIAS_TESTING": + plog("WARN", "Purpose switched from hs to non-hs: "+ \ + str(event.raw_content())) + + if event.purpose[0:3] == "HS_" or event.old_purpose[0:3] == "HS_": + if not event.path[0][0] in self.layer1.guards: + plog("WARN", "Guard "+event.path[0][0]+" not in "+ \ + str(self.layer1.guards.keys())) + if len(event.path) > 1 and not event.path[1][0] in self.layer2: + plog("WARN", "Layer2 "+event.path[1][0]+" not in "+ \ + str(self.layer2)) + if self.num_layer3 and len(event.path) > 2 and not event.path[2][0] in self.layer3: + plog("WARN", "Layer3 "+event.path[1][0]+" not in "+ \ + str(self.layer3)) + diff --git a/build/lib/vanguards/rendguard.py b/build/lib/vanguards/rendguard.py new file mode 100644 index 0000000..35b1627 --- /dev/null +++ b/build/lib/vanguards/rendguard.py @@ -0,0 +1,135 @@ +from . import control + +from .logger import plog + +############## Rendguard options ##################### + +# Minimum number of hops we have to see before applying use stat checks +REND_USE_GLOBAL_START_COUNT = 1000 + +# Number of hops to scale counts down by two at +REND_USE_SCALE_AT_COUNT = 20000 + +# Minimum number of times a relay has to be used before we check it for +# overuse +REND_USE_RELAY_START_COUNT = 100 + +# How many times more than its bandwidth must a relay be used? +REND_USE_MAX_USE_TO_BW_RATIO = 5.0 + +# What is percent of the network weight is not in the consensus right now? +# Put another way, the max number of rend requests not in the consensus is +# REND_USE_MAX_USE_TO_BW_RATIO times this churn rate. +REND_USE_MAX_CONSENSUS_WEIGHT_CHURN = 1.0 + +# Should we close circuits on rend point overuse? +REND_USE_CLOSE_CIRCUITS_ON_OVERUSE = True + +_NOT_IN_CONSENSUS_ID = "NOT_IN_CONSENSUS" + +class RendUseCount: + def __init__(self, idhex, weight): + self.idhex = idhex + self.used = 0 + self.weight = weight + +class RendGuard: + def __init__(self): + self.use_counts = {} + self.total_use_counts = 0.0 + self.pickle_revision = 1.0 + + def valid_rend_use(self, r): + r_name = r + if r not in self.use_counts: + plog("INFO", "Relay "+r+" is not in our consensus.") + r_name = r+" (not in-consensus)" + r = _NOT_IN_CONSENSUS_ID + if r not in self.use_counts: + self.use_counts[r] = RendUseCount(r, 0) + + self.use_counts[r].used += 1.0 + self.total_use_counts += 1.0 + plog("DEBUG", "Relay "+r_name+" used %d times out of %d, "+\ + "for a use rate of %f%%. It has a consensus " + "weight of %f%%", int(self.use_counts[r].used), + int(self.total_use_counts), + (100.0*self.use_counts[r].used)/self.total_use_counts, + 100.0*self.use_counts[r].weight) + + # TODO: Can we base this check on statistical confidence intervals? + if self.total_use_counts >= REND_USE_GLOBAL_START_COUNT and \ + self.use_counts[r].used >= REND_USE_RELAY_START_COUNT and \ + self.use_counts[r].used/self.total_use_counts > \ + self.use_counts[r].weight*REND_USE_MAX_USE_TO_BW_RATIO: + + # Let's warn if they disable ciruit closing. + if REND_USE_CLOSE_CIRCUITS_ON_OVERUSE: + loglevel = "NOTICE" + else: + loglevel = "WARN" + plog(loglevel, "Relay "+r_name+" used %d times out of %d, "+\ + "for a use rate of %f%%. This is above its consensus " + "weight of %f%%", int(self.use_counts[r].used), + int(self.total_use_counts), + (100.0*self.use_counts[r].used)/self.total_use_counts, + 100.0*self.use_counts[r].weight) + return 0 + return 1 + + def xfer_use_counts(self, node_gen): + old_counts = self.use_counts + self.use_counts = {} + for r in node_gen.sorted_r: + self.use_counts[r.fingerprint] = RendUseCount(r.fingerprint, 0) + + if _NOT_IN_CONSENSUS_ID not in old_counts: + old_counts[_NOT_IN_CONSENSUS_ID] = \ + RendUseCount(_NOT_IN_CONSENSUS_ID, + REND_USE_MAX_CONSENSUS_WEIGHT_CHURN/100.0) + + self.use_counts[_NOT_IN_CONSENSUS_ID] = \ + RendUseCount(_NOT_IN_CONSENSUS_ID, + REND_USE_MAX_CONSENSUS_WEIGHT_CHURN/100.0) + + i = 0 + rlen = len(node_gen.rstr_routers) + while i < rlen: + r = node_gen.rstr_routers[i] + + if "Exit" in r.flags: + self.use_counts[r.fingerprint].weight = \ + node_gen.node_weights[i]/node_gen.exit_total + else: + self.use_counts[r.fingerprint].weight = \ + node_gen.node_weights[i]/node_gen.weight_total + i+=1 + + if self.total_use_counts >= REND_USE_SCALE_AT_COUNT: + plog("INFO", "Total use counts %d reached the scale count %d. Scaling.", + self.total_use_counts, REND_USE_SCALE_AT_COUNT) + + # Periodically we divide counts by two, to avoid overcounting + # high-uptime relays vs old ones + for r in old_counts: + if r != _NOT_IN_CONSENSUS_ID and r not in self.use_counts: + continue + if self.total_use_counts >= REND_USE_SCALE_AT_COUNT: + self.use_counts[r].used = old_counts[r].used/2.0 + else: + self.use_counts[r].used = old_counts[r].used + + + self.total_use_counts = sum(map(lambda x: self.use_counts[x].used, + self.use_counts)) + self.total_use_counts = float(self.total_use_counts) + + def circ_event(self, controller, event): + if event.status == "BUILT" and \ + event.purpose == "HS_SERVICE_REND" and \ + event.hs_state == "HSSR_CONNECTING": + if not self.valid_rend_use(event.path[-1][0]): + if REND_USE_CLOSE_CIRCUITS_ON_OVERUSE: + control.try_close_circuit(controller, event.id) + + plog("DEBUG", event.raw_content()) diff --git a/build/lib/vanguards/vanguards.py b/build/lib/vanguards/vanguards.py new file mode 100644 index 0000000..975cf21 --- /dev/null +++ b/build/lib/vanguards/vanguards.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python + +import random +import os +import time +import pickle +import sys +import string + +from ipaddress import ip_network as net + +import stem + +from .NodeSelection import BwWeightedGenerator, NodeRestrictionList +from .NodeSelection import FlagsRestriction +from .logger import plog + +from . import control +from . import rendguard + +# Unicode, damnit +try: + _UNICODE_DAMNIT = bool(type(unicode)) +except NameError: + unicode = str + +################### Vanguard options ################## +# +NUM_LAYER1_GUARDS = 2 # 0 is Tor default +NUM_LAYER2_GUARDS = 4 +NUM_LAYER3_GUARDS = 8 + +# In days: +LAYER1_LIFETIME_DAYS = 0 # Use tor default + +# In hours +MIN_LAYER2_LIFETIME_HOURS = 24*1 +MAX_LAYER2_LIFETIME_HOURS = 24*45 + +# In hours +MIN_LAYER3_LIFETIME_HOURS = 1 +MAX_LAYER3_LIFETIME_HOURS = 48 + +_SEC_PER_HOUR = (60*60) + +class GuardNode: + def __init__(self, idhex, chosen_at, expires_at): + self.idhex = idhex + self.chosen_at = chosen_at + self.expires_at = expires_at + +class ExcludeNodes: + def __init__(self, controller): + self.networks = [] + self.idhexes = set() + self.nicks = set() + self.countries = set() + self.controller = controller + self.exclude_unknowns = controller.get_conf("GeoIPExcludeUnknown") + self._parse_line(controller.get_conf("ExcludeNodes")) + + def _parse_line(self, conf_line): + # We assume Tor has validated the line already. So this parsing + # is very dumb+simple (except for semantics of valid data). + # See routerset_parse() in tor for parser semantic ordering. + if self.exclude_unknowns == "1": + self.countries.add("??") + self.countries.add("a1") + + if conf_line == None: + return + + parts = conf_line.split(",") + for p in parts: + if p[0] == "$": + p = p[1:] + if "~" in p: + p = p.split("~")[0] + if "=" in p: + p = p.split("=")[0] + + if len(p) == 40 and all(c in string.hexdigits for c in p): + self.idhexes.add(p) + elif p[0] == "{" and p[-1] == "}": + self.countries.add(p[1:-1].lower()) + elif ":" in p or "." in p: + self.networks.append(net(unicode(p), strict=False)) + else: + self.nicks.add(p) + plog("INFO", "Honoring ExcludeNodes line: "+conf_line) + if len(self.networks): + plog("INFO", "Excluding networks "+str(self.networks)) + if len(self.idhexes): + plog("INFO", "Excluding idhexes "+str(self.idhexes)) + if len(self.nicks): + plog("INFO", "Excluding nicks "+str(self.nicks)) + if len(self.countries): + if self.exclude_unknowns == "auto": + self.countries.add("??") + self.countries.add("a1") + + if self.controller.get_info("ip-to-country/ipv4-available", "0") == "0": + plog("WARN", "ExcludeNodes contains countries, but Tor has no GeoIP file! "+ + "Tor is not excluding countries!") + else: + plog("INFO", "Excluding countries "+str(self.countries)) + + def router_is_excluded(self, r): + if r.fingerprint in self.idhexes: + return True + if r.nickname in self.nicks: + return True + if "or_addresses" in r.__dict__: # Stem 1.7.0 only + addresses = r.or_addresses + else: + addresses = [(r.address, 9001, False)] + for addr in addresses: + is_ipv6 = addr[2] + if len(self.countries): + country = None + if is_ipv6 and \ + self.controller.get_info("ip-to-country/ipv6-available", "0") == "1": + country = self.controller.get_info("ip-to-country/"+addr[0]) + if not is_ipv6 and \ + self.controller.get_info("ip-to-country/ipv4-available", "0") == "1": + country = self.controller.get_info("ip-to-country/"+addr[0]) + + if country != None and country.lower() in self.countries: + return True + + for network in self.networks: + if is_ipv6: + if network.version == 6 and net(addr[0]+"/128").overlaps(network): + return True + else: + if network.version == 4 and net(addr[0]+"/32").overlaps(network): + return True + return False + +class VanguardState: + def __init__(self, state_file): + self.layer2 = [] + self.layer3 = [] + self.state_file = state_file + self.rendguard = rendguard.RendGuard() + self.pickle_revision = 1 + self.enable_vanguards = True # Set from main, irrelevant to pickle + + def set_state_file(self, state_file): + self.state_file = state_file + + def sort_and_index_routers(self, routers): + sorted_r = list(routers) + dict_r = {} + + for r in sorted_r: + if r.measured == None: + # FIXME: Hrmm... + r.measured = r.bandwidth + sorted_r.sort(key = lambda x: x.measured, reverse = True) + for r in sorted_r: dict_r[r.fingerprint] = r + return (sorted_r, dict_r) + + def consensus_update(self, routers, weights, exclude): + (sorted_r, dict_r) = self.sort_and_index_routers(routers) + ng = BwWeightedGenerator(sorted_r, + NodeRestrictionList( + [FlagsRestriction(["Fast", "Stable", "Valid"], + ["Authority"])]), + weights, BwWeightedGenerator.POSITION_MIDDLE) + gen = ng.generate() + if self.enable_vanguards: + # Remove any nodes that are now down in the consensus + self.remove_down_from_layer(self.layer2, dict_r) + self.remove_down_from_layer(self.layer3, dict_r) + + # Remove any nodes whose rotation times are past due. + # FIXME: We should check this more often... But we also + # need to replenish our layers if they get too low/empty. + # This can be slow (consensus parse required)... :/ + self.remove_expired_from_layer(self.layer2) + self.remove_expired_from_layer(self.layer3) + + # Remove any nodes in case ExcludeNodes changed. + self.remove_excluded_from_layer(self.layer2, dict_r, exclude) + self.remove_excluded_from_layer(self.layer3, dict_r, exclude) + + # Replenish our guard lists with new nodes + self.replenish_layers(gen, exclude) + + ng = BwWeightedGenerator(sorted_r, + NodeRestrictionList( + [FlagsRestriction(["Fast", "Valid"], + ["Authority"])]), + weights, BwWeightedGenerator.POSITION_MIDDLE) + + # Repair Exit-flagged node weights, since they can be chosen + # sometimes by other clients as RPs (when cannibalized) + ng.repair_exits() + # Transfer and scale RP use counts to this consensus + self.rendguard.xfer_use_counts(ng) + + def new_consensus_event(self, controller, event): + routers = controller.get_network_statuses() + + exclude_nodes = ExcludeNodes(controller) + + data_dir = controller.get_conf("DataDirectory") + if data_dir == None: + plog("ERROR", + "You must set a DataDirectory location option in your torrc.") + sys.exit(1) + + consensus_file = os.path.join(controller.get_conf("DataDirectory"), + "cached-microdesc-consensus") + + try: + weights = control.get_consensus_weights(consensus_file) + except IOError as e: + raise stem.DescriptorUnavailable("Cannot read "+consensus_file+": "+str(e)) + + self.consensus_update(routers, weights, exclude_nodes) + + if self.enable_vanguards: + self.configure_tor(controller) + + try: + self.write_to_file(open(self.state_file, "wb")) + except IOError as e: + plog("ERROR", "Cannot write state to "+self.state_file+": "+str(e)) + sys.exit(1) + + def signal_event(self, controller, event): + if event.signal == "RELOAD": + plog("NOTICE", "Tor got SIGHUP. Reapplying vanguards.") + self.configure_tor(controller) + + def configure_tor(self, controller): + if NUM_LAYER1_GUARDS: + controller.set_conf("NumEntryGuards", str(NUM_LAYER1_GUARDS)) + controller.set_conf("NumDirectoryGuards", str(NUM_LAYER1_GUARDS)) + + if LAYER1_LIFETIME_DAYS > 0: + controller.set_conf("GuardLifetime", str(LAYER1_LIFETIME_DAYS)+" days") + + try: + controller.set_conf("HSLayer2Nodes", self.layer2_guardset()) + + if NUM_LAYER3_GUARDS: + controller.set_conf("HSLayer3Nodes", self.layer3_guardset()) + except stem.InvalidArguments: + plog("ERROR", + "Vanguards requires Tor 0.3.3.x (and ideally 0.3.4.x or newer).") + sys.exit(1) + + def write_to_file(self, outfile): + return pickle.dump(self, outfile) + + @staticmethod + def read_from_file(infile): + ret = pickle.load(open(infile, "rb")) + ret.set_state_file(infile) + return ret + + def layer2_guardset(self): + return ",".join(map(lambda g: g.idhex, self.layer2)) + + def layer3_guardset(self): + return ",".join(map(lambda g: g.idhex, self.layer3)) + + # Adds a new layer2 guard + def add_new_layer2(self, generator, excluded): + guard = next(generator) + while guard.fingerprint in map(lambda g: g.idhex, self.layer2) or \ + excluded.router_is_excluded(guard): + guard = next(generator) + + now = time.time() + expires = now + max(random.uniform(MIN_LAYER2_LIFETIME_HOURS*_SEC_PER_HOUR, + MAX_LAYER2_LIFETIME_HOURS*_SEC_PER_HOUR), + random.uniform(MIN_LAYER2_LIFETIME_HOURS*_SEC_PER_HOUR, + MAX_LAYER2_LIFETIME_HOURS*_SEC_PER_HOUR)) + self.layer2.append(GuardNode(guard.fingerprint, now, expires)) + plog("INFO", "New layer2 guard: "+guard.fingerprint) + + def add_new_layer3(self, generator, excluded): + guard = next(generator) + while guard.fingerprint in map(lambda g: g.idhex, self.layer3) or \ + excluded.router_is_excluded(guard): + guard = next(generator) + + now = time.time() + expires = now + max(random.uniform(MIN_LAYER3_LIFETIME_HOURS*_SEC_PER_HOUR, + MAX_LAYER3_LIFETIME_HOURS*_SEC_PER_HOUR), + random.uniform(MIN_LAYER3_LIFETIME_HOURS*_SEC_PER_HOUR, + MAX_LAYER3_LIFETIME_HOURS*_SEC_PER_HOUR)) + self.layer3.append(GuardNode(guard.fingerprint, now, expires)) + plog("INFO", "New layer3 guard: "+guard.fingerprint) + + def remove_excluded_from_layer(self, layer, dict_r, excluded): + for g in list(layer): + if excluded.router_is_excluded(dict_r[g.idhex]): + layer.remove(g) + plog("INFO", "Removing newly-excluded guard "+g.idhex) + + def remove_expired_from_layer(self, layer): + now = time.time() + for g in list(layer): + if g.expires_at < now: + layer.remove(g) + plog("INFO", "Removing expired guard "+g.idhex) + + def remove_down_from_layer(self, layer, dict_r): + for g in list(layer): + if not g.idhex in dict_r: + layer.remove(g) + plog("INFO", "Removing down guard "+g.idhex) + + def replenish_layers(self, generator, excluded): + # Trim layers in case params changed + self.layer2 = self.layer2[:NUM_LAYER2_GUARDS] + self.layer3 = self.layer3[:NUM_LAYER3_GUARDS] + + while len(self.layer2) < NUM_LAYER2_GUARDS: + self.add_new_layer2(generator, excluded) + + while len(self.layer3) < NUM_LAYER3_GUARDS: + self.add_new_layer3(generator, excluded) diff --git a/service/termux/run b/service/termux/run new file mode 100755 index 0000000..1ed6523 --- /dev/null +++ b/service/termux/run @@ -0,0 +1,4 @@ +#!/data/data/com.termux/files/usr/bin/sh +mkdir -p "$PREFIX/var/lib/vanguards" +rm -f "$PREFIX/var/lib/vanguards/vanguards.state" +exec vanguards --logfile "$PREFIX/var/log/vanguards.log" --state "$PREFIX/var/lib/vanguards/vanguards.state" diff --git a/service/vanguards.service b/service/vanguards.service new file mode 100644 index 0000000..65da17e --- /dev/null +++ b/service/vanguards.service @@ -0,0 +1,15 @@ +[Unit] +Description=Vanguards Tor guard protection daemon +After=network.target tor.service +Wants=tor.service + +[Service] +Type=simple +ExecStartPre=/bin/rm -f /var/lib/vanguards/vanguards.state +ExecStart=vanguards --logfile /var/log/vanguards.log --state /var/lib/vanguards/vanguards.state +Restart=on-failure +RestartSec=5 +StateDirectory=vanguards + +[Install] +WantedBy=multi-user.target diff --git a/setup.py b/setup.py index 3954706..5a2fbac 100644 --- a/setup.py +++ b/setup.py @@ -55,6 +55,10 @@ def read(*names, **kwargs): "console_scripts": [ 'vanguards = vanguards.main:main', ]}, + data_files=[ + ('lib/systemd/system', ['service/vanguards.service']), + ('var/service/vanguards', ['service/termux/run']), + ], description="Vanguards help guard you from getting vanned...", long_description=DESCRIPTION, include_package_data=True, diff --git a/setup.sh b/setup.sh index 4ae1b69..8c88e1d 100755 --- a/setup.sh +++ b/setup.sh @@ -43,7 +43,13 @@ pip install --require-hashes -r requirements.txt $(basename $SYS_PYTHON) setup.py install -# 4. Inform user what to do +# 4. Create state directory +mkdir -p /var/lib/vanguards 2>/dev/null || true +if [ -n "$PREFIX" ]; then + mkdir -p "$PREFIX/var/lib/vanguards" +fi + +# 5. Inform user what to do echo echo "If we got this far, everything should be ready!" echo diff --git a/src/vanguards.egg-info/PKG-INFO b/src/vanguards.egg-info/PKG-INFO new file mode 100644 index 0000000..b8eda42 --- /dev/null +++ b/src/vanguards.egg-info/PKG-INFO @@ -0,0 +1,31 @@ +Metadata-Version: 2.4 +Name: vanguards +Version: 0.4.1 +Summary: Vanguards help guard you from getting vanned... +Home-page: https://github.com/mikeperry-tor/vanguards +Author: Mike Perry +License: MIT/Expat +Keywords: tor +Classifier: Development Status :: 4 - Beta +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +License-File: LICENSE +Requires-Dist: setuptools +Requires-Dist: ipaddress>=1.0.17; python_version < "3" +Requires-Dist: stem>=1.7.0 +Dynamic: author +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: requires-dist +Dynamic: summary + + +For the full README and other project information, please see the +`Vanguard project page on github `_. diff --git a/src/vanguards.egg-info/SOURCES.txt b/src/vanguards.egg-info/SOURCES.txt new file mode 100644 index 0000000..4553ecb --- /dev/null +++ b/src/vanguards.egg-info/SOURCES.txt @@ -0,0 +1,53 @@ +.travis.yml +CHANGES.rst +LICENSE +MANIFEST.in +README.md +README_SECURITY.md +README_TECHNICAL.md +README_TESTS.md +TODO.txt +requirements.txt +setup.cfg +setup.py +setup.sh +test-requirements.txt +tox.ini +vanguards-example.conf +vanguards.1 +vanguards_parallel.sh +service/vanguards.service +service/termux/run +src/vanguards.py +src/vanguards/NodeSelection.py +src/vanguards/__init__.py +src/vanguards/bandguards.py +src/vanguards/cbtverify.py +src/vanguards/config.py +src/vanguards/control.py +src/vanguards/logger.py +src/vanguards/logguard.py +src/vanguards/main.py +src/vanguards/pathverify.py +src/vanguards/rendguard.py +src/vanguards/vanguards.py +src/vanguards.egg-info/PKG-INFO +src/vanguards.egg-info/SOURCES.txt +src/vanguards.egg-info/dependency_links.txt +src/vanguards.egg-info/entry_points.txt +src/vanguards.egg-info/requires.txt +src/vanguards.egg-info/top_level.txt +tests/2-3-8-perf.conf +tests/2-4-8-perf.conf +tests/__init__.py +tests/cached-microdesc-consensus +tests/conf.mock +tests/default.conf +tests/state.mock +tests/test_bandguards.py +tests/test_cbtverify.py +tests/test_logguard.py +tests/test_main.py +tests/test_pathverify.py +tests/test_rendguard.py +tests/test_vanguards.py \ No newline at end of file diff --git a/src/vanguards.egg-info/dependency_links.txt b/src/vanguards.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/vanguards.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/vanguards.egg-info/entry_points.txt b/src/vanguards.egg-info/entry_points.txt new file mode 100644 index 0000000..c759502 --- /dev/null +++ b/src/vanguards.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +vanguards = vanguards.main:main diff --git a/src/vanguards.egg-info/requires.txt b/src/vanguards.egg-info/requires.txt new file mode 100644 index 0000000..7893c1a --- /dev/null +++ b/src/vanguards.egg-info/requires.txt @@ -0,0 +1,5 @@ +setuptools +stem>=1.7.0 + +[:python_version < "3"] +ipaddress>=1.0.17 diff --git a/src/vanguards.egg-info/top_level.txt b/src/vanguards.egg-info/top_level.txt new file mode 100644 index 0000000..58ff1a5 --- /dev/null +++ b/src/vanguards.egg-info/top_level.txt @@ -0,0 +1 @@ +vanguards diff --git a/src/vanguards/__init__.py b/src/vanguards/__init__.py index 0acd5fc..5a34dd8 100644 --- a/src/vanguards/__init__.py +++ b/src/vanguards/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__version__ = "0.4.0-dev1" +__version__ = "0.4.1" __author__ = "Mike Perry" __license__ = "MIT/Expat" __url__ = "https://github.com/mikeperry-tor/vanguards" diff --git a/src/vanguards/__pycache__/NodeSelection.cpython-313.pyc b/src/vanguards/__pycache__/NodeSelection.cpython-313.pyc new file mode 100644 index 0000000..76c051c Binary files /dev/null and b/src/vanguards/__pycache__/NodeSelection.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/__init__.cpython-313.pyc b/src/vanguards/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..531d457 Binary files /dev/null and b/src/vanguards/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/bandguards.cpython-313.pyc b/src/vanguards/__pycache__/bandguards.cpython-313.pyc new file mode 100644 index 0000000..e1458bc Binary files /dev/null and b/src/vanguards/__pycache__/bandguards.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/cbtverify.cpython-313.pyc b/src/vanguards/__pycache__/cbtverify.cpython-313.pyc new file mode 100644 index 0000000..e8dbc2f Binary files /dev/null and b/src/vanguards/__pycache__/cbtverify.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/config.cpython-313.pyc b/src/vanguards/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..cb20962 Binary files /dev/null and b/src/vanguards/__pycache__/config.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/control.cpython-313.pyc b/src/vanguards/__pycache__/control.cpython-313.pyc new file mode 100644 index 0000000..2ead0ee Binary files /dev/null and b/src/vanguards/__pycache__/control.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/logger.cpython-313.pyc b/src/vanguards/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000..b377dc0 Binary files /dev/null and b/src/vanguards/__pycache__/logger.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/logguard.cpython-313.pyc b/src/vanguards/__pycache__/logguard.cpython-313.pyc new file mode 100644 index 0000000..6041558 Binary files /dev/null and b/src/vanguards/__pycache__/logguard.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/main.cpython-313.pyc b/src/vanguards/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..401cd87 Binary files /dev/null and b/src/vanguards/__pycache__/main.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/pathverify.cpython-313.pyc b/src/vanguards/__pycache__/pathverify.cpython-313.pyc new file mode 100644 index 0000000..36f2972 Binary files /dev/null and b/src/vanguards/__pycache__/pathverify.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/rendguard.cpython-313.pyc b/src/vanguards/__pycache__/rendguard.cpython-313.pyc new file mode 100644 index 0000000..79a5240 Binary files /dev/null and b/src/vanguards/__pycache__/rendguard.cpython-313.pyc differ diff --git a/src/vanguards/__pycache__/vanguards.cpython-313.pyc b/src/vanguards/__pycache__/vanguards.cpython-313.pyc new file mode 100644 index 0000000..8e4d94b Binary files /dev/null and b/src/vanguards/__pycache__/vanguards.cpython-313.pyc differ diff --git a/src/vanguards/config.py b/src/vanguards/config.py index 1c33391..8922269 100644 --- a/src/vanguards/config.py +++ b/src/vanguards/config.py @@ -6,6 +6,7 @@ import os import socket import sys +import tempfile from . import bandguards from . import rendguard @@ -15,10 +16,7 @@ from . import logger from .logger import plog -try: - from configparser import SafeConfigParser, Error -except ImportError: - from ConfigParser import SafeConfigParser, Error +from configparser import ConfigParser, Error ################# Global options ################## @@ -35,10 +33,10 @@ ENABLE_PATHVERIFY=False # State file location -STATE_FILE = "vanguards.state" +STATE_FILE = os.path.join(tempfile.gettempdir(), "vanguards.state") # Config file location -_CONFIG_FILE = "vanguards.conf" +_CONFIG_FILE = os.path.join(tempfile.gettempdir(), "vanguards.conf") # Loglevel LOGLEVEL = "NOTICE" @@ -58,12 +56,31 @@ _RETRY_LIMIT = None +DAEMONIZE = False + +def daemonize(): + pid = os.fork() + if pid > 0: + sys.exit(0) + os.setsid() + pid = os.fork() + if pid > 0: + sys.exit(0) + sys.stdout.flush() + sys.stderr.flush() + with open(os.devnull, "r") as f: + os.dup2(f.fileno(), sys.stdin.fileno()) + with open(os.devnull, "w") as f: + os.dup2(f.fileno(), sys.stdout.fileno()) + os.dup2(f.fileno(), sys.stderr.fileno()) + def setup_options(): global CONTROL_IP, CONTROL_PORT, CONTROL_SOCKET, CONTROL_PASS, STATE_FILE global ENABLE_BANDGUARDS, ENABLE_RENDGUARD, ENABLE_LOGGUARD, ENABLE_CBTVERIFY global ENABLE_PATHVERIFY global LOGLEVEL, LOGFILE global ONE_SHOT_VANGUARDS, ENABLE_VANGUARDS + global DAEMONIZE parser = argparse.ArgumentParser() @@ -138,17 +155,23 @@ def setup_options(): help="Enable path selection monitoring") parser.set_defaults(pathverify_enabled=ENABLE_PATHVERIFY) + parser.add_argument("--daemonize", "-d", dest="daemonize", + action="store_true", + help="Run vanguards as a background daemon") + options = parser.parse_args() (STATE_FILE, CONTROL_IP, CONTROL_PORT, CONTROL_SOCKET, CONTROL_PASS, ENABLE_BANDGUARDS, ENABLE_RENDGUARD, ENABLE_LOGGUARD, ENABLE_CBTVERIFY, - ENABLE_PATHVERIFY, ONE_SHOT_VANGUARDS, ENABLE_VANGUARDS) = \ + ENABLE_PATHVERIFY, ONE_SHOT_VANGUARDS, ENABLE_VANGUARDS, + DAEMONIZE) = \ (options.state_file, options.control_ip, options.control_port, options.control_socket, options.control_pass, options.bandguards_enabled, options.rendguard_enabled, options.logguard_enabled, options.cbtverify_enabled, options.pathverify_enabled, - options.one_shot_vanguards, options.vanguards_enabled) + options.one_shot_vanguards, options.vanguards_enabled, + options.daemonize) if options.loglevel != None: LOGLEVEL = options.loglevel @@ -209,7 +232,7 @@ def set_options_from_module(config, module, section): config.set(section, param, str(val)) def generate_config(): - config = SafeConfigParser(allow_no_value=True) + config = ConfigParser(allow_no_value=True) set_options_from_module(config, sys.modules[__name__], "Global") set_options_from_module(config, vanguards, "Vanguards") set_options_from_module(config, bandguards, "Bandguards") @@ -219,9 +242,9 @@ def generate_config(): return config def apply_config(config_file): - config = SafeConfigParser(allow_no_value=True) + config = ConfigParser(allow_no_value=True) - config.readfp(open(config_file, "r")) + config.read_file(open(config_file, "r")) get_options_for_module(config, sys.modules[__name__], "Global") get_options_for_module(config, vanguards, "Vanguards") diff --git a/src/vanguards/logger.py b/src/vanguards/logger.py index 6eeba05..66dbdbb 100644 --- a/src/vanguards/logger.py +++ b/src/vanguards/logger.py @@ -42,6 +42,8 @@ def logger_init(): # Default behavior = log to stdout if TorUtil.logfile is None, # or to the open file specified otherwise. logger = logging.getLogger("TorCtl") + for handler in logger.handlers[:]: + logger.removeHandler(handler) if logfile == ":syslog:": if os.path.exists("/dev/log"): diff --git a/src/vanguards/main.py b/src/vanguards/main.py index ddbbf6e..f2297db 100644 --- a/src/vanguards/main.py +++ b/src/vanguards/main.py @@ -1,5 +1,7 @@ import functools +import os import stem +import tempfile import time import sys @@ -16,6 +18,7 @@ from . import config from .logger import plog +from . import logger _MIN_TOR_VERSION_FOR_BW = stem.version.Version("0.3.4.10") @@ -44,6 +47,13 @@ def run_main(): sys.exit(1) options = config.setup_options() + if config.DAEMONIZE: + if config.LOGFILE == "": + logpath = os.path.join(tempfile.gettempdir(), "vanguards.log") + config.LOGFILE = logpath + logger.set_logfile(logpath) + config.daemonize() + try: # TODO: Use tor's data directory.. or our own state = vanguards.VanguardState.read_from_file(config.STATE_FILE)