forked from cloudflare/cloudflared
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathicmp_windows.go
574 lines (527 loc) · 17.2 KB
/
icmp_windows.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
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
//go:build windows && cgo
package ingress
/*
#include <iphlpapi.h>
#include <icmpapi.h>
*/
import "C"
import (
"context"
"encoding/binary"
"fmt"
"net/netip"
"runtime/debug"
"sync"
"syscall"
"time"
"unsafe"
"github.com/google/gopacket/layers"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"github.com/cloudflare/cloudflared/packet"
"github.com/cloudflare/cloudflared/tracing"
)
const (
// Value defined in https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw
AF_INET6 = 23
icmpEchoReplyCode = 0
nullParameter = uintptr(0)
)
var (
Iphlpapi = syscall.NewLazyDLL("Iphlpapi.dll")
IcmpCreateFile_proc = Iphlpapi.NewProc("IcmpCreateFile")
Icmp6CreateFile_proc = Iphlpapi.NewProc("Icmp6CreateFile")
IcmpSendEcho_proc = Iphlpapi.NewProc("IcmpSendEcho")
Icmp6SendEcho_proc = Iphlpapi.NewProc("Icmp6SendEcho2")
echoReplySize = unsafe.Sizeof(echoReply{})
echoV6ReplySize = unsafe.Sizeof(echoV6Reply{})
icmpv6ErrMessageSize = 8
ioStatusBlockSize = unsafe.Sizeof(ioStatusBlock{})
endian = binary.LittleEndian
)
// IP_STATUS code, see https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply32#members
// for possible values
type ipStatus uint32
const (
success ipStatus = 0
bufTooSmall = iota + 11000
destNetUnreachable
destHostUnreachable
destProtocolUnreachable
destPortUnreachable
noResources
badOption
hwError
packetTooBig
reqTimedOut
badReq
badRoute
ttlExpiredTransit
ttlExpiredReassembly
paramProblem
sourceQuench
optionTooBig
badDestination
// Can be returned for malformed ICMP packets
generalFailure = 11050
)
// Additional IP_STATUS codes for ICMPv6 https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmpv6_echo_reply_lh#members
const (
ipv6DestUnreachable ipStatus = iota + 11040
ipv6TimeExceeded
ipv6BadHeader
ipv6UnrecognizedNextHeader
ipv6ICMPError
ipv6DestScopeMismatch
)
func (is ipStatus) String() string {
switch is {
case success:
return "Success"
case bufTooSmall:
return "The reply buffer too small"
case destNetUnreachable:
return "The destination network was unreachable"
case destHostUnreachable:
return "The destination host was unreachable"
case destProtocolUnreachable:
return "The destination protocol was unreachable"
case destPortUnreachable:
return "The destination port was unreachable"
case noResources:
return "Insufficient IP resources were available"
case badOption:
return "A bad IP option was specified"
case hwError:
return "A hardware error occurred"
case packetTooBig:
return "The packet was too big"
case reqTimedOut:
return "The request timed out"
case badReq:
return "Bad request"
case badRoute:
return "Bad route"
case ttlExpiredTransit:
return "The TTL expired in transit"
case ttlExpiredReassembly:
return "The TTL expired during fragment reassembly"
case paramProblem:
return "A parameter problem"
case sourceQuench:
return "Datagrams are arriving too fast to be processed and datagrams may have been discarded"
case optionTooBig:
return "The IP option was too big"
case badDestination:
return "Bad destination"
case ipv6DestUnreachable:
return "IPv6 destination unreachable"
case ipv6TimeExceeded:
return "IPv6 time exceeded"
case ipv6BadHeader:
return "IPv6 bad IP header"
case ipv6UnrecognizedNextHeader:
return "IPv6 unrecognized next header"
case ipv6ICMPError:
return "IPv6 ICMP error"
case ipv6DestScopeMismatch:
return "IPv6 destination scope ID mismatch"
case generalFailure:
return "The ICMP packet might be malformed"
default:
return fmt.Sprintf("Unknown ip status %d", is)
}
}
// https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ip_option_information
type ipOption struct {
TTL uint8
Tos uint8
Flags uint8
OptionsSize uint8
OptionsData uintptr
}
// https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply
type echoReply struct {
Address uint32
Status ipStatus
RoundTripTime uint32
DataSize uint16
Reserved uint16
// The pointer size defers between 32-bit and 64-bit platforms
DataPointer *byte
Options ipOption
}
type echoV6Reply struct {
Address ipv6AddrEx
Status ipStatus
RoundTripTime uint32
}
// https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ipv6_address_ex
// All the fields are in network byte order. The memory alignment is 4 bytes
type ipv6AddrEx struct {
port uint16
// flowInfo is uint32. Because of field alignment, when we cast reply buffer to ipv6AddrEx, it starts at the 5th byte
// But looking at the raw bytes, flowInfo starts at the 3rd byte. We device flowInfo into 2 uint16 so it's aligned
flowInfoUpper uint16
flowInfoLower uint16
addr [8]uint16
scopeID uint32
}
// https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2
type sockAddrIn6 struct {
family int16
// Can't embed ipv6AddrEx, that changes the memory alignment
port uint16
flowInfo uint32
addr [16]byte
scopeID uint32
}
func newSockAddrIn6(addr netip.Addr) (*sockAddrIn6, error) {
if !addr.Is6() {
return nil, fmt.Errorf("%s is not IPv6", addr)
}
return &sockAddrIn6{
family: AF_INET6,
port: 10,
addr: addr.As16(),
}, nil
}
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_status_block#syntax
type ioStatusBlock struct {
// The first field is an union of NTSTATUS and PVOID. NTSTATUS is int32 while PVOID depends on the platform.
// We model it as uintptr whose size depends on if the platform is 32-bit or 64-bit
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55
statusOrPointer uintptr
information uintptr
}
type icmpProxy struct {
// An open handle that can send ICMP requests https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpcreatefile
handle uintptr
// This is a ICMPv6 if srcSocketAddr is not nil
srcSocketAddr *sockAddrIn6
logger *zerolog.Logger
// A pool of reusable *packet.Encoder
encoderPool sync.Pool
}
func newICMPProxy(listenIP netip.Addr, zone string, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
var (
srcSocketAddr *sockAddrIn6
handle uintptr
err error
)
if listenIP.Is4() {
handle, _, err = IcmpCreateFile_proc.Call()
} else {
srcSocketAddr, err = newSockAddrIn6(listenIP)
if err != nil {
return nil, err
}
handle, _, err = Icmp6CreateFile_proc.Call()
}
// Windows procedure calls always return non-nil error constructed from the result of GetLastError.
// Caller need to inspect the primary returned value
if syscall.Handle(handle) == syscall.InvalidHandle {
return nil, errors.Wrap(err, "invalid ICMP handle")
}
return &icmpProxy{
handle: handle,
srcSocketAddr: srcSocketAddr,
logger: logger,
encoderPool: sync.Pool{
New: func() any {
return packet.NewEncoder()
},
},
}, nil
}
func (ip *icmpProxy) Serve(ctx context.Context) error {
<-ctx.Done()
syscall.CloseHandle(syscall.Handle(ip.handle))
return ctx.Err()
}
// Request sends an ICMP echo request and wait for a reply or timeout.
// The async version of Win32 APIs take a callback whose memory is not garbage collected, so we use the synchronous version.
// It's possible that a slow request will block other requests, so we set the timeout to only 1s.
func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *packetResponder) error {
defer func() {
if r := recover(); r != nil {
ip.logger.Error().Interface("error", r).Msgf("Recover panic from sending icmp request/response, error %s", debug.Stack())
}
}()
_, requestSpan := responder.requestSpan(ctx, pk)
defer responder.exportSpan()
echo, err := getICMPEcho(pk.Message)
if err != nil {
return err
}
observeICMPRequest(ip.logger, requestSpan, pk.Src.String(), pk.Dst.String(), echo.ID, echo.Seq)
resp, err := ip.icmpEchoRoundtrip(pk.Dst, echo)
if err != nil {
ip.logger.Err(err).Msg("ICMP echo roundtrip failed")
tracing.EndWithErrorStatus(requestSpan, err)
return err
}
tracing.End(requestSpan)
responder.exportSpan()
_, replySpan := responder.replySpan(ctx, ip.logger)
err = ip.handleEchoReply(pk, echo, resp, responder)
if err != nil {
ip.logger.Err(err).Msg("Failed to send ICMP reply")
tracing.EndWithErrorStatus(replySpan, err)
return errors.Wrap(err, "failed to handle ICMP echo reply")
}
observeICMPReply(ip.logger, replySpan, pk.Dst.String(), echo.ID, echo.Seq)
replySpan.SetAttributes(
attribute.Int64("rtt", int64(resp.rtt())),
attribute.String("status", resp.status().String()),
)
tracing.End(replySpan)
return nil
}
func (ip *icmpProxy) handleEchoReply(request *packet.ICMP, echoReq *icmp.Echo, resp echoResp, responder *packetResponder) error {
var replyType icmp.Type
if request.Dst.Is4() {
replyType = ipv4.ICMPTypeEchoReply
} else {
replyType = ipv6.ICMPTypeEchoReply
}
pk := packet.ICMP{
IP: &packet.IP{
Src: request.Dst,
Dst: request.Src,
Protocol: layers.IPProtocol(request.Type.Protocol()),
TTL: packet.DefaultTTL,
},
Message: &icmp.Message{
Type: replyType,
Code: icmpEchoReplyCode,
Body: &icmp.Echo{
ID: echoReq.ID,
Seq: echoReq.Seq,
Data: resp.payload(),
},
},
}
cachedEncoder := ip.encoderPool.Get()
// The encoded packet is a slice to of the encoder, so we shouldn't return the encoder back to the pool until
// the encoded packet is sent.
defer ip.encoderPool.Put(cachedEncoder)
encoder, ok := cachedEncoder.(*packet.Encoder)
if !ok {
return fmt.Errorf("encoderPool returned %T, expect *packet.Encoder", cachedEncoder)
}
serializedPacket, err := encoder.Encode(&pk)
if err != nil {
return err
}
return responder.returnPacket(serializedPacket)
}
func (ip *icmpProxy) icmpEchoRoundtrip(dst netip.Addr, echo *icmp.Echo) (echoResp, error) {
if dst.Is6() {
if ip.srcSocketAddr == nil {
return nil, fmt.Errorf("cannot send ICMPv6 using ICMPv4 proxy")
}
resp, err := ip.icmp6SendEcho(dst, echo)
if err != nil {
return nil, errors.Wrap(err, "failed to send/receive ICMPv6 echo")
}
return resp, nil
}
if ip.srcSocketAddr != nil {
return nil, fmt.Errorf("cannot send ICMPv4 using ICMPv6 proxy")
}
resp, err := ip.icmpSendEcho(dst, echo)
if err != nil {
return nil, errors.Wrap(err, "failed to send/receive ICMPv4 echo")
}
return resp, nil
}
/*
Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho
Parameters:
- IcmpHandle: Handle created by IcmpCreateFile
- DestinationAddress: IPv4 in the form of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax
- RequestData: A pointer to echo data
- RequestSize: Number of bytes in buffer pointed by echo data
- RequestOptions: IP header options
- ReplyBuffer: A pointer to the buffer for echoReply, options and data
- ReplySize: Number of bytes allocated for ReplyBuffer
- Timeout: Timeout in milliseconds to wait for a reply
Returns:
- the number of replies in uint32 https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho#return-value
To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the
syscall function
*/
func (ip *icmpProxy) icmpSendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV4Resp, error) {
dataSize := len(echo.Data)
replySize := echoReplySize + uintptr(dataSize)
replyBuf := make([]byte, replySize)
noIPHeaderOption := nullParameter
inAddr, err := inAddrV4(dst)
if err != nil {
return nil, err
}
replyCount, _, err := IcmpSendEcho_proc.Call(
ip.handle,
uintptr(inAddr),
uintptr(unsafe.Pointer(&echo.Data[0])),
uintptr(dataSize),
noIPHeaderOption,
uintptr(unsafe.Pointer(&replyBuf[0])),
replySize,
icmpRequestTimeoutMs,
)
if replyCount == 0 {
// status is returned in 5th to 8th byte of reply buffer
if status, parseErr := unmarshalIPStatus(replyBuf[4:8]); parseErr == nil && status != success {
return nil, errors.Wrapf(err, "received ip status: %s", status)
}
return nil, errors.Wrap(err, "did not receive ICMP echo reply")
} else if replyCount > 1 {
ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount)
}
return newEchoV4Resp(replyBuf)
}
// Third definition of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax is address in uint32
func inAddrV4(ip netip.Addr) (uint32, error) {
if !ip.Is4() {
return 0, fmt.Errorf("%s is not IPv4", ip)
}
v4 := ip.As4()
return endian.Uint32(v4[:]), nil
}
type echoResp interface {
status() ipStatus
rtt() uint32
payload() []byte
}
type echoV4Resp struct {
reply *echoReply
data []byte
}
func (r *echoV4Resp) status() ipStatus {
return r.reply.Status
}
func (r *echoV4Resp) rtt() uint32 {
return r.reply.RoundTripTime
}
func (r *echoV4Resp) payload() []byte {
return r.data
}
func newEchoV4Resp(replyBuf []byte) (*echoV4Resp, error) {
if len(replyBuf) == 0 {
return nil, fmt.Errorf("reply buffer is empty")
}
// This is pattern 1 of https://pkg.go.dev/unsafe@master#Pointer, conversion of *replyBuf to *echoReply
// replyBuf size is larger than echoReply
reply := *(*echoReply)(unsafe.Pointer(&replyBuf[0]))
if reply.Status != success {
return nil, fmt.Errorf("status %d", reply.Status)
}
dataBufStart := len(replyBuf) - int(reply.DataSize)
if dataBufStart < int(echoReplySize) {
return nil, fmt.Errorf("reply buffer size %d is too small to hold data of size %d", len(replyBuf), int(reply.DataSize))
}
return &echoV4Resp{
reply: &reply,
data: replyBuf[dataBufStart:],
}, nil
}
/*
Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmp6sendecho2
Parameters:
- IcmpHandle: Handle created by Icmp6CreateFile
- Event (optional): Event object to be signaled when a reply arrives
- ApcRoutine (optional): Routine to call when the calling thread is in an alertable thread and a reply arrives
- ApcContext (optional): Optional parameter to ApcRoutine
- SourceAddress: Source address of the request
- DestinationAddress: Destination address of the request
- RequestData: A pointer to echo data
- RequestSize: Number of bytes in buffer pointed by echo data
- RequestOptions (optional): A pointer to the IPv6 header options
- ReplyBuffer: A pointer to the buffer for echoReply, options and data
- ReplySize: Number of bytes allocated for ReplyBuffer
- Timeout: Timeout in milliseconds to wait for a reply
Returns:
- the number of replies in uint32
To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the
syscall function
*/
func (ip *icmpProxy) icmp6SendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV6Resp, error) {
dstAddr, err := newSockAddrIn6(dst)
if err != nil {
return nil, err
}
dataSize := len(echo.Data)
// Reply buffer needs to be big enough to hold an echoV6Reply, echo data, 8 bytes for ICMP error message
// and ioStatusBlock
replySize := echoV6ReplySize + uintptr(dataSize) + uintptr(icmpv6ErrMessageSize) + ioStatusBlockSize
replyBuf := make([]byte, replySize)
noEvent := nullParameter
noApcRoutine := nullParameter
noAppCtx := nullParameter
noIPHeaderOption := nullParameter
replyCount, _, err := Icmp6SendEcho_proc.Call(
ip.handle,
noEvent,
noApcRoutine,
noAppCtx,
uintptr(unsafe.Pointer(ip.srcSocketAddr)),
uintptr(unsafe.Pointer(dstAddr)),
uintptr(unsafe.Pointer(&echo.Data[0])),
uintptr(dataSize),
noIPHeaderOption,
uintptr(unsafe.Pointer(&replyBuf[0])),
replySize,
icmpRequestTimeoutMs,
)
if replyCount == 0 {
// status is in the 4 bytes after ipv6AddrEx. The reply buffer size is at least size of ipv6AddrEx + 4
if status, parseErr := unmarshalIPStatus(replyBuf[unsafe.Sizeof(ipv6AddrEx{}) : unsafe.Sizeof(ipv6AddrEx{})+4]); parseErr == nil && status != success {
return nil, fmt.Errorf("received ip status: %s", status)
}
return nil, errors.Wrap(err, "did not receive ICMP echo reply")
} else if replyCount > 1 {
ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount)
}
return newEchoV6Resp(replyBuf, dataSize)
}
type echoV6Resp struct {
reply *echoV6Reply
data []byte
}
func (r *echoV6Resp) status() ipStatus {
return r.reply.Status
}
func (r *echoV6Resp) rtt() uint32 {
return r.reply.RoundTripTime
}
func (r *echoV6Resp) payload() []byte {
return r.data
}
func newEchoV6Resp(replyBuf []byte, dataSize int) (*echoV6Resp, error) {
if len(replyBuf) == 0 {
return nil, fmt.Errorf("reply buffer is empty")
}
reply := *(*echoV6Reply)(unsafe.Pointer(&replyBuf[0]))
if reply.Status != success {
return nil, fmt.Errorf("status %d", reply.Status)
}
if uintptr(len(replyBuf)) < unsafe.Sizeof(reply)+uintptr(dataSize) {
return nil, fmt.Errorf("reply buffer size %d is too small to hold reply size %d + data size %d", len(replyBuf), echoV6ReplySize, dataSize)
}
return &echoV6Resp{
reply: &reply,
data: replyBuf[echoV6ReplySize : echoV6ReplySize+uintptr(dataSize)],
}, nil
}
func unmarshalIPStatus(replyBuf []byte) (ipStatus, error) {
if len(replyBuf) != 4 {
return 0, fmt.Errorf("ipStatus needs to be 4 bytes, got %d", len(replyBuf))
}
return ipStatus(endian.Uint32(replyBuf)), nil
}