diff --git a/controller/EmbeddedNetworkController.cpp b/controller/EmbeddedNetworkController.cpp
index 53b345b49..5ba8cf983 100644
--- a/controller/EmbeddedNetworkController.cpp
+++ b/controller/EmbeddedNetworkController.cpp
@@ -140,6 +140,12 @@ static json _renderRule(ZT_VirtualNetworkRule &rule)
 			r["flags"] = (unsigned int)rule.v.fwd.flags;
 			r["length"] = (unsigned int)rule.v.fwd.length;
 			break;
+		case ZT_NETWORK_RULE_ACTION_WATCH:
+			r["type"] = "ACTION_WATCH";
+			r["address"] = Address(rule.v.fwd.address).toString();
+			r["flags"] = (unsigned int)rule.v.fwd.flags;
+			r["length"] = (unsigned int)rule.v.fwd.length;
+			break;
 		case ZT_NETWORK_RULE_ACTION_REDIRECT:
 			r["type"] = "ACTION_REDIRECT";
 			r["address"] = Address(rule.v.fwd.address).toString();
@@ -303,6 +309,12 @@ static bool _parseRule(json &r,ZT_VirtualNetworkRule &rule)
 		rule.v.fwd.flags = (uint32_t)(_jI(r["flags"],0ULL) & 0xffffffffULL);
 		rule.v.fwd.length = (uint16_t)(_jI(r["length"],0ULL) & 0xffffULL);
 		return true;
+	} else if (t == "ACTION_WATCH") {
+		rule.t |= ZT_NETWORK_RULE_ACTION_WATCH;
+		rule.v.fwd.address = Utils::hexStrToU64(_jS(r["address"],"0").c_str()) & 0xffffffffffULL;
+		rule.v.fwd.flags = (uint32_t)(_jI(r["flags"],0ULL) & 0xffffffffULL);
+		rule.v.fwd.length = (uint16_t)(_jI(r["length"],0ULL) & 0xffffULL);
+		return true;
 	} else if (t == "ACTION_REDIRECT") {
 		rule.t |= ZT_NETWORK_RULE_ACTION_REDIRECT;
 		rule.v.fwd.address = Utils::hexStrToU64(_jS(r["zt"],"0").c_str()) & 0xffffffffffULL;
diff --git a/include/ZeroTierOne.h b/include/ZeroTierOne.h
index e0f6ca286..e43c8541b 100644
--- a/include/ZeroTierOne.h
+++ b/include/ZeroTierOne.h
@@ -516,15 +516,20 @@ enum ZT_VirtualNetworkRuleType
 	 */
 	ZT_NETWORK_RULE_ACTION_TEE = 2,
 
+	/**
+	 * Exactly like TEE but frames are dropped if previous TEEs were not acknowledged by the observer
+	 */
+	ZT_NETWORK_RULE_ACTION_WATCH = 3,
+
 	/**
 	 * Drop and redirect this frame to another node (by ZT address)
 	 */
-	ZT_NETWORK_RULE_ACTION_REDIRECT = 3,
+	ZT_NETWORK_RULE_ACTION_REDIRECT = 4,
 
 	/**
 	 * Log if match and if rule debugging is enabled in the build, otherwise does nothing (for developers)
 	 */
-	ZT_NETWORK_RULE_ACTION_DEBUG_LOG = 4,
+	ZT_NETWORK_RULE_ACTION_DEBUG_LOG = 5,
 
 	/**
 	 * Maximum ID for an ACTION, anything higher is a MATCH
diff --git a/node/Capability.hpp b/node/Capability.hpp
index e23d79435..2cf54b5c2 100644
--- a/node/Capability.hpp
+++ b/node/Capability.hpp
@@ -174,6 +174,7 @@ public:
 					b.append((uint8_t)0);
 					break;
 				case ZT_NETWORK_RULE_ACTION_TEE:
+				case ZT_NETWORK_RULE_ACTION_WATCH:
 				case ZT_NETWORK_RULE_ACTION_REDIRECT:
 					b.append((uint8_t)14);
 					b.append((uint64_t)rules[i].v.fwd.address);
@@ -270,6 +271,7 @@ public:
 				default:
 					break;
 				case ZT_NETWORK_RULE_ACTION_TEE:
+				case ZT_NETWORK_RULE_ACTION_WATCH:
 				case ZT_NETWORK_RULE_ACTION_REDIRECT:
 					rules[ruleCount].v.fwd.address = b.template at<uint64_t>(p);
 					rules[ruleCount].v.fwd.flags = b.template at<uint32_t>(p + 8);
diff --git a/node/IncomingPacket.cpp b/node/IncomingPacket.cpp
index b39257739..12766fe25 100644
--- a/node/IncomingPacket.cpp
+++ b/node/IncomingPacket.cpp
@@ -39,6 +39,7 @@
 #include "CertificateOfMembership.hpp"
 #include "Capability.hpp"
 #include "Tag.hpp"
+#include "Revocation.hpp"
 
 namespace ZeroTier {
 
@@ -162,13 +163,8 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,const SharedPtr<Peer>
 				// Peers can send this in response to frames if they do not have a recent enough COM from us
 				SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD)));
 				const uint64_t now = RR->node->now();
-				if ( (network) && (network->config().com) && (peer->rateGateComRequest(now)) ) {
-					Packet outp(peer->address(),RR->identity.address(),Packet::VERB_NETWORK_CREDENTIALS);
-					network->config().com.serialize(outp);
-					outp.append((uint8_t)0);
-					outp.armor(peer->key(),true);
-					_path->send(RR,outp.data(),outp.size(),now);
-				}
+				if ( (network) && (network->config().com) && (peer->rateGateComRequest(now)) )
+					network->pushCredentialsNow(peer->address(),now);
 			}	break;
 
 			case Packet::ERROR_NETWORK_ACCESS_DENIED_: {
@@ -681,9 +677,17 @@ bool IncomingPacket::_doEXT_FRAME(const RuntimeEnvironment *RR,const SharedPtr<P
 						RR->node->putFrame(nwid,network->userPtr(),from,to,etherType,0,(const void *)frameData,frameLen);
 						break;
 				}
-
-				peer->received(_path,hops(),packetId(),Packet::VERB_EXT_FRAME,0,Packet::VERB_NOP,true);
 			}
+
+			if ((flags & 0x10) != 0) {
+				Packet outp(peer->address(),RR->identity.address(),Packet::VERB_OK);
+				outp.append((uint8_t)Packet::VERB_EXT_FRAME);
+				outp.append((uint64_t)packetId());
+				outp.armor(peer->key(),true);
+				_path->send(RR,outp.data(),outp.size(),RR->node->now());
+			}
+
+			peer->received(_path,hops(),packetId(),Packet::VERB_EXT_FRAME,0,Packet::VERB_NOP,true);
 		} else {
 			TRACE("dropped EXT_FRAME from %s(%s): we are not connected to network %.16llx",source().toString().c_str(),_path->address().toString().c_str(),at<uint64_t>(ZT_PROTO_VERB_FRAME_IDX_NETWORK_ID));
 			peer->received(_path,hops(),packetId(),Packet::VERB_EXT_FRAME,0,Packet::VERB_NOP,false);
@@ -775,6 +779,7 @@ bool IncomingPacket::_doNETWORK_CREDENTIALS(const RuntimeEnvironment *RR,const S
 		CertificateOfMembership com;
 		Capability cap;
 		Tag tag;
+		Revocation revocation;
 		bool trustEstablished = false;
 
 		unsigned int p = ZT_PACKET_IDX_PAYLOAD;
@@ -784,8 +789,14 @@ bool IncomingPacket::_doNETWORK_CREDENTIALS(const RuntimeEnvironment *RR,const S
 				SharedPtr<Network> network(RR->node->network(com.networkId()));
 				if (network) {
 					switch (network->addCredential(com)) {
-						case 0: trustEstablished = true; break;
-						case 1: return false; // wait for WHOIS
+						case Membership::ADD_REJECTED:
+							break;
+						case Membership::ADD_ACCEPTED_NEW:
+						case Membership::ADD_ACCEPTED_REDUNDANT:
+							trustEstablished = true;
+							break;
+						case Membership::ADD_DEFERRED_FOR_WHOIS:
+							return false;
 					}
 				} else RR->mc->addCredential(com,false);
 			}
@@ -799,8 +810,14 @@ bool IncomingPacket::_doNETWORK_CREDENTIALS(const RuntimeEnvironment *RR,const S
 				SharedPtr<Network> network(RR->node->network(cap.networkId()));
 				if (network) {
 					switch (network->addCredential(cap)) {
-						case 0: trustEstablished = true; break;
-						case 1: return false; // wait for WHOIS
+						case Membership::ADD_REJECTED:
+							break;
+						case Membership::ADD_ACCEPTED_NEW:
+						case Membership::ADD_ACCEPTED_REDUNDANT:
+							trustEstablished = true;
+							break;
+						case Membership::ADD_DEFERRED_FOR_WHOIS:
+							return false;
 					}
 				}
 			}
@@ -811,11 +828,25 @@ bool IncomingPacket::_doNETWORK_CREDENTIALS(const RuntimeEnvironment *RR,const S
 				SharedPtr<Network> network(RR->node->network(tag.networkId()));
 				if (network) {
 					switch (network->addCredential(tag)) {
-						case 0: trustEstablished = true; break;
-						case 1: return false; // wait for WHOIS
+						case Membership::ADD_REJECTED:
+							break;
+						case Membership::ADD_ACCEPTED_NEW:
+						case Membership::ADD_ACCEPTED_REDUNDANT:
+							trustEstablished = true;
+							break;
+						case Membership::ADD_DEFERRED_FOR_WHOIS:
+							return false;
 					}
 				}
 			}
+
+			const unsigned int numRevocations = at<uint16_t>(p); p += 2;
+			for(unsigned int i=0;i<numRevocations;++i) {
+				p += revocation.deserialize(*this,p);
+				SharedPtr<Network> network(RR->node->network(revocation.networkId()));
+				if (network) {
+				}
+			}
 		}
 
 		peer->received(_path,hops(),packetId(),Packet::VERB_NETWORK_CREDENTIALS,0,Packet::VERB_NOP,trustEstablished);
@@ -932,24 +963,7 @@ bool IncomingPacket::_doNETWORK_CONFIG_REFRESH(const RuntimeEnvironment *RR,cons
 		const uint64_t nwid = at<uint64_t>(ZT_PACKET_IDX_PAYLOAD);
 		bool trustEstablished = false;
 
-		if (Network::controllerFor(nwid) == peer->address()) {
-			SharedPtr<Network> network(RR->node->network(nwid));
-			if (network) {
-				network->requestConfiguration();
-				trustEstablished = true;
-			} else {
-				TRACE("dropped NETWORK_CONFIG_REFRESH from %s(%s): not a member of %.16llx",source().toString().c_str(),_path->address().toString().c_str(),nwid);
-				peer->received(_path,hops(),packetId(),Packet::VERB_NETWORK_CONFIG_REFRESH,0,Packet::VERB_NOP,false);
-				return true;
-			}
 
-			const unsigned int blacklistCount = at<uint16_t>(ZT_PACKET_IDX_PAYLOAD + 8);
-			unsigned int ptr = ZT_PACKET_IDX_PAYLOAD + 10;
-			for(unsigned int i=0;i<blacklistCount;++i) {
-				network->blacklistBefore(Address(field(ptr,ZT_ADDRESS_LENGTH),ZT_ADDRESS_LENGTH),at<uint64_t>(ptr + 5));
-				ptr += 13;
-			}
-		}
 
 		peer->received(_path,hops(),packetId(),Packet::VERB_NETWORK_CONFIG_REFRESH,0,Packet::VERB_NOP,trustEstablished);
 	} catch ( ... ) {
diff --git a/node/Membership.cpp b/node/Membership.cpp
index 8c2ba673a..d579d3037 100644
--- a/node/Membership.cpp
+++ b/node/Membership.cpp
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <algorithm>
+
 #include "Membership.hpp"
 #include "RuntimeEnvironment.hpp"
 #include "Peer.hpp"
@@ -28,28 +30,43 @@
 
 namespace ZeroTier {
 
-void Membership::sendCredentialsIfNeeded(const RuntimeEnvironment *RR,const uint64_t now,const Address &peerAddress,const NetworkConfig &nconf,const Capability *cap)
+Membership::Membership() :
+	_lastUpdatedMulticast(0),
+	_lastPushAttempt(0),
+	_lastPushedCom(0),
+	_comRevocationThreshold(0)
 {
-	if ((now - _lastPushAttempt) < 2000ULL)
+	for(unsigned int i=0;i<ZT_MAX_NETWORK_TAGS;++i) _remoteTags[i] = &(_tagMem[i]);
+	for(unsigned int i=0;i<ZT_MAX_NETWORK_CAPABILITIES;++i) _remoteCaps[i] = &(_capMem[i]);
+}
+
+void Membership::pushCredentials(const RuntimeEnvironment *RR,const uint64_t now,const Address &peerAddress,const NetworkConfig &nconf,int localCapabilityIndex,const bool force)
+{
+	// This limits how often we go through this logic, which prevents us from
+	// doing all this for every single packet or other event.
+	if ( ((now - _lastPushAttempt) < 1000ULL) && (!force) )
 		return;
 	_lastPushAttempt = now;
 
 	try {
-		bool unfinished;
+		unsigned int localTagPtr = 0;
+		bool needCom = ( (nconf.com) && ( ((now - _lastPushedCom) >= ZT_CREDENTIAL_PUSH_EVERY) || (force) ) );
 		do {
-			unfinished = false;
 			Buffer<ZT_PROTO_MAX_PACKET_LENGTH> capsAndTags;
 
 			unsigned int appendedCaps = 0;
-			if (cap) {
+			if (localCapabilityIndex >= 0) {
 				capsAndTags.addSize(2);
-				std::map<uint32_t,CState>::iterator cs(_caps.find(cap->id()));
-				if ((cs != _caps.end())&&((now - cs->second.lastPushed) >= ZT_CREDENTIAL_PUSH_EVERY)) {
-					cap->serialize(capsAndTags);
-					cs->second.lastPushed = now;
+
+				if ( (_localCaps[localCapabilityIndex].id != nconf.capabilities[localCapabilityIndex].id()) || ((now - _localCaps[localCapabilityIndex].lastPushed) >= ZT_CREDENTIAL_PUSH_EVERY) || (force) ) {
+					_localCaps[localCapabilityIndex].lastPushed = now;
+					_localCaps[localCapabilityIndex].id = nconf.capabilities[localCapabilityIndex].id();
+					nconf.capabilities[localCapabilityIndex].serialize(capsAndTags);
 					++appendedCaps;
 				}
+
 				capsAndTags.setAt<uint16_t>(0,(uint16_t)appendedCaps);
+				localCapabilityIndex = -1; // don't send this cap again on subsequent loops if force is true
 			} else {
 				capsAndTags.append((uint16_t)0);
 			}
@@ -57,22 +74,17 @@ void Membership::sendCredentialsIfNeeded(const RuntimeEnvironment *RR,const uint
 			unsigned int appendedTags = 0;
 			const unsigned int tagCountPos = capsAndTags.size();
 			capsAndTags.addSize(2);
-			for(unsigned int i=0;i<nconf.tagCount;++i) {
-				TState *const ts = _tags.get(nconf.tags[i].id());
-				if ((now - ts->lastPushed) >= ZT_CREDENTIAL_PUSH_EVERY) {
-					if ((capsAndTags.size() + sizeof(Tag)) >= (ZT_PROTO_MAX_PACKET_LENGTH - sizeof(CertificateOfMembership))) {
-						unfinished = true;
+			for(;localTagPtr<nconf.tagCount;++localTagPtr) {
+				if ( (_localTags[localTagPtr].id != nconf.tags[localTagPtr].id()) || ((now - _localTags[localTagPtr].lastPushed) >= ZT_CREDENTIAL_PUSH_EVERY) || (force) ) {
+					if ((capsAndTags.size() + sizeof(Tag)) >= (ZT_PROTO_MAX_PACKET_LENGTH - sizeof(CertificateOfMembership)))
 						break;
-					}
-					nconf.tags[i].serialize(capsAndTags);
-					ts->lastPushed = now;
+					nconf.tags[localTagPtr].serialize(capsAndTags);
 					++appendedTags;
 				}
 			}
 			capsAndTags.setAt<uint16_t>(tagCountPos,(uint16_t)appendedTags);
 
-			const bool needCom = ((nconf.com)&&((now - _lastPushedCom) >= ZT_CREDENTIAL_PUSH_EVERY));
-			if ( (needCom) || (appendedCaps) || (appendedTags) ) {
+			if (needCom||appendedCaps||appendedTags) {
 				Packet outp(peerAddress,RR->identity.address(),Packet::VERB_NETWORK_CREDENTIALS);
 				if (needCom) {
 					nconf.com.serialize(outp);
@@ -80,110 +92,148 @@ void Membership::sendCredentialsIfNeeded(const RuntimeEnvironment *RR,const uint
 				}
 				outp.append((uint8_t)0x00);
 				outp.append(capsAndTags.data(),capsAndTags.size());
+				outp.append((uint16_t)0); // no revocations, these propagate differently
 				outp.compress();
 				RR->sw->send(outp,true);
+				needCom = false; // don't send COM again on subsequent loops if force is true
 			}
-		} while (unfinished); // if there are many tags, etc., we can send more than one
+		} while (localTagPtr < nconf.tagCount);
 	} catch ( ... ) {
 		TRACE("unable to send credentials due to unexpected exception");
 	}
 }
 
-int Membership::addCredential(const RuntimeEnvironment *RR,const CertificateOfMembership &com)
+const Capability *Membership::getCapability(const NetworkConfig &nconf,const uint32_t id) const
 {
-	if (_com == com) {
-		TRACE("addCredential(CertificateOfMembership) for %s on %.16llx ACCEPTED (redundant)",com.issuedTo().toString().c_str(),com.networkId());
-		return 0;
+	const _RemoteCapability *const *c = std::lower_bound(&(_remoteCaps[0]),&(_remoteCaps[ZT_MAX_NETWORK_CAPABILITIES]),(uint64_t)id,_RemoteCredentialSorter<_RemoteCapability>());
+	return ( ((c != &(_remoteCaps[ZT_MAX_NETWORK_CAPABILITIES]))&&((*c)->id == (uint64_t)id)) ? ((((*c)->lastReceived)&&(_isCredentialTimestampValid(nconf,(*c)->cap,**c))) ? &((*c)->cap) : (const Capability *)0) : (const Capability *)0);
+}
+
+const Tag *Membership::getTag(const NetworkConfig &nconf,const uint32_t id) const
+{
+	const _RemoteTag *const *t = std::lower_bound(&(_remoteTags[0]),&(_remoteTags[ZT_MAX_NETWORK_TAGS]),(uint64_t)id,_RemoteCredentialSorter<_RemoteTag>());
+	return ( ((t != &(_remoteTags[ZT_MAX_NETWORK_CAPABILITIES]))&&((*t)->id == (uint64_t)id)) ? ((((*t)->lastReceived)&&(_isCredentialTimestampValid(nconf,(*t)->tag,**t))) ? &((*t)->tag) : (const Tag *)0) : (const Tag *)0);
+}
+
+Membership::AddCredentialResult Membership::addCredential(const RuntimeEnvironment *RR,const NetworkConfig &nconf,const CertificateOfMembership &com)
+{
+	const uint64_t newts = com.timestamp().first;
+	if (newts <= _comRevocationThreshold) {
+		TRACE("addCredential(CertificateOfMembership) for %s on %.16llx REJECTED (revoked)",com.issuedTo().toString().c_str(),com.networkId());
+		return ADD_REJECTED;
 	}
 
-	const int vr = com.verify(RR);
+	const uint64_t oldts = _com.timestamp().first;
+	if (newts < oldts) {
+		TRACE("addCredential(CertificateOfMembership) for %s on %.16llx REJECTED (older than current)",com.issuedTo().toString().c_str(),com.networkId());
+		return ADD_REJECTED;
+	}
+	if ((newts == oldts)&&(_com == com)) {
+		TRACE("addCredential(CertificateOfMembership) for %s on %.16llx ACCEPTED (redundant)",com.issuedTo().toString().c_str(),com.networkId());
+		return ADD_ACCEPTED_REDUNDANT;
+	}
 
-	if (vr == 0) {
-		if (com.timestamp().first >= _com.timestamp().first) {
+	switch(com.verify(RR)) {
+		default:
+			TRACE("addCredential(CertificateOfMembership) for %s on %.16llx REJECTED (invalid signature or object)",com.issuedTo().toString().c_str(),com.networkId());
+			return ADD_REJECTED;
+		case 0:
 			TRACE("addCredential(CertificateOfMembership) for %s on %.16llx ACCEPTED (new)",com.issuedTo().toString().c_str(),com.networkId());
 			_com = com;
-		} else {
-			TRACE("addCredential(CertificateOfMembership) for %s on %.16llx ACCEPTED but not used (OK but older than current)",com.issuedTo().toString().c_str(),com.networkId());
-		}
-	} else {
-		TRACE("addCredential(CertificateOfMembership) for %s on %.16llx REJECTED (%d)",com.issuedTo().toString().c_str(),com.networkId(),vr);
+			return ADD_ACCEPTED_NEW;
+		case 1:
+			return ADD_DEFERRED_FOR_WHOIS;
 	}
-
-	return vr;
 }
 
-int Membership::addCredential(const RuntimeEnvironment *RR,const Tag &tag)
+Membership::AddCredentialResult Membership::addCredential(const RuntimeEnvironment *RR,const NetworkConfig &nconf,const Tag &tag)
 {
-	TState *t = _tags.get(tag.id());
-	if ((t)&&(t->lastReceived != 0)&&(t->tag == tag)) {
-		TRACE("addCredential(Tag) for %s on %.16llx ACCEPTED (redundant)",tag.issuedTo().toString().c_str(),tag.networkId());
-		return 0;
+	_RemoteTag *const *htmp = std::lower_bound(&(_remoteTags[0]),&(_remoteTags[ZT_MAX_NETWORK_TAGS]),(uint64_t)tag.id(),_RemoteCredentialSorter<_RemoteTag>());
+	_RemoteTag *have = ((htmp != &(_remoteTags[ZT_MAX_NETWORK_TAGS]))&&((*htmp)->id == (uint64_t)tag.id())) ? *htmp : (_RemoteTag *)0;
+	if (have) {
+		if ( (!_isCredentialTimestampValid(nconf,tag,*have)) || (have->tag.timestamp() > tag.timestamp()) ) {
+			TRACE("addCredential(Tag) for %s on %.16llx REJECTED (revoked or too old)",tag.issuedTo().toString().c_str(),tag.networkId());
+			return ADD_REJECTED;
+		}
+		if (have->tag == tag) {
+			TRACE("addCredential(Tag) for %s on %.16llx ACCEPTED (redundant)",tag.issuedTo().toString().c_str(),tag.networkId());
+			return ADD_ACCEPTED_REDUNDANT;
+		}
 	}
-	const int vr = tag.verify(RR);
-	if (vr == 0) {
-		TRACE("addCredential(Tag) for %s on %.16llx ACCEPTED (new)",tag.issuedTo().toString().c_str(),tag.networkId());
-		if (!t) {
-			while (_tags.size() >= ZT_MAX_NETWORK_TAGS) {
-				uint32_t oldest = 0;
-				uint64_t oldestLastReceived = 0xffffffffffffffffULL;
-				uint32_t *i = (uint32_t *)0;
-				TState *ts = (TState *)0;
-				Hashtable<uint32_t,TState>::Iterator tsi(_tags);
-				while (tsi.next(i,ts)) {
-					if (ts->lastReceived < oldestLastReceived) {
-						oldestLastReceived = ts->lastReceived;
-						oldest = *i;
+
+	switch(tag.verify(RR)) {
+		default:
+			TRACE("addCredential(Tag) for %s on %.16llx REJECTED (invalid)",tag.issuedTo().toString().c_str(),tag.networkId());
+			return ADD_REJECTED;
+		case 0:
+			TRACE("addCredential(Tag) for %s on %.16llx ACCEPTED (new)",tag.issuedTo().toString().c_str(),tag.networkId());
+			if (have) {
+				have->lastReceived = RR->node->now();
+				have->tag = tag;
+			} else {
+				uint64_t minlr = 0xffffffffffffffffULL;
+				for(unsigned int i=0;i<ZT_MAX_NETWORK_TAGS;++i) {
+					if (_remoteTags[i]->id == 0xffffffffffffffffULL) {
+						have = _remoteTags[i];
+						break;
+					} else if (_remoteTags[i]->lastReceived <= minlr) {
+						have = _remoteTags[i];
+						minlr = _remoteTags[i]->lastReceived;
 					}
 				}
-				if (oldestLastReceived != 0xffffffffffffffffULL)
-					_tags.erase(oldest);
+				have->lastReceived = RR->node->now();
+				have->tag = tag;
+				std::sort(&(_remoteTags[0]),&(_remoteTags[ZT_MAX_NETWORK_TAGS]),_RemoteCredentialSorter<_RemoteTag>());
 			}
-			t = &(_tags[tag.id()]);
-		}
-		if (t->tag.timestamp() <= tag.timestamp()) {
-			t->lastReceived = RR->node->now();
-			t->tag = tag;
-		}
-	} else {
-		TRACE("addCredential(Tag) for %s on %.16llx REJECTED (%d)",tag.issuedTo().toString().c_str(),tag.networkId(),vr);
+			return ADD_ACCEPTED_NEW;
+		case 1:
+			return ADD_DEFERRED_FOR_WHOIS;
 	}
-	return vr;
 }
 
-int Membership::addCredential(const RuntimeEnvironment *RR,const Capability &cap)
+Membership::AddCredentialResult Membership::addCredential(const RuntimeEnvironment *RR,const NetworkConfig &nconf,const Capability &cap)
 {
-	std::map<uint32_t,CState>::iterator c(_caps.find(cap.id()));
-	if ((c != _caps.end())&&(c->second.lastReceived != 0)&&(c->second.cap == cap)) {
-		TRACE("addCredential(Capability) for %s on %.16llx ACCEPTED (redundant)",cap.issuedTo().toString().c_str(),cap.networkId());
-		return 0;
+	_RemoteCapability *const *htmp = std::lower_bound(&(_remoteCaps[0]),&(_remoteCaps[ZT_MAX_NETWORK_CAPABILITIES]),(uint64_t)cap.id(),_RemoteCredentialSorter<_RemoteCapability>());
+	_RemoteCapability *have = ((htmp != &(_remoteCaps[ZT_MAX_NETWORK_CAPABILITIES]))&&((*htmp)->id == (uint64_t)cap.id())) ? *htmp : (_RemoteCapability *)0;
+	if (have) {
+		if ( (!_isCredentialTimestampValid(nconf,cap,*have)) || (have->cap.timestamp() > cap.timestamp()) ) {
+			TRACE("addCredential(Tag) for %s on %.16llx REJECTED (revoked or too old)",tag.issuedTo().toString().c_str(),tag.networkId());
+			return ADD_REJECTED;
+		}
+		if (have->cap == cap) {
+			TRACE("addCredential(Tag) for %s on %.16llx ACCEPTED (redundant)",tag.issuedTo().toString().c_str(),tag.networkId());
+			return ADD_ACCEPTED_REDUNDANT;
+		}
 	}
-	const int vr = cap.verify(RR);
-	if (vr == 0) {
-		TRACE("addCredential(Capability) for %s on %.16llx ACCEPTED (new)",cap.issuedTo().toString().c_str(),cap.networkId());
-		if (c == _caps.end()) {
-			while (_caps.size() >= ZT_MAX_NETWORK_CAPABILITIES) {
-				std::map<uint32_t,CState>::iterator oldest;
-				uint64_t oldestLastReceived = 0xffffffffffffffffULL;
-				for(std::map<uint32_t,CState>::iterator i(_caps.begin());i!=_caps.end();++i) {
-					if (i->second.lastReceived < oldestLastReceived) {
-						oldestLastReceived = i->second.lastReceived;
-						oldest = i;
+
+	switch(cap.verify(RR)) {
+		default:
+			TRACE("addCredential(Tag) for %s on %.16llx REJECTED (invalid)",tag.issuedTo().toString().c_str(),tag.networkId());
+			return ADD_REJECTED;
+		case 0:
+			TRACE("addCredential(Tag) for %s on %.16llx ACCEPTED (new)",tag.issuedTo().toString().c_str(),tag.networkId());
+			if (have) {
+				have->lastReceived = RR->node->now();
+				have->cap = cap;
+			} else {
+				uint64_t minlr = 0xffffffffffffffffULL;
+				for(unsigned int i=0;i<ZT_MAX_NETWORK_CAPABILITIES;++i) {
+					if (_remoteCaps[i]->id == 0xffffffffffffffffULL) {
+						have = _remoteCaps[i];
+						break;
+					} else if (_remoteCaps[i]->lastReceived <= minlr) {
+						have = _remoteCaps[i];
+						minlr = _remoteCaps[i]->lastReceived;
 					}
 				}
-				if (oldestLastReceived != 0xffffffffffffffffULL)
-					_caps.erase(oldest);
+				have->lastReceived = RR->node->now();
+				have->cap = cap;
+				std::sort(&(_remoteCaps[0]),&(_remoteCaps[ZT_MAX_NETWORK_CAPABILITIES]),_RemoteCredentialSorter<_RemoteCapability>());
 			}
-			CState &c2 = _caps[cap.id()];
-			c2.lastReceived = RR->node->now();
-			c2.cap = cap;
-		} else if (c->second.cap.timestamp() <= cap.timestamp()) {
-			c->second.lastReceived = RR->node->now();
-			c->second.cap = cap;
-		}
-	} else {
-		TRACE("addCredential(Capability) for %s on %.16llx REJECTED (%d)",cap.issuedTo().toString().c_str(),cap.networkId(),vr);
+			return ADD_ACCEPTED_NEW;
+		case 1:
+			return ADD_DEFERRED_FOR_WHOIS;
 	}
-	return vr;
 }
 
 } // namespace ZeroTier
diff --git a/node/Membership.hpp b/node/Membership.hpp
index 5eb68d344..421e3ee8d 100644
--- a/node/Membership.hpp
+++ b/node/Membership.hpp
@@ -21,14 +21,12 @@
 
 #include <stdint.h>
 
-#include <map>
-
 #include "Constants.hpp"
 #include "../include/ZeroTierOne.h"
 #include "CertificateOfMembership.hpp"
 #include "Capability.hpp"
 #include "Tag.hpp"
-#include "Hashtable.hpp"
+#include "Revocation.hpp"
 #include "NetworkConfig.hpp"
 
 namespace ZeroTier {
@@ -40,77 +38,135 @@ class Network;
  * A container for certificates of membership and other network credentials
  *
  * This is kind of analogous to a join table between Peer and Network. It is
- * presently held by the Network object for each participating Peer.
+ * held by the Network object for each participating Peer.
  *
- * This is not thread safe. It must be locked externally.
+ * This class is not thread safe. It must be locked externally.
  */
 class Membership
 {
 private:
 	// Tags and related state
-	struct TState
+	struct _RemoteTag
 	{
-		TState() : lastPushed(0),lastReceived(0) {}
-		// Last time we pushed OUR tag to this peer (with this ID)
-		uint64_t lastPushed;
+		_RemoteTag() : id(0xffffffffffffffffULL),lastReceived(0),revocationThreshold(0) {}
+		// Tag ID (last 32 bits, first 32 bits are set in unused entries to sort them to end)
+		uint64_t id;
 		// Last time we received THEIR tag (with this ID)
 		uint64_t lastReceived;
+		// Revocation blacklist threshold or 0 if none
+		uint64_t revocationThreshold;
 		// THEIR tag
 		Tag tag;
 	};
 
 	// Credentials and related state
-	struct CState
+	struct _RemoteCapability
 	{
-		CState() : lastPushed(0),lastReceived(0) {}
-		// Last time we pushed OUR capability to this peer (with this ID)
-		uint64_t lastPushed;
+		_RemoteCapability() : id(0xffffffffffffffffULL),lastReceived(0),revocationThreshold(0) {}
+		// Capability ID (last 32 bits, first 32 bits are set in unused entries to sort them to end)
+		uint64_t id;
 		// Last time we received THEIR capability (with this ID)
 		uint64_t lastReceived;
+		// Revocation blacklist threshold or 0 if none
+		uint64_t revocationThreshold;
 		// THEIR capability
 		Capability cap;
 	};
 
+	// Comparison operator for remote credential entries
+	template<typename T>
+	struct _RemoteCredentialSorter
+	{
+		inline bool operator()(const T *a,const T *b) const { return (a->id < b->id); }
+		inline bool operator()(const uint64_t a,const T *b) const { return (a < b->id); }
+		inline bool operator()(const T *a,const uint64_t b) const { return (a->id < b); }
+		inline bool operator()(const uint64_t a,const uint64_t b) const { return (a < b); }
+	};
+
+	// Used to track push state for network config tags[] and capabilities[] entries
+	struct _LocalCredentialPushState
+	{
+		_LocalCredentialPushState() : lastPushed(0),id(0) {}
+		uint64_t lastPushed;
+		uint32_t id;
+	};
+
 public:
+	enum AddCredentialResult
+	{
+		ADD_REJECTED,
+		ADD_ACCEPTED_NEW,
+		ADD_ACCEPTED_REDUNDANT,
+		ADD_DEFERRED_FOR_WHOIS
+	};
+
 	/**
-	 * A wrapper to iterate through member capabilities in ascending order of capability ID and return only valid ones
+	 * Iterator to scan forward through capabilities in ascending order of ID
 	 */
 	class CapabilityIterator
 	{
 	public:
-		CapabilityIterator(const Membership &m) :
-			_m(m),
-			_i(m._caps.begin()),
-			_e(m._caps.end())
-		{
-		}
+		CapabilityIterator(const Membership &m,const NetworkConfig &nconf) :
+			_m(&m),
+			_c(&nconf),
+			_i(&(m._remoteCaps[0])) {}
 
-		inline const Capability *next(const NetworkConfig &nconf)
+		inline const Capability *next()
 		{
-			while (_i != _e) {
-				if ((_i->second.lastReceived)&&(_m.isCredentialTimestampValid(nconf,_i->second.cap)))
-					return &((_i++)->second.cap);
-				else ++_i;
+			for(;;) {
+				if ((_i != &(_m->_remoteCaps[ZT_MAX_NETWORK_CAPABILITIES]))&&((*_i)->id != 0xffffffffffffffffULL)) {
+					const Capability *tmp = &((*_i)->cap);
+					if (_m->_isCredentialTimestampValid(*_c,*tmp,**_i)) {
+						++_i;
+						return tmp;
+					} else ++_i;
+				} else {
+					return (const Capability *)0;
+				}
 			}
-			return (const Capability *)0;
 		}
 
 	private:
-		const Membership &_m;
-		std::map<uint32_t,CState>::const_iterator _i,_e;
+		const Membership *_m;
+		const NetworkConfig *_c;
+		const _RemoteCapability *const *_i;
 	};
 	friend class CapabilityIterator;
 
-	Membership() :
-		_lastUpdatedMulticast(0),
-		_lastPushAttempt(0),
-		_lastPushedCom(0),
-		_blacklistBefore(0),
-		_com(),
-		_caps(),
-		_tags(8)
+	/**
+	 * Iterator to scan forward through tags in ascending order of ID
+	 */
+	class TagIterator
 	{
-	}
+	public:
+		TagIterator(const Membership &m,const NetworkConfig &nconf) :
+			_m(&m),
+			_c(&nconf),
+			_i(&(m._remoteTags[0])) {}
+
+		inline const Tag *next()
+		{
+			for(;;) {
+				if ((_i != &(_m->_remoteTags[ZT_MAX_NETWORK_TAGS]))&&((*_i)->id != 0xffffffffffffffffULL)) {
+					const Tag *tmp = &((*_i)->tag);
+					if (_m->_isCredentialTimestampValid(*_c,*tmp,**_i)) {
+						++_i;
+						return tmp;
+					} else ++_i;
+				} else {
+					return (const Tag *)0;
+				}
+			}
+		}
+
+	private:
+		const Membership *_m;
+		const NetworkConfig *_c;
+		const _RemoteTag *const *_i;
+	};
+	friend class TagIterator;
+
+	Membership();
 
 	/**
 	 * Send COM and other credentials to this peer if needed
@@ -122,9 +178,10 @@ public:
 	 * @param now Current time
 	 * @param peerAddress Address of member peer (the one that this Membership describes)
 	 * @param nconf My network config
-	 * @param cap Capability to send or 0 if none
+	 * @param localCapabilityIndex Index of local capability to include (in nconf.capabilities[]) or -1 if none
+	 * @param force If true, send objects regardless of last push time
 	 */
-	void sendCredentialsIfNeeded(const RuntimeEnvironment *RR,const uint64_t now,const Address &peerAddress,const NetworkConfig &nconf,const Capability *cap);
+	void pushCredentials(const RuntimeEnvironment *RR,const uint64_t now,const Address &peerAddress,const NetworkConfig &nconf,int localCapabilityIndex,const bool force);
 
 	/**
 	 * Check whether we should push MULTICAST_LIKEs to this peer
@@ -142,6 +199,8 @@ public:
 	inline void likingMulticasts(const uint64_t now) { _lastUpdatedMulticast = now; }
 
 	/**
+	 * Check whether the peer represented by this Membership should be allowed on this network at all
+	 *
 	 * @param nconf Our network config
 	 * @return True if this peer is allowed on this network at all
 	 */
@@ -149,126 +208,48 @@ public:
 	{
 		if (nconf.isPublic())
 			return true;
-		if ((_blacklistBefore)&&(_com.timestamp().first <= _blacklistBefore))
+		if ((_comRevocationThreshold)&&(_com.timestamp().first <= _comRevocationThreshold))
 			return false;
 		return nconf.com.agreesWith(_com);
 	}
 
-	/**
-	 * Check whether a capability or tag is within its max delta from the timestamp of our network config and newer than any blacklist cutoff time
-	 *
-	 * @param cred Credential to check -- must have timestamp() accessor method
-	 * @return True if credential is NOT expired
-	 */
-	template<typename C>
-	inline bool isCredentialTimestampValid(const NetworkConfig &nconf,const C &cred) const
-	{
-		const uint64_t ts = cred.timestamp();
-		const uint64_t delta = (ts >= nconf.timestamp) ? (ts - nconf.timestamp) : (nconf.timestamp - ts);
-		return ((delta <= nconf.credentialTimeMaxDelta)&&(ts > _blacklistBefore));
-	}
-
-	/**
-	 * @param nconf Network configuration
-	 * @param id Tag ID
-	 * @return Pointer to tag or NULL if not found
-	 */
-	inline const Tag *getTag(const NetworkConfig &nconf,const uint32_t id) const
-	{
-		const TState *t = _tags.get(id);
-		return ((t) ? (((t->lastReceived != 0)&&(isCredentialTimestampValid(nconf,t->tag))) ? &(t->tag) : (const Tag *)0) : (const Tag *)0);
-	}
-
-	/**
-	 * @param nconf Network configuration
-	 * @param ids Array to store IDs into
-	 * @param values Array to store values into
-	 * @param maxTags Capacity of ids[] and values[]
-	 * @return Number of tags added to arrays
-	 */
-	inline unsigned int getAllTags(const NetworkConfig &nconf,uint32_t *ids,uint32_t *values,unsigned int maxTags) const
-	{
-		unsigned int n = 0;
-		uint32_t *id = (uint32_t *)0;
-		TState *ts = (TState *)0;
-		Hashtable<uint32_t,TState>::Iterator i(const_cast<Membership *>(this)->_tags);
-		while (i.next(id,ts)) {
-			if ((ts->lastReceived)&&(isCredentialTimestampValid(nconf,ts->tag))) {
-				if (n >= maxTags)
-					return n;
-				ids[n] = *id;
-				values[n] = ts->tag.value();
-			}
-		}
-		return n;
-	}
-
 	/**
 	 * @param nconf Network configuration
 	 * @param id Capablity ID
 	 * @return Pointer to capability or NULL if not found
 	 */
-	inline const Capability *getCapability(const NetworkConfig &nconf,const uint32_t id) const
-	{
-		std::map<uint32_t,CState>::const_iterator c(_caps.find(id));
-		return ((c != _caps.end()) ? (((c->second.lastReceived != 0)&&(isCredentialTimestampValid(nconf,c->second.cap))) ? &(c->second.cap) : (const Capability *)0) : (const Capability *)0);
-	}
+	const Capability *getCapability(const NetworkConfig &nconf,const uint32_t id) const;
+
+	/**
+	 * @param nconf Network configuration
+	 * @param id Tag ID
+	 * @return Pointer to tag or NULL if not found
+	 */
+	const Tag *getTag(const NetworkConfig &nconf,const uint32_t id) const;
 
 	/**
 	 * Validate and add a credential if signature is okay and it's otherwise good
-	 *
-	 * @param RR Runtime environment
-	 * @param com Certificate of membership
-	 * @return 0 == OK, 1 == waiting for WHOIS, -1 == BAD signature or credential
 	 */
-	int addCredential(const RuntimeEnvironment *RR,const CertificateOfMembership &com);
+	AddCredentialResult addCredential(const RuntimeEnvironment *RR,const NetworkConfig &nconf,const CertificateOfMembership &com);
 
 	/**
 	 * Validate and add a credential if signature is okay and it's otherwise good
-	 *
-	 * @return 0 == OK, 1 == waiting for WHOIS, -1 == BAD signature or credential
 	 */
-	int addCredential(const RuntimeEnvironment *RR,const Tag &tag);
+	AddCredentialResult addCredential(const RuntimeEnvironment *RR,const NetworkConfig &nconf,const Tag &tag);
 
 	/**
 	 * Validate and add a credential if signature is okay and it's otherwise good
-	 *
-	 * @return 0 == OK, 1 == waiting for WHOIS, -1 == BAD signature or credential
 	 */
-	int addCredential(const RuntimeEnvironment *RR,const Capability &cap);
-
-	/**
-	 * Blacklist COM, tags, and capabilities before this time
-	 *
-	 * @param ts Blacklist cutoff
-	 */
-	inline void blacklistBefore(const uint64_t ts) { _blacklistBefore = ts; }
-
-	/**
-	 * Clean up old or stale entries
-	 *
-	 * @param nconf Network config
-	 */
-	inline void clean(const NetworkConfig &nconf)
-	{
-		for(std::map<uint32_t,CState>::iterator i(_caps.begin());i!=_caps.end();) {
-			if (!isCredentialTimestampValid(nconf,i->second.cap)) {
-				_caps.erase(i++);
-			} else {
-				++i;
-			}
-		}
-
-		uint32_t *i = (uint32_t *)0;
-		TState *ts = (TState *)0;
-		Hashtable<uint32_t,TState>::Iterator tsi(_tags);
-		while (tsi.next(i,ts)) {
-			if (!isCredentialTimestampValid(nconf,ts->tag))
-				_tags.erase(*i);
-		}
-	}
+	AddCredentialResult addCredential(const RuntimeEnvironment *RR,const NetworkConfig &nconf,const Capability &cap);
 
 private:
+	template<typename C,typename CS>
+	inline bool _isCredentialTimestampValid(const NetworkConfig &nconf,const C &cred,const CS &state) const
+	{
+		const uint64_t ts = cred.timestamp();
+		return ( (((ts >= nconf.timestamp) ? (ts - nconf.timestamp) : (nconf.timestamp - ts)) <= nconf.credentialTimeMaxDelta) && (ts > state.revocationThreshold) );
+	}
+
 	// Last time we pushed MULTICAST_LIKE(s)
 	uint64_t _lastUpdatedMulticast;
 
@@ -278,17 +259,23 @@ private:
 	// Last time we pushed our COM to this peer
 	uint64_t _lastPushedCom;
 
-	// Time before which to blacklist credentials from this peer
-	uint64_t _blacklistBefore;
+	// Revocation threshold for COM or 0 if none
+	uint64_t _comRevocationThreshold;
 
-	// COM from this peer
+	// Remote member's latest network COM
 	CertificateOfMembership _com;
 
-	// Capability-related state (we need an ordered container here, hence std::map)
-	std::map<uint32_t,CState> _caps;
+	// Sorted (in ascending order of ID) arrays of pointers to remote tags and capabilities
+	_RemoteTag *_remoteTags[ZT_MAX_NETWORK_TAGS];
+	_RemoteCapability *_remoteCaps[ZT_MAX_NETWORK_CAPABILITIES];
 
-	// Tag-related state
-	Hashtable<uint32_t,TState> _tags;
+	// This is the RAM allocated for remote tags and capabilities from which the sorted arrays are populated
+	_RemoteTag _tagMem[ZT_MAX_NETWORK_TAGS];
+	_RemoteCapability _capMem[ZT_MAX_NETWORK_CAPABILITIES];
+
+	// Local credential push state tracking
+	_LocalCredentialPushState _localTags[ZT_MAX_NETWORK_TAGS];
+	_LocalCredentialPushState _localCaps[ZT_MAX_NETWORK_CAPABILITIES];
 };
 
 } // namespace ZeroTier
diff --git a/node/Network.cpp b/node/Network.cpp
index 455e185eb..0fab6a276 100644
--- a/node/Network.cpp
+++ b/node/Network.cpp
@@ -36,7 +36,7 @@
 #include "Peer.hpp"
 
 // Uncomment to make the rules engine dump trace info to stdout
-//#define ZT_RULES_ENGINE_DEBUGGING 1
+#define ZT_RULES_ENGINE_DEBUGGING 1
 
 namespace ZeroTier {
 
@@ -155,24 +155,21 @@ enum _doZtFilterResult
 static _doZtFilterResult _doZtFilter(
 	const RuntimeEnvironment *RR,
 	const NetworkConfig &nconf,
+	const Membership *membership, // can be NULL
 	const bool inbound,
 	const Address &ztSource,
-	Address &ztDest, // MUTABLE
+	Address &ztDest, // MUTABLE -- is changed on REDIRECT actions
 	const MAC &macSource,
 	const MAC &macDest,
 	const uint8_t *const frameData,
 	const unsigned int frameLen,
 	const unsigned int etherType,
 	const unsigned int vlanId,
-	const ZT_VirtualNetworkRule *rules,
+	const ZT_VirtualNetworkRule *rules, // cannot be NULL
 	const unsigned int ruleCount,
-	const Tag *localTags,
-	const unsigned int localTagCount,
-	const uint32_t *const remoteTagIds,
-	const uint32_t *const remoteTagValues,
-	const unsigned int remoteTagCount,
-	Address &cc, // MUTABLE
-	unsigned int &ccLength) // MUTABLE
+	Address &cc, // MUTABLE -- set to TEE destination if TEE action is taken or left alone otherwise
+	unsigned int &ccLength, // MUTABLE -- set to length of packet payload to TEE
+	bool &ccWatch) // MUTABLE -- set to true for WATCH target as opposed to normal TEE
 {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
 	char dpbuf[1024]; // used by FILTER_TRACE macro
@@ -204,6 +201,7 @@ static _doZtFilterResult _doZtFilter(
 
 					// These are initially handled together since preliminary logic is common
 					case ZT_NETWORK_RULE_ACTION_TEE:
+					case ZT_NETWORK_RULE_ACTION_WATCH:
 					case ZT_NETWORK_RULE_ACTION_REDIRECT:	{
 						const Address fwdAddr(rules[rn].v.fwd.address);
 						if (fwdAddr == ztSource) {
@@ -242,6 +240,7 @@ static _doZtFilterResult _doZtFilter(
 #endif // ZT_RULES_ENGINE_DEBUGGING
 								cc = fwdAddr;
 								ccLength = (rules[rn].v.fwd.length != 0) ? ((frameLen < (unsigned int)rules[rn].v.fwd.length) ? frameLen : (unsigned int)rules[rn].v.fwd.length) : frameLen;
+								ccWatch = (rt == ZT_NETWORK_RULE_ACTION_WATCH);
 							}
 						}
 					}	continue;
@@ -508,25 +507,29 @@ static _doZtFilterResult _doZtFilter(
 			case ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_AND:
 			case ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_OR:
 			case ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_XOR: {
-				const Tag *lt = (const Tag *)0;
-				for(unsigned int i=0;i<localTagCount;++i) {
-					if (rules[rn].v.tag.id == localTags[i].id()) {
-						lt = &(localTags[i]);
-						break;
-					}
-				}
-				if (!lt) {
-					thisRuleMatches = 0;
-					FILTER_TRACE("%u %s %c local tag %u not found -> 0",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id);
-				} else {
-					const uint32_t *rtv = (const uint32_t *)0;
-					for(unsigned int i=0;i<remoteTagCount;++i) {
-						if (rules[rn].v.tag.id == remoteTagIds[i]) {
-							rtv = &(remoteTagValues[i]);
-							break;
+				const Tag *const localTag = std::lower_bound(&(nconf.tags[0]),&(nconf.tags[nconf.tagCount]),rules[rn].v.tag.id,Tag::IdComparePredicate());
+				if ((localTag != &(nconf.tags[nconf.tagCount]))&&(localTag->id() == rules[rn].v.tag.id)) {
+					const Tag *const remoteTag = ((membership) ? membership->getTag(nconf,rules[rn].v.tag.id) : (const Tag *)0);
+					if (remoteTag) {
+						const uint32_t ltv = localTag->value();
+						const uint32_t rtv = remoteTag->value();
+						if (rt == ZT_NETWORK_RULE_MATCH_TAGS_DIFFERENCE) {
+							const uint32_t diff = (ltv > rtv) ? (ltv - rtv) : (rtv - ltv);
+							thisRuleMatches = (uint8_t)(diff <= rules[rn].v.tag.value);
+							FILTER_TRACE("%u %s %c TAG %u local:%u remote:%u difference:%u<=%u -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,ltv,rtv,diff,(unsigned int)rules[rn].v.tag.value,thisRuleMatches);
+						} else if (rt == ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_AND) {
+							thisRuleMatches = (uint8_t)((ltv & rtv) == rules[rn].v.tag.value);
+							FILTER_TRACE("%u %s %c TAG %u local:%.8x & remote:%.8x == %.8x -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,ltv,rtv,(unsigned int)rules[rn].v.tag.value,(unsigned int)thisRuleMatches);
+						} else if (rt == ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_OR) {
+							thisRuleMatches = (uint8_t)((ltv | rtv) == rules[rn].v.tag.value);
+							FILTER_TRACE("%u %s %c TAG %u local:%.8x | remote:%.8x == %.8x -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,ltv,rtv,(unsigned int)rules[rn].v.tag.value,(unsigned int)thisRuleMatches);
+						} else if (rt == ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_XOR) {
+							thisRuleMatches = (uint8_t)((ltv ^ rtv) == rules[rn].v.tag.value);
+							FILTER_TRACE("%u %s %c TAG %u local:%.8x ^ remote:%.8x == %.8x -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,ltv,rtv,(unsigned int)rules[rn].v.tag.value,(unsigned int)thisRuleMatches);
+						} else { // sanity check, can't really happen
+							thisRuleMatches = 0;
 						}
-					}
-					if (!rtv) {
+					} else {
 						if (inbound) {
 							thisRuleMatches = 0;
 							FILTER_TRACE("%u %s %c remote tag %u not found -> 0 (inbound side is strict)",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id);
@@ -534,24 +537,10 @@ static _doZtFilterResult _doZtFilter(
 							thisRuleMatches = 1;
 							FILTER_TRACE("%u %s %c remote tag %u not found -> 1 (outbound side is not strict)",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id);
 						}
-					} else {
-						if (rt == ZT_NETWORK_RULE_MATCH_TAGS_DIFFERENCE) {
-							const uint32_t diff = (lt->value() > *rtv) ? (lt->value() - *rtv) : (*rtv - lt->value());
-							thisRuleMatches = (uint8_t)(diff <= rules[rn].v.tag.value);
-							FILTER_TRACE("%u %s %c TAG %u local:%u remote:%u difference:%u<=%u -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,lt->value(),*rtv,diff,(unsigned int)rules[rn].v.tag.value,thisRuleMatches);
-						} else if (rt == ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_AND) {
-							thisRuleMatches = (uint8_t)((lt->value() & *rtv) == rules[rn].v.tag.value);
-							FILTER_TRACE("%u %s %c TAG %u local:%.8x & remote:%.8x == %.8x -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,lt->value(),*rtv,(unsigned int)rules[rn].v.tag.value,(unsigned int)thisRuleMatches);
-						} else if (rt == ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_OR) {
-							thisRuleMatches = (uint8_t)((lt->value() | *rtv) == rules[rn].v.tag.value);
-							FILTER_TRACE("%u %s %c TAG %u local:%.8x | remote:%.8x == %.8x -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,lt->value(),*rtv,(unsigned int)rules[rn].v.tag.value,(unsigned int)thisRuleMatches);
-						} else if (rt == ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_XOR) {
-							thisRuleMatches = (uint8_t)((lt->value() ^ *rtv) == rules[rn].v.tag.value);
-							FILTER_TRACE("%u %s %c TAG %u local:%.8x ^ remote:%.8x == %.8x -> %u",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id,lt->value(),*rtv,(unsigned int)rules[rn].v.tag.value,(unsigned int)thisRuleMatches);
-						} else { // sanity check, can't really happen
-							thisRuleMatches = 0;
-						}
 					}
+				} else {
+					thisRuleMatches = 0;
+					FILTER_TRACE("%u %s %c local tag %u not found -> 0",rn,_rtn(rt),(((rules[rn].t & 0x80) != 0) ? '!' : '='),(unsigned int)rules[rn].v.tag.id);
 				}
 			}	break;
 
@@ -582,7 +571,6 @@ Network::Network(const RuntimeEnvironment *renv,uint64_t nwid,void *uptr) :
 	_portInitialized(false),
 	_inboundConfigPacketId(0),
 	_lastConfigUpdate(0),
-	_lastRequestedConfiguration(0),
 	_destroyed(false),
 	_netconfFailure(NETCONF_FAILURE_NONE),
 	_portError(0)
@@ -598,7 +586,7 @@ Network::Network(const RuntimeEnvironment *renv,uint64_t nwid,void *uptr) :
 		if (conf.length()) {
 			dconf->load(conf.c_str());
 			if (nconf->fromDictionary(*dconf)) {
-				this->setConfiguration(*nconf,false);
+				this->_setConfiguration(*nconf,false);
 				_lastConfigUpdate = 0; // we still want to re-request a new config from the network
 				gotConf = true;
 			}
@@ -646,32 +634,27 @@ bool Network::filterOutgoingPacket(
 	const unsigned int etherType,
 	const unsigned int vlanId)
 {
-	uint32_t remoteTagIds[ZT_MAX_NETWORK_TAGS];
-	uint32_t remoteTagValues[ZT_MAX_NETWORK_TAGS];
-	Address ztFinalDest(ztDest);
-	Address cc;
-	const Capability *relevantCap = (const Capability *)0;
-	unsigned int ccLength = 0;
-	bool accept = false;
 	const uint64_t now = RR->node->now();
+	Address ztFinalDest(ztDest);
+	int localCapabilityIndex = -1;
+	bool accept = false;
 
 	Mutex::Lock _l(_lock);
 
-	Membership *m = (Membership *)0;
-	unsigned int remoteTagCount = 0;
-	if (ztDest) {
-		m = &(_memberships[ztDest]);
-		remoteTagCount = m->getAllTags(_config,remoteTagIds,remoteTagValues,ZT_MAX_NETWORK_TAGS);
-	}
+	Membership *const membership = (ztDest) ? _memberships.get(ztDest) : (Membership *)0;
 
-	switch(_doZtFilter(RR,_config,false,ztSource,ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc,ccLength)) {
+	Address cc;
+	unsigned int ccLength = 0;
+	bool ccWatch = false;
+	switch(_doZtFilter(RR,_config,membership,false,ztSource,ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,cc,ccLength,ccWatch)) {
 
 		case DOZTFILTER_NO_MATCH:
 			for(unsigned int c=0;c<_config.capabilityCount;++c) {
-				ztFinalDest = ztDest; // sanity check
+				ztFinalDest = ztDest; // sanity check, shouldn't be possible if there was no match
 				Address cc2;
 				unsigned int ccLength2 = 0;
-				switch (_doZtFilter(RR,_config,false,ztSource,ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.capabilities[c].rules(),_config.capabilities[c].ruleCount(),_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc2,ccLength2)) {
+				bool ccWatch2 = false;
+				switch (_doZtFilter(RR,_config,membership,false,ztSource,ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.capabilities[c].rules(),_config.capabilities[c].ruleCount(),cc2,ccLength2,ccWatch2)) {
 					case DOZTFILTER_NO_MATCH:
 					case DOZTFILTER_DROP: // explicit DROP in a capability just terminates its evaluation and is an anti-pattern
 						break;
@@ -679,16 +662,16 @@ bool Network::filterOutgoingPacket(
 					case DOZTFILTER_REDIRECT: // interpreted as ACCEPT but ztFinalDest will have been changed in _doZtFilter()
 					case DOZTFILTER_ACCEPT:
 					case DOZTFILTER_SUPER_ACCEPT: // no difference in behavior on outbound side
-						relevantCap = &(_config.capabilities[c]);
+						localCapabilityIndex = (int)c;
 						accept = true;
 
 						if ((!noTee)&&(cc2)) {
 							Membership &m2 = _membership(cc2);
-							m2.sendCredentialsIfNeeded(RR,now,cc2,_config,relevantCap);
+							m2.pushCredentials(RR,now,cc2,_config,localCapabilityIndex,false);
 
 							Packet outp(cc2,RR->identity.address(),Packet::VERB_EXT_FRAME);
 							outp.append(_id);
-							outp.append((uint8_t)0x02); // TEE/REDIRECT from outbound side: 0x02
+							outp.append((uint8_t)(ccWatch2 ? 0x16 : 0x02));
 							macDest.appendTo(outp);
 							macSource.appendTo(outp);
 							outp.append((uint16_t)etherType);
@@ -715,13 +698,16 @@ bool Network::filterOutgoingPacket(
 	}
 
 	if (accept) {
+		if (membership)
+			membership->pushCredentials(RR,now,ztDest,_config,localCapabilityIndex,false);
+
 		if ((!noTee)&&(cc)) {
 			Membership &m2 = _membership(cc);
-			m2.sendCredentialsIfNeeded(RR,now,cc,_config,relevantCap);
+			m2.pushCredentials(RR,now,cc,_config,localCapabilityIndex,false);
 
 			Packet outp(cc,RR->identity.address(),Packet::VERB_EXT_FRAME);
 			outp.append(_id);
-			outp.append((uint8_t)0x02); // TEE/REDIRECT from outbound side: 0x02
+			outp.append((uint8_t)(ccWatch ? 0x16 : 0x02));
 			macDest.appendTo(outp);
 			macSource.appendTo(outp);
 			outp.append((uint16_t)etherType);
@@ -732,11 +718,11 @@ bool Network::filterOutgoingPacket(
 
 		if ((ztDest != ztFinalDest)&&(ztFinalDest)) {
 			Membership &m2 = _membership(ztFinalDest);
-			m2.sendCredentialsIfNeeded(RR,now,ztFinalDest,_config,relevantCap);
+			m2.pushCredentials(RR,now,ztFinalDest,_config,localCapabilityIndex,false);
 
 			Packet outp(ztFinalDest,RR->identity.address(),Packet::VERB_EXT_FRAME);
 			outp.append(_id);
-			outp.append((uint8_t)0x02); // TEE/REDIRECT from outbound side: 0x02
+			outp.append((uint8_t)0x04);
 			macDest.appendTo(outp);
 			macSource.appendTo(outp);
 			outp.append((uint16_t)etherType);
@@ -745,11 +731,9 @@ bool Network::filterOutgoingPacket(
 			RR->sw->send(outp,true);
 
 			return false; // DROP locally, since we redirected
-		} else if (m) {
-			m->sendCredentialsIfNeeded(RR,now,ztDest,_config,relevantCap);
+		} else {
+			return true;
 		}
-
-		return true;
 	} else {
 		return false;
 	}
@@ -765,28 +749,27 @@ int Network::filterIncomingPacket(
 	const unsigned int etherType,
 	const unsigned int vlanId)
 {
-	uint32_t remoteTagIds[ZT_MAX_NETWORK_TAGS];
-	uint32_t remoteTagValues[ZT_MAX_NETWORK_TAGS];
 	Address ztFinalDest(ztDest);
-	Address cc;
-	unsigned int ccLength = 0;
 	int accept = 0;
 
 	Mutex::Lock _l(_lock);
 
-	Membership &m = _membership(sourcePeer->address());
-	const unsigned int remoteTagCount = m.getAllTags(_config,remoteTagIds,remoteTagValues,ZT_MAX_NETWORK_TAGS);
+	Membership &membership = _membership(sourcePeer->address());
 
-	switch (_doZtFilter(RR,_config,true,sourcePeer->address(),ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc,ccLength)) {
+	Address cc;
+	unsigned int ccLength = 0;
+	bool ccWatch = false;
+	switch (_doZtFilter(RR,_config,&membership,true,sourcePeer->address(),ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,cc,ccLength,ccWatch)) {
 
 		case DOZTFILTER_NO_MATCH: {
-			Membership::CapabilityIterator mci(m);
+			Membership::CapabilityIterator mci(membership,_config);
 			const Capability *c;
-			while ((c = mci.next(_config))) {
-				ztFinalDest = ztDest; // sanity check
+			while ((c = mci.next())) {
+				ztFinalDest = ztDest; // sanity check, should be unmodified if there was no match
 				Address cc2;
 				unsigned int ccLength2 = 0;
-				switch(_doZtFilter(RR,_config,true,sourcePeer->address(),ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,c->rules(),c->ruleCount(),_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc2,ccLength2)) {
+				bool ccWatch2 = false;
+				switch(_doZtFilter(RR,_config,&membership,true,sourcePeer->address(),ztFinalDest,macSource,macDest,frameData,frameLen,etherType,vlanId,c->rules(),c->ruleCount(),cc2,ccLength2,ccWatch2)) {
 					case DOZTFILTER_NO_MATCH:
 					case DOZTFILTER_DROP: // explicit DROP in a capability just terminates its evaluation and is an anti-pattern
 						break;
@@ -801,11 +784,11 @@ int Network::filterIncomingPacket(
 
 				if (accept) {
 					if (cc2) {
-						_membership(cc2).sendCredentialsIfNeeded(RR,RR->node->now(),cc2,_config,(const Capability *)0);
+						_membership(cc2).pushCredentials(RR,RR->node->now(),cc2,_config,-1,false);
 
 						Packet outp(cc2,RR->identity.address(),Packet::VERB_EXT_FRAME);
 						outp.append(_id);
-						outp.append((uint8_t)0x06); // TEE/REDIRECT from inbound side: 0x06
+						outp.append((uint8_t)(ccWatch2 ? 0x1c : 0x08));
 						macDest.appendTo(outp);
 						macSource.appendTo(outp);
 						outp.append((uint16_t)etherType);
@@ -832,11 +815,11 @@ int Network::filterIncomingPacket(
 
 	if (accept) {
 		if (cc) {
-			_membership(cc).sendCredentialsIfNeeded(RR,RR->node->now(),cc,_config,(const Capability *)0);
+			_membership(cc).pushCredentials(RR,RR->node->now(),cc,_config,-1,false);
 
 			Packet outp(cc,RR->identity.address(),Packet::VERB_EXT_FRAME);
 			outp.append(_id);
-			outp.append((uint8_t)0x06); // TEE/REDIRECT from inbound side: 0x06
+			outp.append((uint8_t)(ccWatch ? 0x1c : 0x08));
 			macDest.appendTo(outp);
 			macSource.appendTo(outp);
 			outp.append((uint16_t)etherType);
@@ -846,11 +829,11 @@ int Network::filterIncomingPacket(
 		}
 
 		if ((ztDest != ztFinalDest)&&(ztFinalDest)) {
-			_membership(ztFinalDest).sendCredentialsIfNeeded(RR,RR->node->now(),ztFinalDest,_config,(const Capability *)0);
+			_membership(ztFinalDest).pushCredentials(RR,RR->node->now(),ztFinalDest,_config,-1,false);
 
 			Packet outp(ztFinalDest,RR->identity.address(),Packet::VERB_EXT_FRAME);
 			outp.append(_id);
-			outp.append((uint8_t)0x06); // TEE/REDIRECT from inbound side: 0x06
+			outp.append((uint8_t)0x0a);
 			macDest.appendTo(outp);
 			macSource.appendTo(outp);
 			outp.append((uint16_t)etherType);
@@ -892,60 +875,6 @@ void Network::multicastUnsubscribe(const MulticastGroup &mg)
 		_myMulticastGroups.erase(i);
 }
 
-bool Network::applyConfiguration(const NetworkConfig &conf)
-{
-	if (_destroyed) // sanity check
-		return false;
-	try {
-		if ((conf.networkId == _id)&&(conf.issuedTo == RR->identity.address())) {
-			ZT_VirtualNetworkConfig ctmp;
-			bool portInitialized;
-			{
-				Mutex::Lock _l(_lock);
-				_config = conf;
-				_lastConfigUpdate = RR->node->now();
-				_netconfFailure = NETCONF_FAILURE_NONE;
-				_externalConfig(&ctmp);
-				portInitialized = _portInitialized;
-				_portInitialized = true;
-			}
-			_portError = RR->node->configureVirtualNetworkPort(_id,&_uPtr,(portInitialized) ? ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_CONFIG_UPDATE : ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_UP,&ctmp);
-			return true;
-		} else {
-			TRACE("ignored invalid configuration for network %.16llx (configuration contains mismatched network ID or issued-to address)",(unsigned long long)_id);
-		}
-	} catch (std::exception &exc) {
-		TRACE("ignored invalid configuration for network %.16llx (%s)",(unsigned long long)_id,exc.what());
-	} catch ( ... ) {
-		TRACE("ignored invalid configuration for network %.16llx (unknown exception)",(unsigned long long)_id);
-	}
-	return false;
-}
-
-int Network::setConfiguration(const NetworkConfig &nconf,bool saveToDisk)
-{
-	try {
-		{
-			Mutex::Lock _l(_lock);
-			if (_config == nconf)
-				return 1; // OK config, but duplicate of what we already have
-		}
-		if (applyConfiguration(nconf)) {
-			if (saveToDisk) {
-				char n[64];
-				Utils::snprintf(n,sizeof(n),"networks.d/%.16llx.conf",_id);
-				Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY> d;
-				if (nconf.toDictionary(d,false))
-					RR->node->dataStorePut(n,(const void *)d.data(),d.sizeBytes(),true);
-			}
-			return 2; // OK and configuration has changed
-		}
-	} catch ( ... ) {
-		TRACE("ignored invalid configuration for network %.16llx",(unsigned long long)_id);
-	}
-	return 0;
-}
-
 void Network::handleInboundConfigChunk(const uint64_t inRePacketId,const void *data,unsigned int chunkSize,unsigned int chunkIndex,unsigned int totalSize)
 {
 	std::string newConfig;
@@ -979,7 +908,8 @@ void Network::handleInboundConfigChunk(const uint64_t inRePacketId,const void *d
 			Identity controllerId(RR->topology->getIdentity(this->controller()));
 			if (controllerId) {
 				if (nc->fromDictionary(*dict)) {
-					this->setConfiguration(*nc,true);
+					Mutex::Lock _l(_lock);
+					this->_setConfiguration(*nc,true);
 				} else {
 					TRACE("error parsing new config with length %u: deserialization of NetworkConfig failed (certificate error?)",(unsigned int)newConfig.length());
 				}
@@ -997,12 +927,6 @@ void Network::handleInboundConfigChunk(const uint64_t inRePacketId,const void *d
 
 void Network::requestConfiguration()
 {
-	// Sanity limit: do not request more often than once per second
-	const uint64_t now = RR->node->now();
-	if ((now - _lastRequestedConfiguration) < 1000ULL)
-		return;
-	_lastRequestedConfiguration = RR->node->now();
-
 	const Address ctrl(controller());
 
 	Dictionary<ZT_NETWORKCONFIG_METADATA_DICT_CAPACITY> rmd;
@@ -1024,9 +948,10 @@ void Network::requestConfiguration()
 		if (RR->localNetworkController) {
 			NetworkConfig nconf;
 			switch(RR->localNetworkController->doNetworkConfigRequest(InetAddress(),RR->identity,RR->identity,_id,rmd,nconf)) {
-				case NetworkController::NETCONF_QUERY_OK:
-					this->setConfiguration(nconf,true);
-					return;
+				case NetworkController::NETCONF_QUERY_OK: {
+					Mutex::Lock _l(_lock);
+					this->_setConfiguration(nconf,true);
+				}	return;
 				case NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND:
 					this->setNotFound();
 					return;
@@ -1073,7 +998,7 @@ bool Network::gate(const SharedPtr<Peer> &peer,const Packet::Verb verb,const uin
 			if ( (_config.isPublic()) || ((m)&&(m->isAllowedOnNetwork(_config))) ) {
 				if (!m)
 					m = &(_membership(peer->address()));
-				m->sendCredentialsIfNeeded(RR,now,peer->address(),_config,(const Capability *)0);
+				m->pushCredentials(RR,now,peer->address(),_config,-1,false);
 				if (m->shouldLikeMulticasts(now)) {
 					_announceMulticastGroupsTo(peer->address(),_allMulticastGroups());
 					m->likingMulticasts(now);
@@ -1124,9 +1049,8 @@ void Network::clean()
 		Membership *m = (Membership *)0;
 		Hashtable<Address,Membership>::Iterator i(_memberships);
 		while (i.next(a,m)) {
-			if (RR->topology->getPeerNoCache(*a))
-				m->clean(_config);
-			else _memberships.erase(*a);
+			if (!RR->topology->getPeerNoCache(*a))
+				_memberships.erase(*a);
 		}
 	}
 }
@@ -1177,21 +1101,25 @@ void Network::learnBridgedMulticastGroup(const MulticastGroup &mg,uint64_t now)
 		_sendUpdatesToMembers(&mg);
 }
 
-int Network::addCredential(const CertificateOfMembership &com)
+Membership::AddCredentialResult Network::addCredential(const CertificateOfMembership &com)
 {
 	if (com.networkId() != _id)
-		return -1;
+		return Membership::ADD_REJECTED;
 	const Address a(com.issuedTo());
 	Mutex::Lock _l(_lock);
 	Membership &m = _membership(a);
-	const int result = m.addCredential(RR,com);
-	if (result == 0) {
-		m.sendCredentialsIfNeeded(RR,RR->node->now(),a,_config,(const Capability *)0);
+	const Membership::AddCredentialResult result = m.addCredential(RR,_config,com);
+	if ((result == Membership::ADD_ACCEPTED_NEW)||(result == Membership::ADD_ACCEPTED_REDUNDANT)) {
+		m.pushCredentials(RR,RR->node->now(),a,_config,-1,false);
 		RR->mc->addCredential(com,true);
 	}
 	return result;
 }
 
+Membership::AddCredentialResult Network::addCredential(const Revocation &rev)
+{
+}
+
 void Network::destroy()
 {
 	Mutex::Lock _l(_lock);
@@ -1215,6 +1143,39 @@ ZT_VirtualNetworkStatus Network::_status() const
 	}
 }
 
+int Network::_setConfiguration(const NetworkConfig &nconf,bool saveToDisk)
+{
+	// assumes _lock is locked
+	try {
+		if ((nconf.issuedTo != RR->identity.address())||(nconf.networkId != _id))
+			return 0;
+		if (_config == nconf)
+			return 1; // OK config, but duplicate of what we already have
+
+		ZT_VirtualNetworkConfig ctmp;
+		_config = nconf;
+		_lastConfigUpdate = RR->node->now();
+		_netconfFailure = NETCONF_FAILURE_NONE;
+		_externalConfig(&ctmp);
+		const bool oldPortInitialized = _portInitialized;
+		_portInitialized = true;
+		_portError = RR->node->configureVirtualNetworkPort(_id,&_uPtr,(oldPortInitialized) ? ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_CONFIG_UPDATE : ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_UP,&ctmp);
+
+		if (saveToDisk) {
+			char n[64];
+			Utils::snprintf(n,sizeof(n),"networks.d/%.16llx.conf",_id);
+			Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY> d;
+			if (nconf.toDictionary(d,false))
+				RR->node->dataStorePut(n,(const void *)d.data(),d.sizeBytes(),true);
+		}
+
+		return 2; // OK and configuration has changed
+	} catch ( ... ) {
+		TRACE("ignored invalid configuration for network %.16llx",(unsigned long long)_id);
+	}
+	return 0;
+}
+
 void Network::_externalConfig(ZT_VirtualNetworkConfig *ec) const
 {
 	// assumes _lock is locked
@@ -1308,7 +1269,7 @@ void Network::_sendUpdatesToMembers(const MulticastGroup *const newMulticastGrou
 		Membership *m = (Membership *)0;
 		Hashtable<Address,Membership>::Iterator i(_memberships);
 		while (i.next(a,m)) {
-			m->sendCredentialsIfNeeded(RR,now,*a,_config,(const Capability *)0);
+			m->pushCredentials(RR,now,*a,_config,-1,false);
 			if ( ((newMulticastGroup)||(m->shouldLikeMulticasts(now))) && (m->isAllowedOnNetwork(_config)) ) {
 				if (!newMulticastGroup)
 					m->likingMulticasts(now);
diff --git a/node/Network.hpp b/node/Network.hpp
index c85e5993c..a151fb887 100644
--- a/node/Network.hpp
+++ b/node/Network.hpp
@@ -62,6 +62,11 @@ public:
 	 */
 	static const MulticastGroup BROADCAST;
 
+	/**
+	 * Compute primary controller device ID from network ID
+	 */
+	static inline Address controllerFor(uint64_t nwid) throw() { return Address(nwid >> 24); }
+
 	/**
 	 * Construct a new network
 	 *
@@ -76,14 +81,24 @@ public:
 
 	~Network();
 
+	inline uint64_t id() const { return _id; }
+	inline Address controller() const { return Address(_id >> 24); }
+	inline bool multicastEnabled() const { return (_config.multicastLimit > 0); }
+	inline bool hasConfig() const { return (_config); }
+	inline uint64_t lastConfigUpdate() const throw() { return _lastConfigUpdate; }
+	inline ZT_VirtualNetworkStatus status() const { Mutex::Lock _l(_lock); return _status(); }
+	inline const NetworkConfig &config() const { return _config; }
+	inline const MAC &mac() const { return _mac; }
+
 	/**
 	 * Apply filters to an outgoing packet
 	 *
 	 * This applies filters from our network config and, if that doesn't match,
 	 * our capabilities in ascending order of capability ID. Additional actions
-	 * such as TEE may be taken, and credentials may be pushed.
+	 * such as TEE may be taken, and credentials may be pushed, so this is not
+	 * side-effect-free. It's basically step one in sending something over VL2.
 	 *
-	 * @param noTee If true, do not TEE anything anywhere
+	 * @param noTee If true, do not TEE anything anywhere (for two-pass filtering as done with multicast and bridging)
 	 * @param ztSource Source ZeroTier address
 	 * @param ztDest Destination ZeroTier address
 	 * @param macSource Ethernet layer source address
@@ -134,42 +149,10 @@ public:
 		const unsigned int vlanId);
 
 	/**
-	 * @return Network ID
-	 */
-	inline uint64_t id() const throw() { return _id; }
-
-	/**
-	 * @return Address of network's controller (most significant 40 bits of ID)
-	 */
-	inline Address controller() const throw() { return Address(_id >> 24); }
-
-	/**
-	 * @param nwid Network ID
-	 * @return Address of network's controller
-	 */
-	static inline Address controllerFor(uint64_t nwid) throw() { return Address(nwid >> 24); }
-
-	/**
-	 * @return Multicast group memberships for this network's port (local, not learned via bridging)
-	 */
-	inline std::vector<MulticastGroup> multicastGroups() const
-	{
-		Mutex::Lock _l(_lock);
-		return _myMulticastGroups;
-	}
-
-	/**
-	 * @return All multicast groups including learned groups that are behind any bridges we're attached to
-	 */
-	inline std::vector<MulticastGroup> allMulticastGroups() const
-	{
-		Mutex::Lock _l(_lock);
-		return _allMulticastGroups();
-	}
-
-	/**
+	 * Check whether we are subscribed to a multicast group
+	 *
 	 * @param mg Multicast group
-	 * @param includeBridgedGroups If true, also include any groups we've learned via bridging
+	 * @param includeBridgedGroups If true, also check groups we've learned via bridging
 	 * @return True if this network endpoint / peer is a member
 	 */
 	bool subscribedToMulticastGroup(const MulticastGroup &mg,bool includeBridgedGroups) const;
@@ -188,37 +171,19 @@ public:
 	 */
 	void multicastUnsubscribe(const MulticastGroup &mg);
 
-	/**
-	 * Apply a NetworkConfig to this network
-	 *
-	 * @param conf Configuration in NetworkConfig form
-	 * @return True if configuration was accepted
-	 */
-	bool applyConfiguration(const NetworkConfig &conf);
-
-	/**
-	 * Set or update this network's configuration
-	 *
-	 * @param nconf Network configuration
-	 * @param saveToDisk IF true (default), write config to disk
-	 * @return 0 -- rejected, 1 -- accepted but not new, 2 -- accepted new config
-	 */
-	int setConfiguration(const NetworkConfig &nconf,bool saveToDisk);
-
 	/**
 	 * Handle an inbound network config chunk
 	 *
-	 * Only chunks whose inRePacketId matches the packet ID of the last request
-	 * are handled. If this chunk completes the config, it is decoded and
-	 * setConfiguration() is called.
+	 * This is called from IncomingPacket when we receive a chunk from a network
+	 * controller.
 	 *
-	 * @param inRePacketId In-re packet ID from OK(NETWORK_CONFIG_REQUEST)
+	 * @param requestId An ID for grouping chunks, e.g. in-re packet ID for OK(NETWORK_CONFIG_REQUEST)
 	 * @param data Chunk data
 	 * @param chunkSize Size of data[]
 	 * @param chunkIndex Index of chunk in full config
 	 * @param totalSize Total size of network config
 	 */
-	void handleInboundConfigChunk(const uint64_t inRePacketId,const void *data,unsigned int chunkSize,unsigned int chunkIndex,unsigned int totalSize);
+	void handleInboundConfigChunk(const uint64_t requestId,const void *data,unsigned int chunkSize,unsigned int chunkIndex,unsigned int totalSize);
 
 	/**
 	 * Set netconf failure to 'access denied' -- called in IncomingPacket when controller reports this
@@ -230,7 +195,7 @@ public:
 	}
 
 	/**
-	 * Set netconf failure to 'not found' -- called by PacketDecider when controller reports this
+	 * Set netconf failure to 'not found' -- called by IncomingPacket when controller reports this
 	 */
 	inline void setNotFound()
 	{
@@ -240,10 +205,6 @@ public:
 
 	/**
 	 * Causes this network to request an updated configuration from its master node now
-	 *
-	 * There is a circuit breaker here to prevent this from being done more often
-	 * than once per second. This is to prevent things like NETWORK_CONFIG_REFRESH
-	 * from causing multiple requests.
 	 */
 	void requestConfiguration();
 
@@ -251,7 +212,7 @@ public:
 	 * Determine whether this peer is permitted to communicate on this network
 	 *
 	 * This also performs certain periodic actions such as pushing renewed
-	 * credentials to peers or requesting them if not present.
+	 * credentials to peers, so like the filters it is not side-effect-free.
 	 *
 	 * @param peer Peer to check
 	 * @param verb Packet verb
@@ -266,7 +227,7 @@ public:
 	bool gateMulticastGatherReply(const SharedPtr<Peer> &peer,const Packet::Verb verb,const uint64_t packetId);
 
 	/**
-	 * Perform cleanup and possibly save state
+	 * Do periodic cleanup and housekeeping tasks
 	 */
 	void clean();
 
@@ -279,46 +240,6 @@ public:
 		_sendUpdatesToMembers((const MulticastGroup *)0);
 	}
 
-	/**
-	 * @return Time of last updated configuration or 0 if none
-	 */
-	inline uint64_t lastConfigUpdate() const throw() { return _lastConfigUpdate; }
-
-	/**
-	 * @return Status of this network
-	 */
-	inline ZT_VirtualNetworkStatus status() const
-	{
-		Mutex::Lock _l(_lock);
-		return _status();
-	}
-
-	/**
-	 * @param ec Buffer to fill with externally-visible network configuration
-	 */
-	inline void externalConfig(ZT_VirtualNetworkConfig *ec) const
-	{
-		Mutex::Lock _l(_lock);
-		_externalConfig(ec);
-	}
-
-	/**
-	 * Get current network config
-	 *
-	 * @return Network configuration (may be a null config if we don't have one yet)
-	 */
-	inline const NetworkConfig &config() const { return _config; }
-
-	/**
-	 * @return True if this network has a valid config
-	 */
-	inline bool hasConfig() const { return (_config); }
-
-	/**
-	 * @return Ethernet MAC address for this network's local interface
-	 */
-	inline const MAC &mac() const { return _mac; }
-
 	/**
 	 * Find the node on this network that has this MAC behind it (if any)
 	 *
@@ -349,44 +270,47 @@ public:
 	void learnBridgedMulticastGroup(const MulticastGroup &mg,uint64_t now);
 
 	/**
-	 * @param com Certificate of membership
-	 * @return 0 == OK, 1 == waiting for WHOIS, -1 == BAD signature or credential
+	 * Validate a credential and learn it if it passes certificate and other checks
 	 */
-	int addCredential(const CertificateOfMembership &com);
+	Membership::AddCredentialResult addCredential(const CertificateOfMembership &com);
 
 	/**
-	 * @param cap Capability
-	 * @return 0 == OK, 1 == waiting for WHOIS, -1 == BAD signature or credential
+	 * Validate a credential and learn it if it passes certificate and other checks
 	 */
-	inline int addCredential(const Capability &cap)
+	inline Membership::AddCredentialResult addCredential(const Capability &cap)
 	{
 		if (cap.networkId() != _id)
-			return -1;
+			return Membership::ADD_REJECTED;
 		Mutex::Lock _l(_lock);
-		return _membership(cap.issuedTo()).addCredential(RR,cap);
+		return _membership(cap.issuedTo()).addCredential(RR,_config,cap);
 	}
 
 	/**
-	 * @param cap Tag
-	 * @return 0 == OK, 1 == waiting for WHOIS, -1 == BAD signature or credential
+	 * Validate a credential and learn it if it passes certificate and other checks
 	 */
-	inline int addCredential(const Tag &tag)
+	inline Membership::AddCredentialResult addCredential(const Tag &tag)
 	{
 		if (tag.networkId() != _id)
-			return -1;
+			return Membership::ADD_REJECTED;
 		Mutex::Lock _l(_lock);
-		return _membership(tag.issuedTo()).addCredential(RR,tag);
+		return _membership(tag.issuedTo()).addCredential(RR,_config,tag);
 	}
 
 	/**
-	 * Blacklist COM, tags, and capabilities before this time
-	 *
-	 * @param ts Blacklist cutoff
+	 * Validate a credential and learn it if it passes certificate and other checks
 	 */
-	inline void blacklistBefore(const Address &peerAddress,const uint64_t ts)
+	Membership::AddCredentialResult addCredential(const Revocation &rev);
+
+	/**
+	 * Force push credentials (COM, etc.) to a peer now
+	 *
+	 * @param to Destination peer address
+	 * @param now Current time
+	 */
+	inline void pushCredentialsNow(const Address &to,const uint64_t now)
 	{
 		Mutex::Lock _l(_lock);
-		_membership(peerAddress).blacklistBefore(ts);
+		_membership(to).pushCredentials(RR,now,to,_config,-1,true);
 	}
 
 	/**
@@ -399,11 +323,23 @@ public:
 	void destroy();
 
 	/**
-	 * @return Pointer to user PTR (modifiable user ptr used in API)
+	 * Get this network's config for export via the ZT core API
+	 *
+	 * @param ec Buffer to fill with externally-visible network configuration
+	 */
+	inline void externalConfig(ZT_VirtualNetworkConfig *ec) const
+	{
+		Mutex::Lock _l(_lock);
+		_externalConfig(ec);
+	}
+
+	/**
+	 * @return Externally usable pointer-to-pointer exported via the core API
 	 */
 	inline void **userPtr() throw() { return &_uPtr; }
 
 private:
+	int _setConfiguration(const NetworkConfig &nconf,bool saveToDisk);
 	ZT_VirtualNetworkStatus _status() const;
 	void _externalConfig(ZT_VirtualNetworkConfig *ec) const; // assumes _lock is locked
 	bool _gate(const SharedPtr<Peer> &peer);
@@ -412,9 +348,9 @@ private:
 	std::vector<MulticastGroup> _allMulticastGroups() const;
 	Membership &_membership(const Address &a);
 
-	const RuntimeEnvironment *RR;
+	const RuntimeEnvironment *const RR;
 	void *_uPtr;
-	uint64_t _id;
+	const uint64_t _id;
 	uint64_t _lastAnnouncedMulticastGroupsUpstream;
 	MAC _mac; // local MAC address
 	volatile bool _portInitialized;
@@ -428,7 +364,6 @@ private:
 
 	NetworkConfig _config;
 	volatile uint64_t _lastConfigUpdate;
-	volatile uint64_t _lastRequestedConfiguration;
 
 	volatile bool _destroyed;
 
diff --git a/node/Packet.hpp b/node/Packet.hpp
index 2ca73a847..03b9b113d 100644
--- a/node/Packet.hpp
+++ b/node/Packet.hpp
@@ -655,15 +655,27 @@ public:
 		 *
 		 * Flags:
 		 *   0x01 - Certificate of network membership attached (DEPRECATED)
-		 *   0x02 - This is a TEE'd or REDIRECT'ed packet
-		 *   0x04 - TEE/REDIRECT'ed packet is from inbound side
+		 *   0x02 - Most significant bit of subtype (see below)
+		 *   0x04 - Middle bit of subtype (see below)
+		 *   0x08 - Least significant bit of subtype (see below)
+		 *   0x10 - ACK requested in the form of OK(EXT_FRAME)
 		 *
+		 * Subtypes (0..7):
+		 *   0x0 - Normal frame (bridging can be determined by checking MAC)
+		 *   0x1 - TEEd outbound frame
+		 *   0x2 - REDIRECTed outbound frame
+		 *   0x3 - WATCHed outbound frame (TEE with ACK, ACK bit also set)
+		 *   0x4 - TEEd inbound frame
+		 *   0x5 - REDIRECTed inbound frame
+		 *   0x6 - WATCHed inbound frame
+		 *   0x7 - (reserved for future use)
+		 *   
 		 * An extended frame carries full MAC addressing, making them a
 		 * superset of VERB_FRAME. They're used for bridging or when we
 		 * want to attach a certificate since FRAME does not support that.
 		 *
-		 * ERROR may be generated if a membership certificate is needed for a
-		 * closed network. Payload will be network ID.
+		 * If the ACK flag (0x08) is set, an OK(EXT_FRAME) is sent with
+		 * no payload to acknowledge receipt of the frame.
 		 */
 		VERB_EXT_FRAME = 0x07,
 
@@ -698,7 +710,7 @@ public:
 		VERB_MULTICAST_LIKE = 0x09,
 
 		/**
-		 * Network membership credential push:
+		 * Network credentials push:
 		 *   <[...] serialized certificate of membership>
 		 *   [<[...] additional certificates of membership>]
 		 *   <[1] 0x00, null byte marking end of COM array>
@@ -706,12 +718,12 @@ public:
 		 *   <[...] one or more serialized Capability>
 		 *   <[2] 16-bit number of tags>
 		 *   <[...] one or more serialized Tags>
+		 *   <[2] 16-bit number of revocations>
+		 *   <[...] one or more serialized Revocations>
 		 *
-		 * This is sent in response to ERROR_NEED_MEMBERSHIP_CERTIFICATE and may
-		 * be pushed at any other time to keep exchanged certificates up to date.
-		 *
-		 * COMs and other credentials need not be for the same network, since each
-		 * includes its own network ID and signature.
+		 * This can be sent by anyone at any time to push network credentials.
+		 * These will of course only be accepted if they are properly signed.
+		 * Credentials can be for any number of networks.
 		 *
 		 * OK/ERROR are not generated.
 		 */
@@ -742,23 +754,18 @@ public:
 		VERB_NETWORK_CONFIG_REQUEST = 0x0b,
 
 		/**
-		 * Network configuration update push:
-		 *   <[8] network ID to refresh>
-		 *   <[2] 16-bit number of address/timestamp pairs to blacklist>
-		 *  [<[5] ZeroTier address of peer being revoked>]
-		 *  [<[8] blacklist credentials older than this timestamp>]
-		 *  [<[...] additional address/timestamp pairs>]
+		 * Network configuration push:
+		 *   <[8] 64-bit network ID>
+		 *   <[8] 64-bit value used to group chunks in this push>
+		 *   <[2] 16-bit length of network configuration dictionary chunk>
+		 *   <[...] network configuration dictionary (may be incomplete)>
+		 *   <[4] 32-bit total length of assembled dictionary>
+		 *   <[4] 32-bit index of chunk in this reply>
 		 *
-		 * This can be sent by a network controller to both request that a network
-		 * config be updated and push instantaneous revocations of specific peers
-		 * or peer credentials.
-		 *
-		 * Specific revocations can be pushed to blacklist a specific peer's
-		 * credentials (COM, tags, and capabilities) if older than a specified
-		 * timestamp. This can be used to accomplish expedited revocation of
-		 * a peer's access to things on a network or to the network itself among
-		 * those other peers that can currently reach the controller. This is not
-		 * the only mechanism for revocation of course, but it's the fastest.
+		 * This is a direct push variant for network config updates. It otherwise
+		 * carries the same payload as OK(NETWORK_CONFIG_REQUEST). There is an
+		 * extra number after network ID in this version that is used in place of
+		 * the in-re packet ID sent with OKs to group chunks together.
 		 */
 		VERB_NETWORK_CONFIG_REFRESH = 0x0c,
 
diff --git a/node/Revocation.cpp b/node/Revocation.cpp
new file mode 100644
index 000000000..420476a4f
--- /dev/null
+++ b/node/Revocation.cpp
@@ -0,0 +1,46 @@
+/*
+ * ZeroTier One - Network Virtualization Everywhere
+ * Copyright (C) 2011-2016  ZeroTier, Inc.  https://www.zerotier.com/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "Revocation.hpp"
+#include "RuntimeEnvironment.hpp"
+#include "Identity.hpp"
+#include "Topology.hpp"
+#include "Switch.hpp"
+#include "Network.hpp"
+
+namespace ZeroTier {
+
+int Revocation::verify(const RuntimeEnvironment *RR) const
+{
+	if ((!_signedBy)||(_signedBy != Network::controllerFor(_networkId)))
+		return -1;
+	const Identity id(RR->topology->getIdentity(_signedBy));
+	if (!id) {
+		RR->sw->requestWhois(_signedBy);
+		return 1;
+	}
+	try {
+		Buffer<sizeof(Revocation) + 64> tmp;
+		this->serialize(tmp,true);
+		return (id.verify(tmp.data(),tmp.size(),_signature) ? 0 : -1);
+	} catch ( ... ) {
+		return -1;
+	}
+}
+
+} // namespace ZeroTier
diff --git a/node/Revocation.hpp b/node/Revocation.hpp
new file mode 100644
index 000000000..58757465f
--- /dev/null
+++ b/node/Revocation.hpp
@@ -0,0 +1,178 @@
+/*
+ * ZeroTier One - Network Virtualization Everywhere
+ * Copyright (C) 2011-2016  ZeroTier, Inc.  https://www.zerotier.com/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ZT_REVOCATION_HPP
+#define ZT_REVOCATION_HPP
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+#include "Constants.hpp"
+#include "../include/ZeroTierOne.h"
+#include "Address.hpp"
+#include "C25519.hpp"
+#include "Utils.hpp"
+#include "Buffer.hpp"
+#include "Identity.hpp"
+
+/**
+ * Flag: fast propagation via rumor mill algorithm
+ */
+#define ZT_REVOCATION_FLAG_FAST_PROPAGATE 0x1ULL
+
+namespace ZeroTier {
+
+class RuntimeEnvironment;
+
+/**
+ * Revocation certificate to instantaneously revoke a COM, capability, or tag
+ */
+class Revocation
+{
+public:
+	enum CredentialType
+	{
+		CREDENTIAL_TYPE_NIL = 0,
+		CREDENTIAL_TYPE_COM = 1,
+		CREDENTIAL_TYPE_CAPABILITY = 2,
+		CREDENTIAL_TYPE_TAG = 3
+	};
+
+	Revocation()
+	{
+		memset(this,0,sizeof(Revocation));
+	}
+
+	Revocation(const uint64_t i,const uint64_t nwid,const uint64_t cid,const uint64_t thr,const uint64_t fl,const Address &tgt,const CredentialType ct) :
+		_id(i),
+		_networkId(nwid),
+		_credentialId(cid),
+		_threshold(thr),
+		_flags(fl),
+		_target(tgt),
+		_signedBy(),
+		_type(ct) {}
+
+	inline uint64_t id() const { return _id; }
+	inline uint64_t networkId() const { return _networkId; }
+	inline uint64_t credentialId() const { return _credentialId; }
+	inline uint64_t threshold() const { return _threshold; }
+	inline const Address &target() const { return _target; }
+	inline const Address &signer() const { return _signedBy; }
+	inline CredentialType type() const { return _type; }
+
+	inline bool fastPropagate() const { return ((_flags & ZT_REVOCATION_FLAG_FAST_PROPAGATE) != 0); }
+
+	/**
+	 * @param signer Signing identity, must have private key
+	 * @return True if signature was successful
+	 */
+	inline bool sign(const Identity &signer)
+	{
+		if (signer.hasPrivate()) {
+			Buffer<sizeof(Revocation) + 64> tmp;
+			this->serialize(tmp,true);
+			_signedBy = signer.address();
+			_signature = signer.sign(tmp.data(),tmp.size());
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Verify this revocation's signature
+	 *
+	 * @param RR Runtime environment to provide for peer lookup, etc.
+	 * @return 0 == OK, 1 == waiting for WHOIS, -1 == BAD signature or chain
+	 */
+	int verify(const RuntimeEnvironment *RR) const;
+
+	template<unsigned int C>
+	inline void serialize(Buffer<C> &b,const bool forSign = false) const
+	{
+		if (forSign) b.append((uint64_t)0x7f7f7f7f7f7f7f7fULL);
+
+		b.append(_id);
+		b.append(_networkId);
+		b.append(_credentialId);
+		b.append(_threshold);
+		b.append(_flags);
+		_target.appendTo(b);
+		_signedBy.appendTo(b);
+		b.append((uint8_t)_type);
+
+		if (!forSign) {
+			b.append((uint8_t)1); // 1 == Ed25519 signature
+			b.append((uint16_t)ZT_C25519_SIGNATURE_LEN);
+			b.append(_signature.data,ZT_C25519_SIGNATURE_LEN);
+		}
+
+		// This is the size of any additional fields, currently 0.
+		b.append((uint16_t)0);
+
+		if (forSign) b.append((uint64_t)0x7f7f7f7f7f7f7f7fULL);
+	}
+
+	template<unsigned int C>
+	inline unsigned int deserialize(const Buffer<C> &b,unsigned int startAt = 0)
+	{
+		memset(this,0,sizeof(Revocation));
+
+		unsigned int p = startAt;
+
+		_id = b.template at<uint64_t>(p); p += 8;
+		_networkId = b.template at<uint64_t>(p); p += 8;
+		_credentialId = b.template at<uint64_t>(p); p += 8;
+		_threshold = b.template at<uint64_t>(p); p += 8;
+		_flags = b.template at<uint64_t>(p); p += 8;
+		_target.setTo(b.field(p,ZT_ADDRESS_LENGTH),ZT_ADDRESS_LENGTH); p += ZT_ADDRESS_LENGTH;
+		_signedBy.setTo(b.field(p,ZT_ADDRESS_LENGTH),ZT_ADDRESS_LENGTH); p += ZT_ADDRESS_LENGTH;
+		_type = (CredentialType)b[p++];
+
+		if (b[p++] == 1) {
+			if (b.template at<uint16_t>(p) == ZT_C25519_SIGNATURE_LEN) {
+				p += 2;
+				memcpy(_signature.data,b.field(p,ZT_C25519_SIGNATURE_LEN),ZT_C25519_SIGNATURE_LEN);
+				p += ZT_C25519_SIGNATURE_LEN;
+			} else throw std::runtime_error("invalid signature");
+		}
+
+		p += 2 + b.template at<uint16_t>(p);
+		if (p > b.size())
+			throw std::runtime_error("extended field overflow");
+
+		return (p - startAt);
+	}
+
+private:
+	uint64_t _id;
+	uint64_t _networkId;
+	uint64_t _credentialId;
+	uint64_t _threshold;
+	uint64_t _flags;
+	Address _target;
+	Address _signedBy;
+	CredentialType _type;
+	C25519::Signature _signature;
+};
+
+} // namespace ZeroTier
+
+#endif
diff --git a/node/Tag.cpp b/node/Tag.cpp
index 352ecde8e..eb4026bc5 100644
--- a/node/Tag.cpp
+++ b/node/Tag.cpp
@@ -27,7 +27,7 @@ namespace ZeroTier {
 
 int Tag::verify(const RuntimeEnvironment *RR) const
 {
-	if ((!_signedBy)||(_signedBy != Network::controllerFor(_nwid)))
+	if ((!_signedBy)||(_signedBy != Network::controllerFor(_networkId)))
 		return -1;
 	const Identity id(RR->topology->getIdentity(_signedBy));
 	if (!id) {
diff --git a/node/Tag.hpp b/node/Tag.hpp
index 14cc3a5dd..972281572 100644
--- a/node/Tag.hpp
+++ b/node/Tag.hpp
@@ -67,7 +67,7 @@ public:
 	 * @param value Tag value
 	 */
 	Tag(const uint64_t nwid,const uint64_t ts,const Address &issuedTo,const uint32_t id,const uint32_t value) :
-		_nwid(nwid),
+		_networkId(nwid),
 		_ts(ts),
 		_id(id),
 		_value(value),
@@ -76,7 +76,7 @@ public:
 	{
 	}
 
-	inline uint64_t networkId() const { return _nwid; }
+	inline uint64_t networkId() const { return _networkId; }
 	inline uint64_t timestamp() const { return _ts; }
 	inline uint32_t id() const { return _id; }
 	inline const uint32_t &value() const { return _value; }
@@ -91,13 +91,13 @@ public:
 	 */
 	inline bool sign(const Identity &signer)
 	{
-		try {
-			Buffer<(sizeof(Tag) * 2)> tmp;
+		if (signer.hasPrivate()) {
+			Buffer<sizeof(Tag) + 64> tmp;
 			_signedBy = signer.address();
 			this->serialize(tmp,true);
 			_signature = signer.sign(tmp.data(),tmp.size());
 			return true;
-		} catch ( ... ) {}
+		}
 		return false;
 	}
 
@@ -115,7 +115,7 @@ public:
 		if (forSign) b.append((uint64_t)0x7f7f7f7f7f7f7f7fULL);
 
 		// These are the same between Tag and Capability
-		b.append(_nwid);
+		b.append(_networkId);
 		b.append(_ts);
 		b.append(_id);
 
@@ -140,7 +140,7 @@ public:
 		unsigned int p = startAt;
 
 		// These are the same between Tag and Capability
-		_nwid = b.template at<uint64_t>(p); p += 8;
+		_networkId = b.template at<uint64_t>(p); p += 8;
 		_ts = b.template at<uint64_t>(p); p += 8;
 		_id = b.template at<uint32_t>(p); p += 4;
 
@@ -168,8 +168,22 @@ public:
 	inline bool operator==(const Tag &t) const { return (memcmp(this,&t,sizeof(Tag)) == 0); }
 	inline bool operator!=(const Tag &t) const { return (memcmp(this,&t,sizeof(Tag)) != 0); }
 
+	// For searching sorted arrays or lists of Tags by ID
+	struct IdComparePredicate
+	{
+		inline bool operator()(const Tag &a,const Tag &b) const { return (a.id() < b.id()); }
+		inline bool operator()(const uint32_t a,const Tag &b) const { return (a < b.id()); }
+		inline bool operator()(const Tag &a,const uint32_t b) const { return (a.id() < b); }
+		inline bool operator()(const Tag *a,const Tag *b) const { return (a->id() < b->id()); }
+		inline bool operator()(const Tag *a,const Tag &b) const { return (a->id() < b.id()); }
+		inline bool operator()(const Tag &a,const Tag *b) const { return (a.id() < b->id()); }
+		inline bool operator()(const uint32_t a,const Tag *b) const { return (a < b->id()); }
+		inline bool operator()(const Tag *a,const uint32_t b) const { return (a->id() < b); }
+		inline bool operator()(const uint32_t a,const uint32_t b) const { return (a < b); }
+	};
+
 private:
-	uint64_t _nwid;
+	uint64_t _networkId;
 	uint64_t _ts;
 	uint32_t _id;
 	uint32_t _value;
diff --git a/objects.mk b/objects.mk
index f92a907e7..5738e769b 100644
--- a/objects.mk
+++ b/objects.mk
@@ -17,6 +17,7 @@ OBJS=\
 	node/Path.o \
 	node/Peer.o \
 	node/Poly1305.o \
+	node/Revocation.o \
 	node/Salsa20.o \
 	node/SelfAwareness.o \
 	node/SHA512.o \