icmp_linux.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. //go:build linux
  2. package ingress
  3. // This file implements ICMPProxy for Linux. Each (source IP, destination IP, echo ID) opens a non-privileged ICMP socket.
  4. // The source IP of the requests are rewritten to the bind IP of the socket and echo ID rewritten to the port number of
  5. // the socket. The kernel ensures the socket only reads replies whose echo ID matches the port number.
  6. // For more information about the socket, see https://man7.org/linux/man-pages/man7/icmp.7.html and https://lwn.net/Articles/422330/
  7. import (
  8. "context"
  9. "fmt"
  10. "net"
  11. "net/netip"
  12. "os"
  13. "regexp"
  14. "strconv"
  15. "time"
  16. "github.com/pkg/errors"
  17. "github.com/rs/zerolog"
  18. "go.opentelemetry.io/otel/attribute"
  19. "github.com/cloudflare/cloudflared/packet"
  20. "github.com/cloudflare/cloudflared/tracing"
  21. )
  22. const (
  23. // https://lwn.net/Articles/550551/ IPv4 and IPv6 share the same path
  24. pingGroupPath = "/proc/sys/net/ipv4/ping_group_range"
  25. )
  26. var (
  27. findGroupIDRegex = regexp.MustCompile(`\d+`)
  28. )
  29. type icmpProxy struct {
  30. srcFunnelTracker *packet.FunnelTracker
  31. listenIP netip.Addr
  32. logger *zerolog.Logger
  33. idleTimeout time.Duration
  34. }
  35. func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) {
  36. if err := testPermission(listenIP, logger); err != nil {
  37. return nil, err
  38. }
  39. return &icmpProxy{
  40. srcFunnelTracker: packet.NewFunnelTracker(),
  41. listenIP: listenIP,
  42. logger: logger,
  43. idleTimeout: idleTimeout,
  44. }, nil
  45. }
  46. func testPermission(listenIP netip.Addr, logger *zerolog.Logger) error {
  47. // Opens a non-privileged ICMP socket. On Linux the group ID of the process needs to be in ping_group_range
  48. // Only check ping_group_range once for IPv4
  49. if listenIP.Is4() {
  50. if err := checkInPingGroup(); err != nil {
  51. 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)
  52. return err
  53. }
  54. }
  55. conn, err := newICMPConn(listenIP)
  56. if err != nil {
  57. return err
  58. }
  59. // This conn is only to test if cloudflared has permission to open this type of socket
  60. conn.Close()
  61. return nil
  62. }
  63. func checkInPingGroup() error {
  64. file, err := os.ReadFile(pingGroupPath)
  65. if err != nil {
  66. return err
  67. }
  68. groupID := uint64(os.Getegid())
  69. // Example content: 999 59999
  70. found := findGroupIDRegex.FindAll(file, 2)
  71. if len(found) == 2 {
  72. groupMin, err := strconv.ParseUint(string(found[0]), 10, 32)
  73. if err != nil {
  74. return errors.Wrapf(err, "failed to determine minimum ping group ID")
  75. }
  76. groupMax, err := strconv.ParseUint(string(found[1]), 10, 32)
  77. if err != nil {
  78. return errors.Wrapf(err, "failed to determine maximum ping group ID")
  79. }
  80. if groupID < groupMin || groupID > groupMax {
  81. return fmt.Errorf("Group ID %d is not between ping group %d to %d", groupID, groupMin, groupMax)
  82. }
  83. return nil
  84. }
  85. return fmt.Errorf("did not find group range in %s", pingGroupPath)
  86. }
  87. func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error {
  88. ctx, span := responder.RequestSpan(ctx, pk)
  89. defer responder.ExportSpan()
  90. originalEcho, err := getICMPEcho(pk.Message)
  91. if err != nil {
  92. tracing.EndWithErrorStatus(span, err)
  93. return err
  94. }
  95. observeICMPRequest(ip.logger, span, pk.Src.String(), pk.Dst.String(), originalEcho.ID, originalEcho.Seq)
  96. shouldReplaceFunnelFunc := createShouldReplaceFunnelFunc(ip.logger, responder, pk, originalEcho.ID)
  97. newFunnelFunc := func() (packet.Funnel, error) {
  98. conn, err := newICMPConn(ip.listenIP)
  99. if err != nil {
  100. tracing.EndWithErrorStatus(span, err)
  101. return nil, errors.Wrap(err, "failed to open ICMP socket")
  102. }
  103. ip.logger.Debug().Msgf("Opened ICMP socket listen on %s", conn.LocalAddr())
  104. closeCallback := func() error {
  105. return conn.Close()
  106. }
  107. localUDPAddr, ok := conn.LocalAddr().(*net.UDPAddr)
  108. if !ok {
  109. return nil, fmt.Errorf("ICMP listener address %s is not net.UDPAddr", conn.LocalAddr())
  110. }
  111. span.SetAttributes(attribute.Int("port", localUDPAddr.Port))
  112. echoID := localUDPAddr.Port
  113. icmpFlow := newICMPEchoFlow(pk.Src, closeCallback, conn, responder, echoID, originalEcho.ID)
  114. return icmpFlow, nil
  115. }
  116. funnelID := flow3Tuple{
  117. srcIP: pk.Src,
  118. dstIP: pk.Dst,
  119. originalEchoID: originalEcho.ID,
  120. }
  121. funnel, isNew, err := ip.srcFunnelTracker.GetOrRegister(funnelID, shouldReplaceFunnelFunc, newFunnelFunc)
  122. if err != nil {
  123. tracing.EndWithErrorStatus(span, err)
  124. return err
  125. }
  126. icmpFlow, err := toICMPEchoFlow(funnel)
  127. if err != nil {
  128. tracing.EndWithErrorStatus(span, err)
  129. return err
  130. }
  131. if isNew {
  132. span.SetAttributes(attribute.Bool("newFlow", true))
  133. ip.logger.Debug().
  134. Str("src", pk.Src.String()).
  135. Str("dst", pk.Dst.String()).
  136. Int("originalEchoID", originalEcho.ID).
  137. Msg("New flow")
  138. go func() {
  139. ip.listenResponse(ctx, icmpFlow)
  140. ip.srcFunnelTracker.Unregister(funnelID, icmpFlow)
  141. }()
  142. }
  143. if err := icmpFlow.sendToDst(pk.Dst, pk.Message); err != nil {
  144. tracing.EndWithErrorStatus(span, err)
  145. return errors.Wrap(err, "failed to send ICMP echo request")
  146. }
  147. tracing.End(span)
  148. return nil
  149. }
  150. func (ip *icmpProxy) Serve(ctx context.Context) error {
  151. ip.srcFunnelTracker.ScheduleCleanup(ctx, ip.idleTimeout)
  152. return ctx.Err()
  153. }
  154. func (ip *icmpProxy) listenResponse(ctx context.Context, flow *icmpEchoFlow) {
  155. buf := make([]byte, mtu)
  156. for {
  157. if done := ip.handleResponse(ctx, flow, buf); done {
  158. return
  159. }
  160. }
  161. }
  162. // Listens for ICMP response and handles error logging
  163. func (ip *icmpProxy) handleResponse(ctx context.Context, flow *icmpEchoFlow, buf []byte) (done bool) {
  164. _, span := flow.responder.ReplySpan(ctx, ip.logger)
  165. defer flow.responder.ExportSpan()
  166. span.SetAttributes(
  167. attribute.Int("originalEchoID", flow.originalEchoID),
  168. )
  169. n, from, err := flow.originConn.ReadFrom(buf)
  170. if err != nil {
  171. if flow.IsClosed() {
  172. tracing.EndWithErrorStatus(span, fmt.Errorf("flow was closed"))
  173. return true
  174. }
  175. ip.logger.Error().Err(err).Str("socket", flow.originConn.LocalAddr().String()).Msg("Failed to read from ICMP socket")
  176. tracing.EndWithErrorStatus(span, err)
  177. return true
  178. }
  179. reply, err := parseReply(from, buf[:n])
  180. if err != nil {
  181. ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply")
  182. tracing.EndWithErrorStatus(span, err)
  183. return false
  184. }
  185. if !isEchoReply(reply.msg) {
  186. err := fmt.Errorf("Expect ICMP echo reply, got %s", reply.msg.Type)
  187. ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type)
  188. tracing.EndWithErrorStatus(span, err)
  189. return false
  190. }
  191. if err := flow.returnToSrc(reply); err != nil {
  192. ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply")
  193. tracing.EndWithErrorStatus(span, err)
  194. return false
  195. }
  196. observeICMPReply(ip.logger, span, from.String(), reply.echo.ID, reply.echo.Seq)
  197. tracing.End(span)
  198. return false
  199. }
  200. // Only linux uses flow3Tuple as FunnelID
  201. func (ft flow3Tuple) Type() string {
  202. return "srcIP_dstIP_echoID"
  203. }
  204. func (ft flow3Tuple) String() string {
  205. return fmt.Sprintf("%s:%s:%d", ft.srcIP, ft.dstIP, ft.originalEchoID)
  206. }