upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/nostr.cpp')
-rw-r--r--src/nostr.cpp150
1 files changed, 129 insertions, 21 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 35fde43..f44dc08 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -6,6 +6,7 @@
6#include "headers.h" 6#include "headers.h"
7#include <openssl/ssl.h> 7#include <openssl/ssl.h>
8#include <openssl/err.h> 8#include <openssl/err.h>
9#include <openssl/x509.h>
9#include <openssl/x509v3.h> 10#include <openssl/x509v3.h>
10#ifdef _WIN32 11#ifdef _WIN32
11#include <wincrypt.h> 12#include <wincrypt.h>
@@ -82,9 +83,20 @@ static CCriticalSection cs_btfPeers;
82// independent relays. Kept only one Cloudflare-fronted relay (damus.io, the 83// independent relays. Kept only one Cloudflare-fronted relay (damus.io, the
83// most established) and filled the rest with relays on different hosts 84// most established) and filled the rest with relays on different hosts
84// (Hetzner, various independents) so no single upstream provider is a 85// (Hetzner, various independents) so no single upstream provider is a
85// single point of failure for peer discovery. 7 relays lands inside the 86// single point of failure for peer discovery. 8 relays lands inside the
86// commonly recommended 5-10 range for redundancy without keeping an 87// commonly recommended 5-10 range for redundancy without keeping an
87// excessive number of persistent sockets open per node. 88// excessive number of persistent sockets open per node.
89//
90// Four entries were removed after probing all twelve from two independent
91// networks (a residential line and the pool's VPS, over both v4 and v6):
92// relay.current.fyi, relay.nostr.bg -- no A record, and the AAAA for both
93// lands on the same parked host (2a04:c300:1000::381) serving a
94// certificate that fails verification. The domains are gone.
95// relay.nostr.band, nostr.fmt.wiz.biz -- TCP connects, TLS handshake never
96// completes, from both networks. Whether they are down or refusing
97// non-browser clients, this node cannot use them.
98// A dead relay is not free: each one holds a connect/retry slot and delays
99// discovery, so carrying it costs latency for no redundancy.
88const char* pszNostrRelays[] = { 100const char* pszNostrRelays[] = {
89 "wss://relay.damus.io", 101 "wss://relay.damus.io",
90 "wss://nos.lol", 102 "wss://nos.lol",
@@ -97,6 +109,8 @@ const char* pszNostrRelays[] = {
97 "wss://offchain.pub", 109 "wss://offchain.pub",
98 "wss://nostr.mom", 110 "wss://nostr.mom",
99 "wss://relay.primal.net", 111 "wss://relay.primal.net",
112 "wss://nostr.oxtr.dev",
113 "wss://relay.nos.social",
100}; 114};
101const int nNostrRelays = ARRAYLEN(pszNostrRelays); 115const int nNostrRelays = ARRAYLEN(pszNostrRelays);
102 116
@@ -110,6 +124,12 @@ vector<string> vBtfMeetingRelays = {
110 "31.44.4.249:8434", // New Jersey, US 124 "31.44.4.249:8434", // New Jersey, US
111 "90.156.222.107:8434", // Almaty, KZ 125 "90.156.222.107:8434", // Almaty, KZ
112}; 126};
127// True when the operator pinned the relay list with /rvrelay: discovered
128// relays must NOT be merged in (BtfAllRelays), otherwise one accept thread
129// per discovered relay is spawned and each one optimistically overwrites the
130// advertised meeting node (BtfSetActiveRelay) before registering -- a
131// discovered but unreachable relay then flaps the published .btf descriptor.
132bool fBtfRelayPinned = false;
113string strBtfActiveRelay; 133string strBtfActiveRelay;
114static CCriticalSection cs_activeRelay; 134static CCriticalSection cs_activeRelay;
115static string strBtfOnionEndpoint; 135static string strBtfOnionEndpoint;
@@ -131,6 +151,19 @@ string BtfActiveRelay()
131 151
132void BtfSetActiveRelay(const string& relay) 152void BtfSetActiveRelay(const string& relay)
133{ 153{
154 // With /rvrelay set, the descriptor must never name anything but the pin.
155 // No caller can reach here with another relay today -- both registration
156 // paths draw their list from BtfAllRelays(), which returns the pin alone --
157 // but the descriptor is the one thing a miner obeys, so the invariant is
158 // enforced at the sink instead of being retrusted to every future caller.
159 if (fBtfRelayPinned &&
160 find(vBtfMeetingRelays.begin(), vBtfMeetingRelays.end(), relay) == vBtfMeetingRelays.end())
161 {
162 LogPrint("pool", "refusing to advertise meeting relay %s: /rvrelay pins %s\n",
163 relay.c_str(),
164 vBtfMeetingRelays.empty() ? "an empty list" : vBtfMeetingRelays[0].c_str());
165 return;
166 }
134 CRITICAL_BLOCK(cs_activeRelay) 167 CRITICAL_BLOCK(cs_activeRelay)
135 strBtfActiveRelay = relay; 168 strBtfActiveRelay = relay;
136} 169}
@@ -166,7 +199,7 @@ static map<string, BtfPoolAnnouncement> g_discoveredPools;
166static CCriticalSection cs_discoveredPools; 199static CCriticalSection cs_discoveredPools;
167 200
168// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host. 201// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host.
169static bool LooksLikeRelay(const string& r) 202bool LooksLikeRelay(const string& r)
170{ 203{
171 size_t colon = r.rfind(':'); 204 size_t colon = r.rfind(':');
172 if (colon == string::npos || colon == 0 || colon + 1 >= r.size()) 205 if (colon == string::npos || colon == 0 || colon + 1 >= r.size())
@@ -194,6 +227,8 @@ static bool LooksLikeRelay(const string& r)
194vector<string> BtfAllRelays() 227vector<string> BtfAllRelays()
195{ 228{
196 vector<string> all = vBtfMeetingRelays; 229 vector<string> all = vBtfMeetingRelays;
230 if (fBtfRelayPinned)
231 return all;
197 CRITICAL_BLOCK(cs_discoveredRelays) 232 CRITICAL_BLOCK(cs_discoveredRelays)
198 foreach(const string& r, g_discoveredRelays) 233 foreach(const string& r, g_discoveredRelays)
199 if (find(all.begin(), all.end(), r) == all.end()) 234 if (find(all.begin(), all.end(), r) == all.end())
@@ -201,6 +236,18 @@ vector<string> BtfAllRelays()
201 return all; 236 return all;
202} 237}
203 238
239// Relays learned from Nostr, whether or not a pin is in force. Reporting only:
240// it exists so the pool can name what /rvrelay is excluding. Anything choosing
241// a relay to register or dial at must keep using BtfAllRelays().
242vector<string> BtfDiscoveredRelays()
243{
244 vector<string> out;
245 CRITICAL_BLOCK(cs_discoveredRelays)
246 foreach(const string& r, g_discoveredRelays)
247 out.push_back(r);
248 return out;
249}
250
204 251
205// 252//
206// Utilities 253// Utilities
@@ -578,26 +625,52 @@ public:
578 { 625 {
579 sslctx = SSL_CTX_new(TLS_client_method()); 626 sslctx = SSL_CTX_new(TLS_client_method());
580 if (!sslctx) return false; 627 if (!sslctx) return false;
628
629 // SSL_CTX_new defaults to SSL_VERIFY_NONE with no trust store
630 // loaded, so until now every wss:// relay connection was accepted
631 // without looking at the certificate at all -- any on-path
632 // attacker could impersonate a relay. Our own events are signed,
633 // so a MITM cannot forge them, but it could drop the rendezvous
634 // registration (leaving the pool unreachable to miners) or answer
635 // subscriptions with whatever it liked.
581 SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); 636 SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL);
637
638 // OpenSSL's default verify paths are usually empty on Windows, so
639 // the system ROOT store is loaded too -- without it verification
640 // fails closed there and every relay becomes unreachable.
582 bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1); 641 bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1);
583#ifdef _WIN32 642#ifdef _WIN32
584 fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded; 643 fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded;
585#endif 644#endif
586 if (!fTrustLoaded) 645 if (!fTrustLoaded)
587 return error("Nostr: failed to load TLS trust store"); 646 return error("Nostr: failed to load TLS trust store");
647
648 // Floor the protocol version. Verification says who the peer is,
649 // not how weak a cipher suite we will agree to; without this a
650 // relay offering nothing better still gets TLS 1.0.
651 SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION);
652
588 ssl = SSL_new(sslctx); 653 ssl = SSL_new(sslctx);
589 if (!ssl) return false; 654 if (!ssl) return false;
590 SSL_set_fd(ssl, (int)hSocket); 655 SSL_set_fd(ssl, (int)hSocket);
591 if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) 656 if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) // SNI
592 return error("Nostr: failed to set TLS SNI for %s", host.c_str()); 657 return error("Nostr: failed to set TLS SNI for %s", host.c_str());
658
659 // Verifying the chain is not enough on its own: without this the
660 // relay could present any certificate a trusted CA ever issued,
661 // for any name. SSL_set1_host binds the check to the host we asked
662 // for.
593 if (SSL_set1_host(ssl, host.c_str()) != 1) 663 if (SSL_set1_host(ssl, host.c_str()) != 1)
594 return error("Nostr: failed to set TLS hostname for %s", host.c_str()); 664 return error("Nostr: failed to set TLS hostname for %s", host.c_str());
665
595 if (SSL_connect(ssl) != 1) 666 if (SSL_connect(ssl) != 1)
667 {
668 long nVerify = SSL_get_verify_result(ssl);
669 if (nVerify != X509_V_OK)
670 return error("Nostr: certificate for %s rejected: %s",
671 host.c_str(), X509_verify_cert_error_string(nVerify));
596 return error("Nostr: TLS handshake with %s failed", host.c_str()); 672 return error("Nostr: TLS handshake with %s failed", host.c_str());
597 long nVerify = SSL_get_verify_result(ssl); 673 }
598 if (nVerify != X509_V_OK)
599 return error("Nostr: TLS certificate verification failed for %s: %s",
600 host.c_str(), X509_verify_cert_error_string(nVerify));
601 } 674 }
602 675
603 // WebSocket handshake (HTTP Upgrade) 676 // WebSocket handshake (HTTP Upgrade)
@@ -1049,9 +1122,15 @@ static void HandlePoolAnnouncement(const json& ev)
1049 1122
1050// Resolve a `.btf` address via a relay: fetch the descriptor published by that 1123// Resolve a `.btf` address via a relay: fetch the descriptor published by that
1051// address's key and verify it self-certifies under the decoded pubkey. 1124// address's key and verify it self-certifies under the decoded pubkey.
1125// fExchangeOk reports whether the relay actually finished the conversation
1126// (an EOSE arrived) as opposed to the socket dying mid-read. The caller pools
1127// this socket for reuse, and only a completed exchange leaves it in a state
1128// the next caller can trust.
1052static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 1129static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1053 btf::Descriptor& out, string* rawOut = NULL) 1130 btf::Descriptor& out, bool& fExchangeOk,
1131 string* rawOut = NULL)
1054{ 1132{
1133 fExchangeOk = false;
1055 unsigned char pk[32]; 1134 unsigned char pk[32];
1056 if (!btf::ParseAddress(btfAddr, pk)) 1135 if (!btf::ParseAddress(btfAddr, pk))
1057 return false; 1136 return false;
@@ -1063,30 +1142,46 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1063 json req = json::array({ "REQ", "btf-resolve", filter }); 1142 json req = json::array({ "REQ", "btf-resolve", filter });
1064 if (!ws.SendText(req.dump())) 1143 if (!ws.SendText(req.dump()))
1065 return false; 1144 return false;
1145 bool fFound = false;
1066 for (;;) 1146 for (;;)
1067 { 1147 {
1068 string msg; 1148 string msg;
1069 if (!ws.RecvText(msg)) 1149 if (!ws.RecvText(msg))
1070 break; 1150 return fFound; // socket died mid-exchange; fExchangeOk stays false
1071 json j; 1151 json j;
1072 try { j = json::parse(msg); } catch (...) { continue; } 1152 try { j = json::parse(msg); } catch (...) { continue; }
1073 if (!j.is_array() || j.empty() || !j[0].is_string()) continue; 1153 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1074 string t = j[0].get<string>(); 1154 string t = j[0].get<string>();
1155 // Every relay message names the subscription it belongs to. This socket
1156 // is pooled and reused, so a message left over from an earlier exchange
1157 // is the ordinary case here, not an exotic one -- and an unmatched EOSE
1158 // ends this query before it has begun, which is indistinguishable from
1159 // "that descriptor does not exist". Check the id.
1160 if (j.size() >= 2 && j[1].is_string() && j[1].get<string>() != "btf-resolve")
1161 continue;
1075 if (t == "EVENT" && j.size() >= 3) 1162 if (t == "EVENT" && j.size() >= 3)
1076 { 1163 {
1077 const json& ev = j[2]; 1164 const json& ev = j[2];
1078 if (ev.contains("content") && 1165 if (!fFound && ev.contains("content") &&
1079 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) 1166 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1080 { 1167 {
1168 fFound = true;
1081 if (rawOut) 1169 if (rawOut)
1082 *rawOut = ev["content"].get<string>(); 1170 *rawOut = ev["content"].get<string>();
1083 return true;
1084 } 1171 }
1172 // Deliberately keep reading to EOSE rather than returning here.
1173 // Returning early leaves the rest of the relay's answer queued on a
1174 // socket that goes straight back into the pool, and the next caller
1175 // reads our leftovers as its own -- one message out of step, for
1176 // the life of the process.
1085 } 1177 }
1086 else if (t == "EOSE") 1178 else if (t == "EOSE")
1179 {
1180 fExchangeOk = true; // relay said "that is all": socket is clean
1087 break; 1181 break;
1182 }
1088 } 1183 }
1089 return false; 1184 return fFound;
1090} 1185}
1091 1186
1092// Resolve a `.btf` address into its rendezvous coordinates by querying the 1187// Resolve a `.btf` address into its rendezvous coordinates by querying the
@@ -1107,12 +1202,22 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1107 if (!lease.Connected()) 1202 if (!lease.Connected())
1108 continue; 1203 continue;
1109 btf::Descriptor d; 1204 btf::Descriptor d;
1110 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) 1205 bool fExchangeOk = false;
1206 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d, fExchangeOk))
1111 { 1207 {
1112 lease.fOk = true; // exchange completed, just no match 1208 // Only keep the pooled socket if the relay actually finished
1209 // the conversation. This used to say "exchange completed, just
1210 // no match" on every failure -- including a dead socket -- so
1211 // ~CRelayLease never closed it and fConnected was never
1212 // cleared. A relay connection that died was then pinned in the
1213 // pool, reported healthy, and reused for every later lookup,
1214 // which is why "no Nostr descriptor found" persisted for a
1215 // descriptor that was demonstrably published, and why only a
1216 // restart cleared it.
1217 lease.fOk = fExchangeOk;
1113 continue; 1218 continue;
1114 } 1219 }
1115 lease.fOk = true; 1220 lease.fOk = fExchangeOk;
1116 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1221 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1117 { 1222 {
1118 if (!onionHostPort || d.onion.empty()) 1223 if (!onionHostPort || d.onion.empty())
@@ -1360,7 +1465,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
1360 else if (createdAt > it->second) it->second = createdAt; // refresh 1465 else if (createdAt > it->second) it->second = createdAt; // refresh
1361 } 1466 }
1362 if (fNew && fDebug) 1467 if (fNew && fDebug)
1363 LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); 1468 LogPrint("nostrdiscovery", "Nostr: discovered .btf peer %s\n", peerAddr.c_str());
1364 if (++nPeers > 500) break; 1469 if (++nPeers > 500) break;
1365 } 1470 }
1366 else if (type == "EOSE") 1471 else if (type == "EOSE")
@@ -1450,7 +1555,7 @@ void ThreadBtfPoolAnnouncer(void*)
1450 LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); 1555 LogPrint("pool", "ThreadBtfPoolAnnouncer started\n");
1451 while (!fShutdown) 1556 while (!fShutdown)
1452 { 1557 {
1453 if (nMineMode == MINE_OPERATOR && gPoolRunning) 1558 if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce)
1454 { 1559 {
1455 BtfPoolAnnouncement ann; 1560 BtfPoolAnnouncement ann;
1456 ann.btfAddress = BtfLocalAddress(); 1561 ann.btfAddress = BtfLocalAddress();
@@ -1785,14 +1890,17 @@ void ThreadNostrSeed(void* parg)
1785 PublishDescriptor(ws, key); 1890 PublishDescriptor(ws, key);
1786 Sleep(700); // let the relay store the replaceable event 1891 Sleep(700); // let the relay store the replaceable event
1787 btf::Descriptor d; 1892 btf::Descriptor d;
1788 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) 1893 bool fExchangeOk = false;
1894 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d, fExchangeOk))
1789 { 1895 {
1790 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", 1896 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n",
1791 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); 1897 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str());
1792 lease.fOk = true; 1898 lease.fOk = fExchangeOk;
1793 break; 1899 break;
1794 } 1900 }
1795 lease.fOk = true; 1901 // Same rule as BtfResolve: a socket only goes back in the pool if
1902 // the relay finished the conversation on it.
1903 lease.fOk = fExchangeOk;
1796 } 1904 }
1797 CATCH_PRINT_EXCEPTION("btf self-test") 1905 CATCH_PRINT_EXCEPTION("btf self-test")
1798 } 1906 }
@@ -1824,7 +1932,7 @@ void ThreadNostrSeed(void* parg)
1824 } 1932 }
1825 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers") 1933 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers")
1826 1934
1827 if (nMineMode == MINE_OPERATOR && gPoolRunning) 1935 if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce)
1828 { 1936 {
1829 BtfPoolAnnouncement ann; 1937 BtfPoolAnnouncement ann;
1830 ann.btfAddress = BtfLocalAddress(); 1938 ann.btfAddress = BtfLocalAddress();