mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-11 09:46:35 +00:00

Remove send and return methods from Funnel interface. Users of Funnel can provide their own send and return methods without wrapper to comply with the interface. Move packet router to ingress package to avoid circular dependency
528 lines
16 KiB
Go
528 lines
16 KiB
Go
//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"
|
|
"golang.org/x/net/icmp"
|
|
"golang.org/x/net/ipv4"
|
|
"golang.org/x/net/ipv6"
|
|
|
|
"github.com/cloudflare/cloudflared/packet"
|
|
)
|
|
|
|
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 {
|
|
if pk == nil {
|
|
return errPacketNil
|
|
}
|
|
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())
|
|
}
|
|
}()
|
|
|
|
echo, err := getICMPEcho(pk.Message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
respData, err := ip.icmpEchoRoundtrip(pk.Dst, echo)
|
|
if err != nil {
|
|
ip.logger.Err(err).Msg("ICMP echo roundtrip failed")
|
|
return err
|
|
}
|
|
|
|
err = ip.handleEchoReply(pk, echo, respData, responder)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to handle ICMP echo reply")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ip *icmpProxy) handleEchoReply(request *packet.ICMP, echoReq *icmp.Echo, data []byte, 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: data,
|
|
},
|
|
},
|
|
}
|
|
|
|
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) ([]byte, 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.data, 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.data, 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) (*echoResp, 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 newEchoResp(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 struct {
|
|
reply *echoReply
|
|
data []byte
|
|
}
|
|
|
|
func newEchoResp(replyBuf []byte) (*echoResp, 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 &echoResp{
|
|
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 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
|
|
}
|