feat(blocking): определять и показывать хостер в матрице блокировок
Docker / build (push) Failing after 26s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-25 13:19:02 +07:00
co-authored by Cursor
parent efebcaf16d
commit 60a22b9450
15 changed files with 273 additions and 13 deletions
+88 -3
View File
@@ -5,7 +5,7 @@ set -euo pipefail
VT_API_URL="${VT_API_URL:-__VT_API_URL__}"
VT_INGEST_TOKEN="${VT_INGEST_TOKEN:-__VT_INGEST_TOKEN__}"
LAUNCHER_VERSION="4"
LAUNCHER_VERSION="5"
VENDOR_SHA="12c5839"
OS_ID="unknown"
@@ -165,6 +165,88 @@ detect_public_ip() {
printf '%s' "$ip"
}
# Короткий таймаут: на обычном VPS link-local просто не ответит.
curl_meta() {
curl -fsS --connect-timeout 1 --max-time 1 "$@" 2>/dev/null || true
}
detect_cloud_hoster() {
local body=""
body="$(curl_meta http://169.254.169.254/hetzner/v1/metadata)"
if [ -n "$body" ]; then
printf 'Hetzner'
return 0
fi
body="$(curl_meta http://169.254.169.254/metadata/v1/id)"
if [ -n "$body" ]; then
printf 'DigitalOcean'
return 0
fi
body="$(curl_meta http://169.254.169.254/v1/instanceid)"
if [ -n "$body" ]; then
printf 'Vultr'
return 0
fi
body="$(curl_meta http://169.254.169.254/linode/v1/instance-id)"
if [ -n "$body" ]; then
printf 'Linode'
return 0
fi
body="$(curl_meta -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/id)"
if [ -n "$body" ]; then
printf 'Google Cloud'
return 0
fi
body="$(curl_meta -H 'Metadata: true' 'http://169.254.169.254/metadata/instance?api-version=2021-02-01')"
if [ -n "$body" ]; then
printf 'Azure'
return 0
fi
body="$(curl_meta http://169.254.169.254/latest/meta-data/instance-id)"
if [ -n "$body" ]; then
printf 'AWS'
return 0
fi
return 1
}
detect_asn_org() {
local ip="$1" json="" org=""
json="$(curl -fsS --connect-timeout 4 --max-time 8 "https://ipwho.is/${ip}" 2>/dev/null || true)"
if [ -n "$json" ]; then
org="$(printf '%s' "$json" | jq -r '.connection.org // .org // empty' 2>/dev/null || true)"
if [ -n "$org" ] && [ "$org" != "null" ]; then
printf '%s' "$org"
return 0
fi
fi
json="$(curl -fsS --connect-timeout 4 --max-time 8 "https://ipinfo.io/${ip}/json" 2>/dev/null || true)"
if [ -n "$json" ]; then
org="$(printf '%s' "$json" | jq -r '.org // empty' 2>/dev/null || true)"
if [ -n "$org" ] && [ "$org" != "null" ]; then
printf '%s' "$org"
return 0
fi
fi
return 1
}
detect_ptr_hint() {
local ip="$1" ptr=""
command -v dig >/dev/null 2>&1 || return 1
ptr="$(dig +short -x "$ip" 2>/dev/null | awk 'NF{print; exit}' | tr -d '\r' | sed 's/\.$//')"
[ -n "$ptr" ] || return 1
printf '%s' "$ptr"
}
detect_hoster() {
local ip="$1" value=""
value="$(detect_cloud_hoster)" && { printf '%s' "$value"; return 0; }
value="$(detect_asn_org "$ip")" && { printf '%s' "$value"; return 0; }
value="$(detect_ptr_hint "$ip")" && { printf '%s' "$value"; return 0; }
return 1
}
write_vendor() {
local dest="$1"
if [ -n "${CENSORCHECK_VENDOR_B64:-}" ]; then
@@ -203,11 +285,14 @@ chmod +x "$VENDOR"
PUBLIC_IP="$(detect_public_ip)"
[ -n "$PUBLIC_IP" ] || die "Не удалось определить публичный IP"
HOSTER="$(detect_hoster "$PUBLIC_IP" || true)"
RUN_ID="$(uuid4)"
[ -n "$RUN_ID" ] || die "Не удалось сгенерировать runId"
log "censorcheck launcher ${LAUNCHER_VERSION} (vendor ${VENDOR_SHA})"
log "probe IP: ${PUBLIC_IP}"
log "хостер: ${HOSTER:-не определён}"
log "runId: ${RUN_ID}"
log "Проверяю сайты (последовательно, несколько минут)..."
@@ -222,11 +307,11 @@ if [ "$CC_EXIT" -ne 0 ]; then
fi
PAYLOAD="$TMPDIR/payload.json"
printf '%s' "$RAW_JSON" | jq --arg runId "$RUN_ID" --arg ip "$PUBLIC_IP" --arg lv "$LAUNCHER_VERSION" '
printf '%s' "$RAW_JSON" | jq --arg runId "$RUN_ID" --arg ip "$PUBLIC_IP" --arg lv "$LAUNCHER_VERSION" --arg hoster "$HOSTER" '
{
schemaVersion: 1,
runId: $runId,
probe: { publicIp: $ip },
probe: ({ publicIp: $ip } + if ($hoster | length) > 0 then { hoster: $hoster } else {} end),
launcherVersion: $lv,
censorcheck: {
version: ((.version | tostring) // "1"),
+40
View File
@@ -161,4 +161,44 @@ describe('censorcheck ingest + reads', () => {
expect(detail.statusCode).toBe(200)
expect(detail.json().results).toHaveLength(1)
})
it('сохраняет хостер из probe и канонизирует ASN', async () => {
const res = await post(
ingestPayload({
runId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
probe: { publicIp: '203.0.113.10', hoster: 'AS14061 DigitalOcean, LLC' },
}),
)
expect(res.statusCode).toBe(200)
const current = await app.inject({ method: 'GET', url: '/api/censorcheck/current' })
expect(current.json().items[0].detectedHoster).toBe('DigitalOcean')
expect(current.json().items[0].vps).toBeNull()
})
it('для matched VPS отдаёт имя хостера из инвентаря', async () => {
runWithSpace(MAIN_SPACE_ID, () =>
vpsRepository.create({
ip: '203.0.113.10',
dns: 'edge.example.com',
providerId: 'p1',
providerAccountId: 'a1',
status: 'active',
tariffType: 'monthly',
currency: 'RUB',
vcpu: 2,
ramGb: 4,
diskGb: 40,
}),
)
await post(
ingestPayload({
runId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
probe: { publicIp: '203.0.113.10', hoster: 'Hetzner Online GmbH' },
}),
)
const current = await app.inject({ method: 'GET', url: '/api/censorcheck/current' })
const item = current.json().items[0]
expect(item.vps.providerName).toBe('Test Host')
expect(item.detectedHoster).toBe('Hetzner')
})
})
+2 -1
View File
@@ -1,5 +1,5 @@
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import { censorcheckIngestBodySchema } from '@cfdm/shared/contracts/censorcheck'
import { canonicalizeHoster, censorcheckIngestBodySchema } from '@cfdm/shared/contracts/censorcheck'
import { censorcheckRepository } from '@cfdm/db/repositories/censorcheck'
import { actorFromRequest } from '../lib/audit-actor.js'
import {
@@ -82,6 +82,7 @@ export const censorcheckRoutes: FastifyPluginAsync = async (app) => {
censorcheckVersion: parsed.data.censorcheck?.version ?? null,
summary,
observedSourceIp: observed ?? null,
detectedHoster: canonicalizeHoster(parsed.data.probe.hoster),
results,
})
+2
View File
@@ -28,6 +28,8 @@ describe('GET /cc launcher', () => {
expect(res.body).toContain('VT_INGEST_TOKEN')
expect(res.body).toContain('ensure_cmds jq dig column')
expect(res.body).toContain('detect_os')
expect(res.body).toContain('detect_hoster')
expect(res.body).toContain('ipwho.is')
expect(res.body).toContain('/etc/os-release')
expect(res.body).toContain('apt-get install -y -qq')
expect(res.body).toContain('Проверяю сайты')
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { canonicalizeHoster } from '@cfdm/shared/contracts/censorcheck'
describe('canonicalizeHoster', () => {
it('мапит ASN/org на короткое имя', () => {
expect(canonicalizeHoster('AS14061 DigitalOcean, LLC')).toBe('DigitalOcean')
expect(canonicalizeHoster('Hetzner Online GmbH')).toBe('Hetzner')
expect(canonicalizeHoster('hosted-by.vdsina.ru')).toBe('VDSina')
expect(canonicalizeHoster(' aeza.net. ')).toBe('Aeza')
})
it('пустую строку отбрасывает', () => {
expect(canonicalizeHoster('')).toBeNull()
expect(canonicalizeHoster(' ')).toBeNull()
expect(canonicalizeHoster(undefined)).toBeNull()
})
it('неизвестную org чистит без alias', () => {
expect(canonicalizeHoster('AS12345 Example Hosting LLC')).toBe('Example Hosting')
})
})