feat: Reality target scanner (item 16)
Panel-side POST /api/reality/scan performs real TLS handshakes against operator input (domains / IPs / CIDRs, port suffixes) or the built-in pool (form presets plus microsoft/google/nvidia/samsung/ cloudflare and friends) and reports per target: usable or not, TLS version, ALPN, key exchange, first cert SAN, latency, and a short failure reason. Key-exchange support is proven by restricted-group handshakes (X25519MLKEM768 first, then X25519, then a combined offer) since the negotiated group is not exposed to Go clients. Bounds: 32 targets, 8 concurrent, 4s per probe. Node form gains a 查找目标 button beside the Reality preset; the scanner is a stacked overlay so form state survives, and 使用 backfills SNI/dest/port.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
// Item 16 scanner tests: input parsing (offline) and the probe against
|
||||
// a local TLS server (loopback only — the live-domain check happens in
|
||||
// the sandbox smoke run).
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseScanTargetsPool(t *testing.T) {
|
||||
targets, err := parseScanTargets("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(targets) < len(RealityPresets) || len(targets) > scanMaxTargets {
|
||||
t.Fatalf("pool size %d out of bounds", len(targets))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, target := range targets {
|
||||
seen[target.Host] = true
|
||||
if target.Port != 443 {
|
||||
t.Fatalf("pool target %s port %d, want 443", target.Host, target.Port)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"www.microsoft.com", "www.google.com", "www.nvidia.com", "www.samsung.com", "www.intel.com"} {
|
||||
if !seen[want] {
|
||||
t.Fatalf("pool missing %s", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseScanTargetsInput(t *testing.T) {
|
||||
targets, err := parseScanTargets("a.example.com, b.example.com:8443 203.0.113.7")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(targets) != 3 || targets[0].Port != 443 || targets[1].Port != 8443 || targets[2].Host != "203.0.113.7" {
|
||||
t.Fatalf("targets %+v", targets)
|
||||
}
|
||||
cidr, err := parseScanTargets("203.0.113.0/30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cidr) != 4 {
|
||||
t.Fatalf("cidr /30 expanded to %d hosts, want 4", len(cidr))
|
||||
}
|
||||
if _, err := parseScanTargets("10.0.0.0/33"); err == nil {
|
||||
t.Fatal("invalid CIDR must error")
|
||||
}
|
||||
if _, err := parseScanTargets("a.example.com:99999"); err == nil {
|
||||
t.Fatal("invalid port must error")
|
||||
}
|
||||
many := strings.TrimSuffix(strings.Repeat("a.example.com,", scanMaxTargets+1), ",")
|
||||
if _, err := parseScanTargets(many); err == nil {
|
||||
t.Fatal("over-cap input must error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeRealityTargetLocalTLS(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "localhost"},
|
||||
DNSNames: []string{"localhost"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cert := tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
|
||||
listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
_ = conn.(*tls.Conn).Handshake()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
result := probeRealityTarget(scanTarget{Host: "127.0.0.1", Port: port})
|
||||
if !result.OK {
|
||||
t.Fatalf("local TLS target must be OK: %+v", result)
|
||||
}
|
||||
if result.TLSVersion != "TLS 1.3" {
|
||||
t.Fatalf("tlsVersion %q, want TLS 1.3", result.TLSVersion)
|
||||
}
|
||||
if result.KeyExchange == "" || !result.KeyExchangeOK {
|
||||
t.Fatalf("keyExchange %q ok=%v", result.KeyExchange, result.KeyExchangeOK)
|
||||
}
|
||||
// A dead port reports not-OK with a reason, never a panic.
|
||||
dead := probeRealityTarget(scanTarget{Host: "127.0.0.1", Port: 1})
|
||||
if dead.OK || dead.Reason == "" {
|
||||
t.Fatalf("dead target: %+v", dead)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user