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.cpp122
1 files changed, 102 insertions, 20 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp
index d286767..32cea18 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;
115 135
@@ -144,7 +164,7 @@ static map<string, BtfPoolAnnouncement> g_discoveredPools;
144static CCriticalSection cs_discoveredPools; 164static CCriticalSection cs_discoveredPools;
145 165
146// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host. 166// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host.
147static bool LooksLikeRelay(const string& r) 167bool LooksLikeRelay(const string& r)
148{ 168{
149 size_t colon = r.rfind(':'); 169 size_t colon = r.rfind(':');
150 if (colon == string::npos || colon == 0 || colon + 1 >= r.size()) 170 if (colon == string::npos || colon == 0 || colon + 1 >= r.size())
@@ -172,6 +192,8 @@ static bool LooksLikeRelay(const string& r)
172vector<string> BtfAllRelays() 192vector<string> BtfAllRelays()
173{ 193{
174 vector<string> all = vBtfMeetingRelays; 194 vector<string> all = vBtfMeetingRelays;
195 if (fBtfRelayPinned)
196 return all;
175 CRITICAL_BLOCK(cs_discoveredRelays) 197 CRITICAL_BLOCK(cs_discoveredRelays)
176 foreach(const string& r, g_discoveredRelays) 198 foreach(const string& r, g_discoveredRelays)
177 if (find(all.begin(), all.end(), r) == all.end()) 199 if (find(all.begin(), all.end(), r) == all.end())
@@ -550,26 +572,52 @@ public:
550 { 572 {
551 sslctx = SSL_CTX_new(TLS_client_method()); 573 sslctx = SSL_CTX_new(TLS_client_method());
552 if (!sslctx) return false; 574 if (!sslctx) return false;
575
576 // SSL_CTX_new defaults to SSL_VERIFY_NONE with no trust store
577 // loaded, so until now every wss:// relay connection was accepted
578 // without looking at the certificate at all -- any on-path
579 // attacker could impersonate a relay. Our own events are signed,
580 // so a MITM cannot forge them, but it could drop the rendezvous
581 // registration (leaving the pool unreachable to miners) or answer
582 // subscriptions with whatever it liked.
553 SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); 583 SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL);
584
585 // OpenSSL's default verify paths are usually empty on Windows, so
586 // the system ROOT store is loaded too -- without it verification
587 // fails closed there and every relay becomes unreachable.
554 bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1); 588 bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1);
555#ifdef _WIN32 589#ifdef _WIN32
556 fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded; 590 fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded;
557#endif 591#endif
558 if (!fTrustLoaded) 592 if (!fTrustLoaded)
559 return error("Nostr: failed to load TLS trust store"); 593 return error("Nostr: failed to load TLS trust store");
594
595 // Floor the protocol version. Verification says who the peer is,
596 // not how weak a cipher suite we will agree to; without this a
597 // relay offering nothing better still gets TLS 1.0.
598 SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION);
599
560 ssl = SSL_new(sslctx); 600 ssl = SSL_new(sslctx);
561 if (!ssl) return false; 601 if (!ssl) return false;
562 SSL_set_fd(ssl, (int)hSocket); 602 SSL_set_fd(ssl, (int)hSocket);
563 if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) 603 if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) // SNI
564 return error("Nostr: failed to set TLS SNI for %s", host.c_str()); 604 return error("Nostr: failed to set TLS SNI for %s", host.c_str());
605
606 // Verifying the chain is not enough on its own: without this the
607 // relay could present any certificate a trusted CA ever issued,
608 // for any name. SSL_set1_host binds the check to the host we asked
609 // for.
565 if (SSL_set1_host(ssl, host.c_str()) != 1) 610 if (SSL_set1_host(ssl, host.c_str()) != 1)
566 return error("Nostr: failed to set TLS hostname for %s", host.c_str()); 611 return error("Nostr: failed to set TLS hostname for %s", host.c_str());
612
567 if (SSL_connect(ssl) != 1) 613 if (SSL_connect(ssl) != 1)
614 {
615 long nVerify = SSL_get_verify_result(ssl);
616 if (nVerify != X509_V_OK)
617 return error("Nostr: certificate for %s rejected: %s",
618 host.c_str(), X509_verify_cert_error_string(nVerify));
568 return error("Nostr: TLS handshake with %s failed", host.c_str()); 619 return error("Nostr: TLS handshake with %s failed", host.c_str());
569 long nVerify = SSL_get_verify_result(ssl); 620 }
570 if (nVerify != X509_V_OK)
571 return error("Nostr: TLS certificate verification failed for %s: %s",
572 host.c_str(), X509_verify_cert_error_string(nVerify));
573 } 621 }
574 622
575 // WebSocket handshake (HTTP Upgrade) 623 // WebSocket handshake (HTTP Upgrade)
@@ -1017,9 +1065,14 @@ static void HandlePoolAnnouncement(const json& ev)
1017 1065
1018// Resolve a `.btf` address via a relay: fetch the descriptor published by that 1066// Resolve a `.btf` address via a relay: fetch the descriptor published by that
1019// address's key and verify it self-certifies under the decoded pubkey. 1067// address's key and verify it self-certifies under the decoded pubkey.
1068// fExchangeOk reports whether the relay actually finished the conversation
1069// (an EOSE arrived) as opposed to the socket dying mid-read. The caller pools
1070// this socket for reuse, and only a completed exchange leaves it in a state
1071// the next caller can trust.
1020static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 1072static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1021 btf::Descriptor& out) 1073 btf::Descriptor& out, bool& fExchangeOk)
1022{ 1074{
1075 fExchangeOk = false;
1023 unsigned char pk[32]; 1076 unsigned char pk[32];
1024 if (!btf::ParseAddress(btfAddr, pk)) 1077 if (!btf::ParseAddress(btfAddr, pk))
1025 return false; 1078 return false;
@@ -1031,26 +1084,42 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1031 json req = json::array({ "REQ", "btf-resolve", filter }); 1084 json req = json::array({ "REQ", "btf-resolve", filter });
1032 if (!ws.SendText(req.dump())) 1085 if (!ws.SendText(req.dump()))
1033 return false; 1086 return false;
1087 bool fFound = false;
1034 for (;;) 1088 for (;;)
1035 { 1089 {
1036 string msg; 1090 string msg;
1037 if (!ws.RecvText(msg)) 1091 if (!ws.RecvText(msg))
1038 break; 1092 return fFound; // socket died mid-exchange; fExchangeOk stays false
1039 json j; 1093 json j;
1040 try { j = json::parse(msg); } catch (...) { continue; } 1094 try { j = json::parse(msg); } catch (...) { continue; }
1041 if (!j.is_array() || j.empty() || !j[0].is_string()) continue; 1095 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1042 string t = j[0].get<string>(); 1096 string t = j[0].get<string>();
1097 // Every relay message names the subscription it belongs to. This socket
1098 // is pooled and reused, so a message left over from an earlier exchange
1099 // is the ordinary case here, not an exotic one -- and an unmatched EOSE
1100 // ends this query before it has begun, which is indistinguishable from
1101 // "that descriptor does not exist". Check the id.
1102 if (j.size() >= 2 && j[1].is_string() && j[1].get<string>() != "btf-resolve")
1103 continue;
1043 if (t == "EVENT" && j.size() >= 3) 1104 if (t == "EVENT" && j.size() >= 3)
1044 { 1105 {
1045 const json& ev = j[2]; 1106 const json& ev = j[2];
1046 if (ev.contains("content") && 1107 if (!fFound && ev.contains("content") &&
1047 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) 1108 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1048 return true; 1109 fFound = true;
1110 // Deliberately keep reading to EOSE rather than returning here.
1111 // Returning early leaves the rest of the relay's answer queued on a
1112 // socket that goes straight back into the pool, and the next caller
1113 // reads our leftovers as its own -- one message out of step, for
1114 // the life of the process.
1049 } 1115 }
1050 else if (t == "EOSE") 1116 else if (t == "EOSE")
1117 {
1118 fExchangeOk = true; // relay said "that is all": socket is clean
1051 break; 1119 break;
1120 }
1052 } 1121 }
1053 return false; 1122 return fFound;
1054} 1123}
1055 1124
1056// Resolve a `.btf` address into its rendezvous coordinates by querying the 1125// Resolve a `.btf` address into its rendezvous coordinates by querying the
@@ -1069,12 +1138,22 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1069 if (!lease.Connected()) 1138 if (!lease.Connected())
1070 continue; 1139 continue;
1071 btf::Descriptor d; 1140 btf::Descriptor d;
1072 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) 1141 bool fExchangeOk = false;
1142 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d, fExchangeOk))
1073 { 1143 {
1074 lease.fOk = true; // exchange completed, just no match 1144 // Only keep the pooled socket if the relay actually finished
1145 // the conversation. This used to say "exchange completed, just
1146 // no match" on every failure -- including a dead socket -- so
1147 // ~CRelayLease never closed it and fConnected was never
1148 // cleared. A relay connection that died was then pinned in the
1149 // pool, reported healthy, and reused for every later lookup,
1150 // which is why "no Nostr descriptor found" persisted for a
1151 // descriptor that was demonstrably published, and why only a
1152 // restart cleared it.
1153 lease.fOk = fExchangeOk;
1075 continue; 1154 continue;
1076 } 1155 }
1077 lease.fOk = true; 1156 lease.fOk = fExchangeOk;
1078 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1157 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1079 continue; // descriptor predates the owner's relay config 1158 continue; // descriptor predates the owner's relay config
1080 if (!HexDecode(d.enc, enc_pub, 32)) 1159 if (!HexDecode(d.enc, enc_pub, 32))
@@ -1404,7 +1483,7 @@ void ThreadBtfPoolAnnouncer(void*)
1404 LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); 1483 LogPrint("pool", "ThreadBtfPoolAnnouncer started\n");
1405 while (!fShutdown) 1484 while (!fShutdown)
1406 { 1485 {
1407 if (nMineMode == MINE_OPERATOR && gPoolRunning) 1486 if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce)
1408 { 1487 {
1409 BtfPoolAnnouncement ann; 1488 BtfPoolAnnouncement ann;
1410 ann.btfAddress = BtfLocalAddress(); 1489 ann.btfAddress = BtfLocalAddress();
@@ -1736,14 +1815,17 @@ void ThreadNostrSeed(void* parg)
1736 PublishDescriptor(ws, key); 1815 PublishDescriptor(ws, key);
1737 Sleep(700); // let the relay store the replaceable event 1816 Sleep(700); // let the relay store the replaceable event
1738 btf::Descriptor d; 1817 btf::Descriptor d;
1739 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) 1818 bool fExchangeOk = false;
1819 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d, fExchangeOk))
1740 { 1820 {
1741 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", 1821 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n",
1742 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); 1822 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str());
1743 lease.fOk = true; 1823 lease.fOk = fExchangeOk;
1744 break; 1824 break;
1745 } 1825 }
1746 lease.fOk = true; 1826 // Same rule as BtfResolve: a socket only goes back in the pool if
1827 // the relay finished the conversation on it.
1828 lease.fOk = fExchangeOk;
1747 } 1829 }
1748 CATCH_PRINT_EXCEPTION("btf self-test") 1830 CATCH_PRINT_EXCEPTION("btf self-test")
1749 } 1831 }
@@ -1775,7 +1857,7 @@ void ThreadNostrSeed(void* parg)
1775 } 1857 }
1776 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers") 1858 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers")
1777 1859
1778 if (nMineMode == MINE_OPERATOR && gPoolRunning) 1860 if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce)
1779 { 1861 {
1780 BtfPoolAnnouncement ann; 1862 BtfPoolAnnouncement ann;
1781 ann.btfAddress = BtfLocalAddress(); 1863 ann.btfAddress = BtfLocalAddress();