diff options
Diffstat (limited to 'src/nostr.cpp')
| -rw-r--r-- | src/nostr.cpp | 122 |
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. | ||
| 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 | 135 | ||
| @@ -144,7 +164,7 @@ static map<string, BtfPoolAnnouncement> g_discoveredPools; | |||
| 144 | static CCriticalSection cs_discoveredPools; | 164 | static 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. |
| 147 | static bool LooksLikeRelay(const string& r) | 167 | bool 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) | |||
| 172 | vector<string> BtfAllRelays() | 192 | vector<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. | ||
| 1020 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 1072 | static 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(); |