upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/btftunnel.h13
-rw-r--r--src/rpc.cpp28
-rw-r--r--src/test_rvaccept.cpp180
3 files changed, 211 insertions, 10 deletions
diff --git a/src/btftunnel.h b/src/btftunnel.h
index 1fa4f01..24a7509 100644
--- a/src/btftunnel.h
+++ b/src/btftunnel.h
@@ -10,8 +10,12 @@
10// caller), then BtfClientTunnel connects through the relay, runs the echan 10// caller), then BtfClientTunnel connects through the relay, runs the echan
11// handshake, and returns a local SOCKET to hand a new outbound CNode. 11// handshake, and returns a local SOCKET to hand a new outbound CNode.
12// 12//
13// Service side: after RvServiceRegister pairs us with a client, BtfServiceWrap 13// Service side: RvServiceRegister only registers -- callers must then block in
14// completes the echan handshake and returns a local SOCKET for an inbound CNode. 14// RvServiceWaitPaired until a client is actually paired to them. BtfServiceWrap
15// completes the echan handshake on that paired socket and returns a local
16// SOCKET for an inbound CNode. Wrapping a registered-but-unpaired socket reads
17// the relay's 0x01 pairing byte as the first byte of the client's ephemeral
18// key, which silently corrupts the tunnel instead of failing.
15 19
16#ifndef BITFLASH_BTFTUNNEL_H 20#ifndef BITFLASH_BTFTUNNEL_H
17#define BITFLASH_BTFTUNNEL_H 21#define BITFLASH_BTFTUNNEL_H
@@ -39,8 +43,9 @@ btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port,
39 const unsigned char target_pubkey[32], 43 const unsigned char target_pubkey[32],
40 const unsigned char service_enc_pub[32]); 44 const unsigned char service_enc_pub[32]);
41 45
42// Service: wrap an already-paired rendezvous socket (from RvServiceRegister) 46// Service: wrap an already-paired rendezvous socket (RvServiceRegister followed
43// with echan using my static x25519 secret, returning a local plaintext SOCKET 47// by RvServiceWaitPaired -- the pairing byte MUST already be consumed) with
48// echan using my static x25519 secret, returning a local plaintext SOCKET
44// for an inbound CNode. Consumes `rv` (closed when the tunnel tears down). 49// for an inbound CNode. Consumes `rv` (closed when the tunnel tears down).
45btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]); 50btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]);
46 51
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 3dd05bb..21a8d77 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -6,8 +6,8 @@
6// Architecture: 6// Architecture:
7// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout 7// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout
8// state. No locks needed on any of that data. 8// state. No locks needed on any of that data.
9// AcceptOneFn -- one thread per relay. Blocks on RvServiceRegister + 9// AcceptOneFn -- one thread per relay. Registers, waits to be paired,
10// BtfServiceWrap, pushes finished socket to gReadyQueue. 10// wraps the tunnel, pushes finished socket to gReadyQueue.
11// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every 11// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every
12// 60s so dynamically discovered relays get picked up. 12// 60s so dynamically discovered relays get picked up.
13// 13//
@@ -1388,12 +1388,11 @@ static void AcceptOneFn(void* arg)
1388 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 1388 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str());
1389 1389
1390 while (!fShutdown && gPoolRunning) { 1390 while (!fShutdown && gPoolRunning) {
1391 // Advertise this relay as the pool's meeting node BEFORE registering: 1391 // Advertise this relay as the pool's meeting node BEFORE registering,
1392 // RvServiceRegister blocks until a miner pairs, and miners can only 1392 // so that miners which resolve our descriptor find a relay where the
1393 // find us if the published descriptor already names a relay where the
1394 // pool (not the node's peer-accept) holds the registration. 1393 // pool (not the node's peer-accept) holds the registration.
1395 BtfSetActiveRelay(relay); 1394 BtfSetActiveRelay(relay);
1396 // Blocks until a miner connects to us at this relay 1395 // Returns as soon as the relay lists us -- registered, not yet paired.
1397 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 1396 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
1398 1397
1399 if (fShutdown || !gPoolRunning) { 1398 if (fShutdown || !gPoolRunning) {
@@ -1407,6 +1406,23 @@ static void AcceptOneFn(void* arg)
1407 continue; 1406 continue;
1408 } 1407 }
1409 1408
1409 // Block for a miner to actually arrive. RvServiceRegister used to do
1410 // this itself and returned with the relay's 0x01 pairing byte already
1411 // consumed; it no longer does. Without waiting here, BtfServiceWrap
1412 // reads that 0x01 as the first byte of the miner's 32-byte ephemeral
1413 // key -- every subsequent byte is shifted by one, so the X25519
1414 // handshake completes against a garbage shared secret (it has no
1415 // confirmation step to catch it), the tunnel is handed to the event
1416 // loop, and the miner's mining.subscribe never decrypts into a valid
1417 // line. The miner sees the pool close on it; the pool sees a peer that
1418 // never authorized.
1419 if (!btf::RvServiceWaitPaired(rv)) {
1420 btf::RvClose(rv);
1421 LogPrint("pool", "[pool] relay %s: dropped us before any miner dialed"
1422 " -- re-registering\n", relay.c_str());
1423 continue;
1424 }
1425
1410 // Echan handshake -- blocking but fast (local + relay round-trip) 1426 // Echan handshake -- blocking but fast (local + relay round-trip)
1411 btf_socket_t fd = btf::BtfServiceWrap(rv, sk); 1427 btf_socket_t fd = btf::BtfServiceWrap(rv, sk);
1412 if (fd == INVALID_SOCKET) { 1428 if (fd == INVALID_SOCKET) {
diff --git a/src/test_rvaccept.cpp b/src/test_rvaccept.cpp
new file mode 100644
index 0000000..afa9046
--- /dev/null
+++ b/src/test_rvaccept.cpp
@@ -0,0 +1,180 @@
1// Local regression test for the pool's rendezvous accept path.
2//
3// Reproduces the exact failure that took remote miners off the pool: since
4// upstream #30, RvServiceRegister returns as soon as the relay lists the
5// service and no longer consumes the relay's 0x01 pairing byte. A service that
6// goes straight from RvServiceRegister to BtfServiceWrap therefore reads that
7// 0x01 as the first byte of the client's 32-byte ephemeral key, shifting the
8// whole stream by one. X25519 has no confirmation step, so the handshake
9// "succeeds" against a garbage shared secret and the corruption only shows up
10// as a tunnel that never carries a valid byte.
11//
12// Build: g++ -std=gnu++14 -O2 -pthread test_rvaccept.cpp btftunnel.cpp btfrv.cpp \
13// btfchan.cpp -lsodium -o test_rvaccept
14//
15// Build: g++ -std=gnu++14 -O2 -pthread test_rvaccept.cpp btftunnel.cpp \
16// btfrv.cpp btfchan.cpp -lsodium -o test_rvaccept
17//
18// Run against a bitflashd relay already listening on 127.0.0.1:<port>.
19// test_rvaccept broken <port> -- the pre-fix pool code path, expected to FAIL
20// test_rvaccept fixed <port> -- with RvServiceWaitPaired, expected to PASS
21
22#include "btftunnel.h"
23#include "btfrv.h"
24#include "btfchan.h"
25
26#include <sys/socket.h>
27#include <sys/time.h>
28#include <unistd.h>
29#include <cstdio>
30#include <cstdlib>
31#include <cstring>
32#include <string>
33#include <vector>
34#include <thread>
35#include <chrono>
36
37using namespace btf;
38
39static const char* HOST = "127.0.0.1";
40static unsigned short PORT = 18446;
41static const char* MSG = "mining.subscribe stand-in payload";
42
43static void SetTimeout(int fd, int secs)
44{
45 struct timeval tv;
46 tv.tv_sec = secs;
47 tv.tv_usec = 0;
48 setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
49 setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
50}
51
52static bool RecvN(int fd, void* buf, int n)
53{
54 char* p = (char*)buf;
55 int off = 0;
56 while (off < n) {
57 int r = recv(fd, p + off, n - off, 0);
58 if (r <= 0) return false;
59 off += r;
60 }
61 return true;
62}
63
64static bool SendN(int fd, const void* buf, int n)
65{
66 const char* p = (const char*)buf;
67 int off = 0;
68 while (off < n) {
69 int r = send(fd, p + off, n - off, 0);
70 if (r <= 0) return false;
71 off += r;
72 }
73 return true;
74}
75
76static bool g_serviceEcho = false;
77static std::string g_serviceSaw;
78
79// The pool's AcceptOneFn, in both shapes.
80static void ServiceThread(bool fWaitPaired,
81 std::vector<unsigned char> id,
82 std::vector<unsigned char> enc_sk,
83 int len)
84{
85 RvSocket rv = RvServiceRegister(HOST, PORT, id.data());
86 if (rv == RV_INVALID) { printf(" service: RvServiceRegister failed\n"); return; }
87
88 if (fWaitPaired) {
89 if (!RvServiceWaitPaired(rv)) {
90 printf(" service: RvServiceWaitPaired failed\n");
91 RvClose(rv);
92 return;
93 }
94 printf(" service: paired (0x01 consumed by RvServiceWaitPaired)\n");
95 } else {
96 printf(" service: skipped RvServiceWaitPaired (pre-fix behaviour)\n");
97 }
98
99 btf_socket_t app = BtfServiceWrap(rv, enc_sk.data());
100 if (app == INVALID_SOCKET) { printf(" service: BtfServiceWrap failed\n"); return; }
101 printf(" service: BtfServiceWrap returned a tunnel\n");
102
103 SetTimeout(app, 5);
104 std::vector<char> buf(len);
105 if (!RecvN(app, buf.data(), len)) {
106 printf(" service: never received the client's plaintext\n");
107 close(app);
108 return;
109 }
110 g_serviceSaw.assign(buf.data(), len);
111 g_serviceEcho = SendN(app, buf.data(), len);
112 close(app);
113}
114
115int main(int argc, char* argv[])
116{
117 bool fWaitPaired = (argc >= 2 && strcmp(argv[1], "fixed") == 0);
118 if (argc >= 3) PORT = (unsigned short)atoi(argv[2]);
119
120 if (!TunnelInit()) { printf("TunnelInit failed\n"); return 2; }
121
122 unsigned char enc_pk[32], enc_sk[32];
123 ChanKeypair(enc_pk, enc_sk);
124
125 std::vector<unsigned char> id(32);
126 for (int i = 0; i < 32; i++) id[i] = (unsigned char)(i * 7 + 13);
127
128 const std::string msg(MSG);
129 const int len = (int)msg.size();
130
131 printf(" mode: %s relay: %s:%u\n", fWaitPaired ? "fixed" : "broken", HOST, PORT);
132
133 std::thread svc(ServiceThread, fWaitPaired, id,
134 std::vector<unsigned char>(enc_sk, enc_sk + 32), len);
135 std::this_thread::sleep_for(std::chrono::milliseconds(500));
136
137 int nFail = 0;
138
139 btf_socket_t app = BtfClientTunnel(HOST, PORT, id.data(), enc_pk);
140 if (app == INVALID_SOCKET) {
141 printf(" client: BtfClientTunnel failed\n");
142 nFail++;
143 } else {
144 printf(" client: tunnel established\n");
145 SetTimeout(app, 5);
146 if (!SendN(app, msg.data(), len)) { printf(" client: send failed\n"); nFail++; }
147 std::vector<char> echo(len);
148 if (!RecvN(app, echo.data(), len)) {
149 printf(" client: no echo came back\n");
150 nFail++;
151 } else if (std::string(echo.data(), len) != msg) {
152 printf(" client: echo did not match what was sent\n");
153 nFail++;
154 } else {
155 printf(" client: round-tripped the payload intact\n");
156 }
157 close(app);
158 }
159
160 svc.join();
161
162 if (g_serviceSaw != msg) {
163 printf(" service did not see the correct plaintext (saw %d bytes)\n",
164 (int)g_serviceSaw.size());
165 nFail++;
166 }
167 if (!g_serviceEcho) { printf(" service could not echo\n"); nFail++; }
168
169 bool fPassed = (nFail == 0);
170 printf(" RESULT: %s\n", fPassed ? "PASS -- tunnel carries plaintext end to end"
171 : "FAIL -- tunnel never carried a valid byte");
172
173 // A broken run is the expected outcome pre-fix, so exit 0 when the
174 // observed behaviour matches the expectation for the mode under test.
175 bool fAsExpected = (fWaitPaired ? fPassed : !fPassed);
176 printf(" EXPECTED %s -> %s\n",
177 fWaitPaired ? "PASS" : "FAIL",
178 fAsExpected ? "as expected" : "UNEXPECTED");
179 return fAsExpected ? 0 : 1;
180}