diff --git a/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx b/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx index 7a2cab72..a7a01452 100644 --- a/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx +++ b/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx @@ -256,6 +256,73 @@ describe('P2PStatsSettings', () => { expect(fetchMock).toHaveBeenCalledWith('https://free.freeipapi.com/api/json/91.234.199.189', expect.objectContaining({ signal: expect.any(AbortSignal) })); }); + it('shows peer country flags for DNS6 relay hostnames with embedded IPv6 addresses', async () => { + const fetchMock = vi.fn(async (url: string | URL | Request) => { + const requestUrl = String(url); + if (requestUrl === 'https://free.freeipapi.com/api/json/2a11%3A6100%3A0%3A5e9f%3A%3A0') { + return { + ok: true, + json: async () => ({ + cityName: 'Reykjavik', + countryCode: 'IS', + ipAddress: '2a11:6100:0:5e9f::0', + latitude: 64.1466, + longitude: -21.9426, + }), + }; + } + return { + ok: true, + json: async () => ({ country: 'US', ip: '147.75.84.175' }), + }; + }); + vi.stubGlobal('fetch', fetchMock); + testState.account = { + ...testState.account, + pkcOptions: { + libp2pJsClientsOptions: [{ key: 'libp2pjs' }], + }, + pkc: { + clients: { + libp2pJsClients: { + libp2pjs: { + key: 'libp2pjs', + _helia: { + libp2p: { + getConnections: () => [ + { + remoteAddr: { + toString: () => '/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/ws/p2p/relay-peer/p2p-circuit/p2p/12D3KooWIPv6Peer', + }, + remotePeer: { toString: () => '12D3KooWIPv6Peer' }, + }, + ], + getMultiaddrs: () => ['/ip4/147.75.84.175/tcp/4001/ws'], + getPeers: () => ['12D3KooWIPv6Peer'], + peerId: { toString: () => 'self-peer' }, + }, + }, + }, + }, + }, + }, + }; + + await renderSettings(false); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const connectedPeers = container.querySelector('[data-testid="connected-peers"]'); + expect(connectedPeers?.textContent).toContain('WebSocket through relay'); + expect(connectedPeers?.querySelector('img[aria-label="Iceland"]')).not.toBeNull(); + expect(fetchMock).toHaveBeenCalledWith( + 'https://free.freeipapi.com/api/json/2a11%3A6100%3A0%3A5e9f%3A%3A0', + expect.objectContaining({ signal: expect.any(AbortSignal) }), + ); + }); + it('shows the own IP flag and a red precise map marker when leeching', async () => { const fetchMock = vi.fn(async (url: string | URL | Request) => { const requestUrl = String(url); diff --git a/src/lib/__tests__/peer-geo.test.ts b/src/lib/__tests__/peer-geo.test.ts index 599d7c64..dceb9725 100644 --- a/src/lib/__tests__/peer-geo.test.ts +++ b/src/lib/__tests__/peer-geo.test.ts @@ -37,6 +37,13 @@ describe('extractIpv6FromAddress', () => { expect(extractIpv6FromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888'); expect(extractIpFromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888'); }); + + it('extracts an IPv6 embedded with dashes in a DNS hostname', () => { + const address = '/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/wss/p2p/12D3KooWExample'; + + expect(extractIpv6FromAddress(address)).toBe('2a11:6100:0:5e9f::0'); + expect(extractIpFromAddress(address)).toBe('2a11:6100:0:5e9f::0'); + }); }); describe('isPrivateOrReservedIpv4', () => { @@ -261,6 +268,31 @@ describe('fetchPeerMapLocation', () => { vi.unstubAllGlobals(); }); + + it('resolves a peer location from a DNS6 hostname with an embedded IPv6', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + cityName: 'Reykjavik', + countryCode: 'IS', + ipAddress: '2a11:6100:0:5e9f::0', + latitude: 64.1466, + longitude: -21.9426, + }), + }); + vi.stubGlobal('fetch', fetchMock); + + await expect(fetchPeerMapLocation('/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/wss')).resolves.toMatchObject({ + countryCode: 'is', + label: 'Reykjavik, IS', + lat: 64.1466, + lon: -21.9426, + source: 'geoip', + }); + expect(fetchMock.mock.calls[0][0]).toBe('https://free.freeipapi.com/api/json/2a11%3A6100%3A0%3A5e9f%3A%3A0'); + + vi.unstubAllGlobals(); + }); }); describe('getApproximateCountryCode', () => { diff --git a/src/lib/peer-geo.ts b/src/lib/peer-geo.ts index e5f1c70d..560c74b4 100644 --- a/src/lib/peer-geo.ts +++ b/src/lib/peer-geo.ts @@ -284,7 +284,17 @@ export const extractIpv4FromAddress = (address: string): string | null => { export const extractIpv6FromAddress = (address: string): string | null => { const direct = /\/ip6\/([^/]+)/.exec(address); - return direct ? direct[1] : null; + if (direct) return direct[1]; + const dns = /\/dns6\/([^/]+)/i.exec(address); + const firstLabel = dns?.[1]?.split('.')[0]; + if (!firstLabel || !firstLabel.includes('-') || !/^[0-9a-f-]+$/i.test(firstLabel)) return null; + const candidate = firstLabel.replaceAll('-', ':').toLowerCase(); + try { + new URL(`http://[${candidate}]/`); + return candidate; + } catch { + return null; + } }; const parseOctets = (ip: string): number[] | null => {