Skip to content

Commit bf3d70d

Browse files
committed
TUN-6654: Support ICMPv6 on Linux and Darwin
1 parent a65f8bc commit bf3d70d

File tree

7 files changed

+232
-68
lines changed

7 files changed

+232
-68
lines changed

ingress/icmp_darwin.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"github.com/cloudflare/cloudflared/packet"
2424
)
2525

26-
// TODO: TUN-6654 Extend support to IPv6
2726
type icmpProxy struct {
2827
srcFunnelTracker *packet.FunnelTracker
2928
echoIDTracker *echoIDTracker
@@ -180,25 +179,56 @@ func (ip *icmpProxy) Serve(ctx context.Context) error {
180179
ip.srcFunnelTracker.ScheduleCleanup(ctx, ip.idleTimeout)
181180
}()
182181
buf := make([]byte, mtu)
182+
icmpDecoder := packet.NewICMPDecoder()
183183
for {
184-
n, src, err := ip.conn.ReadFrom(buf)
184+
n, from, err := ip.conn.ReadFrom(buf)
185185
if err != nil {
186186
return err
187187
}
188-
if err := ip.handleResponse(src, buf[:n]); err != nil {
189-
ip.logger.Err(err).Str("src", src.String()).Msg("Failed to handle ICMP response")
188+
reply, err := parseReply(from, buf[:n])
189+
if err != nil {
190+
ip.logger.Debug().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply, continue to parse as full packet")
191+
// In unit test, we found out when the listener listens on 0.0.0.0, the socket reads the full packet after
192+
// the second reply
193+
if err := ip.handleFullPacket(icmpDecoder, buf[:n]); err != nil {
194+
ip.logger.Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply as full packet")
195+
}
196+
continue
197+
}
198+
if !isEchoReply(reply.msg) {
199+
ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type)
200+
continue
201+
}
202+
if ip.sendReply(reply); err != nil {
203+
ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply")
190204
continue
191205
}
192206
}
193207
}
194208

195-
func (ip *icmpProxy) handleResponse(from net.Addr, rawMsg []byte) error {
196-
reply, err := parseReply(from, rawMsg)
209+
func (ip *icmpProxy) handleFullPacket(decoder *packet.ICMPDecoder, rawPacket []byte) error {
210+
icmpPacket, err := decoder.Decode(packet.RawPacket{Data: rawPacket})
197211
if err != nil {
198212
return err
199213
}
200-
funnel, exists := ip.srcFunnelTracker.Get(echoFunnelID(reply.echo.ID))
201-
if !exists {
214+
echo, err := getICMPEcho(icmpPacket.Message)
215+
if err != nil {
216+
return err
217+
}
218+
reply := echoReply{
219+
from: icmpPacket.Src,
220+
msg: icmpPacket.Message,
221+
echo: echo,
222+
}
223+
if ip.sendReply(&reply); err != nil {
224+
return err
225+
}
226+
return nil
227+
}
228+
229+
func (ip *icmpProxy) sendReply(reply *echoReply) error {
230+
funnel, ok := ip.srcFunnelTracker.Get(echoFunnelID(reply.echo.ID))
231+
if !ok {
202232
return packet.ErrFunnelNotFound
203233
}
204234
icmpFlow, err := toICMPEchoFlow(funnel)

ingress/icmp_linux.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,26 +110,26 @@ func (ip *icmpProxy) Serve(ctx context.Context) error {
110110
func (ip *icmpProxy) listenResponse(flow *icmpEchoFlow, conn *icmp.PacketConn) error {
111111
buf := make([]byte, mtu)
112112
for {
113-
n, src, err := conn.ReadFrom(buf)
113+
n, from, err := conn.ReadFrom(buf)
114114
if err != nil {
115115
return err
116116
}
117-
118-
if err := ip.handleResponse(flow, src, buf[:n]); err != nil {
119-
ip.logger.Err(err).Str("dst", src.String()).Msg("Failed to handle ICMP response")
117+
reply, err := parseReply(from, buf[:n])
118+
if err != nil {
119+
ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply")
120+
continue
121+
}
122+
if !isEchoReply(reply.msg) {
123+
ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type)
124+
continue
125+
}
126+
if err := flow.returnToSrc(reply); err != nil {
127+
ip.logger.Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply")
120128
continue
121129
}
122130
}
123131
}
124132

125-
func (ip *icmpProxy) handleResponse(flow *icmpEchoFlow, from net.Addr, rawMsg []byte) error {
126-
reply, err := parseReply(from, rawMsg)
127-
if err != nil {
128-
return err
129-
}
130-
return flow.returnToSrc(reply)
131-
}
132-
133133
// originSender wraps icmp.PacketConn to implement packet.FunnelUniPipe interface
134134
type originSender struct {
135135
conn *icmp.PacketConn

ingress/icmp_posix.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,22 @@ type echoReply struct {
113113
}
114114

115115
func parseReply(from net.Addr, rawMsg []byte) (*echoReply, error) {
116-
// TODO: TUN-6654 Check for IPv6
117-
msg, err := icmp.ParseMessage(int(layers.IPProtocolICMPv4), rawMsg)
116+
fromAddr, ok := netipAddr(from)
117+
if !ok {
118+
return nil, fmt.Errorf("cannot convert %s to netip.Addr", from)
119+
}
120+
proto := layers.IPProtocolICMPv4
121+
if fromAddr.Is6() {
122+
proto = layers.IPProtocolICMPv6
123+
}
124+
msg, err := icmp.ParseMessage(int(proto), rawMsg)
118125
if err != nil {
119126
return nil, err
120127
}
121128
echo, err := getICMPEcho(msg)
122129
if err != nil {
123130
return nil, err
124131
}
125-
fromAddr, ok := netipAddr(from)
126-
if !ok {
127-
return nil, fmt.Errorf("cannot convert %s to netip.Addr", from)
128-
}
129132
return &echoReply{
130133
from: fromAddr,
131134
msg: msg,

ingress/icmp_windows.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ type icmpProxy struct {
145145
}
146146

147147
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")
150+
}
148151
handle, _, err := IcmpCreateFile_proc.Call()
149152
// Windows procedure calls always return non-nil error constructed from the result of GetLastError.
150153
// Caller need to inspect the primary returned value

ingress/origin_icmp_proxy.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88

99
"github.com/rs/zerolog"
1010
"golang.org/x/net/icmp"
11+
"golang.org/x/net/ipv4"
12+
"golang.org/x/net/ipv6"
1113

1214
"github.com/cloudflare/cloudflared/packet"
1315
)
@@ -32,8 +34,65 @@ type ICMPProxy interface {
3234
Request(pk *packet.ICMP, responder packet.FunnelUniPipe) error
3335
}
3436

35-
func NewICMPProxy(listenIP netip.Addr, logger *zerolog.Logger) (ICMPProxy, error) {
36-
return newICMPProxy(listenIP, logger, funnelIdleTimeout)
37+
type icmpRouter struct {
38+
ipv4Proxy ICMPProxy
39+
ipv6Proxy ICMPProxy
40+
}
41+
42+
// NewICMPProxy doesn't return an error if either ipv4 proxy or ipv6 proxy can be created. The machine might only
43+
// support one of them
44+
func NewICMPProxy(logger *zerolog.Logger) (ICMPProxy, error) {
45+
// TODO: TUN-6741: don't bind to all interface
46+
ipv4Proxy, ipv4Err := newICMPProxy(netip.IPv4Unspecified(), logger, funnelIdleTimeout)
47+
ipv6Proxy, ipv6Err := newICMPProxy(netip.IPv6Unspecified(), logger, funnelIdleTimeout)
48+
if ipv4Err != nil && ipv6Err != nil {
49+
return nil, fmt.Errorf("cannot create ICMPv4 proxy: %v nor ICMPv6 proxy: %v", ipv4Err, ipv6Err)
50+
}
51+
if ipv4Err != nil {
52+
logger.Warn().Err(ipv4Err).Msg("failed to create ICMPv4 proxy, only ICMPv6 proxy is created")
53+
ipv4Proxy = nil
54+
}
55+
if ipv6Err != nil {
56+
logger.Warn().Err(ipv6Err).Msg("failed to create ICMPv6 proxy, only ICMPv4 proxy is created")
57+
ipv6Proxy = nil
58+
}
59+
return &icmpRouter{
60+
ipv4Proxy: ipv4Proxy,
61+
ipv6Proxy: ipv6Proxy,
62+
}, nil
63+
}
64+
65+
func (ir *icmpRouter) Serve(ctx context.Context) error {
66+
if ir.ipv4Proxy != nil && ir.ipv6Proxy != nil {
67+
errC := make(chan error, 2)
68+
go func() {
69+
errC <- ir.ipv4Proxy.Serve(ctx)
70+
}()
71+
go func() {
72+
errC <- ir.ipv6Proxy.Serve(ctx)
73+
}()
74+
return <-errC
75+
}
76+
if ir.ipv4Proxy != nil {
77+
return ir.ipv4Proxy.Serve(ctx)
78+
}
79+
if ir.ipv6Proxy != nil {
80+
return ir.ipv6Proxy.Serve(ctx)
81+
}
82+
return fmt.Errorf("ICMPv4 proxy and ICMPv6 proxy are both nil")
83+
}
84+
85+
func (ir *icmpRouter) Request(pk *packet.ICMP, responder packet.FunnelUniPipe) error {
86+
if pk.Dst.Is4() {
87+
if ir.ipv4Proxy != nil {
88+
return ir.ipv4Proxy.Request(pk, responder)
89+
}
90+
return fmt.Errorf("ICMPv4 proxy was not instantiated")
91+
}
92+
if ir.ipv6Proxy != nil {
93+
return ir.ipv6Proxy.Request(pk, responder)
94+
}
95+
return fmt.Errorf("ICMPv6 proxy was not instantiated")
3796
}
3897

3998
func getICMPEcho(msg *icmp.Message) (*icmp.Echo, error) {
@@ -43,3 +102,7 @@ func getICMPEcho(msg *icmp.Message) (*icmp.Echo, error) {
43102
}
44103
return echo, nil
45104
}
105+
106+
func isEchoReply(msg *icmp.Message) bool {
107+
return msg.Type == ipv4.ICMPTypeEchoReply || msg.Type == ipv6.ICMPTypeEchoReply
108+
}

0 commit comments

Comments
 (0)