2022-08-25 11:34:19 +00:00
//go:build linux
package ingress
2022-09-02 16:29:50 +00:00
// This file implements ICMPProxy for Linux. Each (source IP, destination IP, echo ID) opens a non-privileged ICMP socket.
// The source IP of the requests are rewritten to the bind IP of the socket and echo ID rewritten to the port number of
// the socket. The kernel ensures the socket only reads replies whose echo ID matches the port number.
// For more information about the socket, see https://man7.org/linux/man-pages/man7/icmp.7.html and https://lwn.net/Articles/422330/
2022-08-25 11:34:19 +00:00
import (
"context"
"fmt"
"net"
"net/netip"
2022-09-27 14:56:42 +00:00
"os"
"regexp"
"strconv"
2022-08-25 11:34:19 +00:00
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/net/icmp"
"github.com/cloudflare/cloudflared/packet"
)
2022-09-27 14:56:42 +00:00
const (
// https://lwn.net/Articles/550551/ IPv4 and IPv6 share the same path
pingGroupPath = "/proc/sys/net/ipv4/ping_group_range"
)
var (
findGroupIDRegex = regexp . MustCompile ( ` \d+ ` )
)
2022-08-25 11:34:19 +00:00
type icmpProxy struct {
2022-09-02 16:29:50 +00:00
srcFunnelTracker * packet . FunnelTracker
2022-08-25 11:34:19 +00:00
listenIP netip . Addr
2022-09-20 10:39:51 +00:00
ipv6Zone string
2022-08-25 11:34:19 +00:00
logger * zerolog . Logger
2022-09-02 16:29:50 +00:00
idleTimeout time . Duration
2022-08-25 11:34:19 +00:00
}
2022-09-20 10:39:51 +00:00
func newICMPProxy ( listenIP netip . Addr , zone string , logger * zerolog . Logger , idleTimeout time . Duration ) ( * icmpProxy , error ) {
2022-09-27 14:56:42 +00:00
if err := testPermission ( listenIP , zone , logger ) ; err != nil {
2022-08-25 11:34:19 +00:00
return nil , err
}
return & icmpProxy {
2022-09-02 16:29:50 +00:00
srcFunnelTracker : packet . NewFunnelTracker ( ) ,
2022-08-25 11:34:19 +00:00
listenIP : listenIP ,
2022-09-20 10:39:51 +00:00
ipv6Zone : zone ,
2022-08-25 11:34:19 +00:00
logger : logger ,
2022-09-02 16:29:50 +00:00
idleTimeout : idleTimeout ,
2022-08-25 11:34:19 +00:00
} , nil
}
2022-09-27 14:56:42 +00:00
func testPermission ( listenIP netip . Addr , zone string , logger * zerolog . Logger ) error {
2022-08-25 11:34:19 +00:00
// Opens a non-privileged ICMP socket. On Linux the group ID of the process needs to be in ping_group_range
2022-09-27 14:56:42 +00:00
// Only check ping_group_range once for IPv4
if listenIP . Is4 ( ) {
if err := checkInPingGroup ( ) ; err != nil {
logger . Warn ( ) . Err ( err ) . Msgf ( "The user running cloudflared process has a GID (group ID) that is not within ping_group_range. You might need to add that user to a group within that range, or instead update the range to encompass a group the user is already in by modifying %s. Otherwise cloudflared will not be able to ping this network" , pingGroupPath )
return err
}
}
2022-09-20 10:39:51 +00:00
conn , err := newICMPConn ( listenIP , zone )
2022-08-25 11:34:19 +00:00
if err != nil {
return err
}
// This conn is only to test if cloudflared has permission to open this type of socket
conn . Close ( )
return nil
}
2022-09-27 14:56:42 +00:00
func checkInPingGroup ( ) error {
file , err := os . ReadFile ( pingGroupPath )
if err != nil {
return err
}
groupID := os . Getgid ( )
// Example content: 999 59999
found := findGroupIDRegex . FindAll ( file , 2 )
if len ( found ) == 2 {
2022-09-29 11:59:38 +00:00
groupMin , err := strconv . ParseInt ( string ( found [ 0 ] ) , 10 , 32 )
2022-09-27 14:56:42 +00:00
if err != nil {
return errors . Wrapf ( err , "failed to determine minimum ping group ID" )
}
2022-09-29 11:59:38 +00:00
groupMax , err := strconv . ParseInt ( string ( found [ 1 ] ) , 10 , 32 )
2022-09-27 14:56:42 +00:00
if err != nil {
return errors . Wrapf ( err , "failed to determine minimum ping group ID" )
}
if groupID < int ( groupMin ) || groupID > int ( groupMax ) {
return fmt . Errorf ( "Group ID %d is not between ping group %d to %d" , groupID , groupMin , groupMax )
}
return nil
}
return fmt . Errorf ( "did not find group range in %s" , pingGroupPath )
}
2022-10-13 10:01:25 +00:00
func ( ip * icmpProxy ) Request ( ctx context . Context , pk * packet . ICMP , responder * packetResponder ) error {
2022-09-09 15:48:42 +00:00
originalEcho , err := getICMPEcho ( pk . Message )
if err != nil {
return err
}
newConnChan := make ( chan * icmp . PacketConn , 1 )
newFunnelFunc := func ( ) ( packet . Funnel , error ) {
2022-09-20 10:39:51 +00:00
conn , err := newICMPConn ( ip . listenIP , ip . ipv6Zone )
2022-09-02 16:29:50 +00:00
if err != nil {
2022-09-09 15:48:42 +00:00
return nil , errors . Wrap ( err , "failed to open ICMP socket" )
2022-09-02 16:29:50 +00:00
}
2022-09-20 10:39:51 +00:00
ip . logger . Debug ( ) . Msgf ( "Opened ICMP socket listen on %s" , conn . LocalAddr ( ) )
2022-09-09 15:48:42 +00:00
newConnChan <- conn
2022-10-13 10:01:25 +00:00
closeCallback := func ( ) error {
return conn . Close ( )
}
2022-09-02 16:29:50 +00:00
localUDPAddr , ok := conn . LocalAddr ( ) . ( * net . UDPAddr )
if ! ok {
2022-09-09 15:48:42 +00:00
return nil , fmt . Errorf ( "ICMP listener address %s is not net.UDPAddr" , conn . LocalAddr ( ) )
2022-09-02 16:29:50 +00:00
}
echoID := localUDPAddr . Port
2022-10-13 10:01:25 +00:00
icmpFlow := newICMPEchoFlow ( pk . Src , closeCallback , conn , responder , echoID , originalEcho . ID , packet . NewEncoder ( ) )
2022-09-09 15:48:42 +00:00
return icmpFlow , nil
}
funnelID := flow3Tuple {
srcIP : pk . Src ,
dstIP : pk . Dst ,
originalEchoID : originalEcho . ID ,
}
funnel , isNew , err := ip . srcFunnelTracker . GetOrRegister ( funnelID , newFunnelFunc )
if err != nil {
return err
}
icmpFlow , err := toICMPEchoFlow ( funnel )
if err != nil {
return err
}
if isNew {
ip . logger . Debug ( ) .
Str ( "src" , pk . Src . String ( ) ) .
Str ( "dst" , pk . Dst . String ( ) ) .
Int ( "originalEchoID" , originalEcho . ID ) .
Msg ( "New flow" )
conn := <- newConnChan
2022-09-02 16:29:50 +00:00
go func ( ) {
defer ip . srcFunnelTracker . Unregister ( funnelID , icmpFlow )
if err := ip . listenResponse ( icmpFlow , conn ) ; err != nil {
2022-09-09 15:48:42 +00:00
ip . logger . Debug ( ) . Err ( err ) .
Str ( "src" , pk . Src . String ( ) ) .
Str ( "dst" , pk . Dst . String ( ) ) .
Int ( "originalEchoID" , originalEcho . ID ) .
2022-09-02 16:29:50 +00:00
Msg ( "Failed to listen for ICMP echo response" )
}
} ( )
2022-08-25 11:34:19 +00:00
}
2022-09-02 16:29:50 +00:00
if err := icmpFlow . sendToDst ( pk . Dst , pk . Message ) ; err != nil {
return errors . Wrap ( err , "failed to send ICMP echo request" )
}
return nil
2022-08-25 11:34:19 +00:00
}
func ( ip * icmpProxy ) Serve ( ctx context . Context ) error {
2022-09-02 16:29:50 +00:00
ip . srcFunnelTracker . ScheduleCleanup ( ctx , ip . idleTimeout )
2022-08-25 11:34:19 +00:00
return ctx . Err ( )
}
2022-09-02 16:29:50 +00:00
func ( ip * icmpProxy ) listenResponse ( flow * icmpEchoFlow , conn * icmp . PacketConn ) error {
buf := make ( [ ] byte , mtu )
for {
2022-09-06 12:46:21 +00:00
n , from , err := conn . ReadFrom ( buf )
2022-09-02 16:29:50 +00:00
if err != nil {
return err
2022-08-25 11:34:19 +00:00
}
2022-09-06 12:46:21 +00:00
reply , err := parseReply ( from , buf [ : n ] )
if err != nil {
ip . logger . Error ( ) . Err ( err ) . Str ( "dst" , from . String ( ) ) . Msg ( "Failed to parse ICMP reply" )
continue
}
if ! isEchoReply ( reply . msg ) {
ip . logger . Debug ( ) . Str ( "dst" , from . String ( ) ) . Msgf ( "Drop ICMP %s from reply" , reply . msg . Type )
continue
}
if err := flow . returnToSrc ( reply ) ; err != nil {
ip . logger . Err ( err ) . Str ( "dst" , from . String ( ) ) . Msg ( "Failed to send ICMP reply" )
2022-09-02 16:29:50 +00:00
continue
}
2022-08-25 11:34:19 +00:00
}
}
2022-09-09 15:48:42 +00:00
// Only linux uses flow3Tuple as FunnelID
func ( ft flow3Tuple ) Type ( ) string {
return "srcIP_dstIP_echoID"
2022-08-25 11:34:19 +00:00
}
2022-09-09 15:48:42 +00:00
func ( ft flow3Tuple ) String ( ) string {
return fmt . Sprintf ( "%s:%s:%d" , ft . srcIP , ft . dstIP , ft . originalEchoID )
2022-08-25 11:34:19 +00:00
}