forked from Psiphon-Labs/psiphon-tunnel-core
/
TCPConn_bind.go
291 lines (245 loc) · 7.78 KB
/
TCPConn_bind.go
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// +build !windows
/*
* Copyright (c) 2015, Psiphon Inc.
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package psiphon
import (
"context"
"math/rand"
"net"
"os"
"strconv"
"syscall"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
"github.com/creack/goselect"
)
// tcpDial is the platform-specific part of DialTCP
//
// To implement socket device binding, the lower-level syscall APIs are used.
// The sequence of syscalls in this implementation are taken from:
// https://github.com/golang/go/issues/6966
// (originally: https://code.google.com/p/go/issues/detail?id=6966)
//
// TODO: use https://golang.org/pkg/net/#Dialer.Control, introduced in Go 1.11?
func tcpDial(ctx context.Context, addr string, config *DialConfig) (net.Conn, error) {
// Get the remote IP and port, resolving a domain name if necessary
host, strPort, err := net.SplitHostPort(addr)
if err != nil {
return nil, errors.Trace(err)
}
port, err := strconv.Atoi(strPort)
if err != nil {
return nil, errors.Trace(err)
}
ipAddrs, err := LookupIP(ctx, host, config)
if err != nil {
return nil, errors.Trace(err)
}
if len(ipAddrs) < 1 {
return nil, errors.TraceNew("no IP address")
}
// When configured, attempt to synthesize IPv6 addresses from
// an IPv4 addresses for compatibility on DNS64/NAT64 networks.
// If synthesize fails, try the original addresses.
if config.IPv6Synthesizer != nil {
for i, ipAddr := range ipAddrs {
if ipAddr.To4() != nil {
synthesizedIPAddress := config.IPv6Synthesizer.IPv6Synthesize(ipAddr.String())
if synthesizedIPAddress != "" {
synthesizedAddr := net.ParseIP(synthesizedIPAddress)
if synthesizedAddr != nil {
ipAddrs[i] = synthesizedAddr
}
}
}
}
}
// Iterate over a pseudorandom permutation of the destination
// IPs and attempt connections.
//
// Only continue retrying as long as the dial context is not
// done. Unlike net.Dial, we do not fractionalize the context
// deadline, as the dial is generally intended to apply to a
// single attempt. So these serial retries are most useful in
// cases of immediate failure, such as "no route to host"
// errors when a host resolves to both IPv4 and IPv6 but IPv6
// addresses are unreachable.
//
// Retries at higher levels cover other cases: e.g.,
// Controller.remoteServerListFetcher will retry its entire
// operation and tcpDial will try a new permutation; or similarly,
// Controller.establishCandidateGenerator will retry a candidate
// tunnel server dials.
permutedIndexes := rand.Perm(len(ipAddrs))
lastErr := errors.TraceNew("unknown error")
for _, index := range permutedIndexes {
// Get address type (IPv4 or IPv6)
var ipv4 [4]byte
var ipv6 [16]byte
var domain int
var sockAddr syscall.Sockaddr
ipAddr := ipAddrs[index]
if ipAddr != nil && ipAddr.To4() != nil {
copy(ipv4[:], ipAddr.To4())
domain = syscall.AF_INET
} else if ipAddr != nil && ipAddr.To16() != nil {
copy(ipv6[:], ipAddr.To16())
domain = syscall.AF_INET6
} else {
lastErr = errors.TraceNew("invalid IP address")
continue
}
if domain == syscall.AF_INET {
sockAddr = &syscall.SockaddrInet4{Addr: ipv4, Port: port}
} else if domain == syscall.AF_INET6 {
sockAddr = &syscall.SockaddrInet6{Addr: ipv6, Port: port}
}
// Create a socket and bind to device, when configured to do so
socketFD, err := syscall.Socket(domain, syscall.SOCK_STREAM, 0)
if err != nil {
lastErr = errors.Trace(err)
continue
}
syscall.CloseOnExec(socketFD)
setAdditionalSocketOptions(socketFD)
if config.BPFProgramInstructions != nil {
err = setSocketBPF(config.BPFProgramInstructions, socketFD)
if err != nil {
syscall.Close(socketFD)
lastErr = errors.Trace(err)
continue
}
}
if config.DeviceBinder != nil {
_, err = config.DeviceBinder.BindToDevice(socketFD)
if err != nil {
syscall.Close(socketFD)
lastErr = errors.Tracef("BindToDevice failed with %s", err)
continue
}
}
// Connect socket to the server's IP address
err = syscall.SetNonblock(socketFD, true)
if err != nil {
syscall.Close(socketFD)
lastErr = errors.Trace(err)
continue
}
err = syscall.Connect(socketFD, sockAddr)
if err != nil {
if errno, ok := err.(syscall.Errno); !ok || errno != syscall.EINPROGRESS {
syscall.Close(socketFD)
lastErr = errors.Trace(err)
continue
}
}
// Use a control pipe to interrupt if the dial context is done (timeout or
// interrupted) before the TCP connection is established.
var controlFDs [2]int
err = syscall.Pipe(controlFDs[:])
if err != nil {
syscall.Close(socketFD)
lastErr = errors.Trace(err)
continue
}
for _, controlFD := range controlFDs {
syscall.CloseOnExec(controlFD)
err = syscall.SetNonblock(controlFD, true)
if err != nil {
break
}
}
if err != nil {
syscall.Close(socketFD)
lastErr = errors.Trace(err)
continue
}
resultChannel := make(chan error)
go func() {
readSet := goselect.FDSet{}
readSet.Set(uintptr(controlFDs[0]))
writeSet := goselect.FDSet{}
writeSet.Set(uintptr(socketFD))
max := socketFD
if controlFDs[0] > max {
max = controlFDs[0]
}
err := goselect.Select(max+1, &readSet, &writeSet, nil, -1)
if err == nil && !writeSet.IsSet(uintptr(socketFD)) {
err = errors.TraceNew("interrupted")
}
resultChannel <- err
}()
done := false
select {
case err = <-resultChannel:
case <-ctx.Done():
err = ctx.Err()
// Interrupt the goroutine
// TODO: if this Write fails, abandon the goroutine instead of hanging?
var b [1]byte
syscall.Write(controlFDs[1], b[:])
<-resultChannel
done = true
}
syscall.Close(controlFDs[0])
syscall.Close(controlFDs[1])
if err != nil {
syscall.Close(socketFD)
if done {
// Skip retry as dial context has timed out of been canceled.
return nil, errors.Trace(err)
}
lastErr = errors.Trace(err)
continue
}
err = syscall.SetNonblock(socketFD, false)
if err != nil {
syscall.Close(socketFD)
lastErr = errors.Trace(err)
continue
}
// Convert the socket fd to a net.Conn
// This code block is from:
// https://github.com/golang/go/issues/6966
file := os.NewFile(uintptr(socketFD), "")
conn, err := net.FileConn(file) // net.FileConn() dups socketFD
file.Close() // file.Close() closes socketFD
if err != nil {
lastErr = errors.Trace(err)
continue
}
// Handle the case where net.FileConn produces a Conn where RemoteAddr
// unexpectedly returns nil: https://github.com/golang/go/issues/23022.
//
// The net.Conn interface indicates that RemoteAddr returns a usable value,
// and code such as crypto/tls, in loadSession/clientSessionCacheKey, uses
// the RemoteAddr return value without a nil check, resulting in an panic.
//
// As the most likely explanation for this net.FileConn condition is
// getpeername returning ENOTCONN due to the socket connection closing
// during the net.FileConn execution, we choose to abort this dial rather
// than try to mask the RemoteAddr issue.
if conn.RemoteAddr() == nil {
conn.Close()
return nil, errors.TraceNew("RemoteAddr returns nil")
}
return &TCPConn{Conn: conn}, nil
}
return nil, lastErr
}