proxy probe: verify CONNECT+TLS+data-plane, extract accepts target host

HTTP probe previously tested forward-proxy GET only, so extraction
returned "alive" proxies that cannot tunnel HTTPS (CONNECT 400/EOF).
All three protocol probes now require: handshake -> CONNECT target:443
-> real TLS handshake with cert verify -> tunneled HTTPS HEAD reply.
Extraction accepts ?host= to probe the consumer's real target site
(e.g. vivid2api sends host=chatgpt.com); probe timeout 3s->6s and
batches 4->8 since the usable rate against strict checks is ~7%.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-18 17:43:09 +08:00
co-authored by Devin
parent d2206e7aa0
commit 5a884cab97
3 changed files with 109 additions and 34 deletions
+26 -8
View File
@@ -15,9 +15,9 @@ import (
const (
extractBatchSize = 15 // 每批并行探测的候选数
extractBatches = 4 // 最多取几批
extractProbeTO = 3 * time.Second // 单代理探测超时
extractDeadline = 20 * time.Second // 整个请求的最长耗时
extractBatches = 8 // 最多取几批
extractProbeTO = 6 * time.Second // 单代理探测超时(TLS+数据面验证,3s 会误杀慢但可用的)
extractDeadline = 30 * time.Second // 整个请求的最长耗时
)
type extractResult struct {
@@ -26,10 +26,11 @@ type extractResult struct {
ok bool
}
// ProxyExtractHandler GET /v1/proxy?key=xxx[&fmt=url]
// ProxyExtractHandler GET /v1/proxy?key=xxx[&fmt=url][&host=example.com]
// 公开提取接口:key 落库校验,地区绑在 key 上(0=全部)。
// 并行探测一批候选,谁先探活返回谁;返回带握手实测协议的 "scheme://ip:port"(socks5h/http/socks4)。
// 两批全死返回 503。
// 两批全死返回 503。host 可选:探测目标改为主机名(CONNECT+TLS 验证到该站,
// 默认 gstatic)——消费方传真实目标,拿到的才是"对这个站可用"的代理。
func ProxyExtractHandler(st *store.Store) fun.RouteHandler {
return func(rc *fun.RouteCtx) error {
reqCtx := rc.RequestCtx
@@ -43,6 +44,22 @@ func ProxyExtractHandler(st *store.Store) fun.RouteHandler {
routeJSONErr(reqCtx, 401, "invalid key")
return nil
}
// host 参数:提取探测的 CONNECT+TLS 目标,默认 gstatic。
// 只收合法主机名,去掉端口/路径等脏输入。
target := ""
if h := strings.TrimSpace(string(reqCtx.QueryArgs().Peek("host"))); h != "" {
h = strings.SplitN(h, ":", 2)[0]
valid := len(h) <= 253
for _, r := range h {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '.' || r == '-' || r == '_') {
valid = false
break
}
}
if valid && strings.Contains(h, ".") {
target = h
}
}
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(extractDeadline))
defer cancel()
@@ -56,7 +73,7 @@ func ProxyExtractHandler(st *store.Store) fun.RouteHandler {
if len(items) == 0 {
break
}
if p, proto, _, ok := probeBatch(ctx, st, items); ok {
if p, proto, _, ok := probeBatch(ctx, st, items, target); ok {
st.TouchProxyApiKey(k.Id)
reqCtx.SetContentType("text/plain; charset=utf-8")
reqCtx.SetBodyString(fmt.Sprintf("%s://%s:%d\n", urlScheme(proto), p.Ip, p.Port))
@@ -77,8 +94,9 @@ func urlScheme(proto string) string {
}
// probeBatch 并行探测一批候选:先活先赢,取消其余;死代理顺手标记移出池。
// target 非空时把 CONNECT+TLS 探测指向该主机(消费方真实目标)。
// 返回胜出的代理及其握手协议名(socks5/socks4/http)。
func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy) (store.RandomProxy, string, int, bool) {
func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy, target string) (store.RandomProxy, string, int, bool) {
type hit struct {
p store.RandomProxy
proto string
@@ -90,7 +108,7 @@ func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy)
wg.Add(1)
go func(p store.RandomProxy) {
defer wg.Done()
proto, lat, err := proxyx.Check(ctx, p.Ip, int(p.Port), p.Protocols, extractProbeTO)
proto, lat, err := proxyx.Check(ctx, p.Ip, int(p.Port), p.Protocols, extractProbeTO, target)
if err != nil {
_ = st.MarkProxyFailed(p.Id, time.Now().Unix(), p.FailCount+1)
return