upleb.uk

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

summaryrefslogtreecommitdiff
path: root/relay
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 /relay
parentf34113291da7e5e3dc7d2468d4f9c212c05c584b (diff)
public commit
Diffstat (limited to 'relay')
-rw-r--r--relay/README.md57
-rw-r--r--relay/bitflashd.cpp45
-rw-r--r--relay/btfrv.cpp392
-rw-r--r--relay/btfrv.h59
-rw-r--r--relay/build.sh6
-rw-r--r--relay/install-bitflash-relay.sh345
6 files changed, 904 insertions, 0 deletions
diff --git a/relay/README.md b/relay/README.md
new file mode 100644
index 0000000..abf0f8d
--- /dev/null
+++ b/relay/README.md
@@ -0,0 +1,57 @@
1# bitflashd — Bitflash rendezvous relay (the "meeting node")
2
3This is the small always-on server that lets Bitflash nodes behind CGNAT reach
4each other. Both sides connect **out** to this relay; it pairs them by their
5`.btf` pubkey and forwards ciphertext. It holds **no keys** and reads nothing —
6the two nodes are encrypted end-to-end (X25519 + ChaCha20-Poly1305) over it.
7
8Run it on any machine with a public IP (a cheap VPS). It has **no dependencies**
9beyond a C++ compiler.
10
11## Build (on the VPS, Linux)
12
13```
14sudo apt-get update && sudo apt-get install -y g++ # if needed
15bash build.sh
16```
17
18## Run
19
20```
21./bitflashd 8434
22```
23
24Open the port in the firewall (both the VPS provider's panel and the OS):
25
26```
27sudo ufw allow 8434/tcp # if ufw is enabled
28```
29
30## Keep it running (systemd)
31
32Create `/etc/systemd/system/bitflashd.service`:
33
34```
35[Unit]
36Description=Bitflash rendezvous relay
37After=network.target
38
39[Service]
40ExecStart=/root/bitflash/relay/bitflashd 8434
41Restart=always
42User=root
43
44[Install]
45WantedBy=multi-user.target
46```
47
48Then:
49
50```
51sudo systemctl daemon-reload
52sudo systemctl enable --now bitflashd
53sudo systemctl status bitflashd
54```
55
56The relay's address for clients is then `YOUR_VPS_IP:8434` — that is what goes
57into a node's descriptor as its `meeting_node`.
diff --git a/relay/bitflashd.cpp b/relay/bitflashd.cpp
new file mode 100644
index 0000000..d4ced77
--- /dev/null
+++ b/relay/bitflashd.cpp
@@ -0,0 +1,45 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// bitflashd -- the headless Bitflash rendezvous relay (the "meeting node").
5// Runs on a machine with a public IP (a cheap VPS) and lets nodes behind CGNAT
6// reach each other: both sides connect OUT to this relay, which pairs them by
7// `.btf` pubkey and forwards ciphertext (it holds no keys and reads nothing).
8//
9// Build on the VPS (Linux):
10// g++ -std=gnu++14 -O2 -pthread bitflashd.cpp btfrv.cpp -o bitflashd
11// Run:
12// ./bitflashd 8434 # or any port you open in the firewall
13//
14// Cross-builds unchanged on Windows (for local testing):
15// g++ -std=gnu++14 -O2 -pthread bitflashd.cpp btfrv.cpp -lws2_32 -o bitflashd.exe
16
17#include "btfrv.h"
18#include <cstdio>
19#include <cstdlib>
20#include <cstring>
21
22int main(int argc, char* argv[])
23{
24 unsigned short port = 8434;
25 if (argc >= 2)
26 port = (unsigned short)atoi(argv[1]);
27
28 printf("bitflashd -- Bitflash rendezvous relay\n");
29 printf("listening on 0.0.0.0:%u (open this port in your firewall)\n", port);
30 fflush(stdout);
31
32 if (!btf::RvInit())
33 {
34 fprintf(stderr, "bitflashd: socket init failed\n");
35 return 1;
36 }
37
38 // Blocks forever, forwarding between paired peers.
39 if (!btf::RvRelayRun(port))
40 {
41 fprintf(stderr, "bitflashd: could not bind/listen on port %u\n", port);
42 return 1;
43 }
44 return 0;
45}
diff --git a/relay/btfrv.cpp b/relay/btfrv.cpp
new file mode 100644
index 0000000..833c47c
--- /dev/null
+++ b/relay/btfrv.cpp
@@ -0,0 +1,392 @@
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 closesocket
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 ::close
26#ifndef SD_BOTH
27#define SD_BOTH SHUT_RDWR
28#endif
29#endif
30
31#include <cstring>
32#include <cstdio>
33#include <string>
34#include <deque>
35#include <map>
36#include <thread>
37#include <mutex>
38#include <atomic>
39#include <chrono>
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.
48// More than one registration per node, on purpose.
49//
50// This used to be one socket per pubkey, and a dial *consumed* it: the first
51// caller took the registration and everyone else was told "not found" until the
52// node noticed and registered again. A node re-registers only after its pairing
53// has been handed off, so between the two there is a window where it is
54// unreachable to the whole network. Measured from a node's own counters: half
55// of all outbound dials failed that way -- 15 of 30 in eleven minutes -- while
56// descriptors resolved 53 times out of 54 and the node's own registrations
57// paired 11 times out of 12. The rendezvous was not losing anybody; it was
58// letting one caller in at a time.
59//
60// A queue per key lets a node keep spare registrations parked, so a dial that
61// arrives while another is being paired finds the next one instead of a closed
62// door. Old nodes register exactly as before and simply keep one entry: the
63// wire protocol does not change, so a relay running this serves both.
64static const size_t MAX_WAITING_PER_KEY = 4;
65static std::map<std::string, std::deque<SOCKET> > g_waiting;
66static std::mutex g_mtx;
67
68// How often the reaper walks the waiting map.
69static const int REAP_INTERVAL_SECS = 60;
70
71bool RvInit()
72{
73 static bool done = false;
74 if (done) return true;
75#ifdef _WIN32
76 WSADATA wsa;
77 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
78#else
79 signal(SIGPIPE, SIG_IGN); // don't die when writing to a closed peer
80#endif
81 done = true;
82 return true;
83}
84
85// ---- low-level helpers ----
86
87static bool ReadN(SOCKET s, void* buf, int n)
88{
89 char* p = (char*)buf;
90 int off = 0;
91 while (off < n)
92 {
93 int r = recv(s, p + off, n - off, 0);
94 if (r <= 0) return false;
95 off += r;
96 }
97 return true;
98}
99
100static bool WriteN(SOCKET s, const void* buf, int n)
101{
102 const char* p = (const char*)buf;
103 int off = 0;
104 while (off < n)
105 {
106 int r = send(s, p + off, n - off, 0);
107 if (r <= 0) return false;
108 off += r;
109 }
110 return true;
111}
112
113static SOCKET ConnectTo(const char* host, unsigned short port)
114{
115 struct addrinfo hints, *res = NULL;
116 memset(&hints, 0, sizeof(hints));
117 hints.ai_family = AF_INET;
118 hints.ai_socktype = SOCK_STREAM;
119 char portstr[16]; sprintf(portstr, "%u", (unsigned)port);
120 if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) return INVALID_SOCKET;
121 SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
122 if (s == INVALID_SOCKET) { freeaddrinfo(res); return INVALID_SOCKET; }
123 if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0)
124 {
125 CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET;
126 }
127 freeaddrinfo(res);
128 return s;
129}
130
131// ---- liveness of a waiting registration ----
132
133// True when the far end of a registration socket is provably gone.
134//
135// A registered service sends nothing while it waits, so the socket should never
136// be readable. Readable therefore means one of two things, and recv tells them
137// apart: 0 is a clean close, negative is an error, and either way the
138// registration is worthless. Anything else -- including a service that somehow
139// has data pending -- is left alone, because guessing wrong here unregisters a
140// node that is perfectly fine.
141//
142// select() rather than MSG_DONTWAIT so the same code compiles on Windows, and
143// so the socket's blocking mode is never touched.
144static bool RegistrationDead(SOCKET s)
145{
146 fd_set rd;
147 FD_ZERO(&rd);
148 FD_SET(s, &rd);
149 struct timeval tv;
150 tv.tv_sec = 0;
151 tv.tv_usec = 0;
152 int r = select((int)(s + 1), &rd, NULL, NULL, &tv);
153 if (r <= 0)
154 return false; // not readable: still waiting, healthy
155 char c;
156 int n = recv(s, &c, 1, MSG_PEEK);
157 return (n <= 0);
158}
159
160// Drop registrations whose far end has gone away.
161//
162// Deliberately no expiry by age. Nodes older than v1.2.7 register once and then
163// block forever waiting to be dialled -- they never re-register, so a clock
164// based rule would quietly make every one of them unreachable, and being
165// blocked in recv they would never find out. Only provable death counts.
166static void ReapDeadRegistrations()
167{
168 int nBefore = 0, nReaped = 0;
169 {
170 std::lock_guard<std::mutex> lk(g_mtx);
171 std::map<std::string, std::deque<SOCKET> >::iterator it = g_waiting.begin();
172 while (it != g_waiting.end())
173 {
174 std::deque<SOCKET>& q = it->second;
175 nBefore += (int)q.size();
176 for (std::deque<SOCKET>::iterator jt = q.begin(); jt != q.end();)
177 {
178 if (RegistrationDead(*jt))
179 {
180 CLOSESOCK(*jt);
181 jt = q.erase(jt);
182 nReaped++;
183 }
184 else
185 ++jt;
186 }
187 if (q.empty())
188 g_waiting.erase(it++);
189 else
190 ++it;
191 }
192 }
193 if (nReaped > 0)
194 printf("reaper: dropped %d dead registration(s), %d waiting\n",
195 nReaped, nBefore - nReaped);
196 fflush(stdout);
197}
198
199static void ReaperThread()
200{
201 while (!g_stop)
202 {
203 for (int i = 0; i < REAP_INTERVAL_SECS && !g_stop; i++)
204 std::this_thread::sleep_for(std::chrono::seconds(1));
205 if (g_stop) break;
206 ReapDeadRegistrations();
207 }
208}
209
210// ---- relay side ----
211
212// Pump bytes from `from` to `to`. Owns closing `from`; shuts down `to` on exit
213// so the peer-direction thread unblocks and closes its own socket exactly once.
214static void Forward(SOCKET from, SOCKET to)
215{
216 char buf[32 * 1024];
217 for (;;)
218 {
219 int r = recv(from, buf, sizeof(buf), 0);
220 if (r <= 0) break;
221 if (!WriteN(to, buf, r)) break;
222 }
223 shutdown(to, SD_BOTH);
224 CLOSESOCK(from);
225}
226
227static void HandleIncoming(SOCKET s)
228{
229 unsigned char hdr[33];
230 if (!ReadN(s, hdr, 33)) { CLOSESOCK(s); return; }
231 char role = (char)hdr[0];
232 std::string pubkey((char*)hdr + 1, 32);
233
234 if (role == 'S')
235 {
236 // Register and wait; a future client will pair us.
237 std::lock_guard<std::mutex> lk(g_mtx);
238 std::deque<SOCKET>& q = g_waiting[pubkey];
239
240 // Drop registrations this node has already lost the far end of, rather
241 // than dropping the previous one on principle. The old code closed the
242 // prior registration on every new one, which is why a node could never
243 // hold a spare.
244 for (std::deque<SOCKET>::iterator it = q.begin(); it != q.end();)
245 {
246 if (RegistrationDead(*it)) { CLOSESOCK(*it); it = q.erase(it); }
247 else ++it;
248 }
249
250 // A ceiling, because a stranger can open these. Oldest goes first: it
251 // is the one most likely to be stale.
252 while (q.size() >= MAX_WAITING_PER_KEY)
253 {
254 CLOSESOCK(q.front());
255 q.pop_front();
256 }
257 q.push_back(s);
258 // leave the socket open in the queue; do not close here.
259 }
260 else if (role == 'C')
261 {
262 SOCKET svc = INVALID_SOCKET;
263 {
264 std::lock_guard<std::mutex> lk(g_mtx);
265 std::map<std::string, std::deque<SOCKET> >::iterator it = g_waiting.find(pubkey);
266 if (it != g_waiting.end())
267 {
268 std::deque<SOCKET>& q = it->second;
269 // Take the oldest one that is still alive, discarding the dead
270 // as we go. Checking before handing it over matters: pairing a
271 // client with a dead registration produces a tunnel that fails
272 // after both sides think they succeeded, which is the failure
273 // the dialling node reports as "tunnel to X failed" with
274 // nothing to explain it.
275 while (!q.empty())
276 {
277 SOCKET candidate = q.front();
278 q.pop_front();
279 if (RegistrationDead(candidate))
280 {
281 CLOSESOCK(candidate);
282 continue;
283 }
284 svc = candidate;
285 break;
286 }
287 if (q.empty())
288 g_waiting.erase(it);
289 }
290 }
291 if (svc == INVALID_SOCKET)
292 {
293 unsigned char no = 0x00;
294 WriteN(s, &no, 1);
295 CLOSESOCK(s);
296 return;
297 }
298 // Pair: tell both sides, then forward raw bytes bidirectionally.
299 unsigned char ok = 0x01;
300 bool a = WriteN(svc, &ok, 1);
301 bool b = WriteN(s, &ok, 1);
302 if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; }
303 std::thread(Forward, s, svc).detach();
304 std::thread(Forward, svc, s).detach();
305 }
306 else
307 {
308 CLOSESOCK(s);
309 }
310}
311
312bool RvRelayRun(unsigned short port)
313{
314 if (!RvInit()) return false;
315 g_stop = false;
316
317 g_listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
318 if (g_listen == INVALID_SOCKET) return false;
319 int yes = 1;
320 setsockopt(g_listen, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
321
322 struct sockaddr_in addr;
323 memset(&addr, 0, sizeof(addr));
324 addr.sin_family = AF_INET;
325 addr.sin_addr.s_addr = INADDR_ANY;
326 addr.sin_port = htons(port);
327 if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; }
328 if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; }
329
330 std::thread(ReaperThread).detach();
331
332 while (!g_stop)
333 {
334 struct sockaddr_in cli;
335 socklen_t len = sizeof(cli);
336 SOCKET s = accept(g_listen, (struct sockaddr*)&cli, &len);
337 if (s == INVALID_SOCKET)
338 {
339 if (g_stop) break;
340 continue;
341 }
342 std::thread(HandleIncoming, s).detach();
343 }
344 CLOSESOCK(g_listen);
345 g_listen = INVALID_SOCKET;
346 return true;
347}
348
349void RvRelayStop()
350{
351 g_stop = true;
352 if (g_listen != INVALID_SOCKET)
353 CLOSESOCK(g_listen); // unblock accept()
354}
355
356// ---- service / client helpers ----
357
358RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
359 const unsigned char my_pubkey[32])
360{
361 if (!RvInit()) return RV_INVALID;
362 SOCKET s = ConnectTo(relay_host, port);
363 if (s == INVALID_SOCKET) return RV_INVALID;
364 unsigned char hdr[33];
365 hdr[0] = (unsigned char)'S';
366 memcpy(hdr + 1, my_pubkey, 32);
367 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
368 unsigned char paired = 0;
369 if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; }
370 return (RvSocket)s;
371}
372
373RvSocket RvClientConnect(const char* relay_host, unsigned short port,
374 const unsigned char target_pubkey[32])
375{
376 if (!RvInit()) return RV_INVALID;
377 SOCKET s = ConnectTo(relay_host, port);
378 if (s == INVALID_SOCKET) return RV_INVALID;
379 unsigned char hdr[33];
380 hdr[0] = (unsigned char)'C';
381 memcpy(hdr + 1, target_pubkey, 32);
382 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
383 unsigned char paired = 0;
384 if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; }
385 return (RvSocket)s;
386}
387
388bool RvReadN(RvSocket s, void* buf, int n) { return ReadN((SOCKET)s, buf, n); }
389bool RvWriteN(RvSocket s, const void* buf, int n) { return WriteN((SOCKET)s, buf, n); }
390void RvClose(RvSocket s) { if (s != RV_INVALID) CLOSESOCK((SOCKET)s); }
391
392} // namespace btf
diff --git a/relay/btfrv.h b/relay/btfrv.h
new file mode 100644
index 0000000..c776201
--- /dev/null
+++ b/relay/btfrv.h
@@ -0,0 +1,59 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Bitflash rendezvous transport -- the "meeting node" that lets two nodes behind
5// CGNAT connect without a public IP or port forwarding. Both sides connect OUT
6// to the relay (which HAS a public IP, e.g. the VPS); the relay pairs them by
7// `.btf` pubkey and forwards raw bytes between them. The endpoints then run the
8// echan end-to-end channel on top, so the relay sees only ciphertext and cannot
9// MITM (the client boxes to the service's STATIC key from its self-certified
10// descriptor -- a substituting relay just breaks the channel, never reads it).
11//
12// This is the server that runs on the VPS (headless, Layer 6 ports it to Linux)
13// plus the client/service helpers the desktop node uses.
14
15#ifndef BITFLASH_BTFRV_H
16#define BITFLASH_BTFRV_H
17
18#include <stdint.h>
19
20namespace btf
21{
22
23// A connected, paired rendezvous socket (opaque). Read/write raw bytes; the
24// caller layers echan on top. Close with RvClose.
25typedef intptr_t RvSocket;
26static const RvSocket RV_INVALID = (RvSocket)-1;
27
28// Initialize sockets (Winsock on Windows). Idempotent.
29bool RvInit();
30
31// ---- relay side (runs on the VPS) ----
32// Run the rendezvous relay forever on `port`, forwarding between paired peers.
33// Blocks; run it in a thread. Returns false if it can't bind/listen.
34bool RvRelayRun(unsigned short port);
35
36// Stop a running relay (signals RvRelayRun to return).
37void RvRelayStop();
38
39// ---- service side (the node being reached by its .btf address) ----
40// Register at the relay under `my_pubkey` (32 bytes) and block until a client is
41// paired to us. Returns the paired socket, or RV_INVALID on error.
42RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
43 const unsigned char my_pubkey[32]);
44
45// ---- client side (the node dialing a .btf address) ----
46// Connect through the relay to the service registered under `target_pubkey`.
47// Returns the paired socket, or RV_INVALID on error.
48RvSocket RvClientConnect(const char* relay_host, unsigned short port,
49 const unsigned char target_pubkey[32]);
50
51// Blocking read/write of exactly n bytes over a rendezvous socket. Return false
52// on error/EOF.
53bool RvReadN(RvSocket s, void* buf, int n);
54bool RvWriteN(RvSocket s, const void* buf, int n);
55void RvClose(RvSocket s);
56
57} // namespace btf
58
59#endif
diff --git a/relay/build.sh b/relay/build.sh
new file mode 100644
index 0000000..7796523
--- /dev/null
+++ b/relay/build.sh
@@ -0,0 +1,6 @@
1#!/usr/bin/env bash
2# Build the Bitflash rendezvous relay (bitflashd) on Linux. No dependencies
3# beyond a C++ compiler -- the relay only forwards bytes, it holds no keys.
4set -e
5g++ -std=gnu++14 -O2 -pthread bitflashd.cpp btfrv.cpp -o bitflashd
6echo "built ./bitflashd -- run it with: ./bitflashd <port> (e.g. ./bitflashd 8434)"
diff --git a/relay/install-bitflash-relay.sh b/relay/install-bitflash-relay.sh
new file mode 100644
index 0000000..fd901ac
--- /dev/null
+++ b/relay/install-bitflash-relay.sh
@@ -0,0 +1,345 @@
1#!/usr/bin/env bash
2# Bitflash rendezvous relay -- one-shot installer for Linux.
3# Anyone can run this on a machine with a PUBLIC IP (a cheap VPS) to become a
4# meeting node that lets Bitflash users behind CGNAT reach each other.
5# The relay holds NO keys and reads NOTHING -- it only forwards ciphertext.
6#
7# Usage:
8# sudo bash install-bitflash-relay.sh [PORT]
9# PORT defaults to 8434. Remember to open that port in your firewall/panel.
10
11set -e
12PORT="${1:-8434}"
13DIR="$HOME/bitflash-relay"
14mkdir -p "$DIR" && cd "$DIR"
15
16echo ">> writing sources to $DIR"
17
18cat > btfrv.h <<'BTFRV_H_EOF'
19// Copyright (c) 2026 Bitflash developers
20// Distributed under the MIT/X11 software license.
21#ifndef BITFLASH_BTFRV_H
22#define BITFLASH_BTFRV_H
23#include <stdint.h>
24namespace btf
25{
26typedef intptr_t RvSocket;
27static const RvSocket RV_INVALID = (RvSocket)-1;
28bool RvInit();
29bool RvRelayRun(unsigned short port);
30void RvRelayStop();
31RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
32 const unsigned char my_pubkey[32]);
33RvSocket RvClientConnect(const char* relay_host, unsigned short port,
34 const unsigned char target_pubkey[32]);
35bool RvReadN(RvSocket s, void* buf, int n);
36bool RvWriteN(RvSocket s, const void* buf, int n);
37void RvClose(RvSocket s);
38} // namespace btf
39#endif
40BTFRV_H_EOF
41
42cat > btfrv.cpp <<'BTFRV_CPP_EOF'
43// Copyright (c) 2026 Bitflash developers
44// Distributed under the MIT/X11 software license.
45#include "btfrv.h"
46#ifdef _WIN32
47#include <winsock2.h>
48#include <ws2tcpip.h>
49#define CLOSESOCK closesocket
50#else
51#include <sys/socket.h>
52#include <netinet/in.h>
53#include <arpa/inet.h>
54#include <netdb.h>
55#include <unistd.h>
56#include <csignal>
57typedef int SOCKET;
58#define INVALID_SOCKET (-1)
59#define SOCKET_ERROR (-1)
60#define CLOSESOCK ::close
61#ifndef SD_BOTH
62#define SD_BOTH SHUT_RDWR
63#endif
64#endif
65#include <cstring>
66#include <cstdio>
67#include <string>
68#include <map>
69#include <thread>
70#include <mutex>
71#include <atomic>
72namespace btf
73{
74static std::atomic<bool> g_stop(false);
75static SOCKET g_listen = INVALID_SOCKET;
76static std::map<std::string, SOCKET> g_waiting;
77static std::mutex g_mtx;
78bool RvInit()
79{
80 static bool done = false;
81 if (done) return true;
82#ifdef _WIN32
83 WSADATA wsa;
84 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
85#else
86 signal(SIGPIPE, SIG_IGN);
87#endif
88 done = true;
89 return true;
90}
91static bool ReadN(SOCKET s, void* buf, int n)
92{
93 char* p = (char*)buf;
94 int off = 0;
95 while (off < n)
96 {
97 int r = recv(s, p + off, n - off, 0);
98 if (r <= 0) return false;
99 off += r;
100 }
101 return true;
102}
103static bool WriteN(SOCKET s, const void* buf, int n)
104{
105 const char* p = (const char*)buf;
106 int off = 0;
107 while (off < n)
108 {
109 int r = send(s, p + off, n - off, 0);
110 if (r <= 0) return false;
111 off += r;
112 }
113 return true;
114}
115static SOCKET ConnectTo(const char* host, unsigned short port)
116{
117 struct addrinfo hints, *res = NULL;
118 memset(&hints, 0, sizeof(hints));
119 hints.ai_family = AF_INET;
120 hints.ai_socktype = SOCK_STREAM;
121 char portstr[16]; sprintf(portstr, "%u", (unsigned)port);
122 if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) return INVALID_SOCKET;
123 SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
124 if (s == INVALID_SOCKET) { freeaddrinfo(res); return INVALID_SOCKET; }
125 if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0)
126 {
127 CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET;
128 }
129 freeaddrinfo(res);
130 return s;
131}
132static void Forward(SOCKET from, SOCKET to)
133{
134 char buf[32 * 1024];
135 for (;;)
136 {
137 int r = recv(from, buf, sizeof(buf), 0);
138 if (r <= 0) break;
139 if (!WriteN(to, buf, r)) break;
140 }
141 shutdown(to, SD_BOTH);
142 CLOSESOCK(from);
143}
144static void HandleIncoming(SOCKET s)
145{
146 unsigned char hdr[33];
147 if (!ReadN(s, hdr, 33)) { CLOSESOCK(s); return; }
148 char role = (char)hdr[0];
149 std::string pubkey((char*)hdr + 1, 32);
150 if (role == 'S')
151 {
152 std::lock_guard<std::mutex> lk(g_mtx);
153 std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey);
154 if (it != g_waiting.end()) CLOSESOCK(it->second);
155 g_waiting[pubkey] = s;
156 }
157 else if (role == 'C')
158 {
159 SOCKET svc = INVALID_SOCKET;
160 {
161 std::lock_guard<std::mutex> lk(g_mtx);
162 std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey);
163 if (it != g_waiting.end()) { svc = it->second; g_waiting.erase(it); }
164 }
165 if (svc == INVALID_SOCKET)
166 {
167 unsigned char no = 0x00;
168 WriteN(s, &no, 1);
169 CLOSESOCK(s);
170 return;
171 }
172 unsigned char ok = 0x01;
173 bool a = WriteN(svc, &ok, 1);
174 bool b = WriteN(s, &ok, 1);
175 if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; }
176 std::thread(Forward, s, svc).detach();
177 std::thread(Forward, svc, s).detach();
178 }
179 else
180 {
181 CLOSESOCK(s);
182 }
183}
184bool RvRelayRun(unsigned short port)
185{
186 if (!RvInit()) return false;
187 g_stop = false;
188 g_listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
189 if (g_listen == INVALID_SOCKET) return false;
190 int yes = 1;
191 setsockopt(g_listen, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
192 struct sockaddr_in addr;
193 memset(&addr, 0, sizeof(addr));
194 addr.sin_family = AF_INET;
195 addr.sin_addr.s_addr = INADDR_ANY;
196 addr.sin_port = htons(port);
197 if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; }
198 if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; }
199 while (!g_stop)
200 {
201 struct sockaddr_in cli;
202 socklen_t len = sizeof(cli);
203 SOCKET s = accept(g_listen, (struct sockaddr*)&cli, &len);
204 if (s == INVALID_SOCKET)
205 {
206 if (g_stop) break;
207 continue;
208 }
209 std::thread(HandleIncoming, s).detach();
210 }
211 CLOSESOCK(g_listen);
212 g_listen = INVALID_SOCKET;
213 return true;
214}
215void RvRelayStop()
216{
217 g_stop = true;
218 if (g_listen != INVALID_SOCKET)
219 CLOSESOCK(g_listen);
220}
221RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
222 const unsigned char my_pubkey[32])
223{
224 if (!RvInit()) return RV_INVALID;
225 SOCKET s = ConnectTo(relay_host, port);
226 if (s == INVALID_SOCKET) return RV_INVALID;
227 unsigned char hdr[33];
228 hdr[0] = (unsigned char)'S';
229 memcpy(hdr + 1, my_pubkey, 32);
230 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
231 unsigned char paired = 0;
232 if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; }
233 return (RvSocket)s;
234}
235RvSocket RvClientConnect(const char* relay_host, unsigned short port,
236 const unsigned char target_pubkey[32])
237{
238 if (!RvInit()) return RV_INVALID;
239 SOCKET s = ConnectTo(relay_host, port);
240 if (s == INVALID_SOCKET) return RV_INVALID;
241 unsigned char hdr[33];
242 hdr[0] = (unsigned char)'C';
243 memcpy(hdr + 1, target_pubkey, 32);
244 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
245 unsigned char paired = 0;
246 if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; }
247 return (RvSocket)s;
248}
249bool RvReadN(RvSocket s, void* buf, int n) { return ReadN((SOCKET)s, buf, n); }
250bool RvWriteN(RvSocket s, const void* buf, int n) { return WriteN((SOCKET)s, buf, n); }
251void RvClose(RvSocket s) { if (s != RV_INVALID) CLOSESOCK((SOCKET)s); }
252} // namespace btf
253BTFRV_CPP_EOF
254
255cat > bitflashd.cpp <<'BITFLASHD_EOF'
256// Copyright (c) 2026 Bitflash developers
257// Distributed under the MIT/X11 software license.
258#include "btfrv.h"
259#include <cstdio>
260#include <cstdlib>
261#include <cstring>
262int main(int argc, char* argv[])
263{
264 unsigned short port = 8434;
265 if (argc >= 2)
266 port = (unsigned short)atoi(argv[1]);
267 printf("bitflashd -- Bitflash rendezvous relay\n");
268 printf("listening on 0.0.0.0:%u (open this port in your firewall)\n", port);
269 fflush(stdout);
270 if (!btf::RvInit())
271 {
272 fprintf(stderr, "bitflashd: socket init failed\n");
273 return 1;
274 }
275 if (!btf::RvRelayRun(port))
276 {
277 fprintf(stderr, "bitflashd: could not bind/listen on port %u\n", port);
278 return 1;
279 }
280 return 0;
281}
282BITFLASHD_EOF
283
284# g++ (Debian/Ubuntu: build-essential; RHEL/Alma: gcc-c++)
285if ! command -v g++ >/dev/null 2>&1; then
286 echo ">> installing g++"
287 if command -v apt-get >/dev/null 2>&1; then apt-get update && apt-get install -y g++
288 elif command -v dnf >/dev/null 2>&1; then dnf install -y gcc-c++
289 elif command -v yum >/dev/null 2>&1; then yum install -y gcc-c++
290 else echo "!! install a C++ compiler (g++) manually and re-run"; exit 1
291 fi
292fi
293
294echo ">> building bitflashd"
295g++ -std=gnu++14 -O2 -pthread bitflashd.cpp btfrv.cpp -o bitflashd
296echo ">> build OK: $DIR/bitflashd"
297
298# Basic anti-flood firewall rules (idempotent). These soak up single-source
299# connection floods; volumetric DDoS still needs provider-level protection.
300# Limits are generous so many users behind one CGNAT public IP aren't blocked.
301if command -v iptables >/dev/null 2>&1; then
302 echo ">> applying connection rate-limit on port $PORT"
303 # Cap concurrent connections per source IP (stops one IP hogging thousands)
304 iptables -C INPUT -p tcp --dport "$PORT" -m connlimit --connlimit-above 200 --connlimit-mask 32 -j REJECT --reject-with tcp-reset 2>/dev/null || \
305 iptables -A INPUT -p tcp --dport "$PORT" -m connlimit --connlimit-above 200 --connlimit-mask 32 -j REJECT --reject-with tcp-reset
306 # Cap NEW-connection rate per source IP (stops connection floods)
307 iptables -C INPUT -p tcp --dport "$PORT" -m conntrack --ctstate NEW -m hashlimit \
308 --hashlimit-name btfrelay --hashlimit-mode srcip --hashlimit-above 60/min --hashlimit-burst 100 -j DROP 2>/dev/null || \
309 iptables -A INPUT -p tcp --dport "$PORT" -m conntrack --ctstate NEW -m hashlimit \
310 --hashlimit-name btfrelay --hashlimit-mode srcip --hashlimit-above 60/min --hashlimit-burst 100 -j DROP
311 # Persist across reboots if the tooling is present
312 command -v netfilter-persistent >/dev/null 2>&1 && netfilter-persistent save >/dev/null 2>&1 || true
313else
314 echo ">> iptables not found; skipping rate-limit (host on a provider with anti-DDoS)"
315fi
316
317# systemd service so it survives reboots (skipped if systemd is absent)
318if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then
319 cat > /etc/systemd/system/bitflashd.service <<EOF
320[Unit]
321Description=Bitflash rendezvous relay
322After=network.target
323
324[Service]
325ExecStart=$DIR/bitflashd $PORT
326Restart=always
327User=$(id -un)
328
329[Install]
330WantedBy=multi-user.target
331EOF
332 systemctl daemon-reload
333 systemctl enable --now bitflashd
334 sleep 1
335 systemctl status bitflashd --no-pager || true
336 echo ">> relay running as a service on port $PORT"
337else
338 echo ">> systemd not found; run it manually: $DIR/bitflashd $PORT"
339fi
340
341echo
342echo "==================================================================="
343echo " DONE. Open TCP port $PORT in your firewall / cloud panel."
344echo " Your relay address for Bitflash users: <YOUR_PUBLIC_IP>:$PORT"
345echo "==================================================================="