pcap-cli/pkg/pcap/pcap_writer.go (156 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pcap
import (
"bufio"
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
"time"
"unsafe"
"dario.cat/mergo"
"github.com/easyCZ/logrotate"
"github.com/itchyny/timefmt-go"
)
var pcapWriterLogger = log.New(os.Stderr, "[writer] - ", log.LstdFlags)
type (
PcapWriter interface {
io.Writer
io.Closer
Rotate()
IsStdOutOrErr() bool
GetIface() *string
}
pcapWriter struct {
*logrotate.Writer
iface *string
isStdOutOrErr bool
v reflect.Value
osFile reflect.Value
osFileSync reflect.Value
bufioWriter reflect.Value
bufioWriterFlush reflect.Value
}
pcapFileNameProvider struct {
directory string
template string
location *time.Location
}
)
var defaultLogrotateOptions logrotate.Options = logrotate.Options{
Directory: "/",
MaximumFileSize: 0,
MaximumLifetime: 0,
FlushAfterEveryWrite: false,
FileNameFunc: func() string { return "" },
}
//go:linkname rotate github.com/easyCZ/logrotate.(*Writer).rotate
func rotate(w *logrotate.Writer)
func makeSetable(v reflect.Value) reflect.Value {
return reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
}
func getField(v reflect.Value, field string) reflect.Value {
return v.Elem().FieldByName(field)
}
func getSetableField(v reflect.Value, field string) reflect.Value {
return makeSetable(getField(v, field))
}
func (w *pcapWriter) Rotate() {
// if `PcapWriter` encapsulates `std[out|err]` do not rotate,
// just call `Flush` on the underlying `bufio.Writer` for `os.Std{out|err}`
if w.isStdOutOrErr {
return
}
// see: https://pkg.go.dev/cmd/compile#:~:text=//-,go%3Alinkname,-localname%20%5Bimportpath.name
rotate(w.Writer)
}
func (w *pcapWriter) GetIface() *string {
return w.iface
}
func (w *pcapWriter) IsStdOutOrErr() bool {
return w.isStdOutOrErr
}
func (p *pcapFileNameProvider) get() string {
fileName := timefmt.Format(time.Now().In(p.location), p.template)
pcapWriterLogger.Printf("new file: %s\n", fileName)
return fileName
}
func getPcapWriterLocationForTimezone(timezone *string) *time.Location {
location, err := time.LoadLocation(*timezone)
if err != nil {
return time.UTC
}
return location
}
func newPcapWriterFileNameProvider(template, timezone *string) *pcapFileNameProvider {
fileNameTemplate := *template
return &pcapFileNameProvider{
directory: filepath.Dir(fileNameTemplate),
template: filepath.Base(fileNameTemplate),
location: getPcapWriterLocationForTimezone(timezone),
}
}
func newPcapWriterForStdout(logger *log.Logger) (*logrotate.Writer, error) {
return logrotate.New(logger, defaultLogrotateOptions)
}
func newPcapWriter(logger *log.Logger, template, extension, timezone *string, interval *int) (*logrotate.Writer, error) {
var fileMaxLifetime time.Duration = 0 // time.Minute
if *interval > 0 {
fileMaxLifetime = time.Duration(*interval) * time.Second
}
fileNameTemplate := fmt.Sprintf("%s.%s", *template, *extension)
fileNameProvider := newPcapWriterFileNameProvider(&fileNameTemplate, timezone)
options := logrotate.Options{
Directory: fileNameProvider.directory,
MaximumLifetime: fileMaxLifetime,
FileNameFunc: func() string { return fileNameProvider.get() },
}
if err := mergo.Merge(&options, defaultLogrotateOptions); err != nil {
return nil, err
}
return logrotate.New(logger, options)
}
func isStdoutPcapWriter(template, extension *string, interval *int) bool {
return ((template == nil && extension == nil) || (*template == "stdout" || *template == "stderr")) && *interval == 0
}
func NewStdoutPcapWriter(ctx context.Context, ifaceAndIndex *string) (PcapWriter, error) {
return NewPcapWriter(ctx, ifaceAndIndex, nil, nil, nil, 0)
}
func NewPcapWriter(ctx context.Context, ifaceAndInfex, template, extension, timezone *string, interval int) (PcapWriter, error) {
isStdOutOrErr := isStdoutPcapWriter(template, extension, &interval)
loggerPrefix := fmt.Sprintf("[pcap/writer] - [%s] – ", *ifaceAndInfex)
if isStdOutOrErr {
loggerPrefix += "[stdout] – "
} else {
loggerPrefix = fmt.Sprintf("%s[%s] - ", loggerPrefix, *extension)
}
logger := log.New(os.Stderr, loggerPrefix, log.LstdFlags)
var err error
var writer *logrotate.Writer
if isStdOutOrErr {
// Using `logrotate` to make `os.Stdout` safe to be concurrently written by PCAP engines
writer, err = newPcapWriterForStdout(logger)
} else {
writer, err = newPcapWriter(logger, template, extension, timezone, &interval)
}
if err != nil {
return nil, err
}
// `logrotate` does not provide handles to `*bufio.Writer::Flush`/`*os.File::Syinc`
// the underlying Writer/File so it is necessary to get handles on them.
// Since PCAP engines are started atomically and current execution must complete
// before a new one can be started; it is safe to `flush` and `sync` PCAP files.
// https://github.com/easyCZ/logrotate/blob/master/writer.go
v := reflect.ValueOf(writer)
osFile := getSetableField(v, "f")
osFileSync := osFile.MethodByName("Sync")
bufioWriter := getSetableField(v, "bw")
bufioWriterFlush := bufioWriter.MethodByName("Flush")
if isStdOutOrErr {
// injecting `os.Stdout` into `logrotate.Writer` instance
osFile.Set(reflect.ValueOf(os.Stdout))
bufioWriter.Set(reflect.ValueOf(bufio.NewWriterSize(os.Stdout, 1)))
}
w := &pcapWriter{writer, ifaceAndInfex, isStdOutOrErr, v, osFile, osFileSync, bufioWriter, bufioWriterFlush}
go func(ctx context.Context, writer *logrotate.Writer, block bool) {
if !block {
return
}
<-ctx.Done()
logger.Println("- ROTATE")
rotate(writer)
}(ctx, writer, !isStdOutOrErr)
logger.Println("- created")
return w, nil
}