diff options
Diffstat (limited to 'src/nostr.cpp')
| -rw-r--r-- | src/nostr.cpp | 123 |
1 files changed, 103 insertions, 20 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp index 35fde43..e2d839c 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. | ||
| 88 | const char* pszNostrRelays[] = { | 100 | const 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 | }; |
| 101 | const int nNostrRelays = ARRAYLEN(pszNostrRelays); | 115 | const 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. | ||
| 132 | bool fBtfRelayPinned = false; | ||
| 113 | string strBtfActiveRelay; | 133 | string strBtfActiveRelay; |
| 114 | static CCriticalSection cs_activeRelay; | 134 | static CCriticalSection cs_activeRelay; |
| 115 | static string strBtfOnionEndpoint; | 135 | static string strBtfOnionEndpoint; |
| @@ -166,7 +186,7 @@ static map<string, BtfPoolAnnouncement> g_discoveredPools; | |||
| 166 | static CCriticalSection cs_discoveredPools; | 186 | static CCriticalSection cs_discoveredPools; |
| 167 | 187 | ||
| 168 | // Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host. | 188 | // Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host. |
| 169 | static bool LooksLikeRelay(const string& r) | 189 | bool LooksLikeRelay(const string& r) |
| 170 | { | 190 | { |
| 171 | size_t colon = r.rfind(':'); | 191 | size_t colon = r.rfind(':'); |
| 172 | if (colon == string::npos || colon == 0 || colon + 1 >= r.size()) | 192 | if (colon == string::npos || colon == 0 || colon + 1 >= r.size()) |
| @@ -194,6 +214,8 @@ static bool LooksLikeRelay(const string& r) | |||
| 194 | vector<string> BtfAllRelays() | 214 | vector<string> BtfAllRelays() |
| 195 | { | 215 | { |
| 196 | vector<string> all = vBtfMeetingRelays; | 216 | vector<string> all = vBtfMeetingRelays; |
| 217 | if (fBtfRelayPinned) | ||
| 218 | return all; | ||
| 197 | CRITICAL_BLOCK(cs_discoveredRelays) | 219 | CRITICAL_BLOCK(cs_discoveredRelays) |
| 198 | foreach(const string& r, g_discoveredRelays) | 220 | foreach(const string& r, g_discoveredRelays) |
| 199 | if (find(all.begin(), all.end(), r) == all.end()) | 221 | if (find(all.begin(), all.end(), r) == all.end()) |
| @@ -578,26 +600,52 @@ public: | |||
| 578 | { | 600 | { |
| 579 | sslctx = SSL_CTX_new(TLS_client_method()); | 601 | sslctx = SSL_CTX_new(TLS_client_method()); |
| 580 | if (!sslctx) return false; | 602 | if (!sslctx) return false; |
| 603 | |||
| 604 | // SSL_CTX_new defaults to SSL_VERIFY_NONE with no trust store | ||
| 605 | // loaded, so until now every wss:// relay connection was accepted | ||
| 606 | // without looking at the certificate at all -- any on-path | ||
| 607 | // attacker could impersonate a relay. Our own events are signed, | ||
| 608 | // so a MITM cannot forge them, but it could drop the rendezvous | ||
| 609 | // registration (leaving the pool unreachable to miners) or answer | ||
| 610 | // subscriptions with whatever it liked. | ||
| 581 | SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); | 611 | SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); |
| 612 | |||
| 613 | // OpenSSL's default verify paths are usually empty on Windows, so | ||
| 614 | // the system ROOT store is loaded too -- without it verification | ||
| 615 | // fails closed there and every relay becomes unreachable. | ||
| 582 | bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1); | 616 | bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1); |
| 583 | #ifdef _WIN32 | 617 | #ifdef _WIN32 |
| 584 | fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded; | 618 | fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded; |
| 585 | #endif | 619 | #endif |
| 586 | if (!fTrustLoaded) | 620 | if (!fTrustLoaded) |
| 587 | return error("Nostr: failed to load TLS trust store"); | 621 | return error("Nostr: failed to load TLS trust store"); |
| 622 | |||
| 623 | // Floor the protocol version. Verification says who the peer is, | ||
| 624 | // not how weak a cipher suite we will agree to; without this a | ||
| 625 | // relay offering nothing better still gets TLS 1.0. | ||
| 626 | SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION); | ||
| 627 | |||
| 588 | ssl = SSL_new(sslctx); | 628 | ssl = SSL_new(sslctx); |
| 589 | if (!ssl) return false; | 629 | if (!ssl) return false; |
| 590 | SSL_set_fd(ssl, (int)hSocket); | 630 | SSL_set_fd(ssl, (int)hSocket); |
| 591 | if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) | 631 | 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()); | 632 | return error("Nostr: failed to set TLS SNI for %s", host.c_str()); |
| 633 | |||
| 634 | // Verifying the chain is not enough on its own: without this the | ||
| 635 | // relay could present any certificate a trusted CA ever issued, | ||
| 636 | // for any name. SSL_set1_host binds the check to the host we asked | ||
| 637 | // for. | ||
| 593 | if (SSL_set1_host(ssl, host.c_str()) != 1) | 638 | if (SSL_set1_host(ssl, host.c_str()) != 1) |
| 594 | return error("Nostr: failed to set TLS hostname for %s", host.c_str()); | 639 | return error("Nostr: failed to set TLS hostname for %s", host.c_str()); |
| 640 | |||
| 595 | if (SSL_connect(ssl) != 1) | 641 | if (SSL_connect(ssl) != 1) |
| 642 | { | ||
| 643 | long nVerify = SSL_get_verify_result(ssl); | ||
| 644 | if (nVerify != X509_V_OK) | ||
| 645 | return error("Nostr: certificate for %s rejected: %s", | ||
| 646 | host.c_str(), X509_verify_cert_error_string(nVerify)); | ||
| 596 | return error("Nostr: TLS handshake with %s failed", host.c_str()); | 647 | return error("Nostr: TLS handshake with %s failed", host.c_str()); |
| 597 | long nVerify = SSL_get_verify_result(ssl); | 648 | } |
| 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 | } | 649 | } |
| 602 | 650 | ||
| 603 | // WebSocket handshake (HTTP Upgrade) | 651 | // WebSocket handshake (HTTP Upgrade) |
| @@ -1049,9 +1097,15 @@ static void HandlePoolAnnouncement(const json& ev) | |||
| 1049 | 1097 | ||
| 1050 | // Resolve a `.btf` address via a relay: fetch the descriptor published by that | 1098 | // 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. | 1099 | // address's key and verify it self-certifies under the decoded pubkey. |
| 1100 | // fExchangeOk reports whether the relay actually finished the conversation | ||
| 1101 | // (an EOSE arrived) as opposed to the socket dying mid-read. The caller pools | ||
| 1102 | // this socket for reuse, and only a completed exchange leaves it in a state | ||
| 1103 | // the next caller can trust. | ||
| 1052 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 1104 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, |
| 1053 | btf::Descriptor& out, string* rawOut = NULL) | 1105 | btf::Descriptor& out, bool& fExchangeOk, |
| 1106 | string* rawOut = NULL) | ||
| 1054 | { | 1107 | { |
| 1108 | fExchangeOk = false; | ||
| 1055 | unsigned char pk[32]; | 1109 | unsigned char pk[32]; |
| 1056 | if (!btf::ParseAddress(btfAddr, pk)) | 1110 | if (!btf::ParseAddress(btfAddr, pk)) |
| 1057 | return false; | 1111 | return false; |
| @@ -1063,30 +1117,46 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | |||
| 1063 | json req = json::array({ "REQ", "btf-resolve", filter }); | 1117 | json req = json::array({ "REQ", "btf-resolve", filter }); |
| 1064 | if (!ws.SendText(req.dump())) | 1118 | if (!ws.SendText(req.dump())) |
| 1065 | return false; | 1119 | return false; |
| 1120 | bool fFound = false; | ||
| 1066 | for (;;) | 1121 | for (;;) |
| 1067 | { | 1122 | { |
| 1068 | string msg; | 1123 | string msg; |
| 1069 | if (!ws.RecvText(msg)) | 1124 | if (!ws.RecvText(msg)) |
| 1070 | break; | 1125 | return fFound; // socket died mid-exchange; fExchangeOk stays false |
| 1071 | json j; | 1126 | json j; |
| 1072 | try { j = json::parse(msg); } catch (...) { continue; } | 1127 | try { j = json::parse(msg); } catch (...) { continue; } |
| 1073 | if (!j.is_array() || j.empty() || !j[0].is_string()) continue; | 1128 | if (!j.is_array() || j.empty() || !j[0].is_string()) continue; |
| 1074 | string t = j[0].get<string>(); | 1129 | string t = j[0].get<string>(); |
| 1130 | // Every relay message names the subscription it belongs to. This socket | ||
| 1131 | // is pooled and reused, so a message left over from an earlier exchange | ||
| 1132 | // is the ordinary case here, not an exotic one -- and an unmatched EOSE | ||
| 1133 | // ends this query before it has begun, which is indistinguishable from | ||
| 1134 | // "that descriptor does not exist". Check the id. | ||
| 1135 | if (j.size() >= 2 && j[1].is_string() && j[1].get<string>() != "btf-resolve") | ||
| 1136 | continue; | ||
| 1075 | if (t == "EVENT" && j.size() >= 3) | 1137 | if (t == "EVENT" && j.size() >= 3) |
| 1076 | { | 1138 | { |
| 1077 | const json& ev = j[2]; | 1139 | const json& ev = j[2]; |
| 1078 | if (ev.contains("content") && | 1140 | if (!fFound && ev.contains("content") && |
| 1079 | btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) | 1141 | btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) |
| 1080 | { | 1142 | { |
| 1143 | fFound = true; | ||
| 1081 | if (rawOut) | 1144 | if (rawOut) |
| 1082 | *rawOut = ev["content"].get<string>(); | 1145 | *rawOut = ev["content"].get<string>(); |
| 1083 | return true; | ||
| 1084 | } | 1146 | } |
| 1147 | // Deliberately keep reading to EOSE rather than returning here. | ||
| 1148 | // Returning early leaves the rest of the relay's answer queued on a | ||
| 1149 | // socket that goes straight back into the pool, and the next caller | ||
| 1150 | // reads our leftovers as its own -- one message out of step, for | ||
| 1151 | // the life of the process. | ||
| 1085 | } | 1152 | } |
| 1086 | else if (t == "EOSE") | 1153 | else if (t == "EOSE") |
| 1154 | { | ||
| 1155 | fExchangeOk = true; // relay said "that is all": socket is clean | ||
| 1087 | break; | 1156 | break; |
| 1157 | } | ||
| 1088 | } | 1158 | } |
| 1089 | return false; | 1159 | return fFound; |
| 1090 | } | 1160 | } |
| 1091 | 1161 | ||
| 1092 | // Resolve a `.btf` address into its rendezvous coordinates by querying the | 1162 | // Resolve a `.btf` address into its rendezvous coordinates by querying the |
| @@ -1107,12 +1177,22 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | |||
| 1107 | if (!lease.Connected()) | 1177 | if (!lease.Connected()) |
| 1108 | continue; | 1178 | continue; |
| 1109 | btf::Descriptor d; | 1179 | btf::Descriptor d; |
| 1110 | if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) | 1180 | bool fExchangeOk = false; |
| 1181 | if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d, fExchangeOk)) | ||
| 1111 | { | 1182 | { |
| 1112 | lease.fOk = true; // exchange completed, just no match | 1183 | // Only keep the pooled socket if the relay actually finished |
| 1184 | // the conversation. This used to say "exchange completed, just | ||
| 1185 | // no match" on every failure -- including a dead socket -- so | ||
| 1186 | // ~CRelayLease never closed it and fConnected was never | ||
| 1187 | // cleared. A relay connection that died was then pinned in the | ||
| 1188 | // pool, reported healthy, and reused for every later lookup, | ||
| 1189 | // which is why "no Nostr descriptor found" persisted for a | ||
| 1190 | // descriptor that was demonstrably published, and why only a | ||
| 1191 | // restart cleared it. | ||
| 1192 | lease.fOk = fExchangeOk; | ||
| 1113 | continue; | 1193 | continue; |
| 1114 | } | 1194 | } |
| 1115 | lease.fOk = true; | 1195 | lease.fOk = fExchangeOk; |
| 1116 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") | 1196 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") |
| 1117 | { | 1197 | { |
| 1118 | if (!onionHostPort || d.onion.empty()) | 1198 | if (!onionHostPort || d.onion.empty()) |
| @@ -1450,7 +1530,7 @@ void ThreadBtfPoolAnnouncer(void*) | |||
| 1450 | LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); | 1530 | LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); |
| 1451 | while (!fShutdown) | 1531 | while (!fShutdown) |
| 1452 | { | 1532 | { |
| 1453 | if (nMineMode == MINE_OPERATOR && gPoolRunning) | 1533 | if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce) |
| 1454 | { | 1534 | { |
| 1455 | BtfPoolAnnouncement ann; | 1535 | BtfPoolAnnouncement ann; |
| 1456 | ann.btfAddress = BtfLocalAddress(); | 1536 | ann.btfAddress = BtfLocalAddress(); |
| @@ -1785,14 +1865,17 @@ void ThreadNostrSeed(void* parg) | |||
| 1785 | PublishDescriptor(ws, key); | 1865 | PublishDescriptor(ws, key); |
| 1786 | Sleep(700); // let the relay store the replaceable event | 1866 | Sleep(700); // let the relay store the replaceable event |
| 1787 | btf::Descriptor d; | 1867 | btf::Descriptor d; |
| 1788 | if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) | 1868 | bool fExchangeOk = false; |
| 1869 | if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d, fExchangeOk)) | ||
| 1789 | { | 1870 | { |
| 1790 | LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", | 1871 | 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()); | 1872 | pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); |
| 1792 | lease.fOk = true; | 1873 | lease.fOk = fExchangeOk; |
| 1793 | break; | 1874 | break; |
| 1794 | } | 1875 | } |
| 1795 | lease.fOk = true; | 1876 | // Same rule as BtfResolve: a socket only goes back in the pool if |
| 1877 | // the relay finished the conversation on it. | ||
| 1878 | lease.fOk = fExchangeOk; | ||
| 1796 | } | 1879 | } |
| 1797 | CATCH_PRINT_EXCEPTION("btf self-test") | 1880 | CATCH_PRINT_EXCEPTION("btf self-test") |
| 1798 | } | 1881 | } |
| @@ -1824,7 +1907,7 @@ void ThreadNostrSeed(void* parg) | |||
| 1824 | } | 1907 | } |
| 1825 | CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers") | 1908 | CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers") |
| 1826 | 1909 | ||
| 1827 | if (nMineMode == MINE_OPERATOR && gPoolRunning) | 1910 | if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce) |
| 1828 | { | 1911 | { |
| 1829 | BtfPoolAnnouncement ann; | 1912 | BtfPoolAnnouncement ann; |
| 1830 | ann.btfAddress = BtfLocalAddress(); | 1913 | ann.btfAddress = BtfLocalAddress(); |