From 15c07c58b610f699fd2a7164fde96712e1595f2b Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Tue, 27 Sep 2016 11:33:48 -0700 Subject: [PATCH] Refactored network config chunking to sign every chunk to prevent stupid DOS attack potential, and implement network config fast propagate (though we probably will not use this for a bit). --- node/Dictionary.hpp | 42 +--------- node/IncomingPacket.cpp | 54 +++++++------ node/Network.cpp | 165 ++++++++++++++++++++++++++++------------ node/Network.hpp | 40 ++++++---- node/Packet.hpp | 49 ++++++++---- 5 files changed, 213 insertions(+), 137 deletions(-) diff --git a/node/Dictionary.hpp b/node/Dictionary.hpp index eab2b162e..15ab9ce3e 100644 --- a/node/Dictionary.hpp +++ b/node/Dictionary.hpp @@ -23,7 +23,6 @@ #include "Utils.hpp" #include "Buffer.hpp" #include "Address.hpp" -#include "C25519.hpp" #include @@ -444,49 +443,14 @@ public: return found; } - /** - * Sign this Dictionary, replacing any previous signature - * - * @param sigKey Key to use for signature in dictionary - * @param kp Key pair to sign with - */ - inline void wrapWithSignature(const char *sigKey,const C25519::Pair &kp) - { - this->erase(sigKey); - C25519::Signature sig(C25519::sign(kp,this->data(),this->sizeBytes())); - this->add(sigKey,reinterpret_cast(sig.data),ZT_C25519_SIGNATURE_LEN); - } - - /** - * Verify signature (and erase signature key) - * - * This erases this Dictionary's signature key (if present) and verifies - * the signature. The key is erased to render the Dictionary into the - * original unsigned form it was signed in for verification purposes. - * - * @param sigKey Key to use for signature in dictionary - * @param pk Public key to check against - * @return True if signature was present and valid - */ - inline bool unwrapAndVerify(const char *sigKey,const C25519::Public &pk) - { - char sig[ZT_C25519_SIGNATURE_LEN+1]; - if (this->get(sigKey,sig,sizeof(sig)) != ZT_C25519_SIGNATURE_LEN) - return false; - this->erase(sigKey); - return C25519::verify(pk,this->data(),this->sizeBytes(),sig); - } - - /** - * @return Dictionary data as a 0-terminated C-string - */ - inline const char *data() const { return _d; } - /** * @return Value of C template parameter */ inline unsigned int capacity() const { return C; } + inline const char *data() const { return _d; } + inline char *unsafeData() { return _d; } + private: char _d[C]; }; diff --git a/node/IncomingPacket.cpp b/node/IncomingPacket.cpp index 72dfbfd82..3988546e9 100644 --- a/node/IncomingPacket.cpp +++ b/node/IncomingPacket.cpp @@ -433,21 +433,9 @@ bool IncomingPacket::_doOK(const RuntimeEnvironment *RR,const SharedPtr &p } break; case Packet::VERB_NETWORK_CONFIG_REQUEST: { - const uint64_t nwid = at(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_NETWORK_ID); - const SharedPtr network(RR->node->network(nwid)); - if ((network)&&(network->controller() == peer->address())) { - trustEstablished = true; - const unsigned int chunkLen = at(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT_LEN); - const void *chunkData = field(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT,chunkLen); - unsigned int chunkIndex = 0; - unsigned int totalSize = chunkLen; - if ((ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT + chunkLen) < size()) { - totalSize = at(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT + chunkLen); - chunkIndex = at(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT + chunkLen + 4); - } - TRACE("%s(%s): OK(NETWORK_CONFIG_REQUEST) chunkLen==%u chunkIndex==%u totalSize==%u",source().toString().c_str(),_path->address().toString().c_str(),chunkLen,chunkIndex,totalSize); - network->handleInboundConfigChunk(inRePacketId,chunkData,chunkLen,chunkIndex,totalSize); - } + const SharedPtr network(RR->node->network(at(ZT_PROTO_VERB_OK_IDX_PAYLOAD))); + if (network) + network->handleConfigChunk(*this,ZT_PROTO_VERB_OK_IDX_PAYLOAD); } break; //case Packet::VERB_ECHO: { @@ -894,20 +882,31 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons Dictionary *dconf = new Dictionary(); try { if (netconf->toDictionary(*dconf,metaData.getUI(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_VERSION,0) < 6)) { - dconf->wrapWithSignature(ZT_NETWORKCONFIG_DICT_KEY_SIGNATURE,RR->identity.privateKeyPair()); - + uint64_t configUpdateId = RR->node->prng(); + if (!configUpdateId) ++configUpdateId; const unsigned int totalSize = dconf->sizeBytes(); unsigned int chunkIndex = 0; while (chunkIndex < totalSize) { - const unsigned int chunkLen = std::min(totalSize - chunkIndex,(unsigned int)(ZT_PROTO_MAX_PACKET_LENGTH - (ZT_PACKET_IDX_PAYLOAD + 32))); + const unsigned int chunkLen = std::min(totalSize - chunkIndex,(unsigned int)(ZT_UDP_DEFAULT_PAYLOAD_MTU - (ZT_PACKET_IDX_PAYLOAD + 256))); Packet outp(peer->address(),RR->identity.address(),Packet::VERB_OK); outp.append((unsigned char)Packet::VERB_NETWORK_CONFIG_REQUEST); outp.append(requestPacketId); + + const unsigned int sigStart = outp.size(); outp.append(nwid); outp.append((uint16_t)chunkLen); outp.append((const void *)(dconf->data() + chunkIndex),chunkLen); + + outp.append((uint8_t)0); // no flags + outp.append((uint64_t)configUpdateId); outp.append((uint32_t)totalSize); outp.append((uint32_t)chunkIndex); + + C25519::Signature sig(RR->identity.sign(reinterpret_cast(outp.data()) + sigStart,outp.size() - sigStart)); + outp.append((uint8_t)1); + outp.append((uint16_t)ZT_C25519_SIGNATURE_LEN); + outp.append(sig.data,ZT_C25519_SIGNATURE_LEN); + outp.compress(); RR->sw->send(outp,true); chunkIndex += chunkLen; @@ -977,12 +976,21 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons bool IncomingPacket::_doNETWORK_CONFIG(const RuntimeEnvironment *RR,const SharedPtr &peer) { try { - const uint64_t nwid = at(ZT_PACKET_IDX_PAYLOAD); - bool trustEstablished = false; + const SharedPtr network(RR->node->network(at(ZT_PACKET_IDX_PAYLOAD))); + if (network) { + const uint64_t configUpdateId = network->handleConfigChunk(*this,ZT_PACKET_IDX_PAYLOAD); + if (configUpdateId) { + Packet outp(peer->address(),RR->identity.address(),Packet::VERB_OK); + outp.append((uint8_t)Packet::VERB_ECHO); + outp.append((uint64_t)packetId()); + outp.append((uint64_t)network->id()); + outp.append((uint64_t)configUpdateId); + outp.armor(peer->key(),true); + _path->send(RR,outp.data(),outp.size(),RR->node->now()); + } + } - - - peer->received(_path,hops(),packetId(),Packet::VERB_NETWORK_CONFIG,0,Packet::VERB_NOP,trustEstablished); + peer->received(_path,hops(),packetId(),Packet::VERB_NETWORK_CONFIG,0,Packet::VERB_NOP,false); } catch ( ... ) { TRACE("dropped NETWORK_CONFIG_REFRESH from %s(%s): unexpected exception",source().toString().c_str(),_path->address().toString().c_str()); } diff --git a/node/Network.cpp b/node/Network.cpp index 487766a77..e24e3e169 100644 --- a/node/Network.cpp +++ b/node/Network.cpp @@ -569,12 +569,14 @@ Network::Network(const RuntimeEnvironment *renv,uint64_t nwid,void *uptr) : _lastAnnouncedMulticastGroupsUpstream(0), _mac(renv->identity.address(),nwid), _portInitialized(false), - _inboundConfigPacketId(0), _lastConfigUpdate(0), _destroyed(false), _netconfFailure(NETCONF_FAILURE_NONE), _portError(0) { + for(int i=0;i(ptr); ptr += 2; + const void *chunkData = chunk.field(ptr,chunkLen); ptr += chunkLen; - unsigned int totalWeHave = 0; - for(std::map::iterator c(_inboundConfigChunks.begin());c!=_inboundConfigChunks.end();++c) - totalWeHave += (unsigned int)c->second.length(); + Mutex::Lock _l(_lock); - if (totalWeHave == totalSize) { - TRACE("have all chunks for network config request %.16llx, assembling...",inRePacketId); - for(std::map::iterator c(_inboundConfigChunks.begin());c!=_inboundConfigChunks.end();++c) - newConfig.append(c->second); - _inboundConfigPacketId = 0; - _inboundConfigChunks.clear(); - } else if (totalWeHave > totalSize) { - _inboundConfigPacketId = 0; - _inboundConfigChunks.clear(); + _IncomingConfigChunk *c = (_IncomingConfigChunk *)0; + uint64_t chunkId = 0; + uint64_t configUpdateId; + unsigned long totalLength,chunkIndex; + if (ptr < chunk.size()) { + const bool fastPropagate = ((chunk[ptr++] & 0x01) != 0); + configUpdateId = chunk.at(ptr); ptr += 8; + totalLength = chunk.at(ptr); ptr += 4; + chunkIndex = chunk.at(ptr); ptr += 4; + + if (((chunkIndex + chunkLen) > totalLength)||(totalLength >= ZT_NETWORKCONFIG_DICT_CAPACITY)) { // >= since we need room for a null at the end + TRACE("discarded chunk from %s: invalid length or length overflow",chunk.source().toString().c_str()); + return 0; } - } else { - return; - } - if ((newConfig.length() > 0)&&(newConfig.length() < ZT_NETWORKCONFIG_DICT_CAPACITY)) { - Dictionary *dict = new Dictionary(newConfig.c_str()); - NetworkConfig *nc = new NetworkConfig(); - try { - Identity controllerId(RR->topology->getIdentity(this->controller())); - if (controllerId) { - if (nc->fromDictionary(*dict)) { - 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()); + if ((chunk[ptr] != 1)||(chunk.at(ptr + 1) != ZT_C25519_SIGNATURE_LEN)) { + TRACE("discarded chunk from %s: unrecognized signature type",chunk.source().toString().c_str()); + return 0; + } + const uint8_t *sig = reinterpret_cast(chunk.field(ptr + 3,ZT_C25519_SIGNATURE_LEN)); + + // We can use the signature, which is unique per chunk, to get a per-chunk ID for local deduplication use + for(unsigned int i=0;i<16;++i) + reinterpret_cast(&chunkId)[i & 7] ^= sig[i]; + + // Find existing or new slot for this update and check if this is a duplicate chunk + for(int i=0;ihaveChunks;++j) { + if (c->haveChunkIds[j] == chunkId) + return 0; + } + + break; + } else if ((!c)||(_incomingConfigChunks[i].ts < c->ts)) { + c = &(_incomingConfigChunks[i]); + } + } + + // If it's not a duplicate, check chunk signature + const Identity controllerId(RR->topology->getIdentity(controller())); + if (!controllerId) { // we should always have the controller identity by now, otherwise how would we have queried it the first time? + TRACE("unable to verify chunk from %s: don't have controller identity",chunk.source().toString().c_str()); + return 0; + } + if (!controllerId.verify(chunk.field(start,ptr - start),ptr - start,sig,ZT_C25519_SIGNATURE_LEN)) { + TRACE("discarded chunk from %s: signature check failed",chunk.source().toString().c_str()); + return 0; + } + + // New properly verified chunks can be flooded "virally" through the network + if (fastPropagate) { + Address *a = (Address *)0; + Membership *m = (Membership *)0; + Hashtable::Iterator i(_memberships); + while (i.next(a,m)) { + if ((*a != chunk.source())&&(*a != controller())) { + Packet outp(*a,RR->identity.address(),Packet::VERB_NETWORK_CONFIG); + outp.append(reinterpret_cast(chunk.data()) + start,chunk.size() - start); + RR->sw->send(outp,true); } } + } + } else if (chunk.source() == controller()) { + // Legacy support for OK(NETWORK_CONFIG_REQUEST) from older controllers + chunkId = chunk.packetId(); + configUpdateId = chunkId; + totalLength = chunkLen; + chunkIndex = 0; + + if (totalLength >= ZT_NETWORKCONFIG_DICT_CAPACITY) + return 0; + + // Find oldest slot for this udpate to use buffer space + for(int i=0;its)) + c = &(_incomingConfigChunks[i]); + } + } else { + TRACE("discarded single-chunk unsigned legacy config: this is only allowed if the sender is the controller itself"); + return 0; + } + + ++c->ts; // newer is higher, that's all we need + + if (c->updateId != configUpdateId) { + c->updateId = configUpdateId; + for(int i=0;ihaveChunkIds[i] = 0; + c->haveChunks = 0; + c->haveBytes = 0; + } + if (c->haveChunks >= ZT_NETWORK_MAX_UPDATE_CHUNKS) + return false; + c->haveChunkIds[c->haveChunks++] = chunkId; + + memcpy(c->data.unsafeData() + chunkIndex,chunkData,chunkLen); + c->haveBytes += chunkLen; + + if (c->haveBytes == totalLength) { + c->data.unsafeData()[c->haveBytes] = (char)0; // ensure null terminated + + NetworkConfig *const nc = new NetworkConfig(); + try { + if (nc->fromDictionary(c->data)) { + this->_setConfiguration(*nc,true); + return configUpdateId; + } delete nc; - delete dict; } catch ( ... ) { - TRACE("error parsing new config with length %u: unexpected exception",(unsigned int)newConfig.length()); delete nc; - delete dict; - throw; } } + + return 0; } void Network::requestConfiguration() @@ -980,10 +1061,7 @@ void Network::requestConfiguration() } else { outp.append((unsigned char)0,16); } - - RR->node->expectReplyTo(_inboundConfigPacketId = outp.packetId()); - _inboundConfigChunks.clear(); - + RR->node->expectReplyTo(outp.packetId()); outp.compress(); RR->sw->send(outp,true); } @@ -1127,13 +1205,6 @@ Membership::AddCredentialResult Network::addCredential(const Address &sentFrom,c const Membership::AddCredentialResult result = m.addCredential(RR,_config,rev); if ((result == Membership::ADD_ACCEPTED_NEW)&&(rev.fastPropagate())) { - /* Fast propagation is done by using a very aggressive rumor mill - * propagation algorithm. When we see a Revocation that we haven't - * seen before we blast it to every known member. This leads to - * a huge number of redundant messages, but eventually everybody - * will get it. This helps revocation speed and also helps in cases - * where the controller is under attack. It need only get one - * revocation out and the rest is history. */ Address *a = (Address *)0; Membership *m = (Membership *)0; Hashtable::Iterator i(_memberships); diff --git a/node/Network.hpp b/node/Network.hpp index 6a1ac8015..128c46683 100644 --- a/node/Network.hpp +++ b/node/Network.hpp @@ -44,6 +44,9 @@ #include "NetworkConfig.hpp" #include "CertificateOfMembership.hpp" +#define ZT_NETWORK_MAX_INCOMING_UPDATES 3 +#define ZT_NETWORK_MAX_UPDATE_CHUNKS ((ZT_NETWORKCONFIG_DICT_CAPACITY / 1024) + 1) + namespace ZeroTier { class RuntimeEnvironment; @@ -174,16 +177,15 @@ public: /** * Handle an inbound network config chunk * - * This is called from IncomingPacket when we receive a chunk from a network - * controller. + * This is called from IncomingPacket to handle incoming network config + * chunks via OK(NETWORK_CONFIG_REQUEST) or NETWORK_CONFIG. It verifies + * each chunk and once assembled applies the configuration. * - * @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 + * @param chunk Packet containing chunk + * @param ptr Index of chunk and related fields in packet + * @return Update ID if update was fully assembled and accepted or 0 otherwise */ - void handleInboundConfigChunk(const uint64_t requestId,const void *data,unsigned int chunkSize,unsigned int chunkIndex,unsigned int totalSize); + uint64_t handleConfigChunk(const Packet &chunk,unsigned int ptr); /** * Set netconf failure to 'access denied' -- called in IncomingPacket when controller reports this @@ -353,19 +355,27 @@ private: const uint64_t _id; uint64_t _lastAnnouncedMulticastGroupsUpstream; MAC _mac; // local MAC address - volatile bool _portInitialized; + bool _portInitialized; std::vector< MulticastGroup > _myMulticastGroups; // multicast groups that we belong to (according to tap) Hashtable< MulticastGroup,uint64_t > _multicastGroupsBehindMe; // multicast groups that seem to be behind us and when we last saw them (if we are a bridge) Hashtable< MAC,Address > _remoteBridgeRoutes; // remote addresses where given MACs are reachable (for tracking devices behind remote bridges) - uint64_t _inboundConfigPacketId; - std::map _inboundConfigChunks; - NetworkConfig _config; - volatile uint64_t _lastConfigUpdate; + uint64_t _lastConfigUpdate; - volatile bool _destroyed; + struct _IncomingConfigChunk + { + uint64_t ts; + uint64_t updateId; + uint64_t haveChunkIds[ZT_NETWORK_MAX_UPDATE_CHUNKS]; + unsigned long haveChunks; + unsigned long haveBytes; + Dictionary data; + }; + _IncomingConfigChunk _incomingConfigChunks[ZT_NETWORK_MAX_INCOMING_UPDATES]; + + bool _destroyed; enum { NETCONF_FAILURE_NONE, @@ -373,7 +383,7 @@ private: NETCONF_FAILURE_NOT_FOUND, NETCONF_FAILURE_INIT_FAILED } _netconfFailure; - volatile int _portError; // return value from port config callback + int _portError; // return value from port config callback Hashtable _memberships; diff --git a/node/Packet.hpp b/node/Packet.hpp index b03ec3274..23597f681 100644 --- a/node/Packet.hpp +++ b/node/Packet.hpp @@ -755,8 +755,26 @@ public: * <[8] 64-bit network ID> * <[2] 16-bit length of network configuration dictionary chunk> * <[...] network configuration dictionary (may be incomplete)> + * [ ... end of legacy single chunk response ... ] + * <[1] 8-bit flags> + * <[8] 64-bit config update ID (should never be 0)> * <[4] 32-bit total length of assembled dictionary> - * <[4] 32-bit index of chunk in this reply> + * <[4] 32-bit index of chunk> + * [ ... end signed portion ... ] + * <[1] 8-bit chunk signature type> + * <[2] 16-bit length of chunk signature> + * <[...] chunk signature> + * + * The chunk signature signs the entire payload of the OK response. + * Currently only one signature type is supported: ed25519 (1). + * + * Each config chunk is signed to prevent memory exhaustion or + * traffic crowding DOS attacks against config fragment assembly. + * + * If the packet is from the network controller it is permitted to end + * before the config update ID or other chunking related or signature + * fields. This is to support older controllers that don't include + * these fields and may be removed in the future. * * ERROR response payload: * <[8] 64-bit network ID> @@ -766,25 +784,30 @@ public: /** * Network configuration data push: * <[8] 64-bit network ID> - * <[8] 64-bit config update ID (token to identify this update)> - * <[1] flags> * <[2] 16-bit length of network configuration dictionary chunk> * <[...] network configuration dictionary (may be incomplete)> + * <[1] 8-bit flags> + * <[8] 64-bit config update ID (should never be 0)> * <[4] 32-bit total length of assembled dictionary> - * <[4] 32-bit index of chunk in this reply> + * <[4] 32-bit index of chunk> + * [ ... end signed portion ... ] + * <[1] 8-bit chunk signature type> + * <[2] 16-bit length of chunk signature> + * <[...] chunk signature> * * 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. - * - * Unlike OK(NETWORK_CONFIG_REQUEST) this can be sent by peers other than - * network controllers. In that case the certificate inside the Dictionary - * is used for verification purposes. + * carries the same payload as OK(NETWORK_CONFIG_REQUEST) and has the same + * semantics. * * Flags: - * 0x01 - Patch, not whole config - * 0x02 - Use fast P2P propagation + * 0x01 - Use fast propagation + * + * An OK should be sent if the config is successfully received and + * accepted. + * + * OK payload: + * <[8] 64-bit network ID> + * <[8] 64-bit config update ID> */ VERB_NETWORK_CONFIG = 0x0c,