starnet/errors.go
starainrt 9ac9b65bc5
fix(starnet): 收紧 TLS ClientHello 嗅探并补齐边界测试
- 用轻量 ClientHello 解析替代假握手式 TLS 嗅探
  - 保留截断和 max-bytes 场景下的 TLS 分类与缓冲回放能力
  - 拒绝首个 record 完整但并非 ClientHello 的伪 TLS 流量
  - 为动态 TLS 配置选择透出更完整的 ClientHello 元数据
  - 拆分 TLS 初始化失败统计为 sniff/config/plain rejected
  - 补充正常、分片、截断、限长、伪 TLS 等回归测试
2026-03-27 12:05:23 +08:00

258 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package starnet
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/url"
"strings"
)
var (
// ErrInvalidMethod 无效的 HTTP 方法
ErrInvalidMethod = errors.New("starnet: invalid HTTP method")
// ErrInvalidURL 无效的 URL
ErrInvalidURL = errors.New("starnet: invalid URL")
// ErrInvalidIP 无效的 IP 地址
ErrInvalidIP = errors.New("starnet: invalid IP address")
// ErrInvalidDNS 无效的 DNS 服务器
ErrInvalidDNS = errors.New("starnet: invalid DNS server")
// ErrNilClient HTTP Client 为 nil
ErrNilClient = errors.New("starnet: http client is nil")
// ErrNilReader Reader 为 nil
ErrNilReader = errors.New("starnet: reader is nil")
// ErrFileNotFound 文件不存在
ErrFileNotFound = errors.New("starnet: file not found")
// ErrRequestNotPrepared 请求未准备好
ErrRequestNotPrepared = errors.New("starnet: request not prepared")
// ErrBodyAlreadyConsumed Body 已被消费
ErrBodyAlreadyConsumed = errors.New("starnet: response body already consumed")
// ErrRespBodyTooLarge 响应体超过允许上限
ErrRespBodyTooLarge = errors.New("starnet: response body too large")
// ErrPingInvalidTimeout ping 超时参数无效
ErrPingInvalidTimeout = errors.New("starnet: invalid ping timeout")
// ErrPingPermissionDenied ping 需要更高权限raw socket
ErrPingPermissionDenied = errors.New("starnet: ping permission denied")
// ErrPingProtocolUnsupported ping 协议/地址族不受当前平台支持
ErrPingProtocolUnsupported = errors.New("starnet: ping protocol unsupported")
// ErrPingNoResolvedTarget ping 目标无法解析为可用地址
ErrPingNoResolvedTarget = errors.New("starnet: ping target not resolved")
)
// wrapError 包装错误,添加上下文信息
func wrapError(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
msg := fmt.Sprintf(format, args...)
return fmt.Errorf("%s: %w", msg, err)
}
var (
// ErrNilConn indicates a nil net.Conn argument.
ErrNilConn = errors.New("starnet: nil connection")
// ErrTLSSniffFailed indicates TLS sniffing/parsing failed before handshake setup.
ErrTLSSniffFailed = errors.New("starnet: tls sniff failed")
// ErrTLSConfigSelectionFailed indicates dynamic TLS config selection failed.
ErrTLSConfigSelectionFailed = errors.New("starnet: tls config selection failed")
// ErrNonTLSNotAllowed indicates plain TCP was detected while non-TLS is forbidden.
ErrNonTLSNotAllowed = errors.New("starnet: non-TLS connection not allowed")
// ErrNotTLS indicates caller asked for TLS-only object but conn is plain TCP.
ErrNotTLS = errors.New("starnet: connection is not TLS")
// ErrNoTLSConfig indicates TLS was detected but no usable TLS config is available.
ErrNoTLSConfig = errors.New("starnet: no TLS config available")
)
// ErrorKind is a normalized high-level category for request errors.
type ErrorKind string
const (
ErrorKindNone ErrorKind = "none"
ErrorKindCanceled ErrorKind = "canceled"
ErrorKindTimeout ErrorKind = "timeout"
ErrorKindDNS ErrorKind = "dns"
ErrorKindTLS ErrorKind = "tls"
ErrorKindProxy ErrorKind = "proxy"
ErrorKindOther ErrorKind = "other"
)
// IsCanceled reports whether err is a cancellation-related error.
func IsCanceled(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.Canceled) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "context canceled") ||
strings.Contains(msg, "operation was canceled") ||
strings.Contains(msg, "request canceled")
}
// ClassifyError maps low-level errors to a stable category for business handling.
func ClassifyError(err error) ErrorKind {
if err == nil {
return ErrorKindNone
}
if IsCanceled(err) {
return ErrorKindCanceled
}
if IsProxy(err) {
return ErrorKindProxy
}
if IsDNS(err) {
return ErrorKindDNS
}
if IsTLS(err) {
return ErrorKindTLS
}
if IsTimeout(err) {
return ErrorKindTimeout
}
return ErrorKindOther
}
// IsTimeout reports whether err is a timeout-related error.
func IsTimeout(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
var uerr *url.Error
if errors.As(err, &uerr) && uerr.Timeout() {
return true
}
var nerr net.Error
if errors.As(err, &nerr) && nerr.Timeout() {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline exceeded")
}
// IsDNS reports whether err is a DNS resolution related error.
func IsDNS(err error) bool {
if err == nil {
return false
}
var derr *net.DNSError
if errors.As(err, &derr) {
return true
}
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "no such host") ||
strings.Contains(msg, "server misbehaving") ||
strings.Contains(msg, "temporary failure in name resolution") {
return true
}
return strings.Contains(msg, "lookup ") &&
(strings.Contains(msg, "dns") || strings.Contains(msg, "i/o timeout"))
}
// IsTLS reports whether err is TLS/Certificate related.
func IsTLS(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrNotTLS) || errors.Is(err, ErrNoTLSConfig) || errors.Is(err, ErrNonTLSNotAllowed) ||
errors.Is(err, ErrTLSSniffFailed) || errors.Is(err, ErrTLSConfigSelectionFailed) {
return true
}
var recErr tls.RecordHeaderError
if errors.As(err, &recErr) {
return true
}
var uaErr x509.UnknownAuthorityError
if errors.As(err, &uaErr) {
return true
}
var hnErr x509.HostnameError
if errors.As(err, &hnErr) {
return true
}
var certErr x509.CertificateInvalidError
if errors.As(err, &certErr) {
return true
}
var rootsErr x509.SystemRootsError
if errors.As(err, &rootsErr) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "tls:") || strings.Contains(msg, "x509:")
}
// IsProxy reports whether err is proxy related.
func IsProxy(err error) bool {
if err == nil {
return false
}
if isProxyMessage(strings.ToLower(err.Error())) {
return true
}
var uerr *url.Error
if errors.As(err, &uerr) {
if strings.Contains(strings.ToLower(uerr.Op), "proxy") {
return true
}
if uerr.Err != nil && isProxyMessage(strings.ToLower(uerr.Err.Error())) {
return true
}
}
var opErr *net.OpError
if errors.As(err, &opErr) && strings.Contains(strings.ToLower(opErr.Op), "proxy") {
return true
}
return false
}
func isProxyMessage(msg string) bool {
return strings.Contains(msg, "proxyconnect") ||
strings.Contains(msg, "proxy error") ||
strings.Contains(msg, "proxy authentication required") ||
strings.Contains(msg, "proxy: unknown scheme") ||
strings.Contains(msg, "socks connect") ||
strings.Contains(msg, "socks5")
}