icmp_windows.go 17 KB

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