Replace TCP DNS ISP block detection with Globalping SNI block detection
Korean ISPs use SNI-based DPI (not DNS hijacking) to block sites. TCP DNS queries to ISP servers from Cloudflare Workers returned real IPs even for blocked domains. Now uses Globalping eyeball probes (KT, LG U+) to detect ECONNRESET on HTTPS — the signature of SNI-based blocking. Verified: pornhub.com correctly detected as blocked by Korea Telecom. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -87,9 +87,10 @@ export class TroubleshootAgent extends BaseAgent<TroubleshootSession> {
|
|||||||
- check_server_status: 서버 상태 확인 (server_id 없으면 전체 목록)
|
- check_server_status: 서버 상태 확인 (server_id 없으면 전체 목록)
|
||||||
- check_domain_status: 도메인 상태 확인 (domain 없으면 전체 목록)
|
- check_domain_status: 도메인 상태 확인 (domain 없으면 전체 목록)
|
||||||
- check_service_status: DDoS/VPN 서비스 상태 확인
|
- check_service_status: DDoS/VPN 서비스 상태 확인
|
||||||
- diagnose_domain: 도메인 네트워크 진단 (DNS 조회, ISP 차단 감지, HTTP/TCP 연결 테스트)
|
- diagnose_domain: 도메인 네트워크 종합 진단
|
||||||
- "접속이 안 돼요", "사이트가 안 열려요", "차단된 것 같아요" 등의 문의 시 즉시 호출
|
- DNS 조회, HTTP/HTTPS 연결 테스트, 한국 내 Ping/HTTP 응답 시간 측정
|
||||||
- DNS, HTTP, ISP 차단 여부를 한번에 확인하여 원인을 빠르게 파악
|
- 한국 ISP(KT, LG U+) eyeball 프로브에서 SNI 차단 여부 자동 감지
|
||||||
|
- "접속이 안 돼요", "사이트가 안 열려요", "차단된 것 같아요", "느려요" 등의 문의 시 즉시 호출
|
||||||
- generate_diagram: 문제 상황을 Mermaid 다이어그램으로 시각화 (충분한 정보 수집 후)`;
|
- generate_diagram: 문제 상황을 Mermaid 다이어그램으로 시각화 (충분한 정보 수집 후)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,32 +3,19 @@
|
|||||||
*
|
*
|
||||||
* 도메인 접속 문제 진단을 위한 네트워크 체크:
|
* 도메인 접속 문제 진단을 위한 네트워크 체크:
|
||||||
* - DoH DNS 조회 (Cloudflare, Google)
|
* - DoH DNS 조회 (Cloudflare, Google)
|
||||||
* - 한국 ISP DNS 차단 감지 (KT, LG, SK) — TCP DNS 쿼리
|
|
||||||
* - HTTP/HTTPS 연결 테스트
|
* - HTTP/HTTPS 연결 테스트
|
||||||
* - TCP 포트 연결 테스트
|
* - TCP 포트 연결 테스트
|
||||||
|
* - Globalping 한국 프로브 (Ping + HTTP 응답 시간 + ISP SNI 차단 감지)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { connect } from 'cloudflare:sockets';
|
import { connect } from 'cloudflare:sockets';
|
||||||
import type { NetworkDiagnosticReport, DnsResolverResult, IspDnsBlockResult, HttpCheckResult, TcpCheckResult, GlobalpingProbeResult } from '../types';
|
import type { NetworkDiagnosticReport, DnsResolverResult, HttpCheckResult, TcpCheckResult, GlobalpingProbeResult } from '../types';
|
||||||
import { createLogger } from '../utils/logger';
|
import { createLogger } from '../utils/logger';
|
||||||
|
|
||||||
const logger = createLogger('network-diagnostic');
|
const logger = createLogger('network-diagnostic');
|
||||||
|
|
||||||
const CHECK_TIMEOUT_MS = 5000;
|
const CHECK_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
// warning.or.kr 및 알려진 ISP 차단 페이지 IP
|
|
||||||
const KNOWN_BLOCK_IPS = new Set([
|
|
||||||
'121.189.57.82', // warning.or.kr (KT)
|
|
||||||
'121.189.57.83', // warning.or.kr
|
|
||||||
'211.234.62.24', // KCSC block page
|
|
||||||
'211.234.62.25',
|
|
||||||
'175.158.50.11', // block redirect
|
|
||||||
'175.158.50.12',
|
|
||||||
'210.91.57.228', // LGU+ block
|
|
||||||
'61.97.65.4', // SKT block
|
|
||||||
'121.167.36.1', // common block redirect
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DoH DNS Resolution (Cloudflare & Google)
|
// DoH DNS Resolution (Cloudflare & Google)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -114,200 +101,6 @@ async function resolveDoh(domain: string): Promise<DnsResolverResult[]> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Korean ISP DNS Block Detection (TCP DNS Query)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const ISP_DNS_SERVERS = [
|
|
||||||
{ isp: 'KT', ip: '168.126.63.1' },
|
|
||||||
{ isp: 'LG U+', ip: '164.124.101.2' },
|
|
||||||
{ isp: 'SK Broadband', ip: '210.220.163.82' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DNS 와이어 포맷 패킷 생성 (TCP용: 2바이트 길이 프리픽스 포함)
|
|
||||||
*/
|
|
||||||
function buildDnsQuery(domain: string): Uint8Array {
|
|
||||||
// DNS 헤더 (12 bytes)
|
|
||||||
const header = new Uint8Array([
|
|
||||||
0xAB, 0xCD, // Transaction ID
|
|
||||||
0x01, 0x00, // Flags: standard query, recursion desired
|
|
||||||
0x00, 0x01, // Questions: 1
|
|
||||||
0x00, 0x00, // Answer RRs: 0
|
|
||||||
0x00, 0x00, // Authority RRs: 0
|
|
||||||
0x00, 0x00, // Additional RRs: 0
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Question section: encode domain labels
|
|
||||||
const labels = domain.split('.');
|
|
||||||
const questionParts: number[] = [];
|
|
||||||
for (const label of labels) {
|
|
||||||
questionParts.push(label.length);
|
|
||||||
for (let i = 0; i < label.length; i++) {
|
|
||||||
questionParts.push(label.charCodeAt(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
questionParts.push(0); // Root label
|
|
||||||
questionParts.push(0x00, 0x01); // Type A
|
|
||||||
questionParts.push(0x00, 0x01); // Class IN
|
|
||||||
|
|
||||||
const question = new Uint8Array(questionParts);
|
|
||||||
const dnsMessage = new Uint8Array(header.length + question.length);
|
|
||||||
dnsMessage.set(header, 0);
|
|
||||||
dnsMessage.set(question, header.length);
|
|
||||||
|
|
||||||
// TCP DNS: 2-byte length prefix
|
|
||||||
const tcpMessage = new Uint8Array(2 + dnsMessage.length);
|
|
||||||
tcpMessage[0] = (dnsMessage.length >> 8) & 0xFF;
|
|
||||||
tcpMessage[1] = dnsMessage.length & 0xFF;
|
|
||||||
tcpMessage.set(dnsMessage, 2);
|
|
||||||
|
|
||||||
return tcpMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TCP DNS 응답에서 A 레코드 IP 파싱
|
|
||||||
*/
|
|
||||||
function parseDnsResponse(raw: Uint8Array): string[] {
|
|
||||||
const ips: string[] = [];
|
|
||||||
|
|
||||||
// Skip 2-byte TCP length prefix
|
|
||||||
const offset = 2;
|
|
||||||
if (raw.length < offset + 12) return ips;
|
|
||||||
|
|
||||||
// Read answer count from header
|
|
||||||
const answerCount = (raw[offset + 6] << 8) | raw[offset + 7];
|
|
||||||
if (answerCount === 0) return ips;
|
|
||||||
|
|
||||||
// Skip header (12 bytes) + question section
|
|
||||||
let pos = offset + 12;
|
|
||||||
|
|
||||||
// Skip question section
|
|
||||||
while (pos < raw.length && raw[pos] !== 0) {
|
|
||||||
if ((raw[pos] & 0xC0) === 0xC0) {
|
|
||||||
pos += 2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pos += raw[pos] + 1;
|
|
||||||
}
|
|
||||||
if (pos < raw.length && raw[pos] === 0) pos++; // null terminator
|
|
||||||
pos += 4; // skip QTYPE + QCLASS
|
|
||||||
|
|
||||||
// Parse answer records
|
|
||||||
for (let i = 0; i < answerCount && pos < raw.length; i++) {
|
|
||||||
// Skip name (may be pointer)
|
|
||||||
if ((raw[pos] & 0xC0) === 0xC0) {
|
|
||||||
pos += 2;
|
|
||||||
} else {
|
|
||||||
while (pos < raw.length && raw[pos] !== 0) {
|
|
||||||
pos += raw[pos] + 1;
|
|
||||||
}
|
|
||||||
pos++; // null terminator
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pos + 10 > raw.length) break;
|
|
||||||
|
|
||||||
const rtype = (raw[pos] << 8) | raw[pos + 1];
|
|
||||||
const rdlength = (raw[pos + 8] << 8) | raw[pos + 9];
|
|
||||||
pos += 10;
|
|
||||||
|
|
||||||
if (rtype === 1 && rdlength === 4 && pos + 4 <= raw.length) {
|
|
||||||
ips.push(`${raw[pos]}.${raw[pos + 1]}.${raw[pos + 2]}.${raw[pos + 3]}`);
|
|
||||||
}
|
|
||||||
pos += rdlength;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TCP 소켓으로 DNS 서버에 질의
|
|
||||||
*/
|
|
||||||
async function queryDnsTcp(serverIp: string, query: Uint8Array): Promise<string[]> {
|
|
||||||
const socket = connect({ hostname: serverIp, port: 53 });
|
|
||||||
|
|
||||||
// Wait for connection to establish
|
|
||||||
await socket.opened;
|
|
||||||
|
|
||||||
// readable/writable are on the socket object directly
|
|
||||||
const writer = socket.writable.getWriter();
|
|
||||||
await writer.write(query);
|
|
||||||
writer.releaseLock();
|
|
||||||
|
|
||||||
const reader = socket.readable.getReader();
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
let totalLength = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (totalLength < 512) { // DNS response shouldn't exceed 512 bytes for simple queries
|
|
||||||
const { value, done } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
chunks.push(value as Uint8Array);
|
|
||||||
totalLength += (value as Uint8Array).length;
|
|
||||||
|
|
||||||
// Check if we have enough data (2-byte length + message)
|
|
||||||
if (totalLength >= 2) {
|
|
||||||
const firstChunk = chunks[0];
|
|
||||||
const expectedLen = (firstChunk[0] << 8) | firstChunk[1];
|
|
||||||
if (totalLength >= expectedLen + 2) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
reader.releaseLock();
|
|
||||||
await socket.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine chunks
|
|
||||||
const raw = new Uint8Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
raw.set(chunk, offset);
|
|
||||||
offset += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseDnsResponse(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkIspBlocking(domain: string): Promise<IspDnsBlockResult[]> {
|
|
||||||
const query = buildDnsQuery(domain);
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
ISP_DNS_SERVERS.map(async (server) => {
|
|
||||||
const start = Date.now();
|
|
||||||
try {
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error('timeout')), CHECK_TIMEOUT_MS)
|
|
||||||
);
|
|
||||||
|
|
||||||
const resolvedIps = await Promise.race([
|
|
||||||
queryDnsTcp(server.ip, query),
|
|
||||||
timeoutPromise,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const blocked = resolvedIps.some((ip) => KNOWN_BLOCK_IPS.has(ip));
|
|
||||||
|
|
||||||
return {
|
|
||||||
isp: server.isp,
|
|
||||||
serverIp: server.ip,
|
|
||||||
resolvedIps,
|
|
||||||
blocked,
|
|
||||||
blockReason: blocked ? 'warning.or.kr 차단 페이지 IP 감지' : undefined,
|
|
||||||
responseTimeMs: Date.now() - start,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
isp: server.isp,
|
|
||||||
serverIp: server.ip,
|
|
||||||
resolvedIps: [],
|
|
||||||
blocked: false,
|
|
||||||
responseTimeMs: Date.now() - start,
|
|
||||||
error: (error as Error).message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// HTTP/HTTPS Connection Test
|
// HTTP/HTTPS Connection Test
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -429,6 +222,7 @@ interface GlobalpingRawResult {
|
|||||||
city: string;
|
city: string;
|
||||||
network: string;
|
network: string;
|
||||||
asn: number;
|
asn: number;
|
||||||
|
tags: string[];
|
||||||
};
|
};
|
||||||
result: {
|
result: {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -516,6 +310,9 @@ async function pollGlobalpingResult(id: string): Promise<GlobalpingRawResult[]>
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SNI 차단 시 나타나는 에러 패턴 (한국 ISP DPI)
|
||||||
|
const SNI_BLOCK_PATTERNS = ['ECONNRESET', 'connection reset', 'socket hang up'];
|
||||||
|
|
||||||
async function checkKoreaProbes(domain: string): Promise<GlobalpingProbeResult[]> {
|
async function checkKoreaProbes(domain: string): Promise<GlobalpingProbeResult[]> {
|
||||||
// Create ping and HTTP measurements in parallel
|
// Create ping and HTTP measurements in parallel
|
||||||
const [pingId, httpId] = await Promise.all([
|
const [pingId, httpId] = await Promise.all([
|
||||||
@@ -540,6 +337,7 @@ async function checkKoreaProbes(domain: string): Promise<GlobalpingProbeResult[]
|
|||||||
city: r.probe.city,
|
city: r.probe.city,
|
||||||
network: r.probe.network,
|
network: r.probe.network,
|
||||||
asn: r.probe.asn,
|
asn: r.probe.asn,
|
||||||
|
isEyeball: (r.probe.tags || []).includes('eyeball-network'),
|
||||||
ping: r.result.stats ? {
|
ping: r.result.stats ? {
|
||||||
min: r.result.stats.min,
|
min: r.result.stats.min,
|
||||||
avg: r.result.stats.avg,
|
avg: r.result.stats.avg,
|
||||||
@@ -552,21 +350,37 @@ async function checkKoreaProbes(domain: string): Promise<GlobalpingProbeResult[]
|
|||||||
|
|
||||||
for (const r of httpResults) {
|
for (const r of httpResults) {
|
||||||
const key = `${r.probe.city}:${r.probe.network}`;
|
const key = `${r.probe.city}:${r.probe.network}`;
|
||||||
|
const isEyeball = (r.probe.tags || []).includes('eyeball-network');
|
||||||
const existing = probeMap.get(key) || {
|
const existing = probeMap.get(key) || {
|
||||||
city: r.probe.city,
|
city: r.probe.city,
|
||||||
network: r.probe.network,
|
network: r.probe.network,
|
||||||
asn: r.probe.asn,
|
asn: r.probe.asn,
|
||||||
|
isEyeball,
|
||||||
};
|
};
|
||||||
existing.http = r.result.timings ? {
|
|
||||||
statusCode: r.result.statusCode || 0,
|
if (r.result.timings) {
|
||||||
totalMs: r.result.timings.total,
|
existing.http = {
|
||||||
dnsMs: r.result.timings.dns,
|
statusCode: r.result.statusCode || 0,
|
||||||
tcpMs: r.result.timings.tcp,
|
totalMs: r.result.timings.total,
|
||||||
tlsMs: r.result.timings.tls,
|
dnsMs: r.result.timings.dns,
|
||||||
firstByteMs: r.result.timings.firstByte,
|
tcpMs: r.result.timings.tcp,
|
||||||
} : undefined;
|
tlsMs: r.result.timings.tls,
|
||||||
if (!existing.error && r.result.status !== 'finished') {
|
firstByteMs: r.result.timings.firstByte,
|
||||||
existing.error = r.result.rawOutput;
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect SNI blocking: eyeball probe HTTP failed with ECONNRESET
|
||||||
|
const rawOutput = r.result.rawOutput || '';
|
||||||
|
if (r.result.status === 'failed' && isEyeball) {
|
||||||
|
const isSniBlock = SNI_BLOCK_PATTERNS.some((p) => rawOutput.includes(p));
|
||||||
|
if (isSniBlock) {
|
||||||
|
existing.blocked = true;
|
||||||
|
existing.blockReason = 'SNI 기반 차단 (ECONNRESET)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.error && r.result.status === 'failed' && !existing.blocked) {
|
||||||
|
existing.error = rawOutput;
|
||||||
}
|
}
|
||||||
probeMap.set(key, existing);
|
probeMap.set(key, existing);
|
||||||
}
|
}
|
||||||
@@ -580,7 +394,6 @@ async function checkKoreaProbes(domain: string): Promise<GlobalpingProbeResult[]
|
|||||||
|
|
||||||
function generateSummary(
|
function generateSummary(
|
||||||
dns: DnsResolverResult[],
|
dns: DnsResolverResult[],
|
||||||
ispBlocking: IspDnsBlockResult[],
|
|
||||||
http: HttpCheckResult[],
|
http: HttpCheckResult[],
|
||||||
tcp: TcpCheckResult[],
|
tcp: TcpCheckResult[],
|
||||||
koreaProbes: GlobalpingProbeResult[]
|
koreaProbes: GlobalpingProbeResult[]
|
||||||
@@ -596,19 +409,15 @@ function generateSummary(
|
|||||||
summary.push(`DNS 정상: ${[...new Set(dnsIps)].join(', ')}로 확인됨`);
|
summary.push(`DNS 정상: ${[...new Set(dnsIps)].join(', ')}로 확인됨`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISP blocking check
|
// ISP SNI blocking detection (from Globalping eyeball probes)
|
||||||
const blockedIsps = ispBlocking.filter((r) => r.blocked);
|
const blockedProbes = koreaProbes.filter((p) => p.blocked);
|
||||||
if (blockedIsps.length > 0) {
|
if (blockedProbes.length > 0) {
|
||||||
const names = blockedIsps.map((r) => r.isp).join(', ');
|
const names = blockedProbes.map((p) => `${p.network}`).join(', ');
|
||||||
summary.push(`ISP 차단 감지: ${names}에서 차단 페이지(warning.or.kr)로 리다이렉트됩니다. 방통위/KCSC 차단일 가능성이 높습니다.`);
|
summary.push(`ISP 차단 감지: ${names}에서 SNI 기반 차단(ECONNRESET)이 확인되었습니다. 방통위/KCSC 차단일 가능성이 높습니다.`);
|
||||||
} else {
|
} else {
|
||||||
const testedIsps = ispBlocking.filter((r) => !r.error);
|
const eyeballProbes = koreaProbes.filter((p) => p.isEyeball && p.http);
|
||||||
if (testedIsps.length > 0) {
|
if (eyeballProbes.length > 0) {
|
||||||
summary.push(`ISP 차단 없음: 테스트한 통신사(${testedIsps.map((r) => r.isp).join(', ')})에서 차단이 감지되지 않았습니다.`);
|
summary.push(`ISP 차단 없음: ${eyeballProbes.map((p) => p.network).join(', ')} 프로브에서 정상 접속 확인`);
|
||||||
}
|
|
||||||
const failedIsps = ispBlocking.filter((r) => r.error);
|
|
||||||
if (failedIsps.length > 0) {
|
|
||||||
summary.push(`ISP DNS 테스트 실패: ${failedIsps.map((r) => `${r.isp}(${r.error})`).join(', ')}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,7 +486,6 @@ function generateSummary(
|
|||||||
|
|
||||||
export interface NetworkDiagnosticOptions {
|
export interface NetworkDiagnosticOptions {
|
||||||
ports?: number[];
|
ports?: number[];
|
||||||
skipIspCheck?: boolean;
|
|
||||||
skipHttpCheck?: boolean;
|
skipHttpCheck?: boolean;
|
||||||
skipTcpCheck?: boolean;
|
skipTcpCheck?: boolean;
|
||||||
skipKoreaProbes?: boolean;
|
skipKoreaProbes?: boolean;
|
||||||
@@ -691,15 +499,14 @@ export async function runNetworkDiagnostic(
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Run all checks in parallel
|
// Run all checks in parallel
|
||||||
const [dns, ispBlocking, http, tcp, koreaProbes] = await Promise.all([
|
const [dns, http, tcp, koreaProbes] = await Promise.all([
|
||||||
resolveDoh(domain),
|
resolveDoh(domain),
|
||||||
options?.skipIspCheck ? Promise.resolve([]) : checkIspBlocking(domain),
|
|
||||||
options?.skipHttpCheck ? Promise.resolve([]) : checkHttp(domain),
|
options?.skipHttpCheck ? Promise.resolve([]) : checkHttp(domain),
|
||||||
options?.skipTcpCheck ? Promise.resolve([]) : checkTcpPorts(domain, options?.ports),
|
options?.skipTcpCheck ? Promise.resolve([]) : checkTcpPorts(domain, options?.ports),
|
||||||
options?.skipKoreaProbes ? Promise.resolve([]) : checkKoreaProbes(domain),
|
options?.skipKoreaProbes ? Promise.resolve([]) : checkKoreaProbes(domain),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const summary = generateSummary(dns, ispBlocking, http, tcp, koreaProbes);
|
const summary = generateSummary(dns, http, tcp, koreaProbes);
|
||||||
|
|
||||||
logger.info('네트워크 진단 완료', {
|
logger.info('네트워크 진단 완료', {
|
||||||
domain,
|
domain,
|
||||||
@@ -712,7 +519,6 @@ export async function runNetworkDiagnostic(
|
|||||||
domain,
|
domain,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
dns,
|
dns,
|
||||||
ispBlocking,
|
|
||||||
http,
|
http,
|
||||||
tcp,
|
tcp,
|
||||||
koreaProbes,
|
koreaProbes,
|
||||||
|
|||||||
26
src/types.ts
26
src/types.ts
@@ -464,13 +464,37 @@ export interface TcpCheckResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GlobalpingProbeResult {
|
||||||
|
city: string;
|
||||||
|
network: string;
|
||||||
|
asn: number;
|
||||||
|
isEyeball?: boolean;
|
||||||
|
blocked?: boolean;
|
||||||
|
blockReason?: string;
|
||||||
|
ping?: {
|
||||||
|
min: number;
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
loss: number;
|
||||||
|
};
|
||||||
|
http?: {
|
||||||
|
statusCode: number;
|
||||||
|
totalMs: number;
|
||||||
|
dnsMs: number;
|
||||||
|
tcpMs: number;
|
||||||
|
tlsMs: number;
|
||||||
|
firstByteMs: number;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NetworkDiagnosticReport {
|
export interface NetworkDiagnosticReport {
|
||||||
domain: string;
|
domain: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
dns: DnsResolverResult[];
|
dns: DnsResolverResult[];
|
||||||
ispBlocking: IspDnsBlockResult[];
|
|
||||||
http: HttpCheckResult[];
|
http: HttpCheckResult[];
|
||||||
tcp: TcpCheckResult[];
|
tcp: TcpCheckResult[];
|
||||||
|
koreaProbes: GlobalpingProbeResult[];
|
||||||
summary: string[];
|
summary: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user