diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 80 |
1 files changed, 72 insertions, 8 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index 6f81153..9cd0ed6 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -553,6 +553,12 @@ static bool SendLine(btf_socket_t fd, const json& j) | |||
| 553 | return true; | 553 | return true; |
| 554 | } | 554 | } |
| 555 | 555 | ||
| 556 | // Ceilings on what an unauthenticated peer can make the pool operator hold. | ||
| 557 | // Stratum lines are a few hundred bytes and a real pool this size will not see | ||
| 558 | // anything near a thousand miners. | ||
| 559 | static const size_t MAX_STRATUM_LINE = 64 * 1024; | ||
| 560 | static const size_t MAX_POOL_MINERS = 1000; | ||
| 561 | |||
| 556 | // Non-blocking recv: pull whatever bytes are available now into buf, then | 562 | // Non-blocking recv: pull whatever bytes are available now into buf, then |
| 557 | // return one complete line if one is ready. Three outcomes: | 563 | // return one complete line if one is ready. Three outcomes: |
| 558 | // true -- line is filled; buf may still contain more lines | 564 | // true -- line is filled; buf may still contain more lines |
| @@ -585,6 +591,16 @@ static bool RecvLine(btf_socket_t fd, std::string& buf, | |||
| 585 | } | 591 | } |
| 586 | #endif | 592 | #endif |
| 587 | 593 | ||
| 594 | // A peer that never sends a newline would otherwise grow this buffer for | ||
| 595 | // as long as it keeps writing, and it does not have to authorize first -- | ||
| 596 | // the read happens before any of that. Stratum lines are a few hundred | ||
| 597 | // bytes; anything past the cap is not a slow line, it is a flood. | ||
| 598 | if (buf.size() > MAX_STRATUM_LINE) { | ||
| 599 | disconnected = true; | ||
| 600 | buf.clear(); | ||
| 601 | return false; | ||
| 602 | } | ||
| 603 | |||
| 588 | size_t pos = buf.find('\n'); | 604 | size_t pos = buf.find('\n'); |
| 589 | if (pos == std::string::npos) return false; // no complete line yet | 605 | if (pos == std::string::npos) return false; // no complete line yet |
| 590 | 606 | ||
| @@ -668,6 +684,16 @@ static bool HandleLine(Miner* m, const std::string& rawLine, | |||
| 668 | return true; // don't disconnect on bad JSON, just ignore the line | 684 | return true; // don't disconnect on bad JSON, just ignore the line |
| 669 | } | 685 | } |
| 670 | 686 | ||
| 687 | // value() throws when the document is not an object, and again when a key | ||
| 688 | // is present with the wrong type -- "[1,2,3]" and {"method":1} are both | ||
| 689 | // valid JSON that parse cleanly and then blow up on the next two lines. | ||
| 690 | if (!req.is_object() || (req.contains("method") && !req["method"].is_string())) { | ||
| 691 | LogPrint("worker", "[worker] malformed request from %s: %.80s\n", | ||
| 692 | m->address.empty() ? "(unauth)" : m->address.c_str(), | ||
| 693 | rawLine.c_str()); | ||
| 694 | return true; | ||
| 695 | } | ||
| 696 | |||
| 671 | json id = req.value("id", json(nullptr)); | 697 | json id = req.value("id", json(nullptr)); |
| 672 | std::string method = req.value("method", ""); | 698 | std::string method = req.value("method", ""); |
| 673 | 699 | ||
| @@ -691,9 +717,13 @@ static bool HandleLine(Miner* m, const std::string& rawLine, | |||
| 691 | 717 | ||
| 692 | // ------------------------------------------------------------------ | 718 | // ------------------------------------------------------------------ |
| 693 | if (method == "mining.authorize") { | 719 | if (method == "mining.authorize") { |
| 720 | // is_array() matters as much as the element checks: indexing a json | ||
| 721 | // object with a number throws, and size() on an object returns its key | ||
| 722 | // count, so "params":{"a":1} would pass a bare size() test. | ||
| 694 | auto& p = req["params"]; | 723 | auto& p = req["params"]; |
| 695 | m->address = (p.size() > 0 && p[0].is_string()) ? p[0].get<std::string>() : ""; | 724 | bool fArr = p.is_array(); |
| 696 | m->worker = (p.size() > 1 && p[1].is_string()) ? p[1].get<std::string>() : "worker"; | 725 | m->address = (fArr && p.size() > 0 && p[0].is_string()) ? p[0].get<std::string>() : ""; |
| 726 | m->worker = (fArr && p.size() > 1 && p[1].is_string()) ? p[1].get<std::string>() : "worker"; | ||
| 697 | m->lastSeen = GetTime(); | 727 | m->lastSeen = GetTime(); |
| 698 | 728 | ||
| 699 | // Validate address before authorising | 729 | // Validate address before authorising |
| @@ -748,9 +778,13 @@ static bool HandleLine(Miner* m, const std::string& rawLine, | |||
| 748 | } | 778 | } |
| 749 | 779 | ||
| 750 | auto& p = req["params"]; | 780 | auto& p = req["params"]; |
| 751 | if (p.size() < 3) { | 781 | // Check the shape as well as the length: a submit carrying |
| 752 | LogPrint("worker", "[worker->pool] submit bad params from %s (size=%zu)\n", | 782 | // "params":[1,2,3] would otherwise reach get<std::string>() and throw |
| 753 | m->address.c_str(), p.size()); | 783 | // a json::type_error that nothing above catches, taking the pool down |
| 784 | // with it. Authorising costs an attacker nothing but an address. | ||
| 785 | if (!p.is_array() || p.size() < 3 || !p[1].is_string() || !p[2].is_string()) { | ||
| 786 | LogPrint("worker", "[worker->pool] submit bad params from %s\n", | ||
| 787 | m->address.c_str()); | ||
| 754 | reply(false, "bad params"); | 788 | reply(false, "bad params"); |
| 755 | return true; | 789 | return true; |
| 756 | } | 790 | } |
| @@ -1010,6 +1044,16 @@ static void PoolEventLoop() | |||
| 1010 | { | 1044 | { |
| 1011 | std::lock_guard<std::mutex> lk(gReadyMutex); | 1045 | std::lock_guard<std::mutex> lk(gReadyMutex); |
| 1012 | for (btf_socket_t fd : gReadyQueue) { | 1046 | for (btf_socket_t fd : gReadyQueue) { |
| 1047 | // Connecting costs nothing and every accepted socket carries a | ||
| 1048 | // read buffer, so an unbounded miner list is an unbounded | ||
| 1049 | // allocation for whoever runs the pool. | ||
| 1050 | if (gMiners.size() >= MAX_POOL_MINERS) { | ||
| 1051 | LogPrint("worker", "[pool] refusing miner fd=%d --" | ||
| 1052 | " already at %zu connections\n", | ||
| 1053 | (int)fd, gMiners.size()); | ||
| 1054 | sock_close(fd); | ||
| 1055 | continue; | ||
| 1056 | } | ||
| 1013 | gMiners.push_back(new Miner(fd)); | 1057 | gMiners.push_back(new Miner(fd)); |
| 1014 | LogPrint("worker", "[pool] miner fd=%d entered event loop" | 1058 | LogPrint("worker", "[pool] miner fd=%d entered event loop" |
| 1015 | " -- awaiting subscribe/authorize\n", (int)fd); | 1059 | " -- awaiting subscribe/authorize\n", (int)fd); |
| @@ -1154,9 +1198,29 @@ static void PoolEventLoop() | |||
| 1154 | bool disc = false; | 1198 | bool disc = false; |
| 1155 | // Drain all complete lines buffered for this miner. | 1199 | // Drain all complete lines buffered for this miner. |
| 1156 | // HandleLine always returns true; disc signals clean disconnect. | 1200 | // HandleLine always returns true; disc signals clean disconnect. |
| 1157 | while (RecvLine(m->fd, m->readBuf, line, disc)) | 1201 | // ThreadRPCServer has no handler of its own, so anything |
| 1158 | HandleLine(m, line, roundShareCount, | 1202 | // thrown here would reach the top of the thread and |
| 1159 | roundShareTotal, blocksFoundThisSession); | 1203 | // terminate the whole node -- not just the pool. Every |
| 1204 | // line arrives from an unauthenticated socket. | ||
| 1205 | while (RecvLine(m->fd, m->readBuf, line, disc)) { | ||
| 1206 | try { | ||
| 1207 | HandleLine(m, line, roundShareCount, | ||
| 1208 | roundShareTotal, blocksFoundThisSession); | ||
| 1209 | } catch (const std::exception& e) { | ||
| 1210 | LogPrint("worker", "[pool] dropping miner %s --" | ||
| 1211 | " exception handling line: %s\n", | ||
| 1212 | m->address.empty() ? "(unauth)" : m->address.c_str(), | ||
| 1213 | e.what()); | ||
| 1214 | disc = true; | ||
| 1215 | break; | ||
| 1216 | } catch (...) { | ||
| 1217 | LogPrint("worker", "[pool] dropping miner %s --" | ||
| 1218 | " unknown exception handling line\n", | ||
| 1219 | m->address.empty() ? "(unauth)" : m->address.c_str()); | ||
| 1220 | disc = true; | ||
| 1221 | break; | ||
| 1222 | } | ||
| 1223 | } | ||
| 1160 | if (disc) dead.push_back(m); | 1224 | if (disc) dead.push_back(m); |
| 1161 | } | 1225 | } |
| 1162 | for (Miner* m : dead) { | 1226 | for (Miner* m : dead) { |