From a3aa6652dd5c858a47d6055b57561b5bf176d160 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Tue, 25 Aug 2026 09:57:02 +0200 Subject: [PATCH] webui v153: read the core's own local/external marker, and see IPv6 The address fallback deduces "local or external" from the RFC1918 ranges, but the core already says which is which: getPeerDetails appends its own marker to every ipAddressList entry, " 123 sec loc" or " 123 sec ext" (p3peers.cc). Guessing instead gets two cases wrong -- a peer behind CGNAT (100.64/10) or a double NAT has a private looking external address, and a loopback entry is not external at all -- where the marker is always right. The parser was also IPv4 only, which misses the very case the fallback exists for: GetRetroshareInvite() clears extAddr and moves the address into ipAddressList when it is IPv6, because the certificate format only carries IPv4 numbers, so a peer added by short invite over IPv6 has an empty external address and an entry the regex could not read. RsUrl wraps IPv6 hosts in brackets and escapes the % of a link-local scope id as %25; both are handled, and the entry with no marker at all -- the one that invite path produces -- is external by construction. --- webui-src/app/main.js | 4 +- webui-src/app/network/network_details_tab.js | 45 ++++++++++++-------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/webui-src/app/main.js b/webui-src/app/main.js index b18d7ed..4b87b26 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -116,7 +116,7 @@ const navbar = () => { ? 'Connected to RetroShare Core' : 'Connection Lost', }), - m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v151'), + m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v153'), m('i.fas.fa-sync-alt.refresh-icon', { style: { cursor: 'pointer', fontSize: '0.8em' }, onclick: () => window.location.reload(true), @@ -239,7 +239,7 @@ const MobileStatus = () => { m('small', statusbar.formatBytes(state.totalOut)), ]), ]), - m('.mobile-status-sheet__version', 'WebUI v151'), + m('.mobile-status-sheet__version', 'WebUI v153'), ])), ]; }, diff --git a/webui-src/app/network/network_details_tab.js b/webui-src/app/network/network_details_tab.js index e739eb0..c96e10b 100644 --- a/webui-src/app/network/network_details_tab.js +++ b/webui-src/app/network/network_details_tab.js @@ -17,27 +17,38 @@ function isUsableAddress(address) { return value !== '' && !value.toUpperCase().includes('INVALID') && value !== '0.0.0.0'; } -function parseIpv4Locator(value) { - const match = String(value || '').match(/ipv4:\/\/([^:\s]+):(\d+)/i); +// An entry of RsPeerDetails::ipAddressList is what sockaddr_storage_tostring() +// produced -- ipv4://1.2.3.4:1234, or ipv6://[fe80::1]:1234 since RsUrl wraps +// IPv6 hosts in brackets -- followed by the core's own marker, " 123 sec +// loc" or " 123 sec ext" (p3peers.cc, getPeerDetails). +// +// That marker is the answer to "local or external", and it is worth more than +// deducing it from the address range: a peer behind CGNAT (100.64/10) or a +// double NAT has a private looking external address, and a loopback entry is +// not external at all. +// +// One entry carries no marker: GetRetroshareInvite() clears extAddr and pushes +// the address here with a trailing space when it is IPv6, because the +// certificate format only carries IPv4 numbers. It is external by construction +// -- and it is exactly the one a peer added by short invite arrives with. +function parseLocator(entry) { + const text = String(entry || ''); + const match = text.match(/^\s*(?:ipv4|ipv6):\/\/(\[[^\]]+\]|[^:/\s]+):(\d+)/i); if (!match) return null; - return { address: match[1], port: Number(match[2]) }; -} - -function isPrivateIpv4(address) { - const octets = address.split('.').map(Number); - if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) { - return false; - } - return octets[0] === 10 || - (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || - (octets[0] === 192 && octets[1] === 168) || - (octets[0] === 169 && octets[1] === 254); + // RsUrl escapes the % of a link-local scope id as %25, as the RFC asks; put + // it back rather than showing fe80::1%25eth0 to a human. + const host = (match[1].startsWith('[') ? match[1].slice(1, -1) : match[1]).replace(/%25/gi, '%'); + return { + address: host, + port: Number(match[2]), + scope: /\bsec\s+loc\b/i.test(text) ? 'local' : 'external', + }; } function displayedAddresses(detail, knownAddresses) { - const locators = knownAddresses.map(parseIpv4Locator).filter(Boolean); - const localLocator = locators.find((locator) => isPrivateIpv4(locator.address)); - const externalLocator = locators.find((locator) => !isPrivateIpv4(locator.address)); + const locators = knownAddresses.map(parseLocator).filter(Boolean); + const localLocator = locators.find((locator) => locator.scope === 'local'); + const externalLocator = locators.find((locator) => locator.scope === 'external'); return { localAddress: isUsableAddress(detail.localAddr) ? detail.localAddr : localLocator && localLocator.address,