Esempio n. 1
0
  def __init__(self, config):
    self.config = config
    self.socket = s.socket(s.AF_INET, s.SOCK_STREAM)
    if platform.system != "Windows":
      # This ensures we don't need to wait for a timeout every time this
      # process exits and then starts and tries to bind to this port again
      self.socket.setsockopt(s.SOL_SOCKET, s.SO_REUSEADDR, 1L)
      self.socket.bind(("",self.config.port))
    self.socket.listen(5)
    self.threads = []
    task = util.ThreadLauncher(self.pinger, self.handle_control_c, respawn=True)
    task.start()

    self.peer_lock = threading.RLock()
    self.peer_ips = {}

    self.mm = Matchmaker(self)

    self.one_forgery = False # for debugging
    self.special_forgery_debugging = False
    if self.special_forgery_debugging and not Reconciliator.hash_archival:
      raise ValueError, "cannot do special debugging without special archival"
Esempio n. 2
0
class SwitzerlandMasterServer:

  def __init__(self, config):
    self.config = config
    self.socket = s.socket(s.AF_INET, s.SOCK_STREAM)
    if platform.system != "Windows":
      # This ensures we don't need to wait for a timeout every time this
      # process exits and then starts and tries to bind to this port again
      self.socket.setsockopt(s.SOL_SOCKET, s.SO_REUSEADDR, 1L)
      self.socket.bind(("",self.config.port))
    self.socket.listen(5)
    self.threads = []
    task = util.ThreadLauncher(self.pinger, self.handle_control_c, respawn=True)
    task.start()

    self.peer_lock = threading.RLock()
    self.peer_ips = {}

    self.mm = Matchmaker(self)

    self.one_forgery = False # for debugging
    self.special_forgery_debugging = False
    if self.special_forgery_debugging and not Reconciliator.hash_archival:
      raise ValueError, "cannot do special debugging without special archival"
        

  def accept_connections(self):
    self.debug_note("Server listening for connections on port " +`self.config.port`+"...")
    while True:
      try:
        incoming, peer_addr = self.socket.accept()
      except KeyboardInterrupt:
        self.handle_control_c()
      # it's important to have new_members calculations performed in
      # sensible order; SwitzerlandLink.initial_members and 
      self.peer_lock.acquire()
      try:
        self.new_link(incoming, peer_addr)
      finally:
        self.peer_lock.release()

  def handle_control_c(self):
    "Urgent shutdown logic"
    errlog.info("Server Exiting...")
    try:
      self.socket.shutdown(s.SHUT_RDWR)
    except:
      errlog.info("(exception on shutdown)")
    try:
      try:
        self.peer_lock.acquire()
        for ip in self.peer_ips.values():
          for link in ip.values():
            try:
              link.close()
            except:
              errlog.error("problem closing %s", `link`)
      except:
        errlog.error("problem iterating over links")
        raise
    finally:
      self.peer_lock.release()
    self.socket.close()
    sys.exit(0)


  def debug_note(self, string, seriousness=0, link=None):
    if seriousness < self.config.seriousness_threshold:
      return
    if link:
      link.debug_note(string)
    else:
      errlog.debug("SwitzMas: " +string)

  def new_link(self, incoming, peer_addr):
    """
    Instantiate a SwitzerlandLink for a new connection and do the relevant
    housekeeping.
    """
    link = SwitzerlandLink(incoming, peer_addr, parent=self,
                           seriousness=self.config.seriousness_threshold)
    link.setDaemon(True)
    self.peer_lock.acquire()
    try:
      peer_ip,peer_port = peer_addr
        
      errlog.info("got a connection from %s %s", peer_ip, peer_port)
      if not self.is_duplicate_alice(link, peer_ip):
        self.peer_ips[peer_ip] = {}     # will become a dict of port numbers
      
      self.peer_ips[peer_ip][peer_port] = link
      self.debug_note("peers: %s" % `self.peer_ips`)
      self.threads.append(link)
    finally:
      self.peer_lock.release()
    
    self.debug_note("Initialising server thread")
    link.start()

  def is_duplicate_alice(self, link, peer_ip):
    """
    Return True if this client is a dupe; dupes may be admissable -- this
    function handles that too but it doesn't affect the return type (yuck)
    """
    # self.peer_lock has already been acquired in new_link()
    if peer_ip in self.peer_ips:
      # Oh dear, a second Alice from this IP!
      others = self.peer_ips[peer_ip]
      self.debug_note("Hmmm, we already have  "+ `len(others)`+ \
                      " connections from" + `peer_ip`)
      for p in others:
        try:
          if not p.alice_firewalled:
            link.bailout("We already have a non-firewalled client from your IP, %s!" % peer_ip)
            return True
        except AttributeError:
          # p.firewalled hasn't been determined yet
          link.bailout("We already have a connection from your IP.  Multiple connections from a single IP are currently disallowed!")
          return True
      #self.debug_note("But they are all firewalled, so we'll let this in")
      # no?
      return True
    else:
      return False

  def joining_circle(self, link):
    "this peer is joining us; tell everyone else"
    new_ip = link.peer[0]
    new_ip_packed = s.inet_aton(new_ip)
    firewalled = link.alice_firewalled
    self.peer_lock.acquire()
    try:
      assert new_ip in self.peer_ips
      self.debug_note("Joining circle, notifying %d others" % 
                                             (len(self.peer_ips) -1) )
      for ip,links in self.peer_ips.items():
        if ip == new_ip:
          continue
        for port,l in links.items():
          if l.ready.isSet():
            l.send_other_message("new-members", [[(new_ip_packed, firewalled)]])

      link.welcomed.set()      # the link threads can now farewell us :)
    finally:
      self.peer_lock.release()

  def link_closed(self, link):
    "Called from within each SwitzerlandLink's listener thread, upon closure."

    # XXX This might be insufficient unless we also find and remove all of the
    # references to this link that have been travelling around Matchmaker.py
    # and Reconciliator.py

    ip, port = link.peer
    self.peer_lock.acquire()
    self.debug_note("Closing link with client "+`ip` +" "+ `port`, link)
    # 1. Remove this link from our peer structures
    try:
      try:
        del self.peer_ips[ip][port]
        if len(self.peer_ips[ip]) == 0:
          self.send_farewells(ip)
          del self.peer_ips[ip]
      except KeyError:
        errlog.error("Error while closing link:\n%s", traceback.format_exc())
    finally:
      self.peer_lock.release()

    # 2. Remove all of its active flows
    link.flow_lock.acquire()
    try:
      for entry in link.flow_table.values():
        if entry != None:
          f_tuple, rec = entry
          self.mm.remove_flow_from_matchmaker(rec)
    finally:
      link.flow_lock.release()
      # XXX we don't want new flows being added after this, but failing
      # to release the lock would seem to be a very poor way of 
      # guaranteeing that

    # 3. Remove the thread
    if not self.config.keep_threads:
      self.threads.remove(link)


  def send_farewells(self, leaving_ip):
    """
    Tell peers that a link has gone away.
    Assumes peer_lock acquired already
    """
    for ip,links in self.peer_ips.items():
      if ip == leaving_ip:
        continue
      for port,link in links.items():
        link.send_other_message("farewell", [leaving_ip])

  def handle_active_flows(self, link, args):
    "The active_flows message from Alice updates our state of flows."
    new_flows, deleted_flows = args

    # Process deleted flows first, because we might want to delete a flow
    # and recreate it simultaneously if it has been closed and re-SYNed

    try:
      if not self.config.keep_reconciliators:
        self.debug_note("deleting flows: %s" % deleted_flows)
        for f_id in deleted_flows:
          self.mm.delete_flow(link, f_id)
    except:
      link.protocol_error("Problem with flow list: %s\n" % 
                                              util.screensafe(new_flows))
      raise

    # Now the new flows:
    try:
      for flow in new_flows:
        f_id = flow[0]
        assert len(flow[1]) == Protocol.hash_length, \
               "hashlen is not %d in %r" % (Protocol.hash_length, flow)
        if ipids_in_matchmaker:
          opening_hash = flow[1]
        else:
          opening_hash = flow[1][:-2]
        f_tuple = flow[2]
        assert len(f_tuple) == 5
        assert type(opening_hash) == str

        match = self.ponder_flow(link, f_id, f_tuple, opening_hash)
        if match:
          self.debug_note("YES", seriousness=-1)
          self.mm.add_flow(link, f_id, f_tuple, match)
        else:
          self.debug_note("NO", seriousness=-1)
          if not self.config.sloppy:
            self.debug_note("Mysteriously Irrelevant Flow!!!%s" %
                            `(link.peer[0],print_flow_tuple(f_tuple))`)

    except:
      errlog.debug("OH NOES %s", traceback.format_exc())
      link.protocol_error("Problem with flow list: %s\n" % 
                                            util.screensafe(new_flows))
      raise

  def judgement_day(self):
    self.mm.judgement_day()


  def print_global_flow_table(self):
    "(An obsolete name.)"
    return self.mm.prettyprint_flows()


  def faking_ip(self, link, to_ip):
    "Our link is faking for testing purposes..."
    self.peer_lock.acquire()
    try:
      from_ip, port = link.peer
      try:
        s.inet_aton(to_ip)
      except s.error:
        link.protocol_error("Invalid fake IP %s\n" % to_ip)
        
      link.debug_note("peer %s:%d is faking ip %s:%d"
                      %(from_ip,port,to_ip,port))
      del self.peer_ips[from_ip][port]
      if self.peer_ips[from_ip] == {}:
        del self.peer_ips[from_ip]
      if to_ip not in self.peer_ips:
        self.peer_ips[to_ip] = {}
      self.peer_ips[to_ip][port] = link
      self.debug_note("peers: %s" % `self.peer_ips`)
    finally:
      self.peer_lock.release()


  def ponder_flow(self, link, alice_id, f_tuple, opening_hash):
    """
    Sanity check before we allow a flow into our data structures.
    If yes, return a representation of the flow for our matchmaker.
    If no, return False.
    """

    # XXX decide whether to add promiscuity here
    self.peer_lock.acquire()
    try:
      self.debug_note("IS THIS RELEVANT to %s? %s %s" % (link.peer[0], 
      `print_flow_tuple(f_tuple)`, `self.peer_ips.keys()`), seriousness=-1)

      # link_ip is the ip we expect to find for this client inside its flows
      # match_ip is the ip we want to use for matchmaking

      match_ip = s.inet_aton(link.peer[0])
      if link.alice_firewalled:
        self.debug_note("..firewalled..", seriousness=-1)
        link_ip = link.peers_private_ip
      else:
        self.debug_note("..not firewalled..%s" % \
          `link.alice_firewalled`, seriousness=-1)
        link_ip  = link.peer[0]
      assert s.inet_aton(link_ip)

      self.debug_note("link and match: %s %s" % 
                      (link_ip,`match_ip`), seriousness=-1)

      ip1bin = f_tuple[FlowTuple.src_ip]
      ip1 = s.inet_ntoa(ip1bin)
      ip2bin = f_tuple[FlowTuple.dest_ip]
      ip2 = s.inet_ntoa(ip2bin)

      if ip1 == link_ip and ip2 in self.peer_ips:
        return (match_ip, ip2bin, opening_hash)
      if ip2 == link_ip and ip1 in self.peer_ips:
        return (ip1bin, match_ip, opening_hash)

    finally:
      self.peer_lock.release()
    link.flow_lock.acquire()
    try:
      link.flow_table[alice_id] = None
    finally:
      link.flow_lock.release()
    return False

  def peers_of(self, ip):
    """
    Return the data we need for new-members messages.  That is,
    (peer, firewalled) for all other peers who are in-circle for this ip.
    peer is a packed binary ip.
    """
    self.peer_lock.acquire()
    try:
      results = []
      for peer_ip, ports in self.peer_ips.items():
        if peer_ip == ip:
          continue

        # we want to add a record of whether each peer is firewalled or not
        # this turns out to be messy

        # some links won't be ready yet
        firewalled_answers = [link.alice_firewalled \
                              for link in ports.values() \
                              if "alice_firewalled" in link.__dict__]

        if len(firewalled_answers) == 0:
          continue
        first_answer = firewalled_answers[0]
        for answer in firewalled_answers[1:]:
          assert answer == first_answer, "Inconsistent firewalling for " + \
                           `peer_ip` + ", " + `firewalled_answers`

        results.append((s.inet_aton(peer_ip), first_answer))
          
      return results
    finally:
      self.peer_lock.release()

  def handle_sent_or_recd(self, link, args, sent):
    """
    Sent and recd messages are conceptually very similar, so this function
    handles both cases.  sent = True|False accordingly.
    """
    flow_id, timestamp, hashes = args
    link.flow_lock.acquire()
    try:
      entry = link.flow_table[flow_id]
    finally:
      link.flow_lock.release()

    if entry:
      rec = entry[1]
    else:
      # This flow is being ignored because it isn't between our peers
      return False
    rec.lock.acquire()
    try:
      try:
        if sent:
          forgeries = rec.sent_by_alice(timestamp, hashes)
          drops = rec.check_for_drops()
          if drops:
            self.debug_note("%d dropped packets!" % len(drops))
        else:
          forgeries = rec.recd_by_bob(timestamp, hashes)
      except Reconciliator.Dangling:
        opening_hash = rec.m_tuple[2]
        link.send_message("dangling-flow", [flow_id, opening_hash])
        log.warn("Flow %s is dangling" % `print_flow_tuple(rec.flow)`)
        link.flow_lock.acquire()
        try:
          link.flow_table[flow_id] = None
        finally:
          link.flow_lock.release()
        return
      except AssertionError:
        # Various things could cause this...
        link.protocol_error("Assertion violated while handling sent/recd\n" +\
                            traceback.format_exc())
        raise
    finally:
      rec.lock.release()
    if forgeries:
      self.spotted_forgeries(forgeries, rec)
    if sent: s = "+"
    else: s = "-"
    if self.config.seriousness_threshold <=0:
      sys.stdout.write(s)
      sys.stdout.flush()

  def spotted_forgeries(self, forgeries, rec):
    # First, we need to find the link object from the other side
    rec.lock.acquire()
    try:
      # XXX XXX right now, only respond to the first forgeries event per flow
      # (to avoid runaway processing when every packet is being modified)
      # later perhaps the Correct Response is to use random sampling of
      # forgeries in the flow
      if not rec.respond_to_forgeries:
        return
      else:
        # This could become a counter that is set to a random number instead
        # of being permanently disabled
        rec.respond_to_forgeries = False

      sl = len(rec.src_links)
      if sl != 1:
        self.debug_note("forgeries with other than one src %d" % sl, 2)
      dl = len(rec.dest_links)
      if dl != 1:
        self.debug_note("forgeries with other than one dest %d" % dl, 2)

      # for debugging: only deal with the first forgery
      if self.special_forgery_debugging:
        if self.one_forgery:
          return
        else:
          self.one_forgery = True
          forgeries = forgeries[:1]
          timestamp, hash = forgeries[0]
          ipids = Reconciliator.bob_ipids[hash]
          #assert len(ipids) == 1, `ipids` + "is not of length 1!"
          for ipid in ipids:
            self.debug_note("We have a forgery.  Debugging IPID %s" % ipid)
            rec.src_links[0][0].send_other_message("debug-ipid", [ipid])
            return
        
      self.debug_note("Observed %d modified or forged packets" %
                      len(forgeries), seriousness=5)

      forgeries = self.select_some_forgeries(forgeries)

      for link, id in rec.dest_links:
        remember = (forgeries, rec)
        link.send_other_message("forged-in", [id, forgeries], \
                                data_for_reply=remember)

      # Now we wait for a response from Bob before talking to Alice
    finally:
      rec.lock.release()

  def select_some_forgeries(self, forgeries):
      """ 
      If we get a large number of forgeries, we can cause all sorts of
      overloading and misery.  So we only repond to max_forgery_set of them
      at once.
      
      The subset is selected pseudorandomly, so that inane activity (such as
      seen from NATs) won't completely obscure something nastier, like
      forged RSTs, just by happening before it.  
      """
      # XXX confirm that we don't need more genuine randomness here.  An
      # attack based on, especially since most Switzerland servers will be
      # continuously running this code for other Alice/Bob pairs that are
      # being affected by funy NATs

      if len(forgeries) > max_forgery_set:
        self.debug_note("(selecting %d of those)" % max_forgery_set)
        subset = []
        for i in xrange(max_forgery_set):
          pos = random.randrange(len(forgeries))
          subset.append(forgeries[pos])
          del forgeries[pos]
        return subset
      else:
        return forgeries

  def pinger(self):
    """ 
    Run this in a thread to ensure that we periodically talk to all of the
    clients.  If any of them are lost to us, that should be enough to raise an
    exception that leads to cleanup.  
    """
    while True:
      time.sleep(random.randrange(3,6))
      for thread in self.threads:
        if thread.time_since_contact() > self.config.client_contact_period:
          thread.send_other_message("ping", missing_ack_callback=thread.bye)