Skip to content

Commit a336518

Browse files
committed
Merge #19643: Add -netinfo peer connections dashboard
bf1f913 cli -netinfo: display multiple levels of details (Jon Atack) 077b3ac cli: change -netinfo optional arg from bool to int (Jon Atack) 4e2f2dd cli: add getpeerinfo last_{block,transaction} to -netinfo (Jon Atack) 644be65 cli: add -netinfo server version check and error message (Jon Atack) ce57bf6 cli: create peer connections report sorted by dir, minping (Jon Atack) f5edd66 cli: create vector of Peer structs for peers data (Jon Atack) 3a0ab93 cli: add NetType enum struct and NetTypeEnumToString() (Jon Atack) c227100 cli: create local addresses, ports, and scores report (Jon Atack) d3f77b7 cli: create inbound/outbound peer connections report (Jon Atack) 19377b2 cli: start dashboard report with chain and version header (Jon Atack) a3653c1 cli: tally peer connections by type (Jon Atack) 54799b6 cli: add ipv6 and onion address type detection helpers (Jon Atack) 12242b1 cli: create initial -netinfo option, NetinfoRequestHandler class (Jon Atack) Pull request description: This PR is inspired by laanwj's python script mentioned in #19405, which it turns out I ended up using every day and extending because I got hooked on using it to monitor Bitcoin peer connections. For the full experience, run `./src/bitcoin-cli -netinfo 4` On Linux, try it with watch `watch ./src/bitcoin-cli -netinfo 4` Help doc ``` $ ./src/bitcoin-cli -help | grep -A3 netinfo -netinfo Get network peer connection information from the remote server. An optional integer argument from 0 to 4 can be passed for different peers listings (default: 0). ``` ACKs for top commit: vasild: ACK bf1f913 0xB10C: ACK bf1f913 practicalswift: ACK bf1f913 -- patch looks correct and is limited to `src/bitcoin-cli.cpp` Tree-SHA512: b9d18e5cc2ffd2bb9f0295b5ac7609da8a9bbecaf823a26dfa706b5f07d5d1a8343081dad98b16aa9dc8efd8f41bc1a4acdc40259727de622dc7195ccf59c572
2 parents 5d5e335 + bf1f913 commit a336518

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed

src/bitcoin-cli.cpp

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ static const char DEFAULT_RPCCONNECT[] = "127.0.0.1";
3939
static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900;
4040
static const bool DEFAULT_NAMED=false;
4141
static const int CONTINUE_EXECUTION=-1;
42+
static const std::string ONION{".onion"};
43+
static const size_t ONION_LEN{ONION.size()};
4244

4345
/** Default number of blocks to generate for RPC generatetoaddress. */
4446
static const std::string DEFAULT_NBLOCKS = "1";
@@ -56,6 +58,8 @@ static void SetupCliArgs(ArgsManager& argsman)
5658
argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
5759
argsman.AddArg("-generate", strprintf("Generate blocks immediately, equivalent to RPC generatenewaddress followed by RPC generatetoaddress. Optional positional integer arguments are number of blocks to generate (default: %s) and maximum iterations to try (default: %s), equivalent to RPC generatetoaddress nblocks and maxtries arguments. Example: bitcoin-cli -generate 4 1000", DEFAULT_NBLOCKS, DEFAULT_MAX_TRIES), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
5860
argsman.AddArg("-getinfo", "Get general information from the remote server. Note that unlike server-side RPC calls, the results of -getinfo is the result of multiple non-atomic requests. Some entries in the result may represent results from different states (e.g. wallet balance may be as of a different block from the chain state reported)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
61+
argsman.AddArg("-netinfo", "Get network peer connection information from the remote server. An optional integer argument from 0 to 4 can be passed for different peers listings (default: 0).", ArgsManager::ALLOW_INT, OptionsCategory::OPTIONS);
62+
5963
SetupChainParamsBaseOptions(argsman);
6064
argsman.AddArg("-named", strprintf("Pass named instead of positional arguments (default: %s)", DEFAULT_NAMED), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
6165
argsman.AddArg("-rpcclienttimeout=<n>", strprintf("Timeout in seconds during HTTP requests, or 0 for no timeout. (default: %d)", DEFAULT_HTTP_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@@ -297,6 +301,211 @@ class GetinfoRequestHandler: public BaseRequestHandler
297301
}
298302
};
299303

304+
/** Process netinfo requests */
305+
class NetinfoRequestHandler : public BaseRequestHandler
306+
{
307+
private:
308+
bool IsAddrIPv6(const std::string& addr) const
309+
{
310+
return !addr.empty() && addr.front() == '[';
311+
}
312+
bool IsInboundOnion(const std::string& addr_local, int mapped_as) const
313+
{
314+
return mapped_as == 0 && addr_local.find(ONION) != std::string::npos;
315+
}
316+
bool IsOutboundOnion(const std::string& addr, int mapped_as) const
317+
{
318+
const size_t addr_len{addr.size()};
319+
const size_t onion_pos{addr.rfind(ONION)};
320+
return mapped_as == 0 && onion_pos != std::string::npos && addr_len > ONION_LEN &&
321+
(onion_pos == addr_len - ONION_LEN || onion_pos == addr.find_last_of(":") - ONION_LEN);
322+
}
323+
uint8_t m_details_level{0}; //!< Optional user-supplied arg to set dashboard details level
324+
bool DetailsRequested() const { return m_details_level > 0 && m_details_level < 5; }
325+
bool IsAddressSelected() const { return m_details_level == 2 || m_details_level == 4; }
326+
bool IsVersionSelected() const { return m_details_level == 3 || m_details_level == 4; }
327+
enum struct NetType {
328+
ipv4,
329+
ipv6,
330+
onion,
331+
};
332+
struct Peer {
333+
int id;
334+
int mapped_as;
335+
int version;
336+
int64_t conn_time;
337+
int64_t last_blck;
338+
int64_t last_recv;
339+
int64_t last_send;
340+
int64_t last_trxn;
341+
double min_ping;
342+
double ping;
343+
std::string addr;
344+
std::string sub_version;
345+
NetType net_type;
346+
bool is_block_relay;
347+
bool is_outbound;
348+
bool operator<(const Peer& rhs) const { return std::tie(is_outbound, min_ping) < std::tie(rhs.is_outbound, rhs.min_ping); }
349+
};
350+
std::string NetTypeEnumToString(NetType t)
351+
{
352+
switch (t) {
353+
case NetType::ipv4: return "ipv4";
354+
case NetType::ipv6: return "ipv6";
355+
case NetType::onion: return "onion";
356+
} // no default case, so the compiler can warn about missing cases
357+
assert(false);
358+
}
359+
std::string ChainToString() const
360+
{
361+
if (gArgs.GetChainName() == CBaseChainParams::TESTNET) return " testnet";
362+
if (gArgs.GetChainName() == CBaseChainParams::REGTEST) return " regtest";
363+
return "";
364+
}
365+
public:
366+
const int ID_PEERINFO = 0;
367+
const int ID_NETWORKINFO = 1;
368+
369+
UniValue PrepareRequest(const std::string& method, const std::vector<std::string>& args) override
370+
{
371+
if (!args.empty()) {
372+
uint8_t n{0};
373+
if (ParseUInt8(args.at(0), &n)) {
374+
m_details_level = n;
375+
}
376+
}
377+
UniValue result(UniValue::VARR);
378+
result.push_back(JSONRPCRequestObj("getpeerinfo", NullUniValue, ID_PEERINFO));
379+
result.push_back(JSONRPCRequestObj("getnetworkinfo", NullUniValue, ID_NETWORKINFO));
380+
return result;
381+
}
382+
383+
UniValue ProcessReply(const UniValue& batch_in) override
384+
{
385+
const std::vector<UniValue> batch{JSONRPCProcessBatchReply(batch_in)};
386+
if (!batch[ID_PEERINFO]["error"].isNull()) return batch[ID_PEERINFO];
387+
if (!batch[ID_NETWORKINFO]["error"].isNull()) return batch[ID_NETWORKINFO];
388+
389+
const UniValue& networkinfo{batch[ID_NETWORKINFO]["result"]};
390+
if (networkinfo["version"].get_int() < 209900) {
391+
throw std::runtime_error("-netinfo requires bitcoind server to be running v0.21.0 and up");
392+
}
393+
394+
// Count peer connection totals, and if DetailsRequested(), store peer data in a vector of structs.
395+
const int64_t time_now{GetSystemTimeInSeconds()};
396+
int ipv4_i{0}, ipv6_i{0}, onion_i{0}, block_relay_i{0}, total_i{0}; // inbound conn counters
397+
int ipv4_o{0}, ipv6_o{0}, onion_o{0}, block_relay_o{0}, total_o{0}; // outbound conn counters
398+
size_t max_peer_id_length{2}, max_addr_length{0};
399+
bool is_asmap_on{false};
400+
std::vector<Peer> peers;
401+
const UniValue& getpeerinfo{batch[ID_PEERINFO]["result"]};
402+
403+
for (const UniValue& peer : getpeerinfo.getValues()) {
404+
const std::string addr{peer["addr"].get_str()};
405+
const std::string addr_local{peer["addrlocal"].isNull() ? "" : peer["addrlocal"].get_str()};
406+
const int mapped_as{peer["mapped_as"].isNull() ? 0 : peer["mapped_as"].get_int()};
407+
const bool is_block_relay{!peer["relaytxes"].get_bool()};
408+
const bool is_inbound{peer["inbound"].get_bool()};
409+
NetType net_type{NetType::ipv4};
410+
if (is_inbound) {
411+
if (IsAddrIPv6(addr)) {
412+
net_type = NetType::ipv6;
413+
++ipv6_i;
414+
} else if (IsInboundOnion(addr_local, mapped_as)) {
415+
net_type = NetType::onion;
416+
++onion_i;
417+
} else {
418+
++ipv4_i;
419+
}
420+
if (is_block_relay) ++block_relay_i;
421+
} else {
422+
if (IsAddrIPv6(addr)) {
423+
net_type = NetType::ipv6;
424+
++ipv6_o;
425+
} else if (IsOutboundOnion(addr, mapped_as)) {
426+
net_type = NetType::onion;
427+
++onion_o;
428+
} else {
429+
++ipv4_o;
430+
}
431+
if (is_block_relay) ++block_relay_o;
432+
}
433+
if (DetailsRequested()) {
434+
// Push data for this peer to the peers vector.
435+
const int peer_id{peer["id"].get_int()};
436+
const int version{peer["version"].get_int()};
437+
const std::string sub_version{peer["subver"].get_str()};
438+
const int64_t conn_time{peer["conntime"].get_int64()};
439+
const int64_t last_blck{peer["last_block"].get_int64()};
440+
const int64_t last_recv{peer["lastrecv"].get_int64()};
441+
const int64_t last_send{peer["lastsend"].get_int64()};
442+
const int64_t last_trxn{peer["last_transaction"].get_int64()};
443+
const double min_ping{peer["minping"].isNull() ? -1 : peer["minping"].get_real()};
444+
const double ping{peer["pingtime"].isNull() ? -1 : peer["pingtime"].get_real()};
445+
peers.push_back({peer_id, mapped_as, version, conn_time, last_blck, last_recv, last_send, last_trxn, min_ping, ping, addr, sub_version, net_type, is_block_relay, !is_inbound});
446+
max_peer_id_length = std::max(ToString(peer_id).length(), max_peer_id_length);
447+
max_addr_length = std::max(addr.length() + 1, max_addr_length);
448+
is_asmap_on |= (mapped_as != 0);
449+
}
450+
}
451+
452+
// Generate report header.
453+
std::string result{strprintf("%s %s%s - %i%s\n\n", PACKAGE_NAME, FormatFullVersion(), ChainToString(), networkinfo["protocolversion"].get_int(), networkinfo["subversion"].get_str())};
454+
455+
// Report detailed peer connections list sorted by direction and minimum ping time.
456+
if (DetailsRequested() && !peers.empty()) {
457+
std::sort(peers.begin(), peers.end());
458+
result += "Peer connections sorted by direction and min ping\n<-> relay net mping ping send recv txn blk uptime ";
459+
if (is_asmap_on) result += " asmap ";
460+
result += strprintf("%*s %-*s%s\n", max_peer_id_length, "id", IsAddressSelected() ? max_addr_length : 0, IsAddressSelected() ? "address" : "", IsVersionSelected() ? "version" : "");
461+
for (const Peer& peer : peers) {
462+
std::string version{ToString(peer.version) + peer.sub_version};
463+
result += strprintf(
464+
"%3s %5s %5s%6s%7s%5s%5s%5s%5s%7s%*i %*s %-*s%s\n",
465+
peer.is_outbound ? "out" : "in",
466+
peer.is_block_relay ? "block" : "full",
467+
NetTypeEnumToString(peer.net_type),
468+
peer.min_ping == -1 ? "" : ToString(round(1000 * peer.min_ping)),
469+
peer.ping == -1 ? "" : ToString(round(1000 * peer.ping)),
470+
peer.last_send == 0 ? "" : ToString(time_now - peer.last_send),
471+
peer.last_recv == 0 ? "" : ToString(time_now - peer.last_recv),
472+
peer.last_trxn == 0 ? "" : ToString((time_now - peer.last_trxn) / 60),
473+
peer.last_blck == 0 ? "" : ToString((time_now - peer.last_blck) / 60),
474+
peer.conn_time == 0 ? "" : ToString((time_now - peer.conn_time) / 60),
475+
is_asmap_on ? 7 : 0, // variable spacing
476+
is_asmap_on && peer.mapped_as != 0 ? ToString(peer.mapped_as) : "",
477+
max_peer_id_length, // variable spacing
478+
peer.id,
479+
IsAddressSelected() ? max_addr_length : 0, // variable spacing
480+
IsAddressSelected() ? peer.addr : "",
481+
IsVersionSelected() && version != "0" ? version : "");
482+
}
483+
result += " ms ms sec sec min min min\n\n";
484+
}
485+
486+
// Report peer connection totals by type.
487+
total_i = ipv4_i + ipv6_i + onion_i;
488+
total_o = ipv4_o + ipv6_o + onion_o;
489+
result += " ipv4 ipv6 onion total block-relay\n";
490+
result += strprintf("in %5i %5i %5i %5i %5i\n", ipv4_i, ipv6_i, onion_i, total_i, block_relay_i);
491+
result += strprintf("out %5i %5i %5i %5i %5i\n", ipv4_o, ipv6_o, onion_o, total_o, block_relay_o);
492+
result += strprintf("total %5i %5i %5i %5i %5i\n", ipv4_i + ipv4_o, ipv6_i + ipv6_o, onion_i + onion_o, total_i + total_o, block_relay_i + block_relay_o);
493+
494+
// Report local addresses, ports, and scores.
495+
result += "\nLocal addresses";
496+
const UniValue& local_addrs{networkinfo["localaddresses"]};
497+
if (local_addrs.empty()) {
498+
result += ": n/a\n";
499+
} else {
500+
for (const UniValue& addr : local_addrs.getValues()) {
501+
result += strprintf("\n%-40i port %5i score %6i", addr["address"].get_str(), addr["port"].get_int(), addr["score"].get_int());
502+
}
503+
}
504+
505+
return JSONRPCReplyObj(UniValue{result}, NullUniValue, 1);
506+
}
507+
};
508+
300509
/** Process RPC generatetoaddress request. */
301510
class GenerateToAddressRequestHandler : public BaseRequestHandler
302511
{
@@ -624,6 +833,8 @@ static int CommandLineRPC(int argc, char *argv[])
624833
std::string method;
625834
if (gArgs.IsArgSet("-getinfo")) {
626835
rh.reset(new GetinfoRequestHandler());
836+
} else if (gArgs.GetBoolArg("-netinfo", false)) {
837+
rh.reset(new NetinfoRequestHandler());
627838
} else if (gArgs.GetBoolArg("-generate", false)) {
628839
const UniValue getnewaddress{GetNewAddress()};
629840
const UniValue& error{find_value(getnewaddress, "error")};

0 commit comments

Comments
 (0)