2026-03-08 20:19:40 +08:00
|
|
|
|
package starnet
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-19 16:42:45 +08:00
|
|
|
|
"context"
|
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
|
"crypto/x509"
|
2026-03-08 20:19:40 +08:00
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2026-03-19 16:42:45 +08:00
|
|
|
|
"net"
|
|
|
|
|
|
"net/url"
|
|
|
|
|
|
"strings"
|
2026-03-08 20:19:40 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
2026-03-19 16:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 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")
|
2026-03-08 20:19:40 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
|
|
2026-03-27 12:05:23 +08:00
|
|
|
|
// 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")
|
|
|
|
|
|
|
2026-03-08 20:19:40 +08:00
|
|
|
|
// 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")
|
|
|
|
|
|
)
|
2026-03-19 16:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-03-27 12:05:23 +08:00
|
|
|
|
if errors.Is(err, ErrNotTLS) || errors.Is(err, ErrNoTLSConfig) || errors.Is(err, ErrNonTLSNotAllowed) ||
|
|
|
|
|
|
errors.Is(err, ErrTLSSniffFailed) || errors.Is(err, ErrTLSConfigSelectionFailed) {
|
2026-03-19 16:42:45 +08:00
|
|
|
|
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")
|
|
|
|
|
|
}
|