pcap-cli/pkg/pcap/tcpdump_engine.go (128 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 ( "context" "errors" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "sync/atomic" "syscall" "time" ps "github.com/mitchellh/go-ps" sf "github.com/wissance/stringFormatter" ) var tcpdumpLogger = log.New(os.Stderr, "[tcpdump] - ", log.LstdFlags) func (t *Tcpdump) IsActive() bool { return t.isActive.Load() } func (t *Tcpdump) buildArgs(ctx context.Context) []string { cfg := t.config args := []string{"-n", "-Z", "root", "-i", cfg.Iface, "-s", fmt.Sprintf("%d", cfg.Snaplen)} if cfg.Output != "stdout" { directory := filepath.Dir(cfg.Output) template := filepath.Base(cfg.Output) fileNameTemplate := sf.Format("{0}/{1}.{2}", directory, template, cfg.Extension) args = append(args, "-w", fileNameTemplate) } if cfg.Interval > 0 { args = append(args, "-G", fmt.Sprintf("%d", cfg.Interval)) } if !cfg.Compat { if filter := providePcapFilter(ctx, &cfg.Filter, cfg.Filters); *filter != "" { args = append(args, *filter) } } return args } func (t *Tcpdump) kill(pid int) error { proc, err := os.FindProcess(pid) if err != nil { return err } return proc.Signal(syscall.SIGTERM) } func (t *Tcpdump) findAndKill(pid int) (uint32, uint32, error) { processes, err := ps.Processes() if err != nil { return 0, 0, err } killCounter := uint32(0) procsCounter := uint32(0) for _, p := range processes { procID := p.Pid() execName := p.Executable() if execName == "tcpdump" && procID == pid { tcpdumpLogger.Printf("killing %s(%d)\n", execName, procID) if err := t.kill(procID); err == nil { killCounter++ } procsCounter++ } } return killCounter, procsCounter, nil } func (t *Tcpdump) Start( ctx context.Context, _ []PcapWriter, stopDeadline <-chan *time.Duration, ) error { // atomically activate the packet capture if !t.isActive.CompareAndSwap(false, true) { return fmt.Errorf("already started") } args := t.buildArgs(ctx) cmd := exec.CommandContext(ctx, t.tcpdump, args...) // prevent child process from hijacking signals cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, Pgid: 0, } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.WaitDelay = 1900 * time.Millisecond cmdLine := strings.Join(cmd.Args[:], " ") if err := cmd.Start(); err != nil { tcpdumpLogger.Printf("'%+v' - error: %+v\n", cmdLine, err) return err } pid := cmd.Process.Pid tcpdumpLogger.Printf("EXEC(%d): %v\n", pid, cmdLine) <-ctx.Done() ctxDoneTS := time.Now() if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { tcpdumpLogger.Printf("[pid:%d] - %+v' - error: %+v\n", pid, cmdLine, err) cmd.Process.Kill() } cmdStopChan := make(chan error, 1) go func(cmd *exec.Cmd, cmdStopChan chan<- error) { cmdStopChan <- cmd.Wait() }(cmd, cmdStopChan) engineStopDeadline := <-stopDeadline engineStopTimeout := *engineStopDeadline - time.Since(ctxDoneTS) timer := time.NewTimer(engineStopTimeout) var err error select { case <-timer.C: err = context.DeadlineExceeded case err = <-cmdStopChan: if !timer.Stop() { <-timer.C } close(cmdStopChan) } // make sure previous execution does not survive killedProcs, numProcs, killErr := t.findAndKill(pid) tcpdumpLogger.Printf("STOP [tcpdump(%d)] <%d/%d>: %+v\n", pid, killedProcs, numProcs, cmdLine) t.isActive.Store(false) return errors.Join(ctx.Err(), err, killErr) } func NewTcpdump(config *PcapConfig) (PcapEngine, error) { tcpdumpBin, err := exec.LookPath("tcpdump") if err != nil { return nil, fmt.Errorf("tcpdump is unavailable") } var isActive atomic.Bool isActive.Store(false) tcpdump := Tcpdump{config: config, tcpdump: tcpdumpBin, isActive: &isActive} return &tcpdump, nil }