forked from Sandertv/go-raknet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdial.go
469 lines (427 loc) · 17.2 KB
/
dial.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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
package raknet
import (
"bytes"
"context"
"fmt"
"log"
"math/rand"
"net"
"os"
"sync/atomic"
"time"
"github.com/OpenFarLands/go-raknet/internal/message"
)
// UpstreamDialer is an interface for anything compatible with net.Dialer.
type UpstreamDialer interface {
Dial(network, address string) (net.Conn, error)
}
// Ping sends a ping to an address and returns the response obtained. If successful, a non-nil response byte
// slice containing the data is returned. If the ping failed, an error is returned describing the failure.
// Note that the packet sent to the server may be lost due to the nature of UDP. If this is the case, an error
// is returned which implies a timeout occurred.
// Ping will timeout after 5 seconds.
func Ping(address string) (response []byte, err error) {
var d Dialer
return d.Ping(address)
}
// PingTimeout sends a ping to an address and returns the response obtained. If successful, a non-nil response
// byte slice containing the data is returned. If the ping failed, an error is returned describing the
// failure.
// Note that the packet sent to the server may be lost due to the nature of UDP. If this is the case, an error
// is returned which implies a timeout occurred.
// PingTimeout will time out after the duration passed.
func PingTimeout(address string, timeout time.Duration) ([]byte, error) {
var d Dialer
return d.PingTimeout(address, timeout)
}
// PingContext sends a ping to an address and returns the response obtained. If successful, a non-nil response
// byte slice containing the data is returned. If the ping failed, an error is returned describing the
// failure.
// Note that the packet sent to the server may be lost due to the nature of UDP. If this is the case,
// PingContext could last indefinitely, hence a timeout should always be attached to the context passed.
// PingContext cancels as soon as the deadline expires.
func PingContext(ctx context.Context, address string) (response []byte, err error) {
var d Dialer
return d.PingContext(ctx, address)
}
// Dial attempts to dial a RakNet connection to the address passed. The address may be either an IP address
// or a hostname, combined with a port that is separated with ':'.
// Dial will attempt to dial a connection within 10 seconds. If not all packets are received after that, the
// connection will timeout and an error will be returned.
// Dial fills out a Dialer struct with a default error logger.
func Dial(address string) (*Conn, error) {
var d Dialer
return d.Dial(address)
}
// DialTimeout attempts to dial a RakNet connection to the address passed. The address may be either an IP
// address or a hostname, combined with a port that is separated with ':'.
// DialTimeout will attempt to dial a connection within the timeout duration passed. If not all packets are
// received after that, the connection will timeout and an error will be returned.
func DialTimeout(address string, timeout time.Duration) (*Conn, error) {
var d Dialer
return d.DialTimeout(address, timeout)
}
// DialContext attempts to dial a RakNet connection to the address passed. The address may be either an IP
// address or a hostname, combined with a port that is separated with ':'.
// DialContext will use the deadline (ctx.Deadline) of the context.Context passed for the maximum amount of
// time that the dialing can take. DialContext will terminate as soon as possible when the context.Context is
// closed.
func DialContext(ctx context.Context, address string) (*Conn, error) {
var d Dialer
return d.DialContext(ctx, address)
}
// Dialer allows dialing a RakNet connection with specific configuration, such as the protocol version of the
// connection and the logger used.
type Dialer struct {
// ErrorLog is a logger that errors from packet decoding are logged to. It may be set to a logger that
// simply discards the messages.
ErrorLog *log.Logger
// UpstreamDialer is a dialer that will override the default dialer for opening outgoing connections.
UpstreamDialer UpstreamDialer
}
// Ping sends a ping to an address and returns the response obtained. If successful, a non-nil response byte
// slice containing the data is returned. If the ping failed, an error is returned describing the failure.
// Note that the packet sent to the server may be lost due to the nature of UDP. If this is the case, an error
// is returned which implies a timeout occurred.
// Ping will timeout after 5 seconds.
func (dialer Dialer) Ping(address string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
return dialer.PingContext(ctx, address)
}
// PingTimeout sends a ping to an address and returns the response obtained. If successful, a non-nil response
// byte slice containing the data is returned. If the ping failed, an error is returned describing the
// failure.
// Note that the packet sent to the server may be lost due to the nature of UDP. If this is the case, an error
// is returned which implies a timeout occurred.
// PingTimeout will time out after the duration passed.
func (dialer Dialer) PingTimeout(address string, timeout time.Duration) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return dialer.PingContext(ctx, address)
}
// PingContext sends a ping to an address and returns the response obtained. If successful, a non-nil response
// byte slice containing the data is returned. If the ping failed, an error is returned describing the
// failure.
// Note that the packet sent to the server may be lost due to the nature of UDP. If this is the case,
// PingContext could last indefinitely, hence a timeout should always be attached to the context passed.
// PingContext cancels as soon as the deadline expires.
func (dialer Dialer) PingContext(ctx context.Context, address string) (response []byte, err error) {
var conn net.Conn
if dialer.UpstreamDialer == nil {
conn, err = net.Dial("udp", address)
} else {
conn, err = dialer.UpstreamDialer.Dial("udp", address)
}
if err != nil {
return nil, &net.OpError{Op: "ping", Net: "raknet", Source: nil, Addr: nil, Err: err}
}
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-done:
case <-ctx.Done():
_ = conn.Close()
}
}()
actual := func(e error) error {
if err := ctx.Err(); err != nil {
return err
}
return e
}
buffer := bytes.NewBuffer(nil)
(&message.UnconnectedPing{SendTimestamp: timestamp(), ClientGUID: atomic.AddInt64(&dialerID, 1)}).Write(buffer)
if _, err := conn.Write(buffer.Bytes()); err != nil {
return nil, &net.OpError{Op: "ping", Net: "raknet", Source: nil, Addr: nil, Err: actual(err)}
}
buffer.Reset()
data := make([]byte, 1492)
n, err := conn.Read(data)
if err != nil {
return nil, &net.OpError{Op: "ping", Net: "raknet", Source: nil, Addr: nil, Err: actual(err)}
}
data = data[:n]
_, _ = buffer.Write(data)
if b, err := buffer.ReadByte(); err != nil || b != message.IDUnconnectedPong {
return nil, &net.OpError{Op: "ping", Net: "raknet", Source: nil, Addr: nil, Err: fmt.Errorf("non-pong packet found: %w", err)}
}
pong := &message.UnconnectedPong{}
if err := pong.Read(buffer); err != nil {
return nil, &net.OpError{Op: "ping", Net: "raknet", Source: nil, Addr: nil, Err: fmt.Errorf("invalid unconnected pong: %w", err)}
}
_ = conn.Close()
return pong.Data, nil
}
// dialerID is a counter used to produce an ID for the client.
var dialerID = rand.New(rand.NewSource(time.Now().Unix())).Int63()
// Dial attempts to dial a RakNet connection to the address passed. The address may be either an IP address
// or a hostname, combined with a port that is separated with ':'.
// Dial will attempt to dial a connection within 10 seconds. If not all packets are received after that, the
// connection will timeout and an error will be returned.
func (dialer Dialer) Dial(address string) (*Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
return dialer.DialContext(ctx, address)
}
// DialTimeout attempts to dial a RakNet connection to the address passed. The address may be either an IP
// address or a hostname, combined with a port that is separated with ':'.
// DialTimeout will attempt to dial a connection within the timeout duration passed. If not all packets are
// received after that, the connection will timeout and an error will be returned.
func (dialer Dialer) DialTimeout(address string, timeout time.Duration) (*Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return dialer.DialContext(ctx, address)
}
// DialContext attempts to dial a RakNet connection to the address passed. The address may be either an IP
// address or a hostname, combined with a port that is separated with ':'.
// DialContext will use the deadline (ctx.Deadline) of the context.Context passed for the maximum amount of
// time that the dialing can take. DialContext will terminate as soon as possible when the context.Context is
// closed.
func (dialer Dialer) DialContext(ctx context.Context, address string) (*Conn, error) {
var udpConn net.Conn
var err error
if dialer.UpstreamDialer == nil {
udpConn, err = net.Dial("udp", address)
} else {
udpConn, err = dialer.UpstreamDialer.Dial("udp", address)
}
if err != nil {
return nil, &net.OpError{Op: "dial", Net: "raknet", Source: nil, Addr: nil, Err: err}
}
packetConn := udpConn.(net.PacketConn)
if deadline, ok := ctx.Deadline(); ok {
_ = packetConn.SetDeadline(deadline)
}
id := atomic.AddInt64(&dialerID, 1)
if dialer.ErrorLog == nil {
dialer.ErrorLog = log.New(os.Stderr, "", log.LstdFlags)
}
state := &connState{
conn: udpConn,
remoteAddr: udpConn.RemoteAddr(),
discoveringMTUSize: 1492,
id: id,
}
wrap := func(ctx context.Context, err error) error {
return &net.OpError{Op: "dial", Net: "raknet", Source: nil, Addr: nil, Err: err}
}
if err := state.discoverMTUSize(ctx); err != nil {
return nil, wrap(ctx, err)
} else if err := state.openConnectionRequest(ctx); err != nil {
return nil, wrap(ctx, err)
}
conn := newConnWithLimits(&wrappedConn{PacketConn: packetConn}, udpConn.RemoteAddr(), uint16(atomic.LoadUint32(&state.mtuSize)), false)
conn.close = func() {
// We have to make the Conn call this method explicitly because it must not close the connection
// established by the Listener. (This would close the entire listener.)
_ = udpConn.Close()
}
if err := conn.requestConnection(id); err != nil {
return nil, wrap(ctx, err)
}
go clientListen(conn, udpConn, dialer.ErrorLog)
select {
case <-conn.connected:
_ = packetConn.SetDeadline(time.Time{})
return conn, nil
case <-ctx.Done():
_ = conn.Close()
return nil, wrap(ctx, ctx.Err())
}
}
// wrappedCon wraps around a 'pre-connected' UDP connection. Its only purpose is to wrap around WriteTo and
// make it call Write instead.
type wrappedConn struct {
net.PacketConn
}
// WriteTo wraps around net.PacketConn to replace functionality of WriteTo with Write. It is used to be able
// to re-use the functionality in raknet.Conn.
func (conn *wrappedConn) WriteTo(b []byte, _ net.Addr) (n int, err error) {
return conn.PacketConn.(net.Conn).Write(b)
}
// clientListen makes the RakNet connection passed listen as a client for packets received in the connection
// passed.
func clientListen(rakConn *Conn, conn net.Conn, errorLog *log.Logger) {
// Create a buffer with the maximum size a UDP packet sent over RakNet is allowed to have. We can re-use
// this buffer for each packet.
b := make([]byte, 1500)
buf := bytes.NewBuffer(b[:0])
for {
n, err := conn.Read(b)
if err != nil {
if ErrConnectionClosed(err) {
// The connection was closed, so we can return from the function without logging the error.
return
}
errorLog.Printf("client: error reading from Conn: %v", err)
return
}
buf.Write(b[:n])
if err := rakConn.receive(buf); err != nil {
errorLog.Printf("error handling packet: %v\n", err)
}
buf.Reset()
}
}
// connState represents a state of a connection before the connection is finalised. It holds some data
// collected during the connection.
type connState struct {
conn net.Conn
remoteAddr net.Addr
id int64
// mtuSize is the final MTU size found by sending open connection request 1 packets. It is the MTU size
// sent by the server.
mtuSize uint32
// discoveringMTUSize is the current MTU size 'discovered'. This MTU size decreases the more the open
// connection request 1 is sent, so that the max packet size can be discovered.
discoveringMTUSize uint16
}
// openConnectionRequest sends open connection request 2 packets continuously until it receives an open
// connection reply 2 packet from the server.
func (state *connState) openConnectionRequest(ctx context.Context) (e error) {
ticker := time.NewTicker(time.Second / 2)
defer ticker.Stop()
stop := make(chan bool)
defer func() {
close(stop)
}()
// Use an intermediate channel to start the ticker immediately.
c := make(chan struct{}, 1)
c <- struct{}{}
go func() {
for {
select {
case <-c:
if err := state.sendOpenConnectionRequest2(uint16(atomic.LoadUint32(&state.mtuSize))); err != nil {
e = err
return
}
case <-ticker.C:
c <- struct{}{}
case <-stop:
return
case <-ctx.Done():
_ = state.conn.Close()
return
}
}
}()
b := make([]byte, 1492)
for {
// Start reading in a loop so that we can find open connection reply 2 packets.
n, err := state.conn.Read(b)
if err != nil {
return err
}
buffer := bytes.NewBuffer(b[:n])
id, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading packet ID: %v", err)
}
if id != message.IDOpenConnectionReply2 {
// We got a packet, but the packet was not an open connection reply 2 packet. We simply discard it
// and continue reading.
continue
}
reply := &message.OpenConnectionReply2{}
if err := reply.Read(buffer); err != nil {
return fmt.Errorf("error reading open connection reply 2: %v", err)
}
atomic.StoreUint32(&state.mtuSize, uint32(reply.MTUSize))
return
}
}
// discoverMTUSize starts discovering an MTU size, the maximum packet size we can send, by sending multiple
// open connection request 1 packets to the server with a decreasing MTU size padding.
func (state *connState) discoverMTUSize(ctx context.Context) (e error) {
ticker := time.NewTicker(time.Second / 2)
defer ticker.Stop()
var staticMTU uint16
stop := make(chan struct{})
defer func() {
close(stop)
}()
// Use an intermediate channel to start the ticker immediately.
c := make(chan struct{}, 1)
c <- struct{}{}
go func() {
for {
select {
case <-c:
mtu := state.discoveringMTUSize
if staticMTU != 0 {
mtu = staticMTU
}
if err := state.sendOpenConnectionRequest1(mtu); err != nil {
e = err
return
}
if staticMTU == 0 {
// Each half second we decrease the MTU size by 40. This means that in 10 seconds, we have an MTU
// size of 692. This is a little above the actual RakNet minimum, but that should not be an issue.
state.discoveringMTUSize -= 40
}
case <-ticker.C:
c <- struct{}{}
case <-stop:
return
case <-ctx.Done():
_ = state.conn.Close()
return
}
}
}()
b := make([]byte, 1492)
for {
// Start reading in a loop so that we can find open connection reply 1 packets.
n, err := state.conn.Read(b)
if err != nil {
return err
}
buffer := bytes.NewBuffer(b[:n])
id, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading packet ID: %v", err)
}
switch id {
case message.IDOpenConnectionReply1:
response := &message.OpenConnectionReply1{}
if err := response.Read(buffer); err != nil {
return fmt.Errorf("error reading open connection reply 1: %v", err)
}
if response.ServerPreferredMTUSize < 400 || response.ServerPreferredMTUSize > 1500 {
// This is an awful hack we cooked up to deal with OVH 'DDoS' protection. For some reason they
// send a broken MTU size first. Sending a Request2 followed by a Request1 deals with this.
_ = state.sendOpenConnectionRequest2(response.ServerPreferredMTUSize)
staticMTU = state.discoveringMTUSize + 40
continue
}
atomic.StoreUint32(&state.mtuSize, uint32(response.ServerPreferredMTUSize))
return
case message.IDIncompatibleProtocolVersion:
response := &message.IncompatibleProtocolVersion{}
if err := response.Read(buffer); err != nil {
return fmt.Errorf("error reading incompatible protocol version: %v", err)
}
return fmt.Errorf("mismatched protocol: client protocol = %v, server protocol = %v", currentProtocol, response.ServerProtocol)
}
}
}
// sendOpenConnectionRequest2 sends an open connection request 2 packet to the server. If not successful, an
// error is returned.
func (state *connState) sendOpenConnectionRequest2(mtu uint16) error {
b := bytes.NewBuffer(nil)
(&message.OpenConnectionRequest2{ServerAddress: *state.remoteAddr.(*net.UDPAddr), ClientPreferredMTUSize: mtu, ClientGUID: state.id}).Write(b)
_, err := state.conn.Write(b.Bytes())
return err
}
// sendOpenConnectionRequest1 sends an open connection request 1 packet to the server. If not successful, an
// error is returned.
func (state *connState) sendOpenConnectionRequest1(mtu uint16) error {
b := bytes.NewBuffer(nil)
(&message.OpenConnectionRequest1{Protocol: currentProtocol, MaximumSizeNotDropped: mtu}).Write(b)
_, err := state.conn.Write(b.Bytes())
return err
}