Skip to content

Commit 7e10ddc

Browse files
chopperbrianoclaude
andcommitted
Dashboard: detect and offer to fix Kubo Addresses.Announce (win.18)
Kubo's AutoNAT in 0.40.x often fails to converge on home routers even when port 4001 is forwarded correctly — the node knows it's listening, peers can reach it, but Kubo never publishes a direct /ip4/WAN/tcp/4001 address in /id. DigiAsset Core then falls back to the relay address we added in win.15/16, which works but adds a hop per PSP verification. Add automatic detection and a one-keystroke repair: 1. Background check on startup and every 10 minutes: - Query local IPFS /id, see whether it already lists our WAN IP - If not, probe port 4001 via ifconfig.co/port/4001 to confirm the router forward is actually live 2. When the fixable condition is detected (port open + not announced), show a yellow IPFS hint line on the dashboard and add a yellow [F] Fix IPFS entry to the help bar 3. Pressing F POSTs to the local Kubo HTTP API to set: Addresses.Announce = ["/ip4/WAN/tcp/4001","/ip4/WAN/udp/4001/quic-v1"] then logs a reminder to restart IPFS Desktop so the new announce list takes effect 4. Once IPFS Desktop is restarted and /id contains the direct address, the hint auto-clears and the next DigiAssetCore keepalive switches from the relay multiaddr to the direct one via tier 1 in findPublicAddress — no user action required The fix is gated: it won't run if the port isn't externally reachable (would otherwise advertise a dead address), it won't run if IPFS is already announcing directly (nothing to do), and it won't run before the initial diagnosis completes. This is specifically for NAT'd Windows users — the most common deployment — and addresses the repeated user feedback that "most Windows users are behind NAT." Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6b78d15 commit 7e10ddc

3 files changed

Lines changed: 215 additions & 3 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ SET(PATCH_VERSION 0)
4040
SET(SO_VERSION 0)
4141

4242
# Windows port build number (increment for each Windows-specific release)
43-
SET(WIN_BUILD 17)
43+
SET(WIN_BUILD 18)
4444

4545
# Add source directory
4646
include_directories(src)

src/ConsoleDashboard.cpp

Lines changed: 203 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "ConsoleDashboard.h"
66
#include "AppMain.h"
7+
#include "Config.h"
78
#include "CurlHandler.h"
89
#include "Log.h"
910
#include "Version.h"
@@ -158,6 +159,15 @@ void ConsoleDashboard::processInput() {
158159
}
159160
}
160161
break;
162+
case 'f':
163+
case 'F':
164+
// Fix IPFS Addresses.Announce when we've detected the fixable condition
165+
{
166+
Log* log = Log::GetInstance();
167+
log->addMessage("Attempting to fix IPFS announce list...");
168+
std::thread([this]() { applyIpfsAnnounceFix(); }).detach();
169+
}
170+
break;
161171
case 'h':
162172
case 'H':
163173
case '?':
@@ -183,7 +193,7 @@ void ConsoleDashboard::processInput() {
183193
log->addMessage("External IP: " + ip);
184194
}
185195
}
186-
log->addMessage("Keys: Q=Quit A=Assets P=Port check L=Log level H=This info");
196+
log->addMessage("Keys: Q=Quit A=Assets P=Port check L=Log level F=Fix IPFS announce H=This info");
187197
}
188198
break;
189199
default:
@@ -232,6 +242,18 @@ void ConsoleDashboard::render() {
232242
std::thread([this]() { checkPspRegistration(); }).detach();
233243
}
234244
}
245+
// Run IPFS announce diagnosis in background (at startup + every 10 min)
246+
{
247+
auto now = std::chrono::steady_clock::now();
248+
auto elapsed = std::chrono::duration<double>(now - _lastIpfsAnnounceCheck).count();
249+
if (elapsed >= 600.0 || !_ipfsAnnounceChecked) {
250+
// Only run once WebServer has had time to resolve external IP
251+
WebServer* ws = app->getWebServerIfSet();
252+
if (ws && !ws->getExternalIP().empty() && ws->getExternalIP() != "unknown") {
253+
std::thread([this]() { checkIpfsAnnounce(); }).detach();
254+
}
255+
}
256+
}
235257

236258
ChainAnalyzer* analyzer = app->getChainAnalyzerIfSet();
237259
if (analyzer) {
@@ -390,6 +412,15 @@ void ConsoleDashboard::render() {
390412
out << "\n"; totalRows++;
391413
}
392414

415+
// IPFS announce hint — shown only when there's actionable advice
416+
{
417+
std::lock_guard<std::mutex> lock(_ipfsAnnounceMutex);
418+
if (!_ipfsAnnounceHint.empty()) {
419+
out << ERASE_LINE << " IPFS: " << FG_YELLOW << _ipfsAnnounceHint << RESET << "\n";
420+
totalRows++;
421+
}
422+
}
423+
393424
// Row 5: separator
394425
out << ERASE_LINE << std::string(w, '-') << "\n"; totalRows++;
395426

@@ -482,7 +513,17 @@ void ConsoleDashboard::render() {
482513
}
483514

484515
// Help bar (cursor is already on the right line after the \n above)
485-
out << ERASE_LINE << DIM << " [Q] Quit [A] Assets [P] Ports [L] Log Level [H] Info" << RESET;
516+
// Include [F] only when the fix is applicable, to avoid cluttering the UI.
517+
bool showFixKey = false;
518+
{
519+
std::lock_guard<std::mutex> lock(_ipfsAnnounceMutex);
520+
showFixKey = !_ipfsAnnouncedDirectly && _ipfsPort4001Open && _ipfsAnnounceChecked;
521+
}
522+
out << ERASE_LINE << DIM << " [Q] Quit [A] Assets [P] Ports [L] Log Level";
523+
if (showFixKey) {
524+
out << " " << RESET << FG_YELLOW << "[F] Fix IPFS" << RESET << DIM;
525+
}
526+
out << " [H] Info" << RESET;
486527

487528
// Write everything in one shot to minimize flicker
488529
std::cout << out.str() << std::flush;
@@ -574,6 +615,166 @@ void ConsoleDashboard::loadPayoutInfo() {
574615
}
575616
}
576617

618+
// ---- IPFS announce detection & repair --------------------------------------
619+
620+
namespace {
621+
std::string urlEncodeComponent(const std::string& s) {
622+
std::string out;
623+
out.reserve(s.size() * 3);
624+
for (unsigned char c : s) {
625+
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
626+
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') {
627+
out += (char)c;
628+
} else {
629+
char buf[4];
630+
snprintf(buf, sizeof(buf), "%%%02X", c);
631+
out += buf;
632+
}
633+
}
634+
return out;
635+
}
636+
637+
// Read ipfspath from config.cfg, default to Kubo's standard HTTP API.
638+
std::string getIpfsApiBase() {
639+
try {
640+
Config config("config.cfg");
641+
std::string p = config.getString("ipfspath", "http://localhost:5001/api/v0/");
642+
if (!p.empty() && p.back() != '/') p += '/';
643+
return p;
644+
} catch (...) {
645+
return "http://localhost:5001/api/v0/";
646+
}
647+
}
648+
}
649+
650+
void ConsoleDashboard::checkIpfsAnnounce() {
651+
// Snapshot the WAN IP from the web server (already resolved via ipify at startup)
652+
AppMain* app = AppMain::GetInstance();
653+
WebServer* ws = app->getWebServerIfSet();
654+
std::string wanIp;
655+
if (ws) wanIp = ws->getExternalIP();
656+
if (wanIp.empty() || wanIp == "unknown") {
657+
std::lock_guard<std::mutex> lock(_ipfsAnnounceMutex);
658+
_ipfsAnnounceHint.clear();
659+
return;
660+
}
661+
662+
std::string apiBase = getIpfsApiBase();
663+
664+
// Step 1: does IPFS /id already list an address containing our WAN IP?
665+
bool announced = false;
666+
try {
667+
std::string idJson = CurlHandler::post(apiBase + "id", {}, 5000);
668+
// Cheap substring check — we don't need full JSON parsing for this.
669+
if (idJson.find(wanIp) != std::string::npos) {
670+
announced = true;
671+
}
672+
} catch (...) {
673+
// IPFS API unreachable — can't diagnose, bail out quietly
674+
std::lock_guard<std::mutex> lock(_ipfsAnnounceMutex);
675+
_ipfsAnnounceHint.clear();
676+
_ipfsAnnounceChecked = false;
677+
return;
678+
}
679+
680+
// Step 2: if not announced, probe port 4001 externally to see whether
681+
// fixing is even possible. If the port isn't reachable, setting
682+
// Addresses.Announce would advertise a dead address.
683+
bool portOpen = false;
684+
if (!announced) {
685+
try {
686+
std::string resp = CurlHandler::get("http://ifconfig.co/port/4001", 10000);
687+
if (resp.find("\"reachable\": true") != std::string::npos ||
688+
resp.find("\"reachable\":true") != std::string::npos) {
689+
portOpen = true;
690+
}
691+
} catch (...) {
692+
// leave portOpen = false
693+
}
694+
}
695+
696+
std::lock_guard<std::mutex> lock(_ipfsAnnounceMutex);
697+
_ipfsAnnouncedDirectly = announced;
698+
_ipfsPort4001Open = portOpen;
699+
_ipfsAnnounceChecked = true;
700+
_lastIpfsAnnounceCheck = std::chrono::steady_clock::now();
701+
702+
if (announced) {
703+
_ipfsAnnounceHint.clear(); // nothing to fix
704+
} else if (portOpen) {
705+
_ipfsAnnounceHint = "Port 4001 is open but IPFS isn't announcing it — press [F] to fix";
706+
} else {
707+
_ipfsAnnounceHint.clear(); // NAT'd with no port forward — relay path is the right answer
708+
}
709+
}
710+
711+
bool ConsoleDashboard::applyIpfsAnnounceFix() {
712+
Log* log = Log::GetInstance();
713+
AppMain* app = AppMain::GetInstance();
714+
715+
// Must have a known WAN IP to announce
716+
WebServer* ws = app->getWebServerIfSet();
717+
std::string wanIp;
718+
if (ws) wanIp = ws->getExternalIP();
719+
if (wanIp.empty() || wanIp == "unknown") {
720+
log->addMessage("Fix aborted: external IP not known yet", Log::WARNING);
721+
return false;
722+
}
723+
724+
// Sanity check: only run if our diagnosis said the port is open. This
725+
// prevents setting Announce to a dead address on a NAT'd box.
726+
{
727+
std::lock_guard<std::mutex> lock(_ipfsAnnounceMutex);
728+
if (!_ipfsAnnounceChecked) {
729+
log->addMessage("Fix aborted: diagnosis hasn't run yet, please wait a moment", Log::WARNING);
730+
return false;
731+
}
732+
if (_ipfsAnnouncedDirectly) {
733+
log->addMessage("Nothing to fix: IPFS is already announcing a direct address");
734+
return false;
735+
}
736+
if (!_ipfsPort4001Open) {
737+
log->addMessage("Fix aborted: port 4001 is NOT reachable from the internet. "
738+
"Forward TCP 4001 on your router first (press [P] to recheck).",
739+
Log::WARNING);
740+
return false;
741+
}
742+
}
743+
744+
// Build the JSON array argument: ["/ip4/<wan>/tcp/4001","/ip4/<wan>/udp/4001/quic-v1"]
745+
std::string jsonArray = "[\"/ip4/" + wanIp + "/tcp/4001\",\"/ip4/" + wanIp + "/udp/4001/quic-v1\"]";
746+
747+
// Build the IPFS config URL: POST .../config?arg=Addresses.Announce&arg=<encoded>&json=true
748+
std::string apiBase = getIpfsApiBase();
749+
std::string url = apiBase + "config?arg=Addresses.Announce&arg="
750+
+ urlEncodeComponent(jsonArray) + "&json=true";
751+
752+
log->addMessage("Setting Kubo Addresses.Announce = " + jsonArray);
753+
try {
754+
std::string response = CurlHandler::post(url, {}, 10000);
755+
// Kubo echoes the new config on success; look for our WAN IP in the response.
756+
if (response.find(wanIp) == std::string::npos) {
757+
log->addMessage("IPFS config set returned unexpected response: " + response.substr(0, 200),
758+
Log::WARNING);
759+
return false;
760+
}
761+
} catch (const std::exception& e) {
762+
log->addMessage(std::string("IPFS config set failed: ") + e.what(), Log::WARNING);
763+
return false;
764+
}
765+
766+
log->addMessage("Addresses.Announce updated successfully.");
767+
log->addMessage("IMPORTANT: restart IPFS Desktop (tray icon -> Quit, relaunch) "
768+
"to activate the new announce list.", Log::WARNING);
769+
log->addMessage("After IPFS restart, DigiAssetCore will automatically pick up "
770+
"the direct address on the next keepalive cycle.");
771+
772+
// Mark as applied so the hint goes away until next check
773+
std::lock_guard<std::mutex> lock(_ipfsAnnounceMutex);
774+
_ipfsAnnounceHint = "Addresses.Announce set — restart IPFS Desktop to activate";
775+
return true;
776+
}
777+
577778
// ---- Port checking ----------------------------------------------------------
578779

579780
void ConsoleDashboard::checkPorts() {

src/ConsoleDashboard.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ class ConsoleDashboard {
9494
bool _portCheckDone = false;
9595
void checkPorts();
9696

97+
// IPFS announce detection/repair (for NAT'd users who have port forwarded
98+
// but Kubo's AutoNAT hasn't yet published a direct address in /id)
99+
std::mutex _ipfsAnnounceMutex;
100+
bool _ipfsAnnouncedDirectly = false; // /id lists an address containing our WAN IP
101+
bool _ipfsPort4001Open = false; // ifconfig.co reports 4001 reachable
102+
bool _ipfsAnnounceChecked = false; // we've completed at least one check
103+
std::string _ipfsAnnounceHint; // dashboard status line, empty if all good
104+
std::chrono::steady_clock::time_point _lastIpfsAnnounceCheck;
105+
void checkIpfsAnnounce(); // background: diagnose state, set hint
106+
bool applyIpfsAnnounceFix(); // [F] handler: POST to IPFS config API
107+
97108
// Console dimensions
98109
int _width = 80;
99110
int _height = 25;

0 commit comments

Comments
 (0)