diff --git a/.gitignore b/.gitignore index fd6babc..ce14d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ *.syso coverage.* __debug_bin +/cshargextcap +/cshargextcap.exe +*.log diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 53f05e2..e0c5b3a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -30,7 +30,7 @@ builds: - netgo - osusergo ldflags: - - 's -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser' + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser' hooks: post: - cmd: packaging/windows/post.sh {{ .Path }} diff --git a/Makefile b/Makefile index 04f1166..bb6183d 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ SHELL:=/bin/bash GOGEN:=go generate . BUILDTAGS:="osusergo,netgo" -.PHONY: help clean dist pkgsite report run vuln +.PHONY: help clean cshargextcap dist pkgsite report run vuln help: ## list available targets @# Derived from Gomega's Makefile (github.com/onsi/gomega) under MIT License @@ -21,6 +21,12 @@ dist: ## build snapshot cshargextcap binary packages+archives in dist/ and test @ls -lh dist/cshargextcap_* @echo "🏁 done" +cshargextcap: ## build local extcap binary + go build \ + -tags netgo,osusergo \ + -ldflags "-s -w" \ + ./cmd/cshargextcap + clean: ## cleans up build and testing artefacts rm -rf dist find . -name __debug_bin -delete diff --git a/capturestream.go b/capturestream.go index 8ee46f6..0f9ed4f 100644 --- a/capturestream.go +++ b/capturestream.go @@ -13,26 +13,36 @@ package cshargextcap import ( - "context" "os" + "os/signal" "strings" + "syscall" "github.com/siemens/csharg" "github.com/siemens/cshargextcap/cli/target" "github.com/siemens/cshargextcap/cli/wireshark" - "github.com/siemens/cshargextcap/pipe" log "github.com/sirupsen/logrus" ) -// Capture is the workhorse: it opens the pipe (fifo) offered by Wireshark, then -// starts a new Capture stream using the given SharkTank client and container -// target description. Then it lets csharg pump all packet Capture data arriving -// from the underlying websocket connected to the Capture service into the -// Wireshark pipe. +// Capture is the workhorse: it opens the named pipe (fifo) offered by +// Wireshark, then starts a new Capture stream using the given SharkTank client +// and container target description. Then it lets csharg pump all packet Capture +// data arriving from the underlying websocket connected to the capture service +// into the Wireshark pipe. func Capture(st csharg.SharkTank) int { + // While Wireshark (and Tshark) currently send SIGTERM (and maybe SIGINT in + // some situations, maybe when using a control pipe which we don't) only on + // unix systems, there are developer discussions to in the future send + // events to a Windows extcap. As Go maps such events to its signal API + // we're already now unconditionally handling SIGINT and SIGTERM in the hope + // that we're future-proof. + defer func() { + signal.Reset(syscall.SIGINT, syscall.SIGTERM) + }() + // Open packet stream pipe to Wireshark to feed it jucy packets... - log.Debugf("fifo to Wireshark %s", wireshark.FifoPath) + log.Debugf("opening fifo to Wireshark %s", wireshark.FifoPath) fifo, err := os.OpenFile(wireshark.FifoPath, os.O_WRONLY, 0) if err != nil { log.Errorf("cannot open fifo: %s", err.Error()) @@ -64,16 +74,39 @@ func Capture(st csharg.SharkTank) int { log.Errorf("cannot start capture: %s", err.Error()) return 1 } - defer cs.Stop() // be overly careful - // Always keep an eye on the fifo getting closed by Wireshark: we then need - // to stop the capture stream. This is necessary because the capture stream - // might be idle for long times and thus we would otherwise not notice that - // Wireshark has already stopped capturing. + // Wireshark on unix systems sends SIGINT upon stopping a capture and + // SIGTERM upon wanting to quit. We here use Debug logs as otherwise + // Wireshark will report the logging as errors to the user. We only accept + // that in case of a fatal abort when catching one of the signals twice or + // one after the other. + sigs := make(chan os.Signal, 1) go func() { - pipe.WaitTillBreak(context.Background(), fifo) - cs.Stop() + fatal := false + for sig := range sigs { + switch sig { + case syscall.SIGINT: + log.Debug("received SIGINT") + case syscall.SIGTERM: + log.Debug("received SIGTERM") + } + if fatal { + // twice a signal --> immediate abort + log.Fatal("aborting") + } + fatal = true + log.Debug("shutting down capture stream") + go func() { + cs.Stop() // blocks, and is also idempotent. + }() + } }() + // As mentioned above, we unconditionally handle SIGINT and SIGTERM on all + // platforms. While this is currently not needed on Windows, some day it + // might become alive. + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + defer cs.Stop() // be overly careful // ...and finally wait for the packet capture to terminate (or getting // ex-term-inated). cs.Wait() diff --git a/pipe/checker_notwin.go b/pipe/checker_notwin.go deleted file mode 100644 index 5afdd43..0000000 --- a/pipe/checker_notwin.go +++ /dev/null @@ -1,65 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -//go:build !windows - -package pipe - -import ( - "context" - "os" - - "golang.org/x/sys/unix" - - log "github.com/sirupsen/logrus" -) - -// WaitTillBreak continuously checks a fifo/pipe's producer end (writing end) to -// see when it breaks. When called, WaitTillBreak blocks until the fifo/pipe -// finally has broken. It also returns when the passed context is done. -// -// This implementation leverages [unix.Poll]. -func WaitTillBreak(ctx context.Context, fifo *os.File) { - log.Debug("constantly monitoring packet capture fifo status...") - for { - select { - case <-ctx.Done(): - log.Debug("context done while monitoring packet capture fifo") - return - default: - } - // Check the fifo becomming readable, which signals that it has been - // closed. In this case, ex-termi-nate ;) Oh, and remember to correctly - // initialize the fdset each time before calling select() ... well, just - // because that's a good idea to do. :( - fd := fifo.Fd() // n.b. a closed *os.File returns a -1 fd. - if fd == ^uintptr(0) { - log.Debug("stopping packet capture fifo monitoring, as write end has been closed") - return - } - fds := []unix.PollFd{ - { - Fd: int32(fd), - Events: 0, // we're interested only in POLLERR and that is ignored here anyway. - }, - } - n, err := unix.Poll(fds, 100 /* ms */) - if err != nil { - if err == unix.EINTR { - continue - } - log.Debugf("capture fifo broken, reason: %s", err.Error()) - return - } - if n <= 0 { - continue - } - if fds[0].Revents&unix.POLLERR != 0 { - // Either the pipe was broken by Wireshark, or we did break it on - // purpose in the piping process. Anyway, we're done. - log.Debug("capture fifo broken, stopped monitoring.") - return - } - } -} diff --git a/pipe/checker_notwin_test.go b/pipe/checker_notwin_test.go deleted file mode 100644 index 6a522b9..0000000 --- a/pipe/checker_notwin_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package pipe - -import ( - "context" - "io" - "os" - "time" - - "golang.org/x/sys/unix" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gleak" - . "github.com/thediveo/success" -) - -var _ = Describe("pipes", func() { - - BeforeEach(func() { - goodgos := Goroutines() - DeferCleanup(func() { - Eventually(Goroutines).Within(2 * time.Second).ProbeEvery(100 * time.Millisecond). - ShouldNot(HaveLeaked(goodgos)) - }) - }) - - It("detects on the write end when a pipe breaks", func(ctx context.Context) { - // As Wireshark uses a named pipe it passes an extcap its name (path) - // and then expects the extcap to open this named pipe for writing - // packet capture data into it. For this test we simulate Wireshark - // closing its reading end and we must properly detect this situation on - // our writing end of the pipe. - By("creating a temporary named pipe/fifo and opening its ends") - tmpfifodir := Successful(os.MkdirTemp("", "test-fifo-*")) - defer os.RemoveAll(tmpfifodir) - - fifoname := tmpfifodir + "/fifo" - unix.Mkfifo(fifoname, 0660) - wch := make(chan *os.File) - go func() { - defer GinkgoRecover() - wch <- Successful(os.OpenFile(fifoname, os.O_WRONLY, 0)) - }() - - rch := make(chan *os.File) - go func() { - defer GinkgoRecover() - rch <- Successful(os.OpenFile(fifoname, os.O_RDONLY, 0)) - }() - - var r, w *os.File - Eventually(rch).Should(Receive(&r)) - Eventually(wch).Should(Receive(&w)) - defer w.Close() - - go func() { - defer GinkgoRecover() - By("continously draining the read end of the pipe into /dev/null") - null := Successful(os.OpenFile("/dev/null", os.O_WRONLY, 0)) - defer null.Close() - io.Copy(null, r) - By("pipe draining done") - }() - - go func() { - defer GinkgoRecover() - time.Sleep(2 * time.Second) - By("closing read end of pipe") - Expect(r.Close()).To(Succeed()) - }() - - go func() { - defer GinkgoRecover() - time.Sleep(300 * time.Microsecond) - By("writing some data into the pipe") - w.WriteString("Wireshark rulez") - }() - - By("waiting for pipe to break") - ctx, cancel := context.WithTimeout(ctx, 4*time.Second) - defer cancel() - start := time.Now() - WaitTillBreak(ctx, w) - Expect(ctx.Err()).To(BeNil(), "break detection failed") - Expect(time.Since(start).Milliseconds()).To( - BeNumerically(">", 1900), "false positive: pipe wasn't broken yet") - }) - -}) diff --git a/pipe/checker_windows.go b/pipe/checker_windows.go deleted file mode 100644 index 78508a2..0000000 --- a/pipe/checker_windows.go +++ /dev/null @@ -1,52 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -//go:build windows - -package pipe - -import ( - "context" - "os" - "syscall" - "time" - - log "github.com/sirupsen/logrus" -) - -// WaitTillBreak continuously checks a fifo/pipe to see when it breaks. When -// called, PipeChecker blocks until the fifo/pipe finally has broken. -// -// As the Windows platform lacks a generally useful [syscall.Select] -// implementation that can also handle pipes. Instead, we will try to write 0 -// octets at regular intervals to see if the pipe is broken. Usually, -// unsynchronized concurrent writes are a really bad idea, but in this case -// we're not really writing anything, but just poking things to see if they're -// dead already. -func WaitTillBreak(ctx context.Context, fifo *os.File) { - log.Debug("constantly monitoring packet capture fifo status...") - nothing := []byte{} - ticker := time.NewTicker(100 * time.Millisecond) - for { - select { - case <-ctx.Done(): - log.Debug("context done while monitoring packet capture fifo") - return - case <-ticker.C: - // Avoid the usual higher level writes, because of their - // optimizations. While at this time the Windows writer - // seems to write even zero-length data, we cannot be sure - // this will hold for all future. So dive down into the - // syscall basement to have full control. - n, err := syscall.Write(syscall.Handle(fifo.Fd()), nothing) - if n != 0 || err != nil { - // Either the pipe was broken by Wireshark, or we - // did break it on purpose in the piping process. - // Anyway, we're done. - log.Debug("capture fifo broken, stopped monitoring.") - return - } - } - } -} diff --git a/pipe/doc.go b/pipe/doc.go deleted file mode 100644 index f099c26..0000000 --- a/pipe/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package pipe implements waiting for a fifo/pipe to break. -*/ -package pipe diff --git a/pipe/package_test.go b/pipe/package_test.go deleted file mode 100644 index eaa254f..0000000 --- a/pipe/package_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package pipe - -import ( - "testing" - - log "github.com/sirupsen/logrus" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestContainerSharkExtCap(t *testing.T) { - log.SetLevel(log.DebugLevel) - - RegisterFailHandler(Fail) - RunSpecs(t, "cshargextcap/pipe") -}