better picking of main local-ip based on gateway-ip

Massimo Melina committed May 8, 2024 at 23:56 UTC 2a62b4b54e15c6f6ab6ed16f0623fd88f9c87bc3
3 files changed +14 -11
admin/src/InternetPage.ts
+6 -5
@@ -35,10 +35,11 @@ export default function InternetPage() {
35 h && s ? 'error' : h || s ? 'warning' : 'success')
36 type GetNat = Awaited<ReturnType<typeof getNatInfo>>
37 const nat = useApiEx<GetNat>('get_nat', {}, { timeout: 20 })
38 + const { data: publicIps } = useApiEx('get_public_ips')
39 const { data } = nat
40 const port = data?.internalPort
41 const wrongMap = data?.mapped && data.mapped.private.port !== port && data.mapped.private.port
41 - const doubleNat = data?.externalIp && data?.publicIps && !data.publicIps.includes(data.externalIp)
42 + const doubleNat = data?.externalIp && publicIps && !publicIps.includes(data.externalIp)
43 const verifyAgain = useRequestRender()
44 useEffect(() => {
45 if (verifyAgain.state) // skip first
@@ -286,8 +287,8 @@ export default function InternetPage() {
287
288 function networkBox() {
289 if (nat.error) return nat.element
289 - if (!data) return h(CircularProgress)
290 - const direct = data?.publicIps.includes(data?.localIp!)
290 + if (!data || !publicIps) return h(CircularProgress)
291 + const direct = publicIps.includes(data?.localIp!)
292 return h(Flex, { justifyContent: 'space-around' },
293 h(Device, { name: "Server", icon: direct ? Storage : HomeWorkTwoTone, color: localColor, ip: data?.localIp,
294 below: port && h(Box, { fontSize: 'smaller' }, "port ", port),
@@ -301,13 +302,13 @@ export default function InternetPage() {
302 "port ", wrongMap ? 'is wrong' : data?.externalPort || "unknown"),
303 }),
304 h(DataLine),
304 - h(Device, { name: "Internet", icon: PublicTwoTone, ip: data?.publicIps,
305 + h(Device, { name: "Internet", icon: PublicTwoTone, ip: publicIps,
306 color: checkResult ? 'success' : checkResult === false ? 'error' : doubleNat ? 'warning' : undefined,
307 below: checking ? h(LinearProgress, { sx: { height: '1em' } }) : h(Box, { fontSize: 'smaller' },
308 doubleNat && h(LinkBtn, { display: 'block', onClick: () => alertDialog(MSG_ISP, 'warning') }, "Double NAT"),
309 checkResult ? "Working!" : checkResult === false ? "Failed!" : '',
310 ' ',
310 - data?.publicIps.length > 0 && data.internalPort && h(LinkBtn, { onClick: () => verify() }, "Verify")
311 + publicIps.length > 0 && data.internalPort && h(LinkBtn, { onClick: () => verify() }, "Verify")
312 )
313 }),
314 )
src/api.net.ts
+4 -3
@@ -15,6 +15,7 @@ import { selfCheck } from './selfCheck'
15
16 const apis: ApiHandlers = {
17 get_nat: getNatInfo,
18 + get_public_ips: getPublicIps,
19
20 async check_domain({ domain }) {
21 apiAssertTypes({ string: domain })
@@ -60,15 +61,15 @@ const apis: ApiHandlers = {
61 if (url)
62 return await selfCheck(url)
63 || new ApiError(HTTP_SERVICE_UNAVAILABLE)
63 - const nat = await getNatInfo()
64 - if (!nat.publicIps.length)
64 + const [publicIps, nat] = await Promise.all([getPublicIps(), getNatInfo()])
65 + if (!publicIps.length)
66 return new ApiError(HTTP_FAILED_DEPENDENCY, 'cannot detect public ip')
67 if (!nat.internalPort)
68 return new ApiError(HTTP_FAILED_DEPENDENCY, 'no internal port')
69 const finalPort = nat.externalPort || nat.internalPort
70 const proto = nat.proto || (getCertObject() ? 'https' : 'http')
71 const defPort = proto === 'https' ? 443 : 80
71 - const results = onlyTruthy(await promiseBestEffort(nat.publicIps.map(ip =>
72 + const results = onlyTruthy(await promiseBestEffort(publicIps.map(ip =>
73 selfCheck(`${proto}://${ip}${finalPort === defPort ? '' : ':' + finalPort}`) )))
74 return results.length ? results : new ApiError(HTTP_SERVICE_UNAVAILABLE)
75 },
src/nat.ts
+4 -3
@@ -59,11 +59,10 @@ export const getPublicIps = debounceAsync(async () => {
59 if (!validIps.length) throw "no good"
60 return validIps
61 }) )))
62 - return _.uniq(ips.flat())
62 + return defaultBaseUrl.publicIps = _.uniq(ips.flat())
63 }, 0, { retain: 10 * MINUTE })
64
65 export const getNatInfo = debounceAsync(async () => {
66 - const gettingIps = getPublicIps() // don't wait, do it in parallel
66 const res = await haveTimeout(10_000, upnpClient.getGateway()).catch(() => null)
67 const status = await getServerStatus()
68 const mappings = res && await haveTimeout(5_000, upnpClient.getMappings()).catch(() => null)
@@ -74,12 +73,13 @@ export const getNatInfo = debounceAsync(async () => {
73 const internalPort = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port || undefined
74 const mapped = _.find(mappings, x => x.private.host === localIp && x.private.port === internalPort)
75 const externalPort = mapped?.public.port
76 + if (localIp)
77 + defaultBaseUrl.localIp = localIp
78 defaultBaseUrl.port = externalPort || internalPort || 0
79 return {
80 upnp: Boolean(res),
81 localIp,
82 gatewayIp,
82 - publicIps: defaultBaseUrl.publicIps = await gettingIps,
83 externalIp: defaultBaseUrl.externalIp,
84 mapped,
85 mapped80: _.find(mappings, x => x.private.host === localIp && x.private.port === 80 && x.public.port === 80),
@@ -88,6 +88,7 @@ export const getNatInfo = debounceAsync(async () => {
88 proto: status?.https?.listening ? 'https' : status?.http?.listening ? 'http' : '',
89 }
90 })
91 +getNatInfo()
92
93 function findGateway(): Promise<string | undefined> {
94 return new Promise((resolve, reject) =>