icmp_windows.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. //go:build windows && cgo
  2. package ingress
  3. /*
  4. #include <iphlpapi.h>
  5. #include <icmpapi.h>
  6. */
  7. import "C"
  8. import (
  9. "context"
  10. "encoding/binary"
  11. "fmt"
  12. "net/netip"
  13. "runtime/debug"
  14. "syscall"
  15. "time"
  16. "unsafe"
  17. "github.com/google/gopacket/layers"
  18. "github.com/pkg/errors"
  19. "github.com/rs/zerolog"
  20. "go.opentelemetry.io/otel/attribute"
  21. "golang.org/x/net/icmp"
  22. "golang.org/x/net/ipv4"
  23. "golang.org/x/net/ipv6"
  24. "github.com/cloudflare/cloudflared/packet"
  25. "github.com/cloudflare/cloudflared/tracing"
  26. )
  27. const (
  28. // Value defined in https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw
  29. AF_INET6 = 23
  30. icmpEchoReplyCode = 0
  31. nullParameter = uintptr(0)
  32. )
  33. var (
  34. Iphlpapi = syscall.NewLazyDLL("Iphlpapi.dll")
  35. IcmpCreateFile_proc = Iphlpapi.NewProc("IcmpCreateFile")
  36. Icmp6CreateFile_proc = Iphlpapi.NewProc("Icmp6CreateFile")
  37. IcmpSendEcho_proc = Iphlpapi.NewProc("IcmpSendEcho")
  38. Icmp6SendEcho_proc = Iphlpapi.NewProc("Icmp6SendEcho2")
  39. echoReplySize = unsafe.Sizeof(echoReply{})
  40. echoV6ReplySize = unsafe.Sizeof(echoV6Reply{})
  41. icmpv6ErrMessageSize = 8
  42. ioStatusBlockSize = unsafe.Sizeof(ioStatusBlock{})
  43. endian = binary.LittleEndian
  44. )
  45. // IP_STATUS code, see https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply32#members
  46. // for possible values
  47. type ipStatus uint32
  48. const (
  49. success ipStatus = 0
  50. bufTooSmall = iota + 11000
  51. destNetUnreachable
  52. destHostUnreachable
  53. destProtocolUnreachable
  54. destPortUnreachable
  55. noResources
  56. badOption
  57. hwError
  58. packetTooBig
  59. reqTimedOut
  60. badReq
  61. badRoute
  62. ttlExpiredTransit
  63. ttlExpiredReassembly
  64. paramProblem
  65. sourceQuench
  66. optionTooBig
  67. badDestination
  68. // Can be returned for malformed ICMP packets
  69. generalFailure = 11050
  70. )
  71. // Additional IP_STATUS codes for ICMPv6 https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmpv6_echo_reply_lh#members
  72. const (
  73. ipv6DestUnreachable ipStatus = iota + 11040
  74. ipv6TimeExceeded
  75. ipv6BadHeader
  76. ipv6UnrecognizedNextHeader
  77. ipv6ICMPError
  78. ipv6DestScopeMismatch
  79. )
  80. func (is ipStatus) String() string {
  81. switch is {
  82. case success:
  83. return "Success"
  84. case bufTooSmall:
  85. return "The reply buffer too small"
  86. case destNetUnreachable:
  87. return "The destination network was unreachable"
  88. case destHostUnreachable:
  89. return "The destination host was unreachable"
  90. case destProtocolUnreachable:
  91. return "The destination protocol was unreachable"
  92. case destPortUnreachable:
  93. return "The destination port was unreachable"
  94. case noResources:
  95. return "Insufficient IP resources were available"
  96. case badOption:
  97. return "A bad IP option was specified"
  98. case hwError:
  99. return "A hardware error occurred"
  100. case packetTooBig:
  101. return "The packet was too big"
  102. case reqTimedOut:
  103. return "The request timed out"
  104. case badReq:
  105. return "Bad request"
  106. case badRoute:
  107. return "Bad route"
  108. case ttlExpiredTransit:
  109. return "The TTL expired in transit"
  110. case ttlExpiredReassembly:
  111. return "The TTL expired during fragment reassembly"
  112. case paramProblem:
  113. return "A parameter problem"
  114. case sourceQuench:
  115. return "Datagrams are arriving too fast to be processed and datagrams may have been discarded"
  116. case optionTooBig:
  117. return "The IP option was too big"
  118. case badDestination:
  119. return "Bad destination"
  120. case ipv6DestUnreachable:
  121. return "IPv6 destination unreachable"
  122. case ipv6TimeExceeded:
  123. return "IPv6 time exceeded"
  124. case ipv6BadHeader:
  125. return "IPv6 bad IP header"
  126. case ipv6UnrecognizedNextHeader:
  127. return "IPv6 unrecognized next header"
  128. case ipv6ICMPError:
  129. return "IPv6 ICMP error"
  130. case ipv6DestScopeMismatch:
  131. return "IPv6 destination scope ID mismatch"
  132. case generalFailure:
  133. return "The ICMP packet might be malformed"
  134. default:
  135. return fmt.Sprintf("Unknown ip status %d", is)
  136. }
  137. }
  138. // https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ip_option_information
  139. type ipOption struct {
  140. TTL uint8
  141. Tos uint8
  142. Flags uint8
  143. OptionsSize uint8
  144. OptionsData uintptr
  145. }
  146. // https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply
  147. type echoReply struct {
  148. Address uint32
  149. Status ipStatus
  150. RoundTripTime uint32
  151. DataSize uint16
  152. Reserved uint16
  153. // The pointer size defers between 32-bit and 64-bit platforms
  154. DataPointer *byte
  155. Options ipOption
  156. }
  157. type echoV6Reply struct {
  158. Address ipv6AddrEx
  159. Status ipStatus
  160. RoundTripTime uint32
  161. }
  162. // https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ipv6_address_ex
  163. // All the fields are in network byte order. The memory alignment is 4 bytes
  164. type ipv6AddrEx struct {
  165. port uint16
  166. // flowInfo is uint32. Because of field alignment, when we cast reply buffer to ipv6AddrEx, it starts at the 5th byte
  167. // But looking at the raw bytes, flowInfo starts at the 3rd byte. We device flowInfo into 2 uint16 so it's aligned
  168. flowInfoUpper uint16
  169. flowInfoLower uint16
  170. addr [8]uint16
  171. scopeID uint32
  172. }
  173. // https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2
  174. type sockAddrIn6 struct {
  175. family int16
  176. // Can't embed ipv6AddrEx, that changes the memory alignment
  177. port uint16
  178. flowInfo uint32
  179. addr [16]byte
  180. scopeID uint32
  181. }
  182. func newSockAddrIn6(addr netip.Addr) (*sockAddrIn6, error) {
  183. if !addr.Is6() {
  184. return nil, fmt.Errorf("%s is not IPv6", addr)
  185. }
  186. return &sockAddrIn6{
  187. family: AF_INET6,
  188. port: 10,
  189. addr: addr.As16(),
  190. }, nil
  191. }
  192. // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_status_block#syntax
  193. type ioStatusBlock struct {
  194. // The first field is an union of NTSTATUS and PVOID. NTSTATUS is int32 while PVOID depends on the platform.
  195. // We model it as uintptr whose size depends on if the platform is 32-bit or 64-bit
  196. // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55
  197. statusOrPointer uintptr
  198. information uintptr
  199. }
  200. type icmpProxy struct {
  201. // An open handle that can send ICMP requests https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpcreatefile
  202. handle uintptr
  203. // This is a ICMPv6 if srcSocketAddr is not nil
  204. srcSocketAddr *sockAddrIn6
  205. logger *zerolog.Logger
  206. }
  207. func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
  208. var (
  209. srcSocketAddr *sockAddrIn6
  210. handle uintptr
  211. err error
  212. )
  213. if listenIP.Is4() {
  214. handle, _, err = IcmpCreateFile_proc.Call()
  215. } else {
  216. srcSocketAddr, err = newSockAddrIn6(listenIP)
  217. if err != nil {
  218. return nil, err
  219. }
  220. handle, _, err = Icmp6CreateFile_proc.Call()
  221. }
  222. // Windows procedure calls always return non-nil error constructed from the result of GetLastError.
  223. // Caller need to inspect the primary returned value
  224. if syscall.Handle(handle) == syscall.InvalidHandle {
  225. return nil, errors.Wrap(err, "invalid ICMP handle")
  226. }
  227. return &icmpProxy{
  228. handle: handle,
  229. srcSocketAddr: srcSocketAddr,
  230. logger: logger,
  231. }, nil
  232. }
  233. func (ip *icmpProxy) Serve(ctx context.Context) error {
  234. <-ctx.Done()
  235. syscall.CloseHandle(syscall.Handle(ip.handle))
  236. return ctx.Err()
  237. }
  238. // Request sends an ICMP echo request and wait for a reply or timeout.
  239. // The async version of Win32 APIs take a callback whose memory is not garbage collected, so we use the synchronous version.
  240. // It's possible that a slow request will block other requests, so we set the timeout to only 1s.
  241. func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error {
  242. defer func() {
  243. if r := recover(); r != nil {
  244. ip.logger.Error().Interface("error", r).Msgf("Recover panic from sending icmp request/response, error %s", debug.Stack())
  245. }
  246. }()
  247. _, requestSpan := responder.RequestSpan(ctx, pk)
  248. defer responder.ExportSpan()
  249. echo, err := getICMPEcho(pk.Message)
  250. if err != nil {
  251. return err
  252. }
  253. observeICMPRequest(ip.logger, requestSpan, pk.Src.String(), pk.Dst.String(), echo.ID, echo.Seq)
  254. resp, err := ip.icmpEchoRoundtrip(pk.Dst, echo)
  255. if err != nil {
  256. ip.logger.Err(err).Msg("ICMP echo roundtrip failed")
  257. tracing.EndWithErrorStatus(requestSpan, err)
  258. return err
  259. }
  260. tracing.End(requestSpan)
  261. responder.ExportSpan()
  262. _, replySpan := responder.ReplySpan(ctx, ip.logger)
  263. err = ip.handleEchoReply(pk, echo, resp, responder)
  264. if err != nil {
  265. ip.logger.Err(err).Msg("Failed to send ICMP reply")
  266. tracing.EndWithErrorStatus(replySpan, err)
  267. return errors.Wrap(err, "failed to handle ICMP echo reply")
  268. }
  269. observeICMPReply(ip.logger, replySpan, pk.Dst.String(), echo.ID, echo.Seq)
  270. replySpan.SetAttributes(
  271. attribute.Int64("rtt", int64(resp.rtt())),
  272. attribute.String("status", resp.status().String()),
  273. )
  274. tracing.End(replySpan)
  275. return nil
  276. }
  277. func (ip *icmpProxy) handleEchoReply(request *packet.ICMP, echoReq *icmp.Echo, resp echoResp, responder ICMPResponder) error {
  278. var replyType icmp.Type
  279. if request.Dst.Is4() {
  280. replyType = ipv4.ICMPTypeEchoReply
  281. } else {
  282. replyType = ipv6.ICMPTypeEchoReply
  283. }
  284. pk := packet.ICMP{
  285. IP: &packet.IP{
  286. Src: request.Dst,
  287. Dst: request.Src,
  288. Protocol: layers.IPProtocol(request.Type.Protocol()),
  289. TTL: packet.DefaultTTL,
  290. },
  291. Message: &icmp.Message{
  292. Type: replyType,
  293. Code: icmpEchoReplyCode,
  294. Body: &icmp.Echo{
  295. ID: echoReq.ID,
  296. Seq: echoReq.Seq,
  297. Data: resp.payload(),
  298. },
  299. },
  300. }
  301. return responder.ReturnPacket(&pk)
  302. }
  303. func (ip *icmpProxy) icmpEchoRoundtrip(dst netip.Addr, echo *icmp.Echo) (echoResp, error) {
  304. if dst.Is6() {
  305. if ip.srcSocketAddr == nil {
  306. return nil, fmt.Errorf("cannot send ICMPv6 using ICMPv4 proxy")
  307. }
  308. resp, err := ip.icmp6SendEcho(dst, echo)
  309. if err != nil {
  310. return nil, errors.Wrap(err, "failed to send/receive ICMPv6 echo")
  311. }
  312. return resp, nil
  313. }
  314. if ip.srcSocketAddr != nil {
  315. return nil, fmt.Errorf("cannot send ICMPv4 using ICMPv6 proxy")
  316. }
  317. resp, err := ip.icmpSendEcho(dst, echo)
  318. if err != nil {
  319. return nil, errors.Wrap(err, "failed to send/receive ICMPv4 echo")
  320. }
  321. return resp, nil
  322. }
  323. /*
  324. Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho
  325. Parameters:
  326. - IcmpHandle: Handle created by IcmpCreateFile
  327. - DestinationAddress: IPv4 in the form of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax
  328. - RequestData: A pointer to echo data
  329. - RequestSize: Number of bytes in buffer pointed by echo data
  330. - RequestOptions: IP header options
  331. - ReplyBuffer: A pointer to the buffer for echoReply, options and data
  332. - ReplySize: Number of bytes allocated for ReplyBuffer
  333. - Timeout: Timeout in milliseconds to wait for a reply
  334. Returns:
  335. - the number of replies in uint32 https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho#return-value
  336. To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the
  337. syscall function
  338. */
  339. func (ip *icmpProxy) icmpSendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV4Resp, error) {
  340. dataSize := len(echo.Data)
  341. replySize := echoReplySize + uintptr(dataSize)
  342. replyBuf := make([]byte, replySize)
  343. noIPHeaderOption := nullParameter
  344. inAddr, err := inAddrV4(dst)
  345. if err != nil {
  346. return nil, err
  347. }
  348. replyCount, _, err := IcmpSendEcho_proc.Call(
  349. ip.handle,
  350. uintptr(inAddr),
  351. uintptr(unsafe.Pointer(&echo.Data[0])),
  352. uintptr(dataSize),
  353. noIPHeaderOption,
  354. uintptr(unsafe.Pointer(&replyBuf[0])),
  355. replySize,
  356. icmpRequestTimeoutMs,
  357. )
  358. if replyCount == 0 {
  359. // status is returned in 5th to 8th byte of reply buffer
  360. if status, parseErr := unmarshalIPStatus(replyBuf[4:8]); parseErr == nil && status != success {
  361. return nil, errors.Wrapf(err, "received ip status: %s", status)
  362. }
  363. return nil, errors.Wrap(err, "did not receive ICMP echo reply")
  364. } else if replyCount > 1 {
  365. ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount)
  366. }
  367. return newEchoV4Resp(replyBuf)
  368. }
  369. // Third definition of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax is address in uint32
  370. func inAddrV4(ip netip.Addr) (uint32, error) {
  371. if !ip.Is4() {
  372. return 0, fmt.Errorf("%s is not IPv4", ip)
  373. }
  374. v4 := ip.As4()
  375. return endian.Uint32(v4[:]), nil
  376. }
  377. type echoResp interface {
  378. status() ipStatus
  379. rtt() uint32
  380. payload() []byte
  381. }
  382. type echoV4Resp struct {
  383. reply *echoReply
  384. data []byte
  385. }
  386. func (r *echoV4Resp) status() ipStatus {
  387. return r.reply.Status
  388. }
  389. func (r *echoV4Resp) rtt() uint32 {
  390. return r.reply.RoundTripTime
  391. }
  392. func (r *echoV4Resp) payload() []byte {
  393. return r.data
  394. }
  395. func newEchoV4Resp(replyBuf []byte) (*echoV4Resp, error) {
  396. if len(replyBuf) == 0 {
  397. return nil, fmt.Errorf("reply buffer is empty")
  398. }
  399. // This is pattern 1 of https://pkg.go.dev/unsafe@master#Pointer, conversion of *replyBuf to *echoReply
  400. // replyBuf size is larger than echoReply
  401. reply := *(*echoReply)(unsafe.Pointer(&replyBuf[0]))
  402. if reply.Status != success {
  403. return nil, fmt.Errorf("status %d", reply.Status)
  404. }
  405. dataBufStart := len(replyBuf) - int(reply.DataSize)
  406. if dataBufStart < int(echoReplySize) {
  407. return nil, fmt.Errorf("reply buffer size %d is too small to hold data of size %d", len(replyBuf), int(reply.DataSize))
  408. }
  409. return &echoV4Resp{
  410. reply: &reply,
  411. data: replyBuf[dataBufStart:],
  412. }, nil
  413. }
  414. /*
  415. Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmp6sendecho2
  416. Parameters:
  417. - IcmpHandle: Handle created by Icmp6CreateFile
  418. - Event (optional): Event object to be signaled when a reply arrives
  419. - ApcRoutine (optional): Routine to call when the calling thread is in an alertable thread and a reply arrives
  420. - ApcContext (optional): Optional parameter to ApcRoutine
  421. - SourceAddress: Source address of the request
  422. - DestinationAddress: Destination address of the request
  423. - RequestData: A pointer to echo data
  424. - RequestSize: Number of bytes in buffer pointed by echo data
  425. - RequestOptions (optional): A pointer to the IPv6 header options
  426. - ReplyBuffer: A pointer to the buffer for echoReply, options and data
  427. - ReplySize: Number of bytes allocated for ReplyBuffer
  428. - Timeout: Timeout in milliseconds to wait for a reply
  429. Returns:
  430. - the number of replies in uint32
  431. To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the
  432. syscall function
  433. */
  434. func (ip *icmpProxy) icmp6SendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV6Resp, error) {
  435. dstAddr, err := newSockAddrIn6(dst)
  436. if err != nil {
  437. return nil, err
  438. }
  439. dataSize := len(echo.Data)
  440. // Reply buffer needs to be big enough to hold an echoV6Reply, echo data, 8 bytes for ICMP error message
  441. // and ioStatusBlock
  442. replySize := echoV6ReplySize + uintptr(dataSize) + uintptr(icmpv6ErrMessageSize) + ioStatusBlockSize
  443. replyBuf := make([]byte, replySize)
  444. noEvent := nullParameter
  445. noApcRoutine := nullParameter
  446. noAppCtx := nullParameter
  447. noIPHeaderOption := nullParameter
  448. replyCount, _, err := Icmp6SendEcho_proc.Call(
  449. ip.handle,
  450. noEvent,
  451. noApcRoutine,
  452. noAppCtx,
  453. uintptr(unsafe.Pointer(ip.srcSocketAddr)),
  454. uintptr(unsafe.Pointer(dstAddr)),
  455. uintptr(unsafe.Pointer(&echo.Data[0])),
  456. uintptr(dataSize),
  457. noIPHeaderOption,
  458. uintptr(unsafe.Pointer(&replyBuf[0])),
  459. replySize,
  460. icmpRequestTimeoutMs,
  461. )
  462. if replyCount == 0 {
  463. // status is in the 4 bytes after ipv6AddrEx. The reply buffer size is at least size of ipv6AddrEx + 4
  464. if status, parseErr := unmarshalIPStatus(replyBuf[unsafe.Sizeof(ipv6AddrEx{}) : unsafe.Sizeof(ipv6AddrEx{})+4]); parseErr == nil && status != success {
  465. return nil, fmt.Errorf("received ip status: %s", status)
  466. }
  467. return nil, errors.Wrap(err, "did not receive ICMP echo reply")
  468. } else if replyCount > 1 {
  469. ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount)
  470. }
  471. return newEchoV6Resp(replyBuf, dataSize)
  472. }
  473. type echoV6Resp struct {
  474. reply *echoV6Reply
  475. data []byte
  476. }
  477. func (r *echoV6Resp) status() ipStatus {
  478. return r.reply.Status
  479. }
  480. func (r *echoV6Resp) rtt() uint32 {
  481. return r.reply.RoundTripTime
  482. }
  483. func (r *echoV6Resp) payload() []byte {
  484. return r.data
  485. }
  486. func newEchoV6Resp(replyBuf []byte, dataSize int) (*echoV6Resp, error) {
  487. if len(replyBuf) == 0 {
  488. return nil, fmt.Errorf("reply buffer is empty")
  489. }
  490. reply := *(*echoV6Reply)(unsafe.Pointer(&replyBuf[0]))
  491. if reply.Status != success {
  492. return nil, fmt.Errorf("status %d", reply.Status)
  493. }
  494. if uintptr(len(replyBuf)) < unsafe.Sizeof(reply)+uintptr(dataSize) {
  495. return nil, fmt.Errorf("reply buffer size %d is too small to hold reply size %d + data size %d", len(replyBuf), echoV6ReplySize, dataSize)
  496. }
  497. return &echoV6Resp{
  498. reply: &reply,
  499. data: replyBuf[echoV6ReplySize : echoV6ReplySize+uintptr(dataSize)],
  500. }, nil
  501. }
  502. func unmarshalIPStatus(replyBuf []byte) (ipStatus, error) {
  503. if len(replyBuf) != 4 {
  504. return 0, fmt.Errorf("ipStatus needs to be 4 bytes, got %d", len(replyBuf))
  505. }
  506. return ipStatus(endian.Uint32(replyBuf)), nil
  507. }