introducer: add sequence-numbers to announcements, ignore replays
authorBrian Warner <warner@lothar.com>
Mon, 11 Jun 2012 02:10:22 +0000 (19:10 -0700)
committerBrian Warner <warner@lothar.com>
Mon, 11 Jun 2012 02:10:22 +0000 (19:10 -0700)
This will support revocation of Accounting recommendation records,
assuming the gossip-based broadcast channel isn't easily jammed.

src/allmydata/introducer/client.py
src/allmydata/introducer/server.py
src/allmydata/test/test_introducer.py

index 260e13cf1558a125bbbb56d2d4092f24fa6f846f..33ce5ad86b3c71fcb409ce85bb20e71afec38f6a 100644 (file)
@@ -201,8 +201,9 @@ class IntroducerClient(service.Service, Referenceable):
         d.addCallback(_publish_stub_client)
         return d
 
-    def create_announcement(self, service_name, ann, signing_key):
+    def create_announcement(self, service_name, ann, signing_key, _mod=None):
         full_ann = { "version": 0,
+                     "seqnum": time.time(),
                      "nickname": self._nickname,
                      "app-versions": self._app_versions,
                      "my-version": self._my_version,
@@ -211,6 +212,8 @@ class IntroducerClient(service.Service, Referenceable):
                      "service-name": service_name,
                      }
         full_ann.update(ann)
+        if _mod:
+            full_ann = _mod(full_ann) # for unit tests
         return sign_to_foolscap(full_ann, signing_key)
 
     def publish(self, service_name, ann, signing_key=None):
@@ -303,8 +306,27 @@ class IntroducerClient(service.Service, Referenceable):
                      parent=lp2, level=log.UNUSUAL, umid="B1MIdA")
             self._debug_counts["duplicate_announcement"] += 1
             return
+
         # does it update an existing one?
         if index in self._current_announcements:
+            old,_,_ = self._current_announcements[index]
+            if "seqnum" in old:
+                # must beat previous sequence number to replace
+                if "seqnum" not in ann:
+                    self.log("not replacing old announcement, no seqnum: %s"
+                             % (ann,),
+                             parent=lp2, level=log.NOISY, umid="zFGH3Q")
+                    return
+                if ann["seqnum"] <= old["seqnum"]:
+                    # note that exact replays are caught earlier, by
+                    # comparing the entire signed announcement.
+                    self.log("not replacing old announcement, "
+                             "new seqnum is too old (%s <= %s) "
+                             "(replay attack?): %s"
+                             % (ann["seqnum"], old["seqnum"], ann),
+                             parent=lp2, level=log.UNUSUAL, umid="JAAAoQ")
+                    return
+                # ok, seqnum is newer, allow replacement
             self._debug_counts["update"] += 1
             self.log("replacing old announcement: %s" % (ann,),
                      parent=lp2, level=log.NOISY, umid="wxwgIQ")
index 394fdf00e0add6d6a1b660495a49cdcd487d5188..1a4b76800e237aad688bd38dae5a5a4c9aeec334 100644 (file)
@@ -120,6 +120,8 @@ class IntroducerService(service.MultiService, Referenceable):
 
         self._debug_counts = {"inbound_message": 0,
                               "inbound_duplicate": 0,
+                              "inbound_no_seqnum": 0,
+                              "inbound_old_replay": 0,
                               "inbound_update": 0,
                               "outbound_message": 0,
                               "outbound_announcements": 0,
@@ -216,6 +218,21 @@ class IntroducerService(service.MultiService, Referenceable):
                 self._debug_counts["inbound_duplicate"] += 1
                 return
             else:
+                if "seqnum" in old_ann:
+                    # must beat previous sequence number to replace
+                    if "seqnum" not in ann:
+                        self.log("not replacing old ann, no seqnum",
+                                 level=log.NOISY, umid="ySbaVw")
+                        self._debug_counts["inbound_no_seqnum"] += 1
+                        return
+                    if ann["seqnum"] <= old_ann["seqnum"]:
+                        self.log("not replacing old ann, new seqnum is too old"
+                                 " (%s <= %s) (replay attack?)"
+                                 % (ann["seqnum"], old_ann["seqnum"]),
+                                 level=log.UNUSUAL, umid="sX7yqQ")
+                        self._debug_counts["inbound_old_replay"] += 1
+                        return
+                    # ok, seqnum is newer, allow replacement
                 self.log("old announcement being updated", level=log.NOISY,
                          umid="304r9g")
                 self._debug_counts["inbound_update"] += 1
index 6624a15bf4c16ad5bd783aee8fa46e7ef51d44c9..e8eca5028443f1251a7ffd72239869ba149ec6aa 100644 (file)
@@ -117,8 +117,13 @@ def make_ann(furl):
             "permutation-seed-base32": get_tubid_string(furl) }
     return ann
 
-def make_ann_t(ic, furl, privkey):
-    return ic.create_announcement("storage", make_ann(furl), privkey)
+def make_ann_t(ic, furl, privkey, seqnum):
+    def mod(ann):
+        ann["seqnum"] = seqnum
+        if seqnum is None:
+            del ann["seqnum"]
+        return ann
+    return ic.create_announcement("storage", make_ann(furl), privkey, mod)
 
 class Client(unittest.TestCase):
     def test_duplicate_receive_v1(self):
@@ -196,10 +201,12 @@ class Client(unittest.TestCase):
         # ann1b: ic2, furl1
         # ann2: ic2, furl2
 
-        self.ann1 = make_ann_t(ic1, furl1, privkey)
-        self.ann1a = make_ann_t(ic1, furl1a, privkey)
-        self.ann1b = make_ann_t(ic2, furl1, privkey)
-        self.ann2 = make_ann_t(ic2, furl2, privkey)
+        self.ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10)
+        self.ann1old = make_ann_t(ic1, furl1, privkey, seqnum=9)
+        self.ann1noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None)
+        self.ann1b = make_ann_t(ic2, furl1, privkey, seqnum=11)
+        self.ann1a = make_ann_t(ic1, furl1a, privkey, seqnum=12)
+        self.ann2 = make_ann_t(ic2, furl2, privkey, seqnum=13)
 
         ic1.remote_announce_v2([self.ann1]) # queues eventual-send
         d = fireEventually()
@@ -219,6 +226,20 @@ class Client(unittest.TestCase):
             self.failUnlessEqual(len(announcements), 1)
         d.addCallback(_then2)
 
+        # an older announcement shouldn't fire the subscriber either
+        d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1old]))
+        d.addCallback(fireEventually)
+        def _then2a(ign):
+            self.failUnlessEqual(len(announcements), 1)
+        d.addCallback(_then2a)
+
+        # announcement with no seqnum cannot replace one with-seqnum
+        d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1noseqnum]))
+        d.addCallback(fireEventually)
+        def _then2b(ign):
+            self.failUnlessEqual(len(announcements), 1)
+        d.addCallback(_then2b)
+
         # and a replacement announcement: same FURL, new other stuff. The
         # subscriber *should* be fired.
         d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1b]))
@@ -298,6 +319,73 @@ class Client(unittest.TestCase):
         d.addCallback(_then2)
         return d
 
+class Server(unittest.TestCase):
+    def test_duplicate(self):
+        i = IntroducerService()
+        ic1 = IntroducerClient(None,
+                               "introducer.furl", u"my_nickname",
+                               "ver23", "oldest_version", {})
+        furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp"
+
+        privkey_s, _ = keyutil.make_keypair()
+        privkey, _ = keyutil.parse_privkey(privkey_s)
+
+        ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10)
+        ann1_old = make_ann_t(ic1, furl1, privkey, seqnum=9)
+        ann1_new = make_ann_t(ic1, furl1, privkey, seqnum=11)
+        ann1_noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None)
+
+        i.remote_publish_v2(ann1, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 10)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
+
+        i.remote_publish_v2(ann1, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 10)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 2)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
+
+        i.remote_publish_v2(ann1_old, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 10)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 3)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
+
+        i.remote_publish_v2(ann1_new, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 11)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 4)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 1)
+
+        i.remote_publish_v2(ann1_noseqnum, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 11)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 5)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 1)
+
+
 NICKNAME = u"n\u00EDickname-%s" # LATIN SMALL LETTER I WITH ACUTE
 
 class SystemTestMixin(ServiceMixin, pollmixin.PollMixin):
@@ -736,7 +824,7 @@ class ClientInfo(unittest.TestCase):
         client_v2 = IntroducerClient(tub, introducer_furl, NICKNAME % u"v2",
                                      "my_version", "oldest", app_versions)
         #furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
-        #ann_s = make_ann_t(client_v2, furl1, None)
+        #ann_s = make_ann_t(client_v2, furl1, None, 10)
         #introducer.remote_publish_v2(ann_s, Referenceable())
         subscriber = FakeRemoteReference()
         introducer.remote_subscribe_v2(subscriber, "storage",
@@ -798,7 +886,7 @@ class Announcements(unittest.TestCase):
                                      "my_version", "oldest", app_versions)
         furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
         tubid = "62ubehyunnyhzs7r6vdonnm2hpi52w6y"
-        ann_s0 = make_ann_t(client_v2, furl1, None)
+        ann_s0 = make_ann_t(client_v2, furl1, None, 10.0)
         canary0 = Referenceable()
         introducer.remote_publish_v2(ann_s0, canary0)
         a = introducer.get_announcements()
@@ -821,7 +909,7 @@ class Announcements(unittest.TestCase):
         sk_s, vk_s = keyutil.make_keypair()
         sk, _ignored = keyutil.parse_privkey(sk_s)
         pks = keyutil.remove_prefix(vk_s, "pub-")
-        ann_t0 = make_ann_t(client_v2, furl1, sk)
+        ann_t0 = make_ann_t(client_v2, furl1, sk, 10.0)
         canary0 = Referenceable()
         introducer.remote_publish_v2(ann_t0, canary0)
         a = introducer.get_announcements()