-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdialer.go
More file actions
163 lines (140 loc) · 4.27 KB
/
Copy pathdialer.go
File metadata and controls
163 lines (140 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package domainfront
import (
"context"
"crypto/x509"
"errors"
"fmt"
"net"
"strings"
"syscall"
"time"
tls "github.com/refraction-networking/utls"
)
const dialTimeout = 5 * time.Second
// Dialer abstracts TCP dialing for testability.
type Dialer interface {
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
}
// NetDialer is the default TCP dialer.
type NetDialer struct{}
func (NetDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, addr)
}
// dialResult is the outcome of dialing a front.
type dialResult struct {
conn net.Conn
retriable bool
err error
}
// dialFront performs a TLS connection to the given front.
// Builds the tls.Config in a single allocation (no Clone).
func dialFront(ctx context.Context, f *front, rootCAs *x509.CertPool, clientHelloID tls.ClientHelloID, dialer Dialer) dialResult {
addr := f.IpAddress
if _, _, err := net.SplitHostPort(addr); err != nil {
addr = net.JoinHostPort(addr, "443")
}
dialCtx, dialCancel := context.WithTimeout(ctx, dialTimeout)
defer dialCancel()
rawConn, err := dialer.DialContext(dialCtx, "tcp", addr)
if err != nil {
return dialResult{nil, classifyError(err), err}
}
// Build final tls.Config directly — single allocation instead of alloc + Clone.
config := &tls.Config{
RootCAs: rootCAs,
InsecureSkipVerify: true,
}
useSNI := f.SNI != ""
if useSNI {
config.ServerName = f.SNI
config.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
var verifyHostname string
if f.VerifyHostname != nil {
verifyHostname = *f.VerifyHostname
}
return verifyPeerCertificate(rawCerts, rootCAs, verifyHostname)
}
}
// When not using SNI, ServerName stays empty so no SNI extension is sent.
chid := clientHelloID
if chid.Client == "" {
chid = tls.HelloGolang
}
conn := tls.UClient(rawConn, config, chid)
deadline := time.Now().Add(dialTimeout)
rawConn.SetDeadline(deadline)
if err := conn.Handshake(); err != nil {
rawConn.Close()
return dialResult{nil, classifyError(err), err}
}
rawConn.SetDeadline(time.Time{})
// For non-SNI case, verify the cert manually after handshake
if !useSNI {
state := conn.ConnectionState()
rawCerts := make([][]byte, len(state.PeerCertificates))
for i, cert := range state.PeerCertificates {
rawCerts[i] = cert.Raw
}
if err := verifyPeerCertificate(rawCerts, rootCAs, f.Domain); err != nil {
rawConn.Close()
return dialResult{nil, false, err}
}
}
return dialResult{conn, false, nil}
}
// verifyPeerCertificate verifies the peer certificate chain against the given roots.
func verifyPeerCertificate(rawCerts [][]byte, roots *x509.CertPool, domain string) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no certificates presented")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("unable to parse certificate: %w", err)
}
opts := x509.VerifyOptions{
Roots: roots,
CurrentTime: time.Now(),
DNSName: domain,
Intermediates: x509.NewCertPool(),
}
for i := 1; i < len(rawCerts); i++ {
intermediate, err := x509.ParseCertificate(rawCerts[i])
if err != nil {
return fmt.Errorf("unable to parse intermediate certificate: %w", err)
}
opts.Intermediates.AddCert(intermediate)
}
if _, err := cert.Verify(opts); err != nil {
return fmt.Errorf("certificate verification failed: %w", err)
}
return nil
}
// classifyError returns true if the error is retriable (network issue),
// false if permanent (cert/handshake error).
func classifyError(err error) bool {
if isNetworkUnreachable(err) {
return true
}
errStr := err.Error()
if strings.Contains(errStr, "certificate") || strings.Contains(errStr, "handshake") {
return false
}
return true
}
func isNetworkUnreachable(err error) bool {
var opErr *net.OpError
if errors.As(err, &opErr) {
if errors.Is(opErr.Err, syscall.ENETUNREACH) || errors.Is(opErr.Err, syscall.EHOSTUNREACH) {
return true
}
errMsg := opErr.Err.Error()
if strings.Contains(errMsg, "network is unreachable") ||
strings.Contains(errMsg, "no route to host") ||
strings.Contains(errMsg, "unreachable network") ||
strings.Contains(errMsg, "unreachable host") {
return true
}
}
return false
}