Skip to content

Commit

Permalink
Introduce the beginnings of anti-DoS resource scheduling.
Browse files Browse the repository at this point in the history
Peers now have a priority that attempts to estimate their importance. Currently it is just based on IP address. The default score is zero. In future it may take into account things like how many blocks were relayed, etc. When a node reaches its max connection slots, it will attempt to find a peer with a lower priority than the one trying to connect and disconnect it, to stay below the max connection limit.

Peer priorities are based on matching the connecting IP against a set of IP groups. For now, the only IP group is one that gives Tor exits a score of -10. This is to address DoS attacks that are being reported on the main network in which an attacker builds many connections via Tor to use up all the connection slots and jam the node for clearnet users. It's a more robust approach than simply banning abused proxies altogether.

For simplicity, a generated list of exits is compiled into the binary. A future enhancement would be to load IP groups from an external file and/or load from HTTPS urls (would require a new dependency on cpp-netlib).

Other anonymizing proxy networks that are attractive to DoS attackers may also be added as alternative IP groups, as a quick fix. Eventually peer priority can be calculated in a more free floating and dynamic manner and the hardcoded IP approach may become unneeded.
  • Loading branch information
mikehearn committed Jul 20, 2015
1 parent 0133d5a commit 5e62628
Show file tree
Hide file tree
Showing 10 changed files with 1,296 additions and 6 deletions.
22 changes: 22 additions & 0 deletions contrib/torips/gen-tor-ips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python

# Script to generate a C++ source file containing known Tor exits.
# This is used to help nodes treat Tor traffic homogenously.

import urllib2, time

data = urllib2.urlopen("https://check.torproject.org/exit-addresses").read()
exitlines = [line for line in data.split('\n') if line.startswith("ExitAddress")]
ipstrs = [line.split()[1] for line in exitlines]
ipstrs.sort()

contents = """// Generated at %s by gen-tor-ips.py: DO NOT EDIT
static const char *pszTorExits[] = {
"0.1.2.3", // For unit testing
%s
NULL
};
""" % (time.asctime(), "\n".join([" \"%s\"," % ip for ip in ipstrs]))

print contents
3 changes: 3 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ BITCOIN_CORE_H = \
ecwrapper.h \
hash.h \
init.h \
ipgroups.h \
key.h \
keystore.h \
leveldbwrapper.h \
Expand Down Expand Up @@ -137,6 +138,7 @@ BITCOIN_CORE_H = \
threadsafety.h \
timedata.h \
tinyformat.h \
torips.h \
txdb.h \
txmempool.h \
ui_interface.h \
Expand Down Expand Up @@ -180,6 +182,7 @@ libbitcoin_server_a_SOURCES = \
chain.cpp \
checkpoints.cpp \
init.cpp \
ipgroups.cpp \
leveldbwrapper.cpp \
main.cpp \
merkleblock.cpp \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ BITCOIN_TESTS =\
test/DoS_tests.cpp \
test/getarg_tests.cpp \
test/hash_tests.cpp \
test/ipgroups_tests.cpp \
test/key_tests.cpp \
test/main_tests.cpp \
test/mempool_tests.cpp \
Expand Down
45 changes: 45 additions & 0 deletions src/ipgroups.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2009-2015 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <boost/foreach.hpp>
#include "ipgroups.h"
#include "sync.h"

#include "torips.h"
#include "util.h"

static CCriticalSection cs_groups;
static std::vector<CIPGroup*> groups;

// Returns NULL if the IP does not belong to any group.
CIPGroup *FindGroupForIP(CNetAddr ip) {
if (!ip.IsValid()) {
LogPrintf("IP is not valid: %s\n", ip.ToString());
return NULL;
}

LOCK(cs_groups);
BOOST_FOREACH(CIPGroup *group, groups)
{
BOOST_FOREACH(const CSubNet &subNet, group->subnets)
{
if (subNet.Match(ip))
return group;
}
}
return NULL;
}

void InitIPGroups() {
// Load Tor Exits as a group.
CIPGroup *ipGroup = new CIPGroup("tor");
ipGroup->priority = -10;
for (const char **ptr = pszTorExits; *ptr; ptr++) {
std::string ip(*ptr);
ip = ip + "/32";
ipGroup->subnets.push_back(CSubNet(ip));
}
LOCK(cs_groups);
groups.push_back(ipGroup); // Deliberately leak it.
}
30 changes: 30 additions & 0 deletions src/ipgroups.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2009-2015 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_CIPGROUPS_H
#define BITCOIN_CIPGROUPS_H

#include "netbase.h"

// A group of logically related IP addresses. Useful for banning or deprioritising
// sources of abusive traffic/DoS attacks.
struct CIPGroup {
std::vector<CSubNet> subnets;
std::string name;
// A priority score indicates how important this group of IP addresses is to this node.
// Importance determines which group wins when the node is out of resources. Any IP
// that is not in a group gets a default priority of zero. Therefore, groups with a priority
// of less than zero will be ignored or disconnected in order to make room for ungrouped
// IPs, and groups with a higher priority will be serviced before ungrouped IPs.
int priority;

CIPGroup(const std::string &name_) : name(name_), priority(0) {}
};

// Returns NULL if the IP does not belong to any group.
CIPGroup *FindGroupForIP(CNetAddr ip);

void InitIPGroups();

#endif //BITCOIN_CIPGROUPS_H
6 changes: 4 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4188,10 +4188,12 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
if (fLogIPs)
remoteAddr = ", peeraddr=" + pfrom->addr.ToString();

LogPrintf("receive version message: %s: version %d, blocks=%d, us=%s, peer=%d%s\n",
string group = pfrom->pIpGroup ? tfm::format(", ipgroup=%s", pfrom->pIpGroup->name) : "";

LogPrintf("receive version message: %s: version %d, blocks=%d, us=%s, peerid=%d%s%s\n",
pfrom->cleanSubVer, pfrom->nVersion,
pfrom->nStartingHeight, addrMe.ToString(), pfrom->id,
remoteAddr);
group, remoteAddr);

int64_t nTimeOffset = nTime - GetTime();
pfrom->nTimeOffset = nTimeOffset;
Expand Down
44 changes: 40 additions & 4 deletions src/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "scheduler.h"
#include "ui_interface.h"
#include "crypto/common.h"
#include "ipgroups.h"

#ifdef WIN32
#include <string.h>
Expand Down Expand Up @@ -840,22 +841,53 @@ void ThreadSocketHandler()
nInbound++;
}

bool shouldConnect = true;

if (hSocket == INVALID_SOCKET)
{
int nErr = WSAGetLastError();
if (nErr != WSAEWOULDBLOCK)
LogPrintf("socket error accept failed: %s\n", NetworkErrorString(nErr));
shouldConnect = false;
}
else if (nInbound >= nMaxConnections - MAX_OUTBOUND_CONNECTIONS)
{
CloseSocket(hSocket);
// Calculate the priority of the new IP to see if we should drop it immediately (normal) or kick
// one of the other peers out to make room for it.
CIPGroup *group = FindGroupForIP(addr);
int priority = group ? group->priority : 0;

bool disconnected = false;
{
LOCK(cs_vNodes);
BOOST_FOREACH(CNode *n, vNodes)
{
int nodePriority = n->pIpGroup ? n->pIpGroup->priority : 0;
if (nodePriority < priority) {
LogPrintf("Connection slots exhausted, evicting peer %d with priority %d (group %s) to free up resources\n",
n->id, nodePriority, n->pIpGroup ? n->pIpGroup->name : "default");
n->fDisconnect = true;
disconnected = true;
// Leave shouldConnect = true to allow this socket through.
break;
}
}
}

if (!disconnected) {
CloseSocket(hSocket);
LogPrintf("Connection slots exhausted, refusing inbound connection from %s\n", addr.ToString());
shouldConnect = false;
}
}
else if (CNode::IsBanned(addr) && !whitelisted)
{
LogPrintf("connection from %s dropped (banned)\n", addr.ToString());
CloseSocket(hSocket);
shouldConnect = false;
}
else

if (shouldConnect)
{
CNode* pnode = new CNode(hSocket, addr, "", true);
pnode->AddRef();
Expand Down Expand Up @@ -1614,6 +1646,8 @@ void StartNode(boost::thread_group& threadGroup, CScheduler& scheduler)
if (pnodeLocalHost == NULL)
pnodeLocalHost = new CNode(INVALID_SOCKET, CAddress(CService("127.0.0.1", 0), nLocalServices));

InitIPGroups();

Discover(threadGroup);

//
Expand Down Expand Up @@ -1949,10 +1983,12 @@ CNode::CNode(SOCKET hSocketIn, CAddress addrIn, std::string addrNameIn, bool fIn
id = nLastNodeId++;
}

pIpGroup = FindGroupForIP(CNetAddr(addr.ToStringIP()));
std::string strIpGroup = pIpGroup ? tfm::format("(group %s)", pIpGroup->name) : "";
if (fLogIPs)
LogPrint("net", "Added connection to %s peer=%d\n", addrName, id);
LogPrint("net", "Added connection to %s peer=%d %s\n", addrName, id, strIpGroup);
else
LogPrint("net", "Added connection peer=%d\n", id);
LogPrint("net", "Added connection peer=%d %s\n", id, strIpGroup);

// Be shy and don't send version until we hear
if (hSocket != INVALID_SOCKET && !fInbound)
Expand Down
3 changes: 3 additions & 0 deletions src/net.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "sync.h"
#include "uint256.h"
#include "utilstrencodings.h"
#include "ipgroups.h"

#include <deque>
#include <stdint.h>
Expand Down Expand Up @@ -280,6 +281,8 @@ class CNode
CBloomFilter* pfilter;
int nRefCount;
NodeId id;
CIPGroup *pIpGroup;

protected:

// Denial-of-service detection/prevention
Expand Down
22 changes: 22 additions & 0 deletions src/test/ipgroups_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2015- The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <boost/test/unit_test_suite.hpp>
#include <ipgroups.h>
#include <boost/test/test_tools.hpp>

BOOST_AUTO_TEST_SUITE(ipgroups_tests);

BOOST_AUTO_TEST_CASE(ipgroup)
{
InitIPGroups();
BOOST_CHECK(FindGroupForIP(CNetAddr("0.0.0.0")) == NULL);
CIPGroup *group = FindGroupForIP(CNetAddr("0.1.2.3"));
BOOST_CHECK_EQUAL(group->name, "tor");

// If/when we have the ability to load labelled subnets from a file or persisted datastore, test that here.
}

BOOST_AUTO_TEST_SUITE_END()

Loading

0 comments on commit 5e62628

Please sign in to comment.