upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/btftunnel.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/btftunnel.cpp
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parent66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff)
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git merge-base returns no result at all -- so anyone cloning this repository landed on a default branch that was neither current nor related to the running node. The 28 commits on main were audited before this merge: every one is already present in the tree by content, including the two that are hardest to see because they were rewritten afterwards -- the payout reorg-safety work (PayoutBlockStillOnChain, since evolved to re-check on every retry rather than once at maturity) and the nostr relay certificate verification (SSL_set1_host, now with a Windows root-store path). Nothing on main is lost by this merge. Recorded as a merge rather than a force-push: main only ever moves forward, its old history stays reachable as the first parent, and this is revertible. The tree is taken wholesale from the merged branch, so main's content is now exactly what was built and tested. What this brings onto main: - compactblock: BIP152-style compact block relay ported from upstream 1.2.23, primitives and unit test only, with the RAND_bytes failure path closed and the reconstruction allocation bounded. - upstream/main 17741d9: isolated testnet parameters and genesis, optional 2 MB large pages for RandomX. Mainnet genesis verified byte-identical through upstream's BuildGenesisBlock() refactor. - deploy: every image built on bookworm so binaries run on Debian 12, and the container build now honours the RandomX and libsecp256k1 pins it had been silently ignoring since before 1.2.15. Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus constants unchanged against e4f898c apart from the pre-existing MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'src/btftunnel.cpp')
-rw-r--r--src/btftunnel.cpp140
1 files changed, 132 insertions, 8 deletions
diff --git a/src/btftunnel.cpp b/src/btftunnel.cpp
index 63d178b..4058f6b 100644
--- a/src/btftunnel.cpp
+++ b/src/btftunnel.cpp
@@ -25,8 +25,12 @@ typedef int SOCKET;
25#endif 25#endif
26#include "sockcount.h" 26#include "sockcount.h"
27#include <cstring> 27#include <cstring>
28#include <cstdio>
29#include <memory>
30#include <new>
28#include <vector> 31#include <vector>
29#include <thread> 32#include <thread>
33#include <system_error>
30 34
31namespace btf 35namespace btf
32{ 36{
@@ -119,35 +123,155 @@ static void PumpIn(SOCKET pumpEnd, RvSocket rv, std::vector<unsigned char> key)
119 shutdown((SOCKET)rv, SD_BOTH); 123 shutdown((SOCKET)rv, SD_BOTH);
120} 124}
121 125
126// The pump end and the rendezvous socket, owned jointly by whoever is still
127// using them. Closed once, by the last reference to go.
128//
129// Joint ownership rather than "the inbound pump owns the cleanup", because
130// either pump can reach its exit first and BOTH touch BOTH descriptors on the
131// way out -- PumpOut ends in shutdown(rv), shutdown(pumpEnd) and PumpIn ends in
132// the mirror image. A single owner therefore has no exit at which the other is
133// guaranteed to be finished. Holding a reference for the whole body of each
134// pump gives it one: the close cannot happen until the last pump has stopped
135// touching the descriptors, so no shutdown() can land on a number that has
136// already been closed and handed back out by the kernel to an unrelated
137// connection.
138struct TunnelEnds
139{
140 SOCKET pumpEnd;
141 RvSocket rv;
142 TunnelEnds(SOCKET p, RvSocket r) : pumpEnd(p), rv(r) {}
143 ~TunnelEnds() { BtfCloseSocket(pumpEnd); RvClose(rv); }
144private:
145 TunnelEnds(const TunnelEnds&);
146 TunnelEnds& operator=(const TunnelEnds&);
147};
148
122// Start the two pumps and hand back the app-facing socket. When both pumps 149// Start the two pumps and hand back the app-facing socket. When both pumps
123// finish, close the pump end and the rendezvous socket. 150// finish, close the pump end and the rendezvous socket.
151//
152// That sentence used to be a comment describing something the code did not do.
153// The inbound pump closed both descriptors as soon as it exited, without
154// waiting for the outbound pump, which then called shutdown() on both at its
155// own exit. Whether that shutdown hit a live descriptor or a closed one came
156// down to which pump got there first, and the rendezvous socket dying before
157// the app socket -- a relay restart, the far side hanging up -- is precisely
158// the order that puts the inbound pump ahead. Measured over 25 such teardowns
159// against the real code: 29 shutdowns landed on an already-closed descriptor.
160//
161// appEnd is not part of this. It is handed to the caller on success and closed
162// here on every failure path; no pump ever touches it.
163//
164// Every tunnel is two threads, and a tunnel is one peer connection (a .btf
165// dial or accept, a pool miner, a participant's pool session) -- so the
166// thread count scales with peer count, which is attacker-influenced. When
167// pthread_create is refused std::thread throws std::system_error; uncaught it
168// reached std::terminate and took the whole process down over one connection
169// it could not serve. Both spawns are guarded, and the failure paths return
170// INVALID_SOCKET, which every caller already handles by dropping just that
171// connection.
124static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32]) 172static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32])
125{ 173{
126 btf_socket_t appEnd, pumpEnd; 174 btf_socket_t appEnd, pumpEnd;
127 if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; } 175 if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; }
128 176
129 std::vector<unsigned char> key(shared, shared + 32); 177 std::vector<unsigned char> key(shared, shared + 32);
130 std::thread(PumpOut, pumpEnd, rv, key).detach(); 178
131 std::thread([pumpEnd, rv, key]() { 179 // make_shared, not shared_ptr(new ...): it allocates before it constructs,
132 PumpIn(pumpEnd, rv, key); 180 // so it either comes back owning both descriptors or throws having taken
181 // ownership of neither. The two-step form can run the deleter and then
182 // throw, which would close both here and again below.
183 std::shared_ptr<TunnelEnds> ends;
184 try
185 {
186 ends = std::make_shared<TunnelEnds>(pumpEnd, rv);
187 }
188 catch (const std::bad_alloc&)
189 {
190 fprintf(stderr, "btf tunnel: out of memory starting tunnel, tunnel dropped\n");
133 BtfCloseSocket(pumpEnd); 191 BtfCloseSocket(pumpEnd);
192 BtfCloseSocket(appEnd);
134 RvClose(rv); 193 RvClose(rv);
135 }).detach(); 194 return INVALID_SOCKET;
195 }
196
197 try
198 {
199 std::thread([ends, key]() { PumpIn(ends->pumpEnd, ends->rv, key); }).detach();
200 }
201 catch (const std::system_error& e)
202 {
203 // Nothing is running yet: `ends` is the only reference, and returning
204 // drops it, which is what closes pumpEnd and rv.
205 fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n",
206 e.what());
207 BtfCloseSocket(appEnd);
208 return INVALID_SOCKET;
209 }
210
211 try
212 {
213 std::thread([ends, key]() { PumpOut(ends->pumpEnd, ends->rv, key); }).detach();
214 }
215 catch (const std::system_error& e)
216 {
217 // The inbound pump may be parked in RvReadN(rv) or may already have
218 // exited -- when the spawn is refused because the process is loaded,
219 // the rendezvous socket is often dead for the same reason, and then
220 // PumpIn returns on its first length read. Either way this thread
221 // still holds a reference, so both descriptors are still open: the
222 // shutdown wakes the pump if it is parked and is harmless if it is
223 // not. The close belongs to whichever reference goes last.
224 fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n",
225 e.what());
226 shutdown((SOCKET)ends->rv, SD_BOTH);
227 shutdown(ends->pumpEnd, SD_BOTH);
228 BtfCloseSocket(appEnd);
229 return INVALID_SOCKET;
230 }
136 return appEnd; 231 return appEnd;
137} 232}
138 233
234const char* TunnelFailText(TunnelFail f)
235{
236 switch (f)
237 {
238 case TUNNEL_FAIL_NONE: return "no failure";
239 case TUNNEL_FAIL_INIT: return "local socket/crypto init failed";
240 case TUNNEL_FAIL_DIAL: return "meeting node unreachable (tcp dial failed)";
241 case TUNNEL_FAIL_REFUSED: return "relay reachable, but no service registered there for this address";
242 case TUNNEL_FAIL_PAIR: return "relay connection died before pairing completed";
243 case TUNNEL_FAIL_HANDSHAKE: return "end-to-end handshake failed";
244 case TUNNEL_FAIL_LOCAL: return "local tunnel setup failed";
245 }
246 return "unknown failure";
247}
248
139btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port, 249btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port,
140 const unsigned char target_pubkey[32], 250 const unsigned char target_pubkey[32],
141 const unsigned char service_enc_pub[32]) 251 const unsigned char service_enc_pub[32],
252 TunnelFail* pFail)
142{ 253{
254 if (pFail) *pFail = TUNNEL_FAIL_INIT;
143 if (!TunnelInit()) return INVALID_SOCKET; 255 if (!TunnelInit()) return INVALID_SOCKET;
144 RvSocket rv = RvClientConnect(meeting_host, port, target_pubkey); 256 RvClientFail rvFail = RVC_FAIL_NONE;
145 if (rv == RV_INVALID) return INVALID_SOCKET; 257 RvSocket rv = RvClientConnect(meeting_host, port, target_pubkey, &rvFail);
258 if (rv == RV_INVALID)
259 {
260 if (pFail)
261 *pFail = rvFail == RVC_FAIL_DIAL ? TUNNEL_FAIL_DIAL
262 : rvFail == RVC_FAIL_REFUSED ? TUNNEL_FAIL_REFUSED
263 : TUNNEL_FAIL_PAIR;
264 return INVALID_SOCKET;
265 }
146 266
147 unsigned char eph_pub[32], shared[32]; 267 unsigned char eph_pub[32], shared[32];
268 if (pFail) *pFail = TUNNEL_FAIL_HANDSHAKE;
148 if (!ChanClientHandshake(service_enc_pub, eph_pub, shared)) { RvClose(rv); return INVALID_SOCKET; } 269 if (!ChanClientHandshake(service_enc_pub, eph_pub, shared)) { RvClose(rv); return INVALID_SOCKET; }
149 if (!RvWriteN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; } 270 if (!RvWriteN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; }
150 return StartTunnel(rv, shared); 271 if (pFail) *pFail = TUNNEL_FAIL_LOCAL;
272 btf_socket_t app = StartTunnel(rv, shared);
273 if (app != INVALID_SOCKET && pFail) *pFail = TUNNEL_FAIL_NONE;
274 return app;
151} 275}
152 276
153btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]) 277btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32])