github.com/cilium/cilium@v1.16.2/pkg/envoy/accesslog_server.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package envoy
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"os"
    13  	"syscall"
    14  
    15  	cilium "github.com/cilium/proxy/go/cilium/api"
    16  	"github.com/cilium/proxy/pkg/policy/api/kafka"
    17  	"github.com/sirupsen/logrus"
    18  	"golang.org/x/sys/unix"
    19  	"google.golang.org/protobuf/proto"
    20  
    21  	"github.com/cilium/cilium/pkg/flowdebug"
    22  	"github.com/cilium/cilium/pkg/identity"
    23  	"github.com/cilium/cilium/pkg/proxy/accesslog"
    24  	"github.com/cilium/cilium/pkg/proxy/logger"
    25  	"github.com/cilium/cilium/pkg/time"
    26  )
    27  
    28  type AccessLogServer struct {
    29  	socketPath         string
    30  	proxyGID           uint
    31  	localEndpointStore *LocalEndpointStore
    32  	stopCh             chan struct{}
    33  }
    34  
    35  func newAccessLogServer(envoySocketDir string, proxyGID uint, localEndpointStore *LocalEndpointStore) *AccessLogServer {
    36  	return &AccessLogServer{
    37  		socketPath:         getAccessLogSocketPath(envoySocketDir),
    38  		proxyGID:           proxyGID,
    39  		localEndpointStore: localEndpointStore,
    40  	}
    41  }
    42  
    43  // start starts the access log server.
    44  func (s *AccessLogServer) start() error {
    45  	socketListener, err := s.newSocketListener()
    46  	if err != nil {
    47  		return fmt.Errorf("failed to create socket listener: %w", err)
    48  	}
    49  
    50  	s.stopCh = make(chan struct{})
    51  
    52  	ctx, cancel := context.WithCancel(context.Background())
    53  
    54  	go func() {
    55  		log.Infof("Envoy: Starting access log server listening on %s", socketListener.Addr())
    56  		for {
    57  			// Each Envoy listener opens a new connection over the Unix domain socket.
    58  			// Multiple worker threads serving the listener share that same connection
    59  			uc, err := socketListener.AcceptUnix()
    60  			if err != nil {
    61  				// These errors are expected when we are closing down
    62  				if errors.Is(err, net.ErrClosed) || errors.Is(err, syscall.EINVAL) {
    63  					break
    64  				}
    65  				log.WithError(err).Warn("Envoy: Failed to accept access log connection")
    66  				continue
    67  			}
    68  			log.Info("Envoy: Accepted access log connection")
    69  
    70  			// Serve this access log socket in a goroutine, so we can serve multiple
    71  			// connections concurrently.
    72  			go s.handleConn(ctx, uc)
    73  		}
    74  	}()
    75  
    76  	go func() {
    77  		<-s.stopCh
    78  		_ = socketListener.Close()
    79  		cancel()
    80  	}()
    81  
    82  	return nil
    83  }
    84  
    85  func (s *AccessLogServer) newSocketListener() (*net.UnixListener, error) {
    86  	// Remove/Unlink the old unix domain socket, if any.
    87  	_ = os.Remove(s.socketPath)
    88  
    89  	// Create the access log listener
    90  	accessLogListener, err := net.ListenUnix("unixpacket", &net.UnixAddr{Name: s.socketPath, Net: "unixpacket"})
    91  	if err != nil {
    92  		return nil, fmt.Errorf("failed to open access log listen socket at %s: %w", s.socketPath, err)
    93  	}
    94  	accessLogListener.SetUnlinkOnClose(true)
    95  
    96  	// Make the socket accessible by owner and group only.
    97  	if err = os.Chmod(s.socketPath, 0660); err != nil {
    98  		return nil, fmt.Errorf("failed to change mode of access log listen socket at %s: %w", s.socketPath, err)
    99  	}
   100  	// Change the group to ProxyGID allowing access from any process from that group.
   101  	if err = os.Chown(s.socketPath, -1, int(s.proxyGID)); err != nil {
   102  		log.WithError(err).Warningf("Envoy: Failed to change the group of access log listen socket at %s", s.socketPath)
   103  	}
   104  	return accessLogListener, nil
   105  }
   106  
   107  func (s *AccessLogServer) stop() {
   108  	if s.stopCh != nil {
   109  		s.stopCh <- struct{}{}
   110  	}
   111  }
   112  
   113  func (s *AccessLogServer) handleConn(ctx context.Context, conn *net.UnixConn) {
   114  	stopCh := make(chan struct{})
   115  
   116  	go func() {
   117  		select {
   118  		case <-stopCh:
   119  		case <-ctx.Done():
   120  			_ = conn.Close()
   121  		}
   122  	}()
   123  
   124  	defer func() {
   125  		log.Info("Envoy: Closing access log connection")
   126  		_ = conn.Close()
   127  		stopCh <- struct{}{}
   128  	}()
   129  
   130  	buf := make([]byte, 4096)
   131  	for {
   132  		n, _, flags, _, err := conn.ReadMsgUnix(buf, nil)
   133  		if err != nil {
   134  			if !errors.Is(err, io.EOF) {
   135  				log.WithError(err).Error("Envoy: Error while reading from access log connection")
   136  			}
   137  			break
   138  		}
   139  		if flags&unix.MSG_TRUNC != 0 {
   140  			log.Warning("Envoy: Discarded truncated access log message")
   141  			continue
   142  		}
   143  		pblog := cilium.LogEntry{}
   144  		err = proto.Unmarshal(buf[:n], &pblog)
   145  		if err != nil {
   146  			log.WithError(err).Warning("Envoy: Discarded invalid access log message")
   147  			continue
   148  		}
   149  
   150  		flowdebug.Log(func() (*logrus.Entry, string) {
   151  			return log, fmt.Sprintf("%s: Access log message: %s", pblog.PolicyName, pblog.String())
   152  		})
   153  
   154  		r := logRecord(ctx, &pblog)
   155  
   156  		// Update proxy stats for the endpoint if it still exists
   157  		localEndpoint := s.localEndpointStore.getLocalEndpoint(pblog.PolicyName)
   158  		if localEndpoint != nil {
   159  			// Update stats for the endpoint.
   160  			ingress := r.ObservationPoint == accesslog.Ingress
   161  			request := r.Type == accesslog.TypeRequest
   162  			port := r.DestinationEndpoint.Port
   163  			if !request {
   164  				port = r.SourceEndpoint.Port
   165  			}
   166  			localEndpoint.UpdateProxyStatistics("envoy", "TCP", port, uint16(pblog.ProxyId), ingress, request, r.Verdict)
   167  		}
   168  	}
   169  }
   170  
   171  func logRecord(ctx context.Context, pblog *cilium.LogEntry) *logger.LogRecord {
   172  	var kafkaRecord *accesslog.LogRecordKafka
   173  	var kafkaTopics []string
   174  
   175  	var l7tags logger.LogTag = func(lr *logger.LogRecord) {}
   176  
   177  	if httpLogEntry := pblog.GetHttp(); httpLogEntry != nil {
   178  		l7tags = logger.LogTags.HTTP(&accesslog.LogRecordHTTP{
   179  			Method:          httpLogEntry.Method,
   180  			Code:            int(httpLogEntry.Status),
   181  			URL:             ParseURL(httpLogEntry.Scheme, httpLogEntry.Host, httpLogEntry.Path),
   182  			Protocol:        GetProtocol(httpLogEntry.HttpProtocol),
   183  			Headers:         GetNetHttpHeaders(httpLogEntry.Headers),
   184  			MissingHeaders:  GetNetHttpHeaders(httpLogEntry.MissingHeaders),
   185  			RejectedHeaders: GetNetHttpHeaders(httpLogEntry.RejectedHeaders),
   186  		})
   187  	} else if kafkaLogEntry := pblog.GetKafka(); kafkaLogEntry != nil {
   188  		kafkaRecord = &accesslog.LogRecordKafka{
   189  			ErrorCode:     int(kafkaLogEntry.ErrorCode),
   190  			APIVersion:    int16(kafkaLogEntry.ApiVersion),
   191  			APIKey:        kafka.ApiKeyToString(int16(kafkaLogEntry.ApiKey)),
   192  			CorrelationID: kafkaLogEntry.CorrelationId,
   193  		}
   194  		if len(kafkaLogEntry.Topics) > 0 {
   195  			kafkaRecord.Topic.Topic = kafkaLogEntry.Topics[0]
   196  			if len(kafkaLogEntry.Topics) > 1 {
   197  				kafkaTopics = kafkaLogEntry.Topics[1:] // Rest of the topics
   198  			}
   199  		}
   200  		l7tags = logger.LogTags.Kafka(kafkaRecord)
   201  	} else if l7LogEntry := pblog.GetGenericL7(); l7LogEntry != nil {
   202  		l7tags = logger.LogTags.L7(&accesslog.LogRecordL7{
   203  			Proto:  l7LogEntry.GetProto(),
   204  			Fields: l7LogEntry.GetFields(),
   205  		})
   206  	}
   207  
   208  	flowType := GetFlowType(pblog)
   209  	// Response access logs from Envoy inherit the source/destination info from the request log
   210  	// message. Swap source/destination info here for the response logs so that they are
   211  	// correct.
   212  	// TODO (jrajahalme): Consider doing this at our Envoy filters instead?
   213  	var addrInfo logger.AddressingInfo
   214  	if flowType == accesslog.TypeResponse {
   215  		addrInfo.DstIPPort = pblog.SourceAddress
   216  		addrInfo.DstIdentity = identity.NumericIdentity(pblog.SourceSecurityId)
   217  		addrInfo.SrcIPPort = pblog.DestinationAddress
   218  		addrInfo.SrcIdentity = identity.NumericIdentity(pblog.DestinationSecurityId)
   219  	} else {
   220  		addrInfo.SrcIPPort = pblog.SourceAddress
   221  		addrInfo.SrcIdentity = identity.NumericIdentity(pblog.SourceSecurityId)
   222  		addrInfo.DstIPPort = pblog.DestinationAddress
   223  		addrInfo.DstIdentity = identity.NumericIdentity(pblog.DestinationSecurityId)
   224  	}
   225  	r := logger.NewLogRecord(flowType, pblog.IsIngress,
   226  		logger.LogTags.Timestamp(time.Unix(int64(pblog.Timestamp/1000000000), int64(pblog.Timestamp%1000000000))),
   227  		logger.LogTags.Verdict(GetVerdict(pblog), pblog.CiliumRuleRef),
   228  		logger.LogTags.Addressing(ctx, addrInfo),
   229  		l7tags,
   230  	)
   231  	r.Log()
   232  
   233  	// Each kafka topic needs to be logged separately, log the rest if any
   234  	for i := range kafkaTopics {
   235  		kafkaRecord.Topic.Topic = kafkaTopics[i]
   236  		r.Log()
   237  	}
   238  
   239  	return r
   240  }