Skip to content

Commit 30c529e

Browse files
committed
TUN-6743: Support ICMPv6 echo on Windows
1 parent bf3d70d commit 30c529e

File tree

3 files changed

+330
-36
lines changed

3 files changed

+330
-36
lines changed

ingress/icmp_windows.go

Lines changed: 233 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,23 @@ import (
2929
)
3030

3131
const (
32+
// Value defined in https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw
33+
AF_INET6 = 23
3234
icmpEchoReplyCode = 0
35+
nullParameter = uintptr(0)
3336
)
3437

3538
var (
36-
Iphlpapi = syscall.NewLazyDLL("Iphlpapi.dll")
37-
IcmpCreateFile_proc = Iphlpapi.NewProc("IcmpCreateFile")
38-
IcmpSendEcho_proc = Iphlpapi.NewProc("IcmpSendEcho")
39-
echoReplySize = unsafe.Sizeof(echoReply{})
40-
endian = binary.LittleEndian
39+
Iphlpapi = syscall.NewLazyDLL("Iphlpapi.dll")
40+
IcmpCreateFile_proc = Iphlpapi.NewProc("IcmpCreateFile")
41+
Icmp6CreateFile_proc = Iphlpapi.NewProc("Icmp6CreateFile")
42+
IcmpSendEcho_proc = Iphlpapi.NewProc("IcmpSendEcho")
43+
Icmp6SendEcho_proc = Iphlpapi.NewProc("Icmp6SendEcho2")
44+
echoReplySize = unsafe.Sizeof(echoReply{})
45+
echoV6ReplySize = unsafe.Sizeof(echoV6Reply{})
46+
icmpv6ErrMessageSize = 8
47+
ioStatusBlockSize = unsafe.Sizeof(ioStatusBlock{})
48+
endian = binary.LittleEndian
4149
)
4250

4351
// IP_STATUS code, see https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply32#members
@@ -68,6 +76,16 @@ const (
6876
generalFailure = 11050
6977
)
7078

79+
// Additional IP_STATUS codes for ICMPv6 https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmpv6_echo_reply_lh#members
80+
const (
81+
ipv6DestUnreachable ipStatus = iota + 11040
82+
ipv6TimeExceeded
83+
ipv6BadHeader
84+
ipv6UnrecognizedNextHeader
85+
ipv6ICMPError
86+
ipv6DestScopeMismatch
87+
)
88+
7189
func (is ipStatus) String() string {
7290
switch is {
7391
case success:
@@ -108,6 +126,18 @@ func (is ipStatus) String() string {
108126
return "The IP option was too big"
109127
case badDestination:
110128
return "Bad destination"
129+
case ipv6DestUnreachable:
130+
return "IPv6 destination unreachable"
131+
case ipv6TimeExceeded:
132+
return "IPv6 time exceeded"
133+
case ipv6BadHeader:
134+
return "IPv6 bad IP header"
135+
case ipv6UnrecognizedNextHeader:
136+
return "IPv6 unrecognized next header"
137+
case ipv6ICMPError:
138+
return "IPv6 ICMP error"
139+
case ipv6DestScopeMismatch:
140+
return "IPv6 destination scope ID mismatch"
111141
case generalFailure:
112142
return "The ICMP packet might be malformed"
113143
default:
@@ -136,27 +166,88 @@ type echoReply struct {
136166
Options ipOption
137167
}
138168

169+
type echoV6Reply struct {
170+
Address ipv6AddrEx
171+
Status ipStatus
172+
RoundTripTime uint32
173+
}
174+
175+
// https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ipv6_address_ex
176+
// All the fields are in network byte order. The memory alignment is 4 bytes
177+
type ipv6AddrEx struct {
178+
port uint16
179+
// flowInfo is uint32. Because of field alignment, when we cast reply buffer to ipv6AddrEx, it starts at the 5th byte
180+
// But looking at the raw bytes, flowInfo starts at the 3rd byte. We device flowInfo into 2 uint16 so it's aligned
181+
flowInfoUpper uint16
182+
flowInfoLower uint16
183+
addr [8]uint16
184+
scopeID uint32
185+
}
186+
187+
// https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2
188+
type sockAddrIn6 struct {
189+
family int16
190+
// Can't embed ipv6AddrEx, that changes the memory alignment
191+
port uint16
192+
flowInfo uint32
193+
addr [16]byte
194+
scopeID uint32
195+
}
196+
197+
func newSockAddrIn6(addr netip.Addr) (*sockAddrIn6, error) {
198+
if !addr.Is6() {
199+
return nil, fmt.Errorf("%s is not IPv6", addr)
200+
}
201+
return &sockAddrIn6{
202+
family: AF_INET6,
203+
port: 10,
204+
addr: addr.As16(),
205+
}, nil
206+
}
207+
208+
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_status_block#syntax
209+
type ioStatusBlock struct {
210+
// The first field is an union of NTSTATUS and PVOID. NTSTATUS is int32 while PVOID depends on the platform.
211+
// We model it as uintptr whose size depends on if the platform is 32-bit or 64-bit
212+
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55
213+
statusOrPointer uintptr
214+
information uintptr
215+
}
216+
139217
type icmpProxy struct {
140218
// An open handle that can send ICMP requests https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpcreatefile
141219
handle uintptr
142-
logger *zerolog.Logger
220+
// This is a ICMPv6 if srcSocketAddr is not nil
221+
srcSocketAddr *sockAddrIn6
222+
logger *zerolog.Logger
143223
// A pool of reusable *packet.Encoder
144224
encoderPool sync.Pool
145225
}
146226

147227
func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (ICMPProxy, error) {
148-
if listenIP.Is6() {
149-
return nil, fmt.Errorf("ICMPv6 not implemented for Windows")
228+
var (
229+
srcSocketAddr *sockAddrIn6
230+
handle uintptr
231+
err error
232+
)
233+
if listenIP.Is4() {
234+
handle, _, err = IcmpCreateFile_proc.Call()
235+
} else {
236+
srcSocketAddr, err = newSockAddrIn6(listenIP)
237+
if err != nil {
238+
return nil, err
239+
}
240+
handle, _, err = Icmp6CreateFile_proc.Call()
150241
}
151-
handle, _, err := IcmpCreateFile_proc.Call()
152242
// Windows procedure calls always return non-nil error constructed from the result of GetLastError.
153243
// Caller need to inspect the primary returned value
154244
if syscall.Handle(handle) == syscall.InvalidHandle {
155245
return nil, errors.Wrap(err, "invalid ICMP handle")
156246
}
157247
return &icmpProxy{
158-
handle: handle,
159-
logger: logger,
248+
handle: handle,
249+
srcSocketAddr: srcSocketAddr,
250+
logger: logger,
160251
encoderPool: sync.Pool{
161252
New: func() any {
162253
return packet.NewEncoder()
@@ -180,24 +271,25 @@ func (ip *icmpProxy) Request(pk *packet.ICMP, responder packet.FunnelUniPipe) er
180271
ip.logger.Error().Interface("error", r).Msgf("Recover panic from sending icmp request/response, error %s", debug.Stack())
181272
}
182273
}()
274+
183275
echo, err := getICMPEcho(pk.Message)
184276
if err != nil {
185277
return err
186278
}
187-
188-
resp, err := ip.icmpSendEcho(pk.Dst, echo)
279+
respData, err := ip.icmpEchoRoundtrip(pk.Dst, echo)
189280
if err != nil {
190-
return errors.Wrap(err, "failed to send/receive ICMP echo")
281+
ip.logger.Err(err).Msg("ICMP echo roundtrip failed")
282+
return err
191283
}
192284

193-
err = ip.handleEchoResponse(pk, echo, resp, responder)
285+
err = ip.handleEchoReply(pk, echo, respData, responder)
194286
if err != nil {
195287
return errors.Wrap(err, "failed to handle ICMP echo reply")
196288
}
197289
return nil
198290
}
199291

200-
func (ip *icmpProxy) handleEchoResponse(request *packet.ICMP, echoReq *icmp.Echo, resp *echoResp, responder packet.FunnelUniPipe) error {
292+
func (ip *icmpProxy) handleEchoReply(request *packet.ICMP, echoReq *icmp.Echo, data []byte, responder packet.FunnelUniPipe) error {
201293
var replyType icmp.Type
202294
if request.Dst.Is4() {
203295
replyType = ipv4.ICMPTypeEchoReply
@@ -217,7 +309,7 @@ func (ip *icmpProxy) handleEchoResponse(request *packet.ICMP, echoReq *icmp.Echo
217309
Body: &icmp.Echo{
218310
ID: echoReq.ID,
219311
Seq: echoReq.Seq,
220-
Data: resp.data,
312+
Data: data,
221313
},
222314
},
223315
}
@@ -239,10 +331,31 @@ func (ip *icmpProxy) encodeICMPReply(pk *packet.ICMP) (packet.RawPacket, error)
239331
return encoder.Encode(pk)
240332
}
241333

334+
func (ip *icmpProxy) icmpEchoRoundtrip(dst netip.Addr, echo *icmp.Echo) ([]byte, error) {
335+
if dst.Is6() {
336+
if ip.srcSocketAddr == nil {
337+
return nil, fmt.Errorf("cannot send ICMPv6 using ICMPv4 proxy")
338+
}
339+
resp, err := ip.icmp6SendEcho(dst, echo)
340+
if err != nil {
341+
return nil, errors.Wrap(err, "failed to send/receive ICMPv6 echo")
342+
}
343+
return resp.data, nil
344+
}
345+
if ip.srcSocketAddr != nil {
346+
return nil, fmt.Errorf("cannot send ICMPv4 using ICMPv6 proxy")
347+
}
348+
resp, err := ip.icmpSendEcho(dst, echo)
349+
if err != nil {
350+
return nil, errors.Wrap(err, "failed to send/receive ICMPv4 echo")
351+
}
352+
return resp.data, nil
353+
}
354+
242355
/*
243356
Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho
244357
Parameters:
245-
- IcmpHandle:
358+
- IcmpHandle: Handle created by IcmpCreateFile
246359
- DestinationAddress: IPv4 in the form of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax
247360
- RequestData: A pointer to echo data
248361
- RequestSize: Number of bytes in buffer pointed by echo data
@@ -259,18 +372,25 @@ func (ip *icmpProxy) icmpSendEcho(dst netip.Addr, echo *icmp.Echo) (*echoResp, e
259372
dataSize := len(echo.Data)
260373
replySize := echoReplySize + uintptr(dataSize)
261374
replyBuf := make([]byte, replySize)
262-
noIPHeaderOption := uintptr(0)
375+
noIPHeaderOption := nullParameter
263376
inAddr, err := inAddrV4(dst)
264377
if err != nil {
265378
return nil, err
266379
}
267-
replyCount, _, err := IcmpSendEcho_proc.Call(ip.handle, uintptr(inAddr), uintptr(unsafe.Pointer(&echo.Data[0])),
268-
uintptr(dataSize), noIPHeaderOption, uintptr(unsafe.Pointer(&replyBuf[0])),
269-
replySize, icmpRequestTimeoutMs)
380+
replyCount, _, err := IcmpSendEcho_proc.Call(
381+
ip.handle,
382+
uintptr(inAddr),
383+
uintptr(unsafe.Pointer(&echo.Data[0])),
384+
uintptr(dataSize),
385+
noIPHeaderOption,
386+
uintptr(unsafe.Pointer(&replyBuf[0])),
387+
replySize,
388+
icmpRequestTimeoutMs,
389+
)
270390
if replyCount == 0 {
271391
// status is returned in 5th to 8th byte of reply buffer
272-
if status, err := unmarshalIPStatus(replyBuf[4:8]); err == nil {
273-
return nil, fmt.Errorf("received ip status: %s", status)
392+
if status, parseErr := unmarshalIPStatus(replyBuf[4:8]); parseErr == nil && status != success {
393+
return nil, errors.Wrapf(err, "received ip status: %s", status)
274394
}
275395
return nil, errors.Wrap(err, "did not receive ICMP echo reply")
276396
} else if replyCount > 1 {
@@ -279,6 +399,15 @@ func (ip *icmpProxy) icmpSendEcho(dst netip.Addr, echo *icmp.Echo) (*echoResp, e
279399
return newEchoResp(replyBuf)
280400
}
281401

402+
// Third definition of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax is address in uint32
403+
func inAddrV4(ip netip.Addr) (uint32, error) {
404+
if !ip.Is4() {
405+
return 0, fmt.Errorf("%s is not IPv4", ip)
406+
}
407+
v4 := ip.As4()
408+
return endian.Uint32(v4[:]), nil
409+
}
410+
282411
type echoResp struct {
283412
reply *echoReply
284413
data []byte
@@ -304,13 +433,87 @@ func newEchoResp(replyBuf []byte) (*echoResp, error) {
304433
}, nil
305434
}
306435

307-
// Third definition of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax is address in uint32
308-
func inAddrV4(ip netip.Addr) (uint32, error) {
309-
if !ip.Is4() {
310-
return 0, fmt.Errorf("%s is not IPv4", ip)
436+
/*
437+
Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmp6sendecho2
438+
Parameters:
439+
- IcmpHandle: Handle created by Icmp6CreateFile
440+
- Event (optional): Event object to be signaled when a reply arrives
441+
- ApcRoutine (optional): Routine to call when the calling thread is in an alertable thread and a reply arrives
442+
- ApcContext (optional): Optional parameter to ApcRoutine
443+
- SourceAddress: Source address of the request
444+
- DestinationAddress: Destination address of the request
445+
- RequestData: A pointer to echo data
446+
- RequestSize: Number of bytes in buffer pointed by echo data
447+
- RequestOptions (optional): A pointer to the IPv6 header options
448+
- ReplyBuffer: A pointer to the buffer for echoReply, options and data
449+
- ReplySize: Number of bytes allocated for ReplyBuffer
450+
- Timeout: Timeout in milliseconds to wait for a reply
451+
Returns:
452+
- the number of replies in uint32
453+
To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the
454+
syscall function
455+
*/
456+
457+
func (ip *icmpProxy) icmp6SendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV6Resp, error) {
458+
dstAddr, err := newSockAddrIn6(dst)
459+
if err != nil {
460+
return nil, err
311461
}
312-
v4 := ip.As4()
313-
return endian.Uint32(v4[:]), nil
462+
dataSize := len(echo.Data)
463+
// Reply buffer needs to be big enough to hold an echoV6Reply, echo data, 8 bytes for ICMP error message
464+
// and ioStatusBlock
465+
replySize := echoV6ReplySize + uintptr(dataSize) + uintptr(icmpv6ErrMessageSize) + ioStatusBlockSize
466+
replyBuf := make([]byte, replySize)
467+
noEvent := nullParameter
468+
noApcRoutine := nullParameter
469+
noAppCtx := nullParameter
470+
noIPHeaderOption := nullParameter
471+
replyCount, _, err := Icmp6SendEcho_proc.Call(
472+
ip.handle,
473+
noEvent,
474+
noApcRoutine,
475+
noAppCtx,
476+
uintptr(unsafe.Pointer(ip.srcSocketAddr)),
477+
uintptr(unsafe.Pointer(dstAddr)),
478+
uintptr(unsafe.Pointer(&echo.Data[0])),
479+
uintptr(dataSize),
480+
noIPHeaderOption,
481+
uintptr(unsafe.Pointer(&replyBuf[0])),
482+
replySize,
483+
icmpRequestTimeoutMs,
484+
)
485+
if replyCount == 0 {
486+
// status is in the 4 bytes after ipv6AddrEx. The reply buffer size is at least size of ipv6AddrEx + 4
487+
if status, parseErr := unmarshalIPStatus(replyBuf[unsafe.Sizeof(ipv6AddrEx{}) : unsafe.Sizeof(ipv6AddrEx{})+4]); parseErr == nil && status != success {
488+
return nil, fmt.Errorf("received ip status: %s", status)
489+
}
490+
return nil, errors.Wrap(err, "did not receive ICMP echo reply")
491+
} else if replyCount > 1 {
492+
ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount)
493+
}
494+
return newEchoV6Resp(replyBuf, dataSize)
495+
}
496+
497+
type echoV6Resp struct {
498+
reply *echoV6Reply
499+
data []byte
500+
}
501+
502+
func newEchoV6Resp(replyBuf []byte, dataSize int) (*echoV6Resp, error) {
503+
if len(replyBuf) == 0 {
504+
return nil, fmt.Errorf("reply buffer is empty")
505+
}
506+
reply := *(*echoV6Reply)(unsafe.Pointer(&replyBuf[0]))
507+
if reply.Status != success {
508+
return nil, fmt.Errorf("status %d", reply.Status)
509+
}
510+
if uintptr(len(replyBuf)) < unsafe.Sizeof(reply)+uintptr(dataSize) {
511+
return nil, fmt.Errorf("reply buffer size %d is too small to hold reply size %d + data size %d", len(replyBuf), echoV6ReplySize, dataSize)
512+
}
513+
return &echoV6Resp{
514+
reply: &reply,
515+
data: replyBuf[echoV6ReplySize : echoV6ReplySize+uintptr(dataSize)],
516+
}, nil
314517
}
315518

316519
func unmarshalIPStatus(replyBuf []byte) (ipStatus, error) {

0 commit comments

Comments
 (0)