upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/btftunnel.cpp
blob: 4acca1a8e4e9c5043294b0872442cebc6e995919 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// Copyright (c) 2026 Bitflash developers
// Distributed under the MIT/X11 software license.
//
// Transparent anonymous tunnel. See btftunnel.h.

#include "btftunnel.h"
#include "btfchan.h"

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
typedef int SOCKET;
#ifndef INVALID_SOCKET
#define INVALID_SOCKET (-1)
#endif
#define closesocket(s) ::close(s)
#ifndef SD_BOTH
#define SD_BOTH SHUT_RDWR
#endif
#endif
#include "sockcount.h"
#include <cstring>
#include <cstdio>
#include <memory>
#include <new>
#include <vector>
#include <thread>
#include <system_error>

namespace btf
{

bool TunnelInit()
{
    return RvInit() && ChanInit();
}

// Create a connected loopback socket pair (no socketpair() on Windows).
// Returns app end and pump end; both are ordinary full-duplex TCP sockets.
static bool MakeLocalPair(btf_socket_t& appEnd, btf_socket_t& pumpEnd)
{
    SOCKET listener = BtfSocketTag(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP), SOCK_PAIR_LISTEN);
    if (listener == INVALID_SOCKET) return false;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_port = 0; // ephemeral
    if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) != 0) { BtfCloseSocket(listener); return false; }
    if (listen(listener, 1) != 0) { BtfCloseSocket(listener); return false; }

#ifdef _WIN32
    int len = sizeof(addr);
#else
    socklen_t len = sizeof(addr);
#endif
    if (getsockname(listener, (struct sockaddr*)&addr, &len) != 0) { BtfCloseSocket(listener); return false; }

    SOCKET a = BtfSocketTag(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP), SOCK_PAIR_APP);
    if (a == INVALID_SOCKET) { BtfCloseSocket(listener); return false; }
    if (connect(a, (struct sockaddr*)&addr, sizeof(addr)) != 0) { BtfCloseSocket(a); BtfCloseSocket(listener); return false; }

    SOCKET b = BtfSocketTag(accept(listener, NULL, NULL), SOCK_PAIR_PUMP);
    BtfCloseSocket(listener);
    if (b == INVALID_SOCKET) { BtfCloseSocket(a); return false; }

    appEnd = a;
    pumpEnd = b;
    return true;
}

static bool SockReadN(SOCKET s, void* buf, int n)
{
    char* p = (char*)buf; int off = 0;
    while (off < n) { int r = recv(s, p + off, n - off, 0); if (r <= 0) return false; off += r; }
    return true;
}
static bool SockWriteN(SOCKET s, const void* buf, int n)
{
    const char* p = (const char*)buf; int off = 0;
    while (off < n) { int r = send(s, p + off, n - off, 0); if (r <= 0) return false; off += r; }
    return true;
}

// Outbound pump: plaintext from the app -> echan frame -> rendezvous socket.
static void PumpOut(SOCKET pumpEnd, RvSocket rv, std::vector<unsigned char> key)
{
    char buf[16 * 1024];
    for (;;)
    {
        int r = recv(pumpEnd, buf, sizeof(buf), 0);
        if (r <= 0) break;
        std::vector<unsigned char> frame = ChanEncryptFrame(key.data(), (const unsigned char*)buf, r);
        if (!RvWriteN(rv, frame.data(), (int)frame.size())) break;
    }
    // Tear down: unblock the inbound pump and the app.
    shutdown((SOCKET)rv, SD_BOTH);
    shutdown(pumpEnd, SD_BOTH);
}

// Inbound pump: echan frames from the rendezvous socket -> plaintext to the app.
static void PumpIn(SOCKET pumpEnd, RvSocket rv, std::vector<unsigned char> key)
{
    for (;;)
    {
        unsigned char len[4];
        if (!RvReadN(rv, len, 4)) break;
        size_t n = ((size_t)len[0]<<24)|((size_t)len[1]<<16)|((size_t)len[2]<<8)|len[3];
        if (n < (size_t)(CHAN_NONCEBYTES + CHAN_MACBYTES) || n > (1u<<20)) break;
        std::vector<unsigned char> body(n);
        if (!RvReadN(rv, body.data(), (int)n)) break;
        std::vector<unsigned char> pt;
        if (!ChanDecryptBody(key.data(), body.data(), (int)n, pt)) break; // auth fail -> drop
        if (!pt.empty() && !SockWriteN(pumpEnd, pt.data(), (int)pt.size())) break;
    }
    shutdown(pumpEnd, SD_BOTH);
    shutdown((SOCKET)rv, SD_BOTH);
}

// The pump end and the rendezvous socket, owned jointly by whoever is still
// using them. Closed once, by the last reference to go.
//
// Joint ownership rather than "the inbound pump owns the cleanup", because
// either pump can reach its exit first and BOTH touch BOTH descriptors on the
// way out -- PumpOut ends in shutdown(rv), shutdown(pumpEnd) and PumpIn ends in
// the mirror image. A single owner therefore has no exit at which the other is
// guaranteed to be finished. Holding a reference for the whole body of each
// pump gives it one: the close cannot happen until the last pump has stopped
// touching the descriptors, so no shutdown() can land on a number that has
// already been closed and handed back out by the kernel to an unrelated
// connection.
struct TunnelEnds
{
    SOCKET   pumpEnd;
    RvSocket rv;
    TunnelEnds(SOCKET p, RvSocket r) : pumpEnd(p), rv(r) {}
    ~TunnelEnds() { BtfCloseSocket(pumpEnd); RvClose(rv); }
private:
    TunnelEnds(const TunnelEnds&);
    TunnelEnds& operator=(const TunnelEnds&);
};

// Start the two pumps and hand back the app-facing socket. When both pumps
// finish, close the pump end and the rendezvous socket.
//
// That sentence used to be a comment describing something the code did not do.
// The inbound pump closed both descriptors as soon as it exited, without
// waiting for the outbound pump, which then called shutdown() on both at its
// own exit. Whether that shutdown hit a live descriptor or a closed one came
// down to which pump got there first, and the rendezvous socket dying before
// the app socket -- a relay restart, the far side hanging up -- is precisely
// the order that puts the inbound pump ahead. Measured over 25 such teardowns
// against the real code: 29 shutdowns landed on an already-closed descriptor.
//
// appEnd is not part of this. It is handed to the caller on success and closed
// here on every failure path; no pump ever touches it.
//
// Every tunnel is two threads, and a tunnel is one peer connection (a .btf
// dial or accept, a pool miner, a participant's pool session) -- so the
// thread count scales with peer count, which is attacker-influenced. When
// pthread_create is refused std::thread throws std::system_error; uncaught it
// reached std::terminate and took the whole process down over one connection
// it could not serve. Both spawns are guarded, and the failure paths return
// INVALID_SOCKET, which every caller already handles by dropping just that
// connection.
static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32])
{
    btf_socket_t appEnd, pumpEnd;
    if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; }

    std::vector<unsigned char> key(shared, shared + 32);

    // make_shared, not shared_ptr(new ...): it allocates before it constructs,
    // so it either comes back owning both descriptors or throws having taken
    // ownership of neither. The two-step form can run the deleter and then
    // throw, which would close both here and again below.
    std::shared_ptr<TunnelEnds> ends;
    try
    {
        ends = std::make_shared<TunnelEnds>(pumpEnd, rv);
    }
    catch (const std::bad_alloc&)
    {
        fprintf(stderr, "btf tunnel: out of memory starting tunnel, tunnel dropped\n");
        BtfCloseSocket(pumpEnd);
        BtfCloseSocket(appEnd);
        RvClose(rv);
        return INVALID_SOCKET;
    }

    try
    {
        std::thread([ends, key]() { PumpIn(ends->pumpEnd, ends->rv, key); }).detach();
    }
    catch (const std::system_error& e)
    {
        // Nothing is running yet: `ends` is the only reference, and returning
        // drops it, which is what closes pumpEnd and rv.
        fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n",
                e.what());
        BtfCloseSocket(appEnd);
        return INVALID_SOCKET;
    }

    try
    {
        std::thread([ends, key]() { PumpOut(ends->pumpEnd, ends->rv, key); }).detach();
    }
    catch (const std::system_error& e)
    {
        // The inbound pump may be parked in RvReadN(rv) or may already have
        // exited -- when the spawn is refused because the process is loaded,
        // the rendezvous socket is often dead for the same reason, and then
        // PumpIn returns on its first length read. Either way this thread
        // still holds a reference, so both descriptors are still open: the
        // shutdown wakes the pump if it is parked and is harmless if it is
        // not. The close belongs to whichever reference goes last.
        fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n",
                e.what());
        shutdown((SOCKET)ends->rv, SD_BOTH);
        shutdown(ends->pumpEnd, SD_BOTH);
        BtfCloseSocket(appEnd);
        return INVALID_SOCKET;
    }
    return appEnd;
}

btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port,
                             const unsigned char target_pubkey[32],
                             const unsigned char service_enc_pub[32])
{
    if (!TunnelInit()) return INVALID_SOCKET;
    RvSocket rv = RvClientConnect(meeting_host, port, target_pubkey);
    if (rv == RV_INVALID) return INVALID_SOCKET;

    unsigned char eph_pub[32], shared[32];
    if (!ChanClientHandshake(service_enc_pub, eph_pub, shared)) { RvClose(rv); return INVALID_SOCKET; }
    if (!RvWriteN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; }
    return StartTunnel(rv, shared);
}

btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32])
{
    if (!TunnelInit()) { RvClose(rv); return INVALID_SOCKET; }
    unsigned char eph_pub[32], shared[32];
    if (!RvReadN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; }
    if (!ChanEndpointHandshake(eph_pub, my_enc_sk, shared)) { RvClose(rv); return INVALID_SOCKET; }
    return StartTunnel(rv, shared);
}

} // namespace btf