Skip to content

Commit 06e9eef

Browse files
ehsan6shaclaude
andcommitted
apps/box: Diagnostics — real TCP probe to relay :4001 (kubo libp2p)
The previous HTTPS-HEAD-on-:443 probe was a false-negative machine for Fula relays. They're kubo-based and only expose libp2p multistream on TCP :4001 — no HTTPS termination on :443, no admin ports. Every working relay was showing red. Replace the probe with a real TCP socket connect to :4001: - Add react-native-tcp-socket@6.4.1 as a native dep. RN's built-in fetch + WebSocket can't probe a port that doesn't speak HTTP or TLS, so a native socket library is the only honest way to do this. - Rewrite probeRelay to: TcpSocket.createConnection({ host: dnsName, port: 4001 }, () => ok) on error → 'failed', on wall-clock timeout (5s) → 'failed' Resolve 'ok' the instant the connect callback fires (TCP SYN-ACK complete) and destroy() the socket immediately so we don't drag through the libp2p multistream handshake we can't speak anyway. The relay closes its side too; both ends settle quickly. - Wall-clock timeout, not TcpSocket's per-socket timeout. The per-socket timeout only fires AFTER connect; we need to guard the connect phase too so a host that silently drops SYNs doesn't hang the probe forever. - i18n string updated: "Relays (libp2p reachability on TCP :4001):" — no more "proxy for libp2p path" weasel-wording. ** REQUIRES NATIVE APK REBUILD ** — react-native-tcp-socket is a native module. Metro bundle reload is not enough. After pulling this commit: - yarn install (pulls 6.4.1 into node_modules) - cd apps/box && cd android && ./gradlew clean (optional, safe) - npx react-native run-android (rebuilds APK) - (iOS, if desired) cd ios && pod install && npx react-native run-ios Tests: 30/30 Diagnostics jest tests pass. Probe behaviour itself isn't unit-tested — the jest react-native mock can't fake socket semantics, and the screen module isn't import-tested (the component-library transitive deps are jest-hostile, per the existing skip-snapshot convention). The probe is exercised on the real device. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 138eb8e commit 06e9eef

4 files changed

Lines changed: 133 additions & 34 deletions

File tree

apps/box/src/i18n/locales/en/translation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"discoveryFailed": "Discovery service is unreachable (your phone can't find a relay)",
1515
"relaysChecking": "Probing relays…",
1616
"relaysUnknown": "No relays are known yet. Try again once the discovery service is reachable.",
17-
"relaysListLabel": "Relays (HTTPS reachability on :443 — proxy for libp2p path):",
17+
"relaysListLabel": "Relays (libp2p reachability on TCP :4001):",
1818
"pluginStatusTitle": "Blox AI plugin",
1919
"pluginChecking": "Checking plugin status…",
2020
"pluginInstalled": "Blox AI is installed on your Blox",

apps/box/src/screens/Diagnostics/Diagnostics.screen.tsx

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import { useTranslation } from 'react-i18next';
3737
import NetInfo from '@react-native-community/netinfo';
3838
import AsyncStorage from '@react-native-async-storage/async-storage';
39+
import TcpSocket from 'react-native-tcp-socket';
3940

4041
import { usePluginsStore } from '../../stores/usePluginsStore';
4142
import { Routes } from '../../navigation/navigationConfig';
@@ -50,6 +51,11 @@ const PHONE_INTERNET_PROBE_TIMEOUT_MS = 5000;
5051
const DISCOVERY_PROBE_URL = `${Constants.FXDiscoveryURL}/relays`;
5152
const DISCOVERY_PROBE_TIMEOUT_MS = 5000;
5253
const RELAY_PROBE_TIMEOUT_MS = 5000;
54+
// Standard kubo / libp2p TCP port. Fula relays are kubo-based and only
55+
// expose the libp2p multistream on this port — no HTTPS on :443, no other
56+
// admin ports. A successful TCP SYN-ACK on :4001 is the only meaningful
57+
// "reachable from this phone" signal.
58+
const RELAY_PROBE_PORT = 4001;
5359
const BLOX_AI_PLUGIN_NAME = 'blox-ai';
5460

5561
type ProbeStatus = 'checking' | 'ok' | 'failed';
@@ -151,42 +157,57 @@ async function probeDiscoveryAndListRelays(): Promise<{
151157
return { discovery, dnsNames: hardcoded };
152158
}
153159

154-
// Probe a single relay's hostname for HTTPS reachability on port 443. This
155-
// is NOT the same as libp2p reachability — Fula relays serve libp2p on TCP
156-
// 4001, and React Native's fetch can only do HTTPS. On a network that blocks
157-
// 4001 but allows 443 (some corporate firewalls), this probe will show ✓
158-
// while real connections still fail.
160+
// Probe a single relay's libp2p port via raw TCP. Fula relays are kubo-based
161+
// and only expose libp2p multistream on TCP :4001 — no HTTPS on :443, no
162+
// admin ports. A TCP SYN-ACK on :4001 is the only meaningful "the phone
163+
// can reach this relay" signal.
159164
//
160-
// Why we ship it anyway: Fula relays are Cloudflare-fronted, so :443 and
161-
// :4001 share DNS + network path. A green ✓ usually means the relay's host
162-
// is reachable; a red ✗ definitively means something is blocking the host.
163-
// The user-facing i18n copy ("HTTPS reachable" rather than "reachable")
164-
// reflects this narrowness. A real TCP/4001 probe would need
165-
// react-native-tcp-socket (not currently a dep); tracked as a follow-up.
165+
// We rely on react-native-tcp-socket here (added as a native dep) because
166+
// RN's built-in fetch + WebSocket can't probe a port that doesn't speak
167+
// HTTP or TLS. An earlier HTTPS-HEAD-on-:443 probe was a false-negative
168+
// machine — kubo relays always returned ✗ even when fully reachable.
166169
//
167-
// Any HTTP response (2xx/3xx/4xx/5xx) means TCP+TLS succeeded — that's the
168-
// signal we care about. The relay may legitimately return 404 on /, since
169-
// it doesn't serve HTTP routes; that's still "reachable" for our purposes.
170+
// We resolve 'ok' the moment the connect callback fires (TCP handshake
171+
// complete) and immediately destroy() the socket so we don't trigger the
172+
// libp2p multistream handshake. The relay will close the connection on
173+
// its side too once it sees us not speak; that's fine, we already have
174+
// our signal.
170175
async function probeRelay(dnsName: string): Promise<ProbeStatus> {
171-
try {
172-
const controller = new AbortController();
173-
const timer = setTimeout(() => controller.abort(), RELAY_PROBE_TIMEOUT_MS);
176+
return new Promise<ProbeStatus>((resolve) => {
177+
let settled = false;
178+
let client: ReturnType<typeof TcpSocket.createConnection> | null = null;
179+
180+
const settle = (status: ProbeStatus) => {
181+
if (settled) return;
182+
settled = true;
183+
if (client) {
184+
try { client.destroy(); } catch { /* socket already torn down */ }
185+
}
186+
resolve(status);
187+
};
188+
189+
// Connect timeout — TcpSocket's per-socket timeout fires AFTER
190+
// connect, not during. We need our own wall-clock guard so a host
191+
// that silently drops SYNs doesn't hang the probe forever.
192+
const timer = setTimeout(() => settle('failed'), RELAY_PROBE_TIMEOUT_MS);
193+
174194
try {
175-
const r = await fetch(`https://${dnsName}/`, {
176-
method: 'HEAD',
177-
signal: controller.signal,
195+
client = TcpSocket.createConnection(
196+
{ host: dnsName, port: RELAY_PROBE_PORT },
197+
() => {
198+
clearTimeout(timer);
199+
settle('ok');
200+
}
201+
);
202+
client.on('error', () => {
203+
clearTimeout(timer);
204+
settle('failed');
178205
});
179-
// Any standard HTTP status counts as reachable — including 4xx/5xx
180-
// because those still prove TCP+TLS+HTTP completed. The only
181-
// "unreachable" outcomes are network errors / DNS fail / timeout,
182-
// all of which throw rather than resolve.
183-
return r.status >= 100 && r.status < 600 ? 'ok' : 'failed';
184-
} finally {
206+
} catch {
185207
clearTimeout(timer);
208+
settle('failed');
186209
}
187-
} catch {
188-
return 'failed';
189-
}
210+
});
190211
}
191212

192213
export const DiagnosticsScreen: React.FC = () => {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@react-navigation/native-stack": "^7.10.1",
4444
"@react-navigation/stack": "^7.6.16",
4545
"@reown/appkit-ethers-react-native": "latest",
46-
"@reown/appkit-react-native": "latest",
46+
"@reown/appkit-react-native": "^2.0.4",
4747
"@rneui/base": "^4.0.0-rc.8",
4848
"@rneui/themed": "^4.0.0-rc.8",
4949
"@shopify/react-native-skia": "^2.4.14",
@@ -88,6 +88,7 @@
8888
"react-native-svg-transformer": "^1.5.2",
8989
"react-native-syntax-highlighter": "^2.1.0",
9090
"react-native-tab-view": "^4.2.2",
91+
"react-native-tcp-socket": "^6.4.1",
9192
"react-native-url-polyfill": "^3.0.0",
9293
"react-native-vision-camera": "^4.6.3",
9394
"react-native-webview": "^13.16.0",

yarn.lock

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5869,6 +5869,19 @@ __metadata:
58695869
languageName: node
58705870
linkType: hard
58715871

5872+
"@reown/appkit-common-react-native@npm:2.0.4":
5873+
version: 2.0.4
5874+
resolution: "@reown/appkit-common-react-native@npm:2.0.4"
5875+
dependencies:
5876+
bignumber.js: 9.1.2
5877+
dayjs: 1.11.10
5878+
peerDependencies:
5879+
react: ">=18"
5880+
react-native: ">=0.72"
5881+
checksum: 3ce6a50644a4236fcdeab7670f0aef8970e60fa7902c367f791deb8d6f8a6bffb90293ec7ae2c2c8e7ade6f502f97092e2321ad27a2f951f557a471c75e425ac
5882+
languageName: node
5883+
linkType: hard
5884+
58725885
"@reown/appkit-core-react-native@npm:2.0.1":
58735886
version: 2.0.1
58745887
resolution: "@reown/appkit-core-react-native@npm:2.0.1"
@@ -5885,6 +5898,22 @@ __metadata:
58855898
languageName: node
58865899
linkType: hard
58875900

5901+
"@reown/appkit-core-react-native@npm:2.0.4":
5902+
version: 2.0.4
5903+
resolution: "@reown/appkit-core-react-native@npm:2.0.4"
5904+
dependencies:
5905+
"@reown/appkit-common-react-native": 2.0.4
5906+
countries-and-timezones: 3.7.2
5907+
derive-valtio: 0.2.0
5908+
valtio: 2.1.8
5909+
peerDependencies:
5910+
"@walletconnect/react-native-compat": ">=2.16.1"
5911+
react: ">=18"
5912+
react-native: ">=0.72"
5913+
checksum: e90d6e202b98ad0757102ba8a8c4ea0998de130c29fc025f5a217826db7762cf8ba5ebf2b4c7a8875ea69363cbfde2c8e9c435f66c388278facb6ff3870eab48
5914+
languageName: node
5915+
linkType: hard
5916+
58885917
"@reown/appkit-ethers-react-native@npm:latest":
58895918
version: 2.0.1
58905919
resolution: "@reown/appkit-ethers-react-native@npm:2.0.1"
@@ -5901,7 +5930,7 @@ __metadata:
59015930
languageName: node
59025931
linkType: hard
59035932

5904-
"@reown/appkit-react-native@npm:2.0.1, @reown/appkit-react-native@npm:latest":
5933+
"@reown/appkit-react-native@npm:2.0.1":
59055934
version: 2.0.1
59065935
resolution: "@reown/appkit-react-native@npm:2.0.1"
59075936
dependencies:
@@ -5921,6 +5950,26 @@ __metadata:
59215950
languageName: node
59225951
linkType: hard
59235952

5953+
"@reown/appkit-react-native@npm:^2.0.4":
5954+
version: 2.0.4
5955+
resolution: "@reown/appkit-react-native@npm:2.0.4"
5956+
dependencies:
5957+
"@reown/appkit-common-react-native": 2.0.4
5958+
"@reown/appkit-core-react-native": 2.0.4
5959+
"@reown/appkit-ui-react-native": 2.0.4
5960+
"@walletconnect/universal-provider": 2.21.10
5961+
valtio: 2.1.8
5962+
peerDependencies:
5963+
"@walletconnect/react-native-compat": ">=2.16.1"
5964+
"@walletconnect/utils": ">=2.16.1"
5965+
react: ">=18"
5966+
react-native: ">=0.72"
5967+
react-native-safe-area-context: ">=4.4.0"
5968+
react-native-svg: ">=13.10"
5969+
checksum: 44087f3d043a23ab698ce6493407f4482a7661760efeaa8d3d4d4b7416461d3cb297a6d2ba3b07c6b6ec44de936e827bfd1e087e604b60d5172e40dd011229b2
5970+
languageName: node
5971+
linkType: hard
5972+
59245973
"@reown/appkit-ui-react-native@npm:2.0.1":
59255974
version: 2.0.1
59265975
resolution: "@reown/appkit-ui-react-native@npm:2.0.1"
@@ -5936,6 +5985,21 @@ __metadata:
59365985
languageName: node
59375986
linkType: hard
59385987

5988+
"@reown/appkit-ui-react-native@npm:2.0.4":
5989+
version: 2.0.4
5990+
resolution: "@reown/appkit-ui-react-native@npm:2.0.4"
5991+
dependencies:
5992+
"@reown/appkit-common-react-native": 2.0.4
5993+
polished: 4.3.1
5994+
qrcode: 1.5.3
5995+
peerDependencies:
5996+
react: ">=18"
5997+
react-native: ">=0.72"
5998+
react-native-svg: ">=13.10"
5999+
checksum: 85f164fda92fc5c6d41e4a33f2d3127af3e023f34540d50e0797a53f779ca9f9b6cfa2f3a90ce7f9ca55559e750e09871786c80bc84d8c65eac6c551f36af1bb
6000+
languageName: node
6001+
linkType: hard
6002+
59396003
"@rneui/base@npm:^4.0.0-rc.8":
59406004
version: 4.0.0-rc.8
59416005
resolution: "@rneui/base@npm:4.0.0-rc.8"
@@ -12384,7 +12448,7 @@ __metadata:
1238412448
languageName: node
1238512449
linkType: hard
1238612450

12387-
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4":
12451+
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4, eventemitter3@npm:^4.0.7":
1238812452
version: 4.0.7
1238912453
resolution: "eventemitter3@npm:4.0.7"
1239012454
checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374
@@ -13117,7 +13181,7 @@ __metadata:
1311713181
"@react-navigation/native-stack": ^7.10.1
1311813182
"@react-navigation/stack": ^7.6.16
1311913183
"@reown/appkit-ethers-react-native": latest
13120-
"@reown/appkit-react-native": latest
13184+
"@reown/appkit-react-native": ^2.0.4
1312113185
"@rneui/base": ^4.0.0-rc.8
1312213186
"@rneui/themed": ^4.0.0-rc.8
1312313187
"@shopify/react-native-skia": ^2.4.14
@@ -13191,6 +13255,7 @@ __metadata:
1319113255
react-native-svg-transformer: ^1.5.2
1319213256
react-native-syntax-highlighter: ^2.1.0
1319313257
react-native-tab-view: ^4.2.2
13258+
react-native-tcp-socket: ^6.4.1
1319413259
react-native-url-polyfill: ^3.0.0
1319513260
react-native-vision-camera: ^4.6.3
1319613261
react-native-webview: ^13.16.0
@@ -19660,6 +19725,18 @@ __metadata:
1966019725
languageName: node
1966119726
linkType: hard
1966219727

19728+
"react-native-tcp-socket@npm:^6.4.1":
19729+
version: 6.4.1
19730+
resolution: "react-native-tcp-socket@npm:6.4.1"
19731+
dependencies:
19732+
buffer: ^5.4.3
19733+
eventemitter3: ^4.0.7
19734+
peerDependencies:
19735+
react-native: ">=0.60.0"
19736+
checksum: 0c0af3a3044a3edeb9fb5077476eba7ba26675b4af2a1d6b11a63cd56a939a63e4675a63cc1da72cd573cf37ae386a4e98b16b442e4adc11c0addbccaf48b925
19737+
languageName: node
19738+
linkType: hard
19739+
1966319740
"react-native-url-polyfill@npm:2.0.0":
1966419741
version: 2.0.0
1966519742
resolution: "react-native-url-polyfill@npm:2.0.0"

0 commit comments

Comments
 (0)