Skip to content

Commit

Permalink
Replace -cmd command with stdin to follow UNIX philosophy
Browse files Browse the repository at this point in the history
  • Loading branch information
VaiTon committed Apr 19, 2024
1 parent 1cc8c14 commit 1ee8749
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 91 deletions.
48 changes: 19 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ More information on PCAP-over-IP can be found here:

`pcap-broker` supports the following features:

* Distributing packet data to one or more PCAP-over-IP listeners
* Execute a command to capture traffic, usually `tcpdump` (expects stdout to be pcap data)
* `pcap-broker` will exit if the capture command exits
* Distributing packet data to one or more PCAP-over-IP listeners
* Read from stdin pcap data (for example from a `tcpdump` command)
* `pcap-broker` will exit if the capture command exits

## Building

Expand All @@ -34,8 +34,6 @@ $ docker run -it pcap-broker --help
```shell
$ ./pcap-broker --help
Usage of ./pcap-broker:
-cmd string
command to execute for pcap data (eg: tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -)
-debug
enable debug logging
-json
Expand All @@ -48,16 +46,17 @@ Usage of ./pcap-broker:
Arguments can be passed via commandline:

```shell
$ ./pcap-broker -cmd "sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -"
$ sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w - | ./pcap-broker -listen :4242
```

Or alternatively via environment variables:

```shell
LISTEN_ADDRESS=:4242 PCAP_COMMAND='sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -' ./pcap-broker
```
```bash
#!/bin/bash
export LISTEN_ADDRESS=:4242

Using environment variables is useful when you are using `pcap-broker` in a Docker setup.
sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w - | ./pcap-broker
```

Now you can connect to it via TCP and stream PCAP data using `nc` and `tcpdump`:

Expand All @@ -74,27 +73,18 @@ $ tshark -i TCP@localhost:4242
# Acquiring PCAP data over SSH

One use case is to acquire PCAP from a remote machine over SSH and make this available via PCAP-over-IP.
Such a use case, including an example SSH command to bootstrap this, has been documented in the `docker-compose.yml.example` file:

```yaml
version: "3.2"

services:
pcap-broker-remote-host:
image: pcap-broker:latest
restart: always
volumes:
# mount local user's SSH key into container
- ~/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro
ports:
# make the PCAP-over-IP port also available on the host on port 4200
- 4200:4242
environment:
# Command to SSH into remote-host and execute tcpdump and filter out it's own SSH client traffic
PCAP_COMMAND: ssh root@remote-host -o StrictHostKeyChecking=no 'IFACE=$$(ip route show to default | grep -Po1 "dev \K\w+") && BPF=$$(echo $$SSH_CLIENT | awk "{printf \"not (host %s and port %s and %s)\", \$$1, \$$2, \$$3;}") && tcpdump -U --immediate-mode -ni $$IFACE $$BPF -s 65535 -w -'
LISTEN_ADDRESS: "0.0.0.0:4242"

```shell
$ ssh user@remotehost "sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -" | ./pcap-broker -listen :4242
```

> [!TIP]
> To filter out SSH traffic, you can use `tcpdump`'s `not port 22` filter:
> ```shell
> $ ssh user@remotehost "sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w - not port 22" | ./pcap-broker -listen :4242
> ```
## Background
This tool was initially written for Attack & Defend CTF purposes but can be useful in other situations where low latency is preferred, or whenever a no-nonsense PCAP-over-IP server is needed. During the CTF that Fox-IT participated in, `pcap-broker` allowed the Blue Team to capture network data once and disseminate this to other tools that natively support PCAP-over-IP, such as:
Expand Down
73 changes: 11 additions & 62 deletions cmd/pcap-broker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import (
"fmt"
"net"
"os"
"os/exec"
"os/signal"
"time"

"github.com/google/shlex"

"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
"github.com/google/gopacket/pcapgo"
Expand Down Expand Up @@ -55,7 +52,6 @@ func lookupHostnameWithTimeout(addr net.Addr, timeout time.Duration) (string, st
}

var (
pcapCommand = flag.String("cmd", "", "command to execute for pcap data (eg: tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -)")
listenAddress = flag.String("listen", "", "listen address for pcap-over-ip (eg: localhost:4242)")
noReverseLookup = flag.Bool("n", false, "disable reverse lookup of connecting PCAP-over-IP client IP address")
debug = flag.Bool("debug", false, "enable debug logging")
Expand All @@ -72,71 +68,30 @@ func main() {
})
}

ctx, cancelFunc := signal.NotifyContext(context.Background(), os.Interrupt)

if *debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}

if *pcapCommand == "" {
*pcapCommand = os.Getenv("PCAP_COMMAND")
if *pcapCommand == "" {
log.Fatal().Msg("PCAP_COMMAND or -cmd not set, see --help for usage")
}
}

if *listenAddress == "" {
*listenAddress = os.Getenv("LISTEN_ADDRESS")
if *listenAddress == "" {
*listenAddress = "localhost:4242"
}
}

log.Debug().Str("pcapCommand", *pcapCommand).Send()
log.Debug().Str("listenAddress", *listenAddress).Send()

ctx, cancelFunc := signal.NotifyContext(context.Background(), os.Interrupt)

// Create connections to PcapClient map
connMap := map[net.Conn]PcapClient{}

// Create a pipe for the command to write to, will be read by pcap.OpenOfflineFile
rStdout, wStdout, err := os.Pipe()
if err != nil {
log.Fatal().Err(err).Msg("failed to create pipe")
}

// Acquire pcap data
args, err := shlex.Split(*pcapCommand)
if err != nil {
log.Fatal().Err(err).Msg("failed to parse PCAP_COMMAND")
}
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
log.Debug().Strs("args", args).Send()

cmd.Stdout = wStdout
cmd.Stderr = log.Logger.Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
e.Str(zerolog.LevelFieldName, zerolog.LevelTraceValue)
}))

err = cmd.Start()
if err != nil {
log.Fatal().Err(err).Msg("failed to start command")
}
// Read from process stdout pipe
var pcapStream *os.File

log.Debug().Int("pid", cmd.Process.Pid).Msg("started process")
log.Info().Msg("reading pcap data from stdin. EOF to stop")
pcapStream = os.Stdin

// close context on process exit
go func() {
err := cmd.Wait()
if err != nil {
log.Fatal().Err(err).Msg("command exited with error")
}
cancelFunc()
}()

// Read from process stdout pipe
handle, err := pcap.OpenOfflineFile(rStdout)
handle, err := pcap.OpenOfflineFile(pcapStream)
if err != nil {
log.Fatal().Err(err).Msg("failed to open pcap file")
}
Expand All @@ -145,6 +100,9 @@ func main() {
packetSource.Lazy = true
packetSource.NoCopy = true

// Create connections to PcapClient map
connMap := map[net.Conn]PcapClient{}

go processPackets(ctx, packetSource, connMap)

log.Info().Msgf("PCAP-over-IP server listening on %v. press CTRL-C to exit", *listenAddress)
Expand All @@ -158,6 +116,7 @@ func main() {
// close listener on context cancel
go func() {
<-ctx.Done()
log.Debug().Msg("closing listener")
cancelFunc()
err := l.Close()
if err != nil {
Expand Down Expand Up @@ -203,16 +162,6 @@ func main() {
}

log.Info().Msg("PCAP-over-IP server exiting")

err = rStdout.Close()
if err != nil {
log.Err(err).Msg("failed to close read pipe")
}

err = wStdout.Close()
if err != nil {
log.Err(err).Msg("failed to close write pipe")
}
}

func processPackets(
Expand Down

0 comments on commit 1ee8749

Please sign in to comment.