fix(system): prevent false offline reports when Cloudflare endpoint is unreachable

This commit is contained in:
akashsalan 2026-05-30 18:41:18 +05:30 committed by jakeaturner
parent bbd62d8ed1
commit 9de6473f6a
No known key found for this signature in database
GPG Key ID: B1072EBDEECE328D
3 changed files with 32 additions and 9 deletions

View File

@ -105,7 +105,7 @@ For answers to common questions about Project N.O.M.A.D., please see our [FAQ](F
## About Internet Usage & Privacy
Project N.O.M.A.D. is designed for offline usage. An internet connection is only required during the initial installation (to download dependencies) and if you (the user) decide to download additional tools and resources at a later time. Otherwise, N.O.M.A.D. does not require an internet connection and has ZERO built-in telemetry.
To test internet connectivity, N.O.M.A.D. attempts to make a request to Cloudflare's utility endpoint, `https://1.1.1.1/cdn-cgi/trace` and checks for a successful response.
To test internet connectivity, N.O.M.A.D. first attempts to make a request to Cloudflare's utility endpoint, `https://1.1.1.1/cdn-cgi/trace`. If that endpoint is unreachable (for example, because your network blocks `1.1.1.1`), it falls back to other endpoints the application already contacts (the GitHub API and the Project N.O.M.A.D. API) and considers the connection online if any of them respond. You can override the endpoint used for this check with the `INTERNET_STATUS_TEST_URL` environment variable.
## About Security
By design, Project N.O.M.A.D. is intended to be open and available without hurdles — it includes no authentication. If you decide to connect your device to a local network after install (e.g. for allowing other devices to access its resources), you can block/open ports to control which services are exposed.

View File

@ -1,6 +1,10 @@
PORT=8080
HOST=localhost
LOG_LEVEL=info
# Optional: override the URL used to test internet connectivity.
# Defaults to https://1.1.1.1/cdn-cgi/trace with fallbacks to hosts the app
# already contacts. Leave unset to use the defaults.
# INTERNET_STATUS_TEST_URL=https://1.1.1.1/cdn-cgi/trace
APP_KEY=some_random_key
NODE_ENV=development
SESSION_DRIVER=cookie

View File

@ -35,29 +35,48 @@ export class SystemService {
}
async getInternetStatus(): Promise<boolean> {
const DEFAULT_TEST_URL = 'https://1.1.1.1/cdn-cgi/trace'
// Primary endpoint stays Cloudflare's privacy-respecting utility endpoint.
// The fallbacks are hosts the application already contacts elsewhere
// (GitHub API for update checks, the Project N.O.M.A.D. API for release-note
// subscriptions), so no new third-party services are introduced. They exist
// to avoid false "offline" reports on networks that block 1.1.1.1.
const DEFAULT_TEST_URLS = [
'https://1.1.1.1/cdn-cgi/trace',
'https://api.github.com',
'https://api.projectnomad.us',
]
const MAX_ATTEMPTS = 3
let testUrl = DEFAULT_TEST_URL
let customTestUrl = env.get('INTERNET_STATUS_TEST_URL')?.trim()
let testUrls = DEFAULT_TEST_URLS
const customTestUrl = env.get('INTERNET_STATUS_TEST_URL')?.trim()
// check that customTestUrl is a valid URL, if provided
// If a custom test URL is provided and valid, use it exclusively. This
// preserves the existing override behavior for operators who intentionally
// point connectivity checks at a specific endpoint.
if (customTestUrl && customTestUrl !== '') {
try {
new URL(customTestUrl)
testUrl = customTestUrl
testUrls = [customTestUrl]
} catch (error) {
logger.warn(
`Invalid INTERNET_STATUS_TEST_URL: ${customTestUrl}. Falling back to default URL.`
`Invalid INTERNET_STATUS_TEST_URL: ${customTestUrl}. Falling back to default URLs.`
)
}
}
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
const res = await axios.get(testUrl, { timeout: 5000 })
return res.status === 200
// Probe all endpoints in parallel and resolve as soon as the first one
// responds. Any HTTP response (including non-2xx) means we reached the
// internet, so accept all status codes rather than requiring a strict 200.
await Promise.any(
testUrls.map((testUrl) =>
axios.get(testUrl, { timeout: 5000, validateStatus: () => true })
)
)
return true
} catch (error) {
// Promise.any only rejects (with an AggregateError) when every endpoint failed.
logger.warn(
`Internet status check attempt ${attempt}/${MAX_ATTEMPTS} failed: ${error instanceof Error ? error.message : error}`
)