@@ -29,15 +29,23 @@ import (
2929)
3030
3131const (
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
3538var (
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+
7189func (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+
139217type 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
147227func 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+
282411type 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
316519func unmarshalIPStatus (replyBuf []byte ) (ipStatus , error ) {
0 commit comments