Просмотр исходного кода

Think through and refine a few things in rules, especially edge case TEE and REDIRECT behavior and semantics.

Adam Ierymenko 9 лет назад
Родитель
Сommit
74afef8eb1

+ 0 - 2
controller/EmbeddedNetworkController.cpp

@@ -144,7 +144,6 @@ static json _renderRule(ZT_VirtualNetworkRule &rule)
 			r["type"] = "ACTION_REDIRECT";
 			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_DEBUG_LOG:
 			r["type"] = "ACTION_DEBUG_LOG";
@@ -308,7 +307,6 @@ static bool _parseRule(json &r,ZT_VirtualNetworkRule &rule)
 		rule.t |= ZT_NETWORK_RULE_ACTION_REDIRECT;
 		rule.v.fwd.address = Utils::hexStrToU64(_jS(r["zt"],"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_DEBUG_LOG") {
 		rule.t |= ZT_NETWORK_RULE_ACTION_DEBUG_LOG;

+ 1 - 1
node/Capability.hpp

@@ -178,7 +178,7 @@ public:
 					b.append((uint8_t)14);
 					b.append((uint64_t)rules[i].v.fwd.address);
 					b.append((uint32_t)rules[i].v.fwd.flags);
-					b.append((uint16_t)rules[i].v.fwd.length);
+					b.append((uint16_t)rules[i].v.fwd.length); // unused for redirect
 					break;
 				case ZT_NETWORK_RULE_MATCH_SOURCE_ZEROTIER_ADDRESS:
 				case ZT_NETWORK_RULE_MATCH_DEST_ZEROTIER_ADDRESS:

+ 3 - 3
node/IncomingPacket.cpp

@@ -560,7 +560,7 @@ bool IncomingPacket::_doFRAME(const RuntimeEnvironment *RR,const SharedPtr<Peer>
 					const MAC sourceMac(peer->address(),network->id());
 					const unsigned int frameLen = size() - ZT_PROTO_VERB_FRAME_IDX_PAYLOAD;
 					const uint8_t *const frameData = reinterpret_cast<const uint8_t *>(data()) + ZT_PROTO_VERB_FRAME_IDX_PAYLOAD;
-					if (network->filterIncomingPacket(peer,RR->identity.address(),sourceMac,network->mac(),frameData,frameLen,etherType,0))
+					if (network->filterIncomingPacket(peer,RR->identity.address(),sourceMac,network->mac(),frameData,frameLen,etherType,0) > 0)
 						RR->node->putFrame(network->id(),network->userPtr(),sourceMac,network->mac(),etherType,0,(const void *)frameData,frameLen);
 					peer->received(_localAddress,_remoteAddress,hops(),packetId(),Packet::VERB_FRAME,0,Packet::VERB_NOP,true);
 				}
@@ -625,7 +625,7 @@ bool IncomingPacket::_doEXT_FRAME(const RuntimeEnvironment *RR,const SharedPtr<P
 								return true;
 							}
 						}
-						// fall through -- 2 means accept regardless of bridging state
+						// fall through -- 2 means accept regardless of bridging checks or other restrictions
 					case 2:
 						RR->node->putFrame(network->id(),network->userPtr(),from,to,etherType,0,(const void *)frameData,frameLen);
 						break;
@@ -981,7 +981,7 @@ bool IncomingPacket::_doMULTICAST_FRAME(const RuntimeEnvironment *RR,const Share
 				}
 
 				const uint8_t *const frameData = (const uint8_t *)field(offset + ZT_PROTO_VERB_MULTICAST_FRAME_IDX_FRAME,frameLen);
-				if (network->filterIncomingPacket(peer,RR->identity.address(),from,to.mac(),frameData,frameLen,etherType,0)) {
+				if (network->filterIncomingPacket(peer,RR->identity.address(),from,to.mac(),frameData,frameLen,etherType,0) > 0) {
 					RR->node->putFrame(network->id(),network->userPtr(),from,to.mac(),etherType,0,(const void *)frameData,frameLen);
 				}
 			}

+ 214 - 76
node/Network.cpp

@@ -76,15 +76,14 @@ static const char *_rtn(const ZT_VirtualNetworkRuleType rt)
 		default: return "BAD_RULE_TYPE";
 	}
 }
-static const void _dumpFilterTrace(const char *ruleName,uint8_t thisSetMatches,bool noRedirect,bool inbound,const Address &ztSource,const Address &ztDest,const MAC &macSource,const MAC &macDest,const std::vector<std::string> &dlog,unsigned int frameLen,unsigned int etherType,const char *msg)
+static const void _dumpFilterTrace(const char *ruleName,uint8_t thisSetMatches,bool inbound,const Address &ztSource,const Address &ztDest,const MAC &macSource,const MAC &macDest,const std::vector<std::string> &dlog,unsigned int frameLen,unsigned int etherType,const char *msg)
 {
 	static volatile unsigned long cnt = 0;
-	printf("%.6lu %c %s inbound=%d noRedirect=%d frameLen=%u etherType=%u" ZT_EOL_S,
+	printf("%.6lu %c %s %s frameLen=%u etherType=%u" ZT_EOL_S,
 		cnt++,
 		((thisSetMatches) ? 'Y' : '.'),
 		ruleName,
-		(int)inbound,
-		(int)noRedirect,
+		((inbound) ? "INBOUND" : "OUTBOUND"),
 		frameLen,
 		etherType
 	);
@@ -143,14 +142,20 @@ static bool _ipv6GetPayload(const uint8_t *frameData,unsigned int frameLen,unsig
 	return false; // overflow == invalid
 }
 
-// 0 == no match, -1 == match/drop, 1 == match/accept, 2 == match/accept even if bridged
-static int _doZtFilter(
+enum _doZtFilterResult
+{
+	DOZTFILTER_NO_MATCH = 0,
+	DOZTFILTER_DROP = 1,
+	DOZTFILTER_REDIRECT = 2,
+	DOZTFILTER_ACCEPT = 3,
+	DOZTFILTER_SUPER_ACCEPT = 4
+};
+static _doZtFilterResult _doZtFilter(
 	const RuntimeEnvironment *RR,
-	const bool noRedirect,
 	const NetworkConfig &nconf,
 	const bool inbound,
 	const Address &ztSource,
-	const Address &ztDest,
+	Address &ztDest, // MUTABLE
 	const MAC &macSource,
 	const MAC &macDest,
 	const uint8_t *const frameData,
@@ -163,7 +168,9 @@ static int _doZtFilter(
 	const unsigned int localTagCount,
 	const uint32_t *const remoteTagIds,
 	const uint32_t *const remoteTagValues,
-	const unsigned int remoteTagCount)
+	const unsigned int remoteTagCount,
+	Address &cc, // MUTABLE
+	unsigned int &ccLength) // MUTABLE
 {
 	// For each set of rules we start by assuming that they match (since no constraints
 	// yields a 'match all' rule).
@@ -181,75 +188,83 @@ static int _doZtFilter(
 			case ZT_NETWORK_RULE_ACTION_DROP:
 				if (thisSetMatches) {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-					_dumpFilterTrace("ACTION_DROP",thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
+					_dumpFilterTrace("ACTION_DROP",thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
 #endif // ZT_RULES_ENGINE_DEBUGGING
-					return -1; // match, drop packet
+					return DOZTFILTER_DROP;
 				} else {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-					_dumpFilterTrace("ACTION_DROP",thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
+					_dumpFilterTrace("ACTION_DROP",thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
 					dlog.clear();
 #endif // ZT_RULES_ENGINE_DEBUGGING
-					thisSetMatches = 1; // no match, evaluate next set
+					thisSetMatches = 1;
 				}
 				continue;
+
 			case ZT_NETWORK_RULE_ACTION_ACCEPT:
 				if (thisSetMatches) {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-					_dumpFilterTrace("ACTION_ACCEPT",thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
+					_dumpFilterTrace("ACTION_ACCEPT",thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
 #endif // ZT_RULES_ENGINE_DEBUGGING
-					return 1; // match, accept packet
+					return DOZTFILTER_ACCEPT; // match, accept packet
 				} else {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-					_dumpFilterTrace("ACTION_ACCEPT",thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
+					_dumpFilterTrace("ACTION_ACCEPT",thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
 					dlog.clear();
 #endif // ZT_RULES_ENGINE_DEBUGGING
-					thisSetMatches = 1; // no match, evaluate next set
+					thisSetMatches = 1;
 				}
 				continue;
+
 			case ZT_NETWORK_RULE_ACTION_TEE:
 			case ZT_NETWORK_RULE_ACTION_REDIRECT: {
 				const Address fwdAddr(rules[rn].v.fwd.address);
-				if (fwdAddr == RR->identity.address()) {
-					// If we are the TEE or REDIRECT destination, don't TEE or REDIRECT
-					// to self. We should also accept here instead of interpreting
-					// REDIRECT as DROP since we are the destination.
+				if (fwdAddr == ztSource) {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-					_dumpFilterTrace(_rtn(rt),thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,"TEE/REDIRECT resulted in 'super-accept' since we are destination");
+					_dumpFilterTrace(_rtn(rt),thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,"TEE/REDIRECT ignored since source is target");
 #endif // ZT_RULES_ENGINE_DEBUGGING
-					return 2; // we should "super-accept" this packet since we are the TEE or REDIRECT destination
-				} else {
-					if (!noRedirect) {
-						Packet outp(fwdAddr,RR->identity.address(),Packet::VERB_EXT_FRAME);
-						outp.append(nconf.networkId);
-						outp.append((uint8_t)( ((rt == ZT_NETWORK_RULE_ACTION_REDIRECT) ? 0x04 : 0x02) | (inbound ? 0x08 : 0x00) ));
-						macDest.appendTo(outp);
-						macSource.appendTo(outp);
-						outp.append((uint16_t)etherType);
-						outp.append(frameData,(rules[rn].v.fwd.length != 0) ? ((frameLen < (unsigned int)rules[rn].v.fwd.length) ? frameLen : (unsigned int)rules[rn].v.fwd.length) : frameLen);
-						outp.compress();
-						RR->sw->send(outp,true);
+					thisSetMatches = 1;
+				} else if (fwdAddr == RR->identity.address()) {
+					if (inbound) {
+#ifdef ZT_RULES_ENGINE_DEBUGGING
+						_dumpFilterTrace(_rtn(rt),thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,"TEE/REDIRECT interpreted as super-accept since we are target");
+#endif // ZT_RULES_ENGINE_DEBUGGING
+						return DOZTFILTER_SUPER_ACCEPT;
+					} else {
+#ifdef ZT_RULES_ENGINE_DEBUGGING
+						_dumpFilterTrace(_rtn(rt),thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,"TEE/REDIRECT ignored on outbound since we are target");
+#endif // ZT_RULES_ENGINE_DEBUGGING
+						thisSetMatches = 1;
 					}
-
+				} else if (fwdAddr == ztDest) {
+#ifdef ZT_RULES_ENGINE_DEBUGGING
+					_dumpFilterTrace(_rtn(rt),thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,"TEE/REDIRECT ignored since destination is target");
+#endif // ZT_RULES_ENGINE_DEBUGGING
+					thisSetMatches = 1;
+				} else {
 					if (rt == ZT_NETWORK_RULE_ACTION_REDIRECT) {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-						_dumpFilterTrace("ACTION_REDIRECT",thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(noRedirect) ? "second-pass match, not actually redirecting" : (const char *)0);
+						_dumpFilterTrace("ACTION_REDIRECT",thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
 #endif // ZT_RULES_ENGINE_DEBUGGING
-						return -1; // match, drop packet (we redirected it)
+						ztDest = fwdAddr;
+						return DOZTFILTER_REDIRECT;
 					} else {
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-						_dumpFilterTrace("ACTION_TEE",thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(noRedirect) ? "second-pass match, not actually teeing" : (const char *)0);
+						_dumpFilterTrace("ACTION_TEE",thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
 						dlog.clear();
 #endif // ZT_RULES_ENGINE_DEBUGGING
-						thisSetMatches = 1; // TEE does not terminate evaluation
+						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;
+						thisSetMatches = 1;
 					}
 				}
 			}	continue;
+
 			case ZT_NETWORK_RULE_ACTION_DEBUG_LOG: // a no-op target specifically for debugging purposes
 #ifdef ZT_RULES_ENGINE_DEBUGGING
-				_dumpFilterTrace("ACTION_DEBUG_LOG",thisSetMatches,noRedirect,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
+				_dumpFilterTrace("ACTION_DEBUG_LOG",thisSetMatches,inbound,ztSource,ztDest,macSource,macDest,dlog,frameLen,etherType,(const char *)0);
 				dlog.clear();
 #endif // ZT_RULES_ENGINE_DEBUGGING
-				thisSetMatches = 1; // DEBUG_LOG does not terminate evaluation
+				thisSetMatches = 1;
 				continue;
 
 			default: break;
@@ -547,7 +562,7 @@ static int _doZtFilter(
 		thisSetMatches &= (thisRuleMatches ^ ((rules[rn].t >> 7) & 1));
 	}
 
-	return 0;
+	return DOZTFILTER_NO_MATCH;
 }
 
 const ZeroTier::MulticastGroup Network::BROADCAST(ZeroTier::MAC(0xffffffffffffULL),0);
@@ -614,7 +629,7 @@ Network::~Network()
 }
 
 bool Network::filterOutgoingPacket(
-	const bool noRedirect,
+	const bool noTee,
 	const Address &ztSource,
 	const Address &ztDest,
 	const MAC &macSource,
@@ -626,39 +641,94 @@ bool Network::filterOutgoingPacket(
 {
 	uint32_t remoteTagIds[ZT_MAX_NETWORK_TAGS];
 	uint32_t remoteTagValues[ZT_MAX_NETWORK_TAGS];
+	Address ztDest2(ztDest);
+	Address cc;
+	unsigned int ccLength = 0;
+	bool mainRuleTableMatch = false;
+	bool accept = false;
 
 	Mutex::Lock _l(_lock);
 
 	Membership &m = _memberships[ztDest];
 	const unsigned int remoteTagCount = m.getAllTags(_config,remoteTagIds,remoteTagValues,ZT_MAX_NETWORK_TAGS);
 
-	switch(_doZtFilter(RR,noRedirect,_config,false,ztSource,ztDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount)) {
-		case -1:
-			if (ztDest)
-				m.sendCredentialsIfNeeded(RR,RR->node->now(),ztDest,_config,(const Capability *)0);
+	switch(_doZtFilter(RR,_config,false,ztSource,ztDest2,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc,ccLength)) {
+		case DOZTFILTER_NO_MATCH:
+			break;
+		case DOZTFILTER_DROP:
 			return false;
-		case 1:
-		case 2:
-			if (ztDest)
-				m.sendCredentialsIfNeeded(RR,RR->node->now(),ztDest,_config,(const Capability *)0);
-			return true;
+		case DOZTFILTER_REDIRECT: // interpreted as ACCEPT but ztDest2 will have been changed in _doZtFilter()
+		case DOZTFILTER_ACCEPT:
+		case DOZTFILTER_SUPER_ACCEPT: // no difference in behavior on outbound side
+			mainRuleTableMatch = true;
+			accept = true;
+			break;
 	}
 
-	for(unsigned int c=0;c<_config.capabilityCount;++c) {
-		switch (_doZtFilter(RR,noRedirect,_config,false,ztSource,ztDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.capabilities[c].rules(),_config.capabilities[c].ruleCount(),_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount)) {
-			case -1:
-				if (ztDest)
-					m.sendCredentialsIfNeeded(RR,RR->node->now(),ztDest,_config,(const Capability *)0);
-				return false;
-			case 1:
-			case 2:
-				if (ztDest)
-					m.sendCredentialsIfNeeded(RR,RR->node->now(),ztDest,_config,&(_config.capabilities[c]));
-				return true;
+	const Capability *relevantCap = (const Capability *)0;
+	if (!mainRuleTableMatch) {
+		for(unsigned int c=0;c<_config.capabilityCount;++c) {
+			ztDest2 = ztDest; // sanity check
+			Address cc2;
+			unsigned int ccLength2 = 0;
+			switch (_doZtFilter(RR,_config,false,ztSource,ztDest2,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.capabilities[c].rules(),_config.capabilities[c].ruleCount(),_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc2,ccLength2)) {
+				case DOZTFILTER_NO_MATCH:
+				case DOZTFILTER_DROP: // explicit DROP in a capability just terminates its evaluation and is an anti-pattern
+					break;
+				case DOZTFILTER_REDIRECT: // interpreted as ACCEPT but ztDest2 will have been changed in _doZtFilter()
+				case DOZTFILTER_ACCEPT:
+				case DOZTFILTER_SUPER_ACCEPT: // no difference in behavior on outbound side
+					if ((!noTee)&&(cc2)) {
+						Packet outp(cc2,RR->identity.address(),Packet::VERB_EXT_FRAME);
+						outp.append(_id);
+						outp.append((uint8_t)0x02); // TEE/REDIRECT from outbound side: 0x02
+						macDest.appendTo(outp);
+						macSource.appendTo(outp);
+						outp.append((uint16_t)etherType);
+						outp.append(frameData,ccLength2);
+						outp.compress();
+						RR->sw->send(outp,true);
+					}
+					relevantCap = &(_config.capabilities[c]);
+					accept = true;
+					break;
+			}
+			if (accept)
+				break;
 		}
 	}
 
-	return false;
+	if (accept) {
+		if (ztDest2)
+			m.sendCredentialsIfNeeded(RR,RR->node->now(),ztDest2,_config,relevantCap);
+
+		if ((!noTee)&&(cc)) {
+			Packet outp(cc,RR->identity.address(),Packet::VERB_EXT_FRAME);
+			outp.append(_id);
+			outp.append((uint8_t)0x02); // TEE/REDIRECT from outbound side: 0x02
+			macDest.appendTo(outp);
+			macSource.appendTo(outp);
+			outp.append((uint16_t)etherType);
+			outp.append(frameData,ccLength);
+			outp.compress();
+			RR->sw->send(outp,true);
+		}
+
+		if (ztDest != ztDest2) {
+			Packet outp(ztDest2,RR->identity.address(),Packet::VERB_EXT_FRAME);
+			outp.append(_id);
+			outp.append((uint8_t)0x02); // TEE/REDIRECT from outbound side: 0x02
+			macDest.appendTo(outp);
+			macSource.appendTo(outp);
+			outp.append((uint16_t)etherType);
+			outp.append(frameData,frameLen);
+			outp.compress();
+			RR->sw->send(outp,true);
+			return false; // DROP locally, since we redirected
+		}
+	}
+
+	return accept;
 }
 
 int Network::filterIncomingPacket(
@@ -673,29 +743,97 @@ int Network::filterIncomingPacket(
 {
 	uint32_t remoteTagIds[ZT_MAX_NETWORK_TAGS];
 	uint32_t remoteTagValues[ZT_MAX_NETWORK_TAGS];
+	Address ztDest2(ztDest);
+	Address cc;
+	unsigned int ccLength = 0;
+	bool mainRuleTableMatch = false;
+	int accept = 0;
 
 	Mutex::Lock _l(_lock);
 
 	Membership &m = _memberships[ztDest];
 	const unsigned int remoteTagCount = m.getAllTags(_config,remoteTagIds,remoteTagValues,ZT_MAX_NETWORK_TAGS);
 
-	switch (_doZtFilter(RR,false,_config,true,sourcePeer->address(),ztDest,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount)) {
-		case -1: return 0; // DROP
-		case 1: return 1; // ACCEPT
-		case 2: return 2; // super-ACCEPT
+	switch (_doZtFilter(RR,_config,true,sourcePeer->address(),ztDest2,macSource,macDest,frameData,frameLen,etherType,vlanId,_config.rules,_config.ruleCount,_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc,ccLength)) {
+		case DOZTFILTER_NO_MATCH:
+			break;
+		case DOZTFILTER_DROP:
+			return 0; // DROP
+		case DOZTFILTER_REDIRECT: // interpreted as ACCEPT but ztDest2 will have been changed in _doZtFilter()
+		case DOZTFILTER_ACCEPT:
+			mainRuleTableMatch = true;
+			accept = 1; // ACCEPT
+			break;
+		case DOZTFILTER_SUPER_ACCEPT:
+			mainRuleTableMatch = true;
+			accept = 2; // super-ACCEPT
+			break;
 	}
 
-	Membership::CapabilityIterator mci(m);
-	const Capability *c;
-	while ((c = mci.next(_config))) {
-		switch(_doZtFilter(RR,false,_config,true,sourcePeer->address(),ztDest,macSource,macDest,frameData,frameLen,etherType,vlanId,c->rules(),c->ruleCount(),_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount)) {
-			case -1: return 0; // DROP
-			case 1: return 1; // ACCEPT
-			case 2: return 2; // super-ACCEPT
+	if (!mainRuleTableMatch) {
+		Membership::CapabilityIterator mci(m);
+		const Capability *c;
+		while ((c = mci.next(_config))) {
+			ztDest2 = ztDest; // sanity check
+			Address cc2;
+			unsigned int ccLength2 = 0;
+			switch(_doZtFilter(RR,_config,true,sourcePeer->address(),ztDest2,macSource,macDest,frameData,frameLen,etherType,vlanId,c->rules(),c->ruleCount(),_config.tags,_config.tagCount,remoteTagIds,remoteTagValues,remoteTagCount,cc2,ccLength2)) {
+				case DOZTFILTER_NO_MATCH:
+				case DOZTFILTER_DROP: // explicit DROP in a capability just terminates its evaluation and is an anti-pattern
+					break;
+				case DOZTFILTER_REDIRECT: // interpreted as ACCEPT but ztDest will have been changed in _doZtFilter()
+				case DOZTFILTER_ACCEPT:
+					accept = 1; // ACCEPT
+					break;
+				case DOZTFILTER_SUPER_ACCEPT:
+					accept = 2; // super-ACCEPT
+					break;
+			}
+			if (accept) {
+				if (cc2) {
+					Packet outp(cc2,RR->identity.address(),Packet::VERB_EXT_FRAME);
+					outp.append(_id);
+					outp.append((uint8_t)0x06); // TEE/REDIRECT from inbound side: 0x06
+					macDest.appendTo(outp);
+					macSource.appendTo(outp);
+					outp.append((uint16_t)etherType);
+					outp.append(frameData,ccLength2);
+					outp.compress();
+					RR->sw->send(outp,true);
+				}
+				break;
+			}
+		}
+	}
+
+	if (accept) {
+		if (cc) {
+			Packet outp(cc,RR->identity.address(),Packet::VERB_EXT_FRAME);
+			outp.append(_id);
+			outp.append((uint8_t)0x06); // TEE/REDIRECT from inbound side: 0x06
+			macDest.appendTo(outp);
+			macSource.appendTo(outp);
+			outp.append((uint16_t)etherType);
+			outp.append(frameData,ccLength);
+			outp.compress();
+			RR->sw->send(outp,true);
+		}
+
+		if (ztDest != ztDest2) {
+			Packet outp(ztDest2,RR->identity.address(),Packet::VERB_EXT_FRAME);
+			outp.append(_id);
+			outp.append((uint8_t)0x06); // TEE/REDIRECT from inbound side: 0x06
+			macDest.appendTo(outp);
+			macSource.appendTo(outp);
+			outp.append((uint16_t)etherType);
+			outp.append(frameData,frameLen);
+			outp.compress();
+			RR->sw->send(outp,true);
+			return 0; // DROP locally, since we redirected
 		}
 	}
 
-	return 0; // DROP
+	return accept;
 }
 
 bool Network::subscribedToMulticastGroup(const MulticastGroup &mg,bool includeBridgedGroups) const

+ 5 - 6
node/Network.hpp

@@ -82,11 +82,10 @@ public:
 	 * 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. If there is a match
-	 * certain actions may be taken such as pushing credentials to ztDest and
-	 * sending a copy of the packet to a TEE or REDIRECT target.
+	 * our capabilities in ascending order of capability ID. Additional actions
+	 * such as TEE may be taken, and credentials may be pushed.
 	 *
-	 * @param noRedirect If true, do not TEE or REDIRECT -- this is set for secondary filtrations done in multicast and bridge send paths
+	 * @param noTee If true, do not TEE anything anywhere
 	 * @param ztSource Source ZeroTier address
 	 * @param ztDest Destination ZeroTier address
 	 * @param macSource Ethernet layer source address
@@ -95,10 +94,10 @@ public:
 	 * @param frameLen Ethernet frame payload length
 	 * @param etherType 16-bit ethernet type ID
 	 * @param vlanId 16-bit VLAN ID
-	 * @return True if packet should be sent to destination peer
+	 * @return True if packet should be sent, false if dropped or redirected
 	 */
 	bool filterOutgoingPacket(
-		const bool noRedirect,
+		const bool noTee,
 		const Address &ztSource,
 		const Address &ztDest,
 		const MAC &macSource,

+ 3 - 2
node/OutboundMulticast.cpp

@@ -86,10 +86,11 @@ void OutboundMulticast::init(
 void OutboundMulticast::sendOnly(const RuntimeEnvironment *RR,const Address &toAddr)
 {
 	const SharedPtr<Network> nw(RR->node->network(_nwid));
-	if ((nw)&&(nw->filterOutgoingPacket(true,RR->identity.address(),toAddr,_macSrc,_macDest,_frameData,_frameLen,_etherType,0))) {
+	Address toAddr2(toAddr);
+	if ((nw)&&(nw->filterOutgoingPacket(true,RR->identity.address(),toAddr2,_macSrc,_macDest,_frameData,_frameLen,_etherType,0))) {
 		//TRACE(">>MC %.16llx -> %s",(unsigned long long)this,toAddr.toString().c_str());
 		_packet.newInitializationVector();
-		_packet.setDestination(toAddr);
+		_packet.setDestination(toAddr2);
 		RR->sw->send(_packet,true);
 	}
 }

+ 2 - 3
node/Packet.hpp

@@ -655,9 +655,8 @@ public:
 		 *
 		 * Flags:
 		 *   0x01 - Certificate of network membership attached (DEPRECATED)
-		 *   0x02 - Packet is a TEE'd packet
-		 *   0x04 - Packet is a REDIRECT'ed packet
-		 *   0x08 - TEE/REDIRECT'ed packet is on inbound side of connection
+		 *   0x02 - This is a TEE'd or REDIRECT'ed packet
+		 *   0x04 - TEE/REDIRECT'ed packet is from inbound side
 		 *
 		 * An extended frame carries full MAC addressing, making them a
 		 * superset of VERB_FRAME. They're used for bridging or when we

+ 1 - 4
node/Switch.cpp

@@ -437,10 +437,7 @@ void Switch::onLocalEthernet(const SharedPtr<Network> &network,const MAC &from,c
 
 		//TRACE("%.16llx: MULTICAST %s -> %s %s %u",network->id(),from.toString().c_str(),mg.toString().c_str(),etherTypeName(etherType),len);
 
-		// We filter with a NULL destination ZeroTier address first. Filtrations
-		// for each ZT destination are also done in OutboundMulticast, but these
-		// set noRedirect to true. This prevents multiple TEEs and REDIRECTs for
-		// multicast packets.
+		// First pass sets noTee to false, but noTee is set to true in OutboundMulticast to prevent duplicates.
 		if (!network->filterOutgoingPacket(false,RR->identity.address(),Address(),from,to,(const uint8_t *)data,len,etherType,vlanId)) {
 			TRACE("%.16llx: %s -> %s %s packet not sent: filterOutgoingPacket() returned false",network->id(),from.toString().c_str(),to.toString().c_str(),etherTypeName(etherType));
 			return;