Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bf14517
Improvements
jason5ng32 Aug 26, 2026
6da1e69
Improvements
jason5ng32 Aug 26, 2026
955ab60
Feat(connectivity): add Germany country list
WilliamK112 Aug 26, 2026
b256cb6
Fix(i18n): refine Traditional Chinese terminology
9anna-na Aug 26, 2026
0c2b2d2
Feat(ipinfo): add country online-activity heatmap dialog
jason5ng32 Aug 26, 2026
d7e46c5
Refactor(ipinfo): extract map dialog into LocationMap component
jason5ng32 Aug 26, 2026
4af258a
Feat(ipinfo): expose map and traffic panels inline in QueryIP
jason5ng32 Aug 26, 2026
3c68dba
Improvements
jason5ng32 Aug 26, 2026
244be60
Merge pull request #462 from 9anna-na/fix/zh-tw-localization
jason5ng32 Aug 26, 2026
40900ae
Merge pull request #461 from WilliamK112/feat/germany-connectivity-460
jason5ng32 Aug 26, 2026
57af5ff
Feat(connectivity): add Music & Audio list
WilliamK112 Aug 26, 2026
78724ba
Improvements
jason5ng32 Aug 27, 2026
983382e
Feat(connectivity): Organize tests into user-managed multiple lists
jason5ng32 Aug 27, 2026
da1b81e
Improvements
jason5ng32 Aug 27, 2026
87765d5
Merge pull request #463 from WilliamK112/feat/music-connectivity-395
jason5ng32 Aug 27, 2026
21311cc
Fix(connectivity): replace Genius with Musixmatch in the music list
jason5ng32 Aug 27, 2026
b4994b9
Improvements
jason5ng32 Aug 27, 2026
097fca6
Refactor(api): consolidate Cloudflare Radar routes into one view-disp…
jason5ng32 Aug 27, 2026
68591df
Feat(connectivity): draw Tier-1 peering and suppress misinferred prov…
jason5ng32 Aug 28, 2026
51238f2
Feat(ui): compact bundled layout for the ASN connectivity graph
jason5ng32 Aug 28, 2026
c4d2bbb
Improvements
jason5ng32 Aug 28, 2026
fd5aa25
Feat(asn): add prefix, relationship and connection quality data to th…
jason5ng32 Aug 28, 2026
da0363d
Chore(ui): version the ASN panel API cache keys
jason5ng32 Aug 28, 2026
7738a84
Chore(changelog): note ASN info and connectivity graph improvements
jason5ng32 Aug 28, 2026
37b29c3
Docs(readme): add CI workflow badge to all four READMEs
sage-s11 Aug 28, 2026
b236dbb
Docs(readme): add CI badge to ZH-TW and PT-BR READMEs for parity
sage-s11 Aug 29, 2026
15f19a9
Merge pull request #464 from sage-s11/feat/ci-badge-readmes
jason5ng32 Aug 30, 2026
8ab3675
Improvements
jason5ng32 Aug 31, 2026
948fdd4
Merge pull request #466 from jason5ng32/dev
jason5ng32 Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

![CodeQL](https://github.com/jason5ng32/MyIP/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)
![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main)
[![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml)

[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH_TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)

An open-source, all-in-one IP toolbox: IP lookup from multiple sources, connectivity tests, WebRTC & DNS-leak detection, speed test, MTR, censorship checks, Whois, and more — self-hostable with one Docker command.

Expand Down
3 changes: 2 additions & 1 deletion README_FR.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

![CodeQL](https://github.com/jason5ng32/MyIP/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)
![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main)
[![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml)

[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH_TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)

Une boîte à outils IP tout-en-un et open source : recherche d'IP depuis plusieurs sources, tests de connectivité, détection WebRTC et de fuites DNS, test de vitesse, MTR, vérification de la censure, Whois et plus encore — auto-hébergeable en une seule commande Docker.

Expand Down
3 changes: 2 additions & 1 deletion README_PT-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

![CodeQL](https://github.com/jason5ng32/MyIP/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)
![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main)
[![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml)

[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH_TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)

Uma caixa de ferramentas de IP completa e de código aberto: consulta de IP a partir de múltiplas fontes, testes de conectividade, detecção de WebRTC e de vazamento de DNS, teste de velocidade, MTR, verificação de censura, Whois e muito mais — auto-hospedável com um único comando Docker.

Expand Down
3 changes: 2 additions & 1 deletion README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

![CodeQL](https://github.com/jason5ng32/MyIP/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)
![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main)
[![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml)

[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH_TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)

Универсальный набор IP-инструментов с открытым исходным кодом: определение IP по нескольким источникам, проверка доступности сайтов, обнаружение утечек WebRTC и DNS, тест скорости, MTR, проверка цензуры, Whois и многое другое — разворачивается на своём сервере одной командой Docker.

Expand Down
3 changes: 2 additions & 1 deletion README_ZH_TW.md → README_ZH-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

![CodeQL](https://github.com/jason5ng32/MyIP/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)
![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main)
[![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml)

[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH_TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)

一個開源的多合一 IP 工具箱:多來源 IP 查詢、連通性測試、WebRTC 與 DNS 洩漏偵測、網速測試、MTR、審查封鎖檢查、Whois 等——一條 Docker 指令即可自行架設。

Expand Down
3 changes: 2 additions & 1 deletion README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

![CodeQL](https://github.com/jason5ng32/MyIP/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)
![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main)
[![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml)

[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH_TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md)

一个开源的一站式 IP 工具箱:多来源 IP 查询、连通性测试、WebRTC 与 DNS 泄露检测、网速测试、MTR、封锁检测、Whois 等等 —— 一条 Docker 命令即可自行部署。

Expand Down
21 changes: 16 additions & 5 deletions api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ services, service-status poller), parts of which the frontend also imports
Roughly one handler file per route: IP-geolocation sources (`ipinfo-io` /
`ipapi-com` / `ipapi-is` / `ip2location-io` / `ip-sb` / `ipcheck-ing` /
`maxmind`), tool backends (`get-whois` / `dns-resolver` / `mac-checker` /
`cf-radar` / `net-outages` / `asn-history` / `asn-connectivity` /
`cf-radar` / `asn-history` / `asn-connectivity` /
`ooni-blocking` / `globalping-probes` / `service-status` / `google-map` /
`github-stars` / `invisibility-test` / `dns-leak-test` / `persona`), user
proxies (`get-user-info` / `update-user-achievement`), platform
(`configs` / `sentry-tunnel` / `share-report`). Each file's header comment
states its route and purpose — read those for specifics.

The exception to one-file-per-route is Cloudflare Radar: all Radar data
rides the single `/api/cfradar` route, dispatched by `?view=` over the
`RADAR_VIEWS` registry in `common/cf-radar.js`. Each view declares its
guards, edge-cache TTL, and fetch function there — new Radar data means a
view function plus a registry row, never a new route.

`api/data/` holds contributor-editable static config consumed by handlers —
currently `dns-resolvers.js`, the country-annotated resolver list behind
`dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
Expand Down Expand Up @@ -83,8 +89,10 @@ these checks:
are reachable — that is what a DMARC or DKIM lookup needs.
- `requireValidPrefix()` — `?prefix=` (CIDR); lets the frontend quantize to
the BGP DFZ floor (/24 v4, /48 v6) for maximal CF edge-cache reuse.
- `requireValidASN()` — `?asn=`, strips `AS`, rewrites to numeric
(`cf-radar` predates it and still validates inline).
- `requireValidASN()` — `?asn=`, strips `AS`, rewrites to numeric.
- `requireValidCountry()` — `?country=` (alpha-2), uppercases in place for one
canonical edge-cache key. Syntactic only — an unassigned code just yields an
empty upstream series.
- `requireValidProviderId()` — whitelists `?id=` against service-status slugs.
- `requireValidRecordType()` — whitelists `?type=` against `DNS_RECORD_TYPES`
in `common/dns-record-types.js` and uppercases it. That list is the single
Expand Down Expand Up @@ -132,8 +140,11 @@ Leave the gate in place when a test covers it.
## Edge caching

Every `/api/*` response defaults to `Cache-Control: no-store`; slowly-changing
public routes opt in via the `cacheable(maxAgeSeconds)` middleware in
`backend-server.js` — e.g. `app.get('/api/cfradar', cacheable(60 * 60), …)`.
public routes opt in via the `cacheable(maxAge)` middleware in
`backend-server.js` — e.g. `app.get('/api/whois', cacheable(24 * 60 * 60), …)`.
`maxAge` also accepts a `(req) => seconds` resolver for routes whose TTL
depends on the request (`/api/cfradar` reads it from the view registry); a
falsy resolution keeps the no-store default, so unknown views never cache.
Write TTLs as multiplied expressions (`24 * 60 * 60`), not raw seconds.
The middleware only sets `public, max-age=N` on status < 400, so CF never
caches error pages; handlers themselves never touch `Cache-Control`.
Expand Down
114 changes: 68 additions & 46 deletions api/asn-connectivity.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,63 @@
// /api/asn-connectivity — BFS over CAIDA AS-Relationships starting at the
// origin AS, halting at Tier 1s or MAX_DEPTH. Returns a graph the frontend
// /api/asn-connectivity — layered BFS over CAIDA AS-Relationships starting at
// the origin AS, halting at Tier 1s or MAX_DEPTH. Returns a graph the frontend
// renders dagre-style: origin (left) → intermediates → Tier 1s (right).
// Inspired by bgp.tools' /as/<N>#connectivity view.
//
// Edges carry a kind: 'transit' (p2c, customer → provider) or 'peering' (p2p
// with a Tier 1). Peering matters because hypergiants buy no transit — their
// backbone connectivity is settlement-free peering with the Tier 1 clique,
// invisible to a p2c-only walk.
//
// Data is fully local (common/as-rel-db.js + common/as-org-db.js), so the
// whole BFS is synchronous. We only hit RIPEstat for as-overview as a
// rare fallback when as2org doesn't have an ASN's org name.

import { resolveAsnOrgName } from '../common/ripestat.js';
import { providersOf, customerCountOf, isTier1 } from '../common/as-rel-db.js';
import { providersOf, peersOf, customerCountOf, isTier1 } from '../common/as-rel-db.js';
import logger from '../common/logger.js';

// How deep to recurse from the origin. 3 covers regional networks reaching
// Tier 1s through 1-2 intermediates; deeper just adds noise at the periphery.
const MAX_DEPTH = 3;

// Per-node cap on non-Tier-1 providers to recurse into. Matters mainly for
// hyperscalers (Cloudflare-class has 100+ providers); ranked by customerCount
// as a proxy for "primary transit".
// Per-node cap on non-Tier-1 providers to recurse into, ranked by
// customerCount as a proxy for "primary transit".
const MAX_INTERMEDIATE_BRANCH = 3;

// Past either bar, a node's non-Tier-1 "providers" are treated as CAIDA
// misinference and dropped. Peering-only bar: the hypergiant signature is
// clique peering (Google 12, Cloudflare 7). Combined bar: multihomed hosters
// with a real non-T1 upstream sit at ≤6 Tier-1 adjacencies.
const TIER1_PEERING_TRUSTED = 5;
const TIER1_ADJACENCY_TRUSTED = 8;

// Two-tier org name resolver lives in common/ripestat.js. No onError hook
// here — connectivity stays silent on as-overview fallback failures (a node
// just keeps name=null); asn-history is the one that warns.
const resolveOrgName = (asn) => resolveAsnOrgName(asn);

async function buildGraph(origin) {
const nodes = new Map();
const edgeSet = new Set();
const orgPromises = new Map();
// Default adjacency API for buildTopology; tests inject a fixture instead.
const asRelApi = { providersOf, peersOf, isTier1, customerCountOf };

// If the queried AS is itself a Tier 1, mark it 'origin-tier1' so the
// frontend can render the combined origin+Tier1 styling; the rest of
// the BFS is a no-op (Tier 1s have no providers) and the graph is
// legitimately a single node.
const originType = isTier1(origin) ? 'origin-tier1' : 'origin';
nodes.set(origin, { asn: origin, type: originType, name: null });
orgPromises.set(origin, resolveOrgName(origin));
// Pure synchronous topology walk — no org names, no I/O. Exported for tests.
export const buildTopology = (origin, rel = asRelApi) => {
const nodes = new Map();
const edges = [];
const edgeKeys = new Set();

const addNode = (asn, type) => {
if (!nodes.has(asn)) nodes.set(asn, { asn, type, name: null });
};
// First kind wins on a duplicate pair; transit is always added first.
const addEdge = (from, to, kind) => {
const key = `${from}->${to}`;
if (edgeKeys.has(key)) return;
edgeKeys.add(key);
edges.push({ from, to, kind });
};

// A Tier-1 origin's graph is its peering edges into the rest of the clique.
addNode(origin, rel.isTier1(origin) ? 'origin-tier1' : 'origin');

let currentLayer = [origin];

Expand All @@ -45,31 +66,33 @@ async function buildGraph(origin) {
const nextLayer = [];

for (const asn of currentLayer) {
const providers = providersOf(asn);
if (providers.length === 0) continue;
const providers = rel.providersOf(asn);
const tier1Providers = providers.filter(p => rel.isTier1(p));
const tier1Peers = rel.peersOf(asn).filter(p => rel.isTier1(p));

// Tier 1 hits are terminal — record the edge + node, no recursion.
for (const p of providers) {
if (!isTier1(p)) continue;
edgeSet.add(`${asn}->${p}`);
if (!nodes.has(p)) {
nodes.set(p, { asn: p, type: 'tier1', name: null });
orgPromises.set(p, resolveOrgName(p));
}
for (const p of tier1Providers) {
addEdge(asn, p, 'transit');
addNode(p, 'tier1');
}
for (const p of tier1Peers) {
addEdge(asn, p, 'peering');
addNode(p, 'tier1');
}

// Clique-adjacent node: non-Tier-1 providers are noise.
if (tier1Peers.length >= TIER1_PEERING_TRUSTED
|| tier1Providers.length + tier1Peers.length >= TIER1_ADJACENCY_TRUSTED) continue;

// Non-Tier-1 providers: pick top-N by customerCount so when we
// truncate, the displayed ones are the more meaningful transits.
const intermediates = providers
.filter(p => !isTier1(p))
.sort((a, b) => customerCountOf(b) - customerCountOf(a))
.filter(p => !rel.isTier1(p))
.sort((a, b) => rel.customerCountOf(b) - rel.customerCountOf(a))
.slice(0, MAX_INTERMEDIATE_BRANCH);

for (const p of intermediates) {
edgeSet.add(`${asn}->${p}`);
addEdge(asn, p, 'transit');
if (!nodes.has(p)) {
nodes.set(p, { asn: p, type: 'intermediate', name: null });
orgPromises.set(p, resolveOrgName(p));
addNode(p, 'intermediate');
nextLayer.push(p);
}
}
Expand All @@ -78,24 +101,23 @@ async function buildGraph(origin) {
currentLayer = nextLayer;
}

// Await all org lookups. They've been running in the background since
// each node was discovered, so most are already settled.
for (const [asn, promise] of orgPromises) {
return pruneLeafIntermediates([...nodes.values()], edges);
};

async function buildGraph(origin) {
const { nodes, edges } = buildTopology(origin);

// Org lookups run concurrently; a failed one just keeps name=null.
await Promise.all(nodes.map(async (node) => {
try {
const name = await promise;
const node = nodes.get(asn);
if (node && name) node.name = name;
const name = await resolveOrgName(node.asn);
if (name) node.name = name;
} catch {
// node keeps name=null
}
}
}));

const allNodes = [...nodes.values()];
const allEdges = [...edgeSet].map(s => {
const [from, to] = s.split('->').map(Number);
return { from, to };
});
return pruneLeafIntermediates(allNodes, allEdges);
return { nodes, edges };
}

// Iteratively drop intermediate nodes with no outgoing edge — visual
Expand Down
Loading
Loading