upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/btfrv.cpp
diff options
context:
space:
mode:
authorbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
committerbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
commit5ce650382352c1503f40abf08129ada69402af55 (patch)
tree6a18198901f1e2c4933f1ac53c9365eec512bf63 /src/btfrv.cpp
parentf34113291da7e5e3dc7d2468d4f9c212c05c584b (diff)
public commit
Diffstat (limited to 'src/btfrv.cpp')
-rw-r--r--src/btfrv.cpp335
1 files changed, 335 insertions, 0 deletions
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
new file mode 100644
index 0000000..6ac3280
--- /dev/null
+++ b/src/btfrv.cpp
@@ -0,0 +1,335 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Bitflash rendezvous transport. See btfrv.h.
5// Windows/Winsock implementation (Layer 6 will #ifdef this for Linux/BSD sockets).
6
7#include "btfrv.h"
8
9// Portable sockets: Winsock on Windows, BSD sockets on Linux/POSIX (for the
10// headless relay `bitflashd` that runs on the VPS).
11#ifdef _WIN32
12#include <winsock2.h>
13#include <ws2tcpip.h>
14#define CLOSESOCK BtfCloseSocket
15#else
16#include <sys/socket.h>
17#include <netinet/in.h>
18#include <arpa/inet.h>
19#include <netdb.h>
20#include <unistd.h>
21#include <csignal>
22typedef int SOCKET;
23#define INVALID_SOCKET (-1)
24#define SOCKET_ERROR (-1)
25#define CLOSESOCK BtfCloseSocket
26#ifndef SD_BOTH
27#define SD_BOTH SHUT_RDWR
28#endif
29#endif
30
31#include "sockcount.h"
32
33#include <cstring>
34#include <cstdio>
35#include <string>
36#include <map>
37#include <thread>
38#include <mutex>
39#include <atomic>
40
41namespace btf
42{
43
44static std::atomic<bool> g_stop(false);
45static SOCKET g_listen = INVALID_SOCKET;
46
47// Services that have registered and are waiting for a client, keyed by pubkey.
48static std::map<std::string, SOCKET> g_waiting;
49static std::mutex g_mtx;
50
51bool RvInit()
52{
53 static bool done = false;
54 if (done) return true;
55#ifdef _WIN32
56 WSADATA wsa;
57 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
58#else
59 signal(SIGPIPE, SIG_IGN); // don't die when writing to a closed peer
60#endif
61 done = true;
62 return true;
63}
64
65// ---- low-level helpers ----
66
67static bool ReadN(SOCKET s, void* buf, int n)
68{
69 char* p = (char*)buf;
70 int off = 0;
71 while (off < n)
72 {
73 int r = recv(s, p + off, n - off, 0);
74 if (r <= 0) return false;
75 off += r;
76 }
77 return true;
78}
79
80static bool WriteN(SOCKET s, const void* buf, int n)
81{
82 const char* p = (const char*)buf;
83 int off = 0;
84 while (off < n)
85 {
86 int r = send(s, p + off, n - off, 0);
87 if (r <= 0) return false;
88 off += r;
89 }
90 return true;
91}
92
93static SOCKET ConnectTo(const char* host, unsigned short port)
94{
95 struct addrinfo hints, *res = NULL;
96 memset(&hints, 0, sizeof(hints));
97 hints.ai_family = AF_INET;
98 hints.ai_socktype = SOCK_STREAM;
99 char portstr[16]; sprintf(portstr, "%u", (unsigned)port);
100 if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) return INVALID_SOCKET;
101 SOCKET s = BtfSocketTag(socket(res->ai_family, res->ai_socktype, res->ai_protocol), SOCK_RV_DIAL);
102 if (s == INVALID_SOCKET) { freeaddrinfo(res); return INVALID_SOCKET; }
103
104 // Apply a short timeout only for the connect() and initial handshake so an
105 // unresponsive relay doesn't block the thread. Once the tunnel is live the
106 // timeout is cleared -- a forwarding pipe must block indefinitely on recv().
107#ifdef _WIN32
108 DWORD tvShort = 10000;
109 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvShort, sizeof(tvShort));
110 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tvShort, sizeof(tvShort));
111#else
112 struct timeval tvShort; tvShort.tv_sec = 10; tvShort.tv_usec = 0;
113 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tvShort, sizeof(tvShort));
114 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tvShort, sizeof(tvShort));
115#endif
116
117 if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0)
118 {
119 CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET;
120 }
121 freeaddrinfo(res);
122
123 // Clear the timeout -- the socket is now a live tunnel and must not
124 // time out when idle. recv() will block until data arrives or peer closes.
125#ifdef _WIN32
126 DWORD tvOff = 0;
127 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvOff, sizeof(tvOff));
128 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tvOff, sizeof(tvOff));
129#else
130 struct timeval tvOff; tvOff.tv_sec = 0; tvOff.tv_usec = 0;
131 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tvOff, sizeof(tvOff));
132 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tvOff, sizeof(tvOff));
133#endif
134
135 return s;
136}
137
138// ---- relay side ----
139
140// Pump bytes from `from` to `to`. Owns closing `from`; shuts down `to` on exit
141// so the peer-direction thread unblocks and closes its own socket exactly once.
142static void Forward(SOCKET from, SOCKET to)
143{
144 char buf[32 * 1024];
145 for (;;)
146 {
147 int r = recv(from, buf, sizeof(buf), 0);
148 if (r <= 0) break;
149 if (!WriteN(to, buf, r)) break;
150 }
151 shutdown(to, SD_BOTH);
152 CLOSESOCK(from);
153}
154
155static void HandleIncoming(SOCKET s)
156{
157 unsigned char hdr[33];
158 if (!ReadN(s, hdr, 33)) { CLOSESOCK(s); return; }
159 char role = (char)hdr[0];
160 std::string pubkey((char*)hdr + 1, 32);
161
162 if (role == 'S')
163 {
164 // Register and wait; a future client will pair us.
165 std::lock_guard<std::mutex> lk(g_mtx);
166 // If a stale service for this key exists, drop it.
167 std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey);
168 if (it != g_waiting.end()) CLOSESOCK(it->second);
169 g_waiting[pubkey] = s;
170 // leave the socket open in the map; do not close here.
171 }
172 else if (role == 'C')
173 {
174 SOCKET svc = INVALID_SOCKET;
175 {
176 std::lock_guard<std::mutex> lk(g_mtx);
177 std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey);
178 if (it != g_waiting.end()) { svc = it->second; g_waiting.erase(it); }
179 }
180 if (svc == INVALID_SOCKET)
181 {
182 unsigned char no = 0x00;
183 WriteN(s, &no, 1);
184 CLOSESOCK(s);
185 return;
186 }
187 // Pair: tell both sides, then forward raw bytes bidirectionally.
188 unsigned char ok = 0x01;
189 bool a = WriteN(svc, &ok, 1);
190 bool b = WriteN(s, &ok, 1);
191 if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; }
192 std::thread(Forward, s, svc).detach();
193 std::thread(Forward, svc, s).detach();
194 }
195 else
196 {
197 CLOSESOCK(s);
198 }
199}
200
201bool RvRelayRun(unsigned short port)
202{
203 if (!RvInit()) return false;
204 g_stop = false;
205
206 g_listen = BtfSocketTag(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP), SOCK_RV_LISTEN);
207 if (g_listen == INVALID_SOCKET) return false;
208 int yes = 1;
209 setsockopt(g_listen, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
210
211 struct sockaddr_in addr;
212 memset(&addr, 0, sizeof(addr));
213 addr.sin_family = AF_INET;
214 addr.sin_addr.s_addr = INADDR_ANY;
215 addr.sin_port = htons(port);
216 if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; }
217 if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; }
218
219 while (!g_stop)
220 {
221 struct sockaddr_in cli;
222 socklen_t len = sizeof(cli);
223 SOCKET s = BtfSocketTag(accept(g_listen, (struct sockaddr*)&cli, &len), SOCK_RV_ACCEPT);
224 if (s == INVALID_SOCKET)
225 {
226 if (g_stop) break;
227 continue;
228 }
229 std::thread(HandleIncoming, s).detach();
230 }
231 CLOSESOCK(g_listen);
232 g_listen = INVALID_SOCKET;
233 return true;
234}
235
236void RvRelayStop()
237{
238 g_stop = true;
239 if (g_listen != INVALID_SOCKET)
240 CLOSESOCK(g_listen); // unblock accept()
241}
242
243// ---- service / client helpers ----
244
245RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
246 const unsigned char my_pubkey[32])
247{
248 if (!RvInit()) return RV_INVALID;
249 SOCKET s = ConnectTo(relay_host, port);
250 if (s == INVALID_SOCKET) return RV_INVALID;
251 unsigned char hdr[33];
252 hdr[0] = (unsigned char)'S';
253 memcpy(hdr + 1, my_pubkey, 32);
254 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
255 // Return here: registered, not yet paired. The relay lists the service as
256 // soon as it reads this header. The 0x01 that used to be consumed below
257 // only arrives when somebody dials us, so blocking on it made "registered"
258 // and "already in use" indistinguishable to the caller -- and left the node
259 // unable to advertise a meeting node until after it had been reached there.
260 return (RvSocket)s;
261}
262
263bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs)
264{
265 // A registration socket is idle on purpose -- we announce ourselves and
266 // wait, possibly for a long time, for someone to dial. ConnectTo() clears
267 // the socket timeout for exactly that reason, and with no timeout at all
268 // this recv() has no way to end when the path dies without either side
269 // saying so: a NAT drops the idle mapping, a relay restarts, a route
270 // changes. No FIN arrives, nothing is readable, and the call never
271 // returns. The thread stays alive and asleep, so ThreadBtfAccept never
272 // reaches the `continue` that would register again, and the node goes deaf
273 // while every outward sign says it is healthy.
274 //
275 // Seen in production on the bootstrap seed: 27 minutes without a block,
276 // eight behind the network, relay reachable throughout, fixed instantly by
277 // a restart.
278 //
279 // TCP keepalive is the obvious answer and it does not work here -- I
280 // measured it. With probes every 5s and a 2-probe limit, a socket whose
281 // replies were being dropped was still ESTAB after four minutes, the probe
282 // counter stuck at zero. So the timeout is ours to enforce, at the one
283 // place that knows what waiting means.
284 //
285 // Expiring is not a failure. It costs one reconnect per interval and
286 // returns the loop to a known state.
287 if (nTimeoutSecs > 0)
288 {
289#ifdef _WIN32
290 DWORD tv = (DWORD)nTimeoutSecs * 1000;
291 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
292#else
293 struct timeval tv; tv.tv_sec = nTimeoutSecs; tv.tv_usec = 0;
294 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
295#endif
296 }
297
298 unsigned char paired = 0;
299 bool fOk = ReadN((SOCKET)s, &paired, 1) && paired == 0x01;
300
301 // Hand back a socket that blocks again: from here it is a data tunnel, and
302 // a tunnel legitimately sits quiet between messages.
303 if (fOk && nTimeoutSecs > 0)
304 {
305#ifdef _WIN32
306 DWORD tvOff = 0;
307 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvOff, sizeof(tvOff));
308#else
309 struct timeval tvOff; tvOff.tv_sec = 0; tvOff.tv_usec = 0;
310 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, &tvOff, sizeof(tvOff));
311#endif
312 }
313 return fOk;
314}
315
316RvSocket RvClientConnect(const char* relay_host, unsigned short port,
317 const unsigned char target_pubkey[32])
318{
319 if (!RvInit()) return RV_INVALID;
320 SOCKET s = ConnectTo(relay_host, port);
321 if (s == INVALID_SOCKET) return RV_INVALID;
322 unsigned char hdr[33];
323 hdr[0] = (unsigned char)'C';
324 memcpy(hdr + 1, target_pubkey, 32);
325 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
326 unsigned char paired = 0;
327 if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; }
328 return (RvSocket)s;
329}
330
331bool RvReadN(RvSocket s, void* buf, int n) { return ReadN((SOCKET)s, buf, n); }
332bool RvWriteN(RvSocket s, const void* buf, int n) { return WriteN((SOCKET)s, buf, n); }
333void RvClose(RvSocket s) { if (s != RV_INVALID) CLOSESOCK((SOCKET)s); }
334
335} // namespace btf