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 }