mirror of
https://github.com/buger/goreplay.git
synced 2026-10-20 00:16:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca8205a5c5 | ||
|
|
8f5f3074b5 |
@@ -30,7 +30,10 @@ vendor:
|
||||
go mod vendor
|
||||
|
||||
release-bin: vendor
|
||||
docker run --rm -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER) go build -mod=vendor -o $(BIN_NAME) -tags netgo $(LDFLAGS)
|
||||
docker run --rm -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER) go build -mod=vendor -o $(BIN_NAME) -tags netgo $(LDFLAGS)
|
||||
|
||||
release-arm64-bin: vendor
|
||||
docker run --rm -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=arm64 -i $(CONTAINER) go build -mod=vendor -o $(BIN_NAME) -tags netgo $(LDFLAGS)
|
||||
|
||||
release-bin-mac: vendor
|
||||
GOOS=darwin go build -mod=vendor -o $(BIN_NAME) $(MAC_LDFLAGS)
|
||||
@@ -80,9 +83,17 @@ build:
|
||||
install:
|
||||
go install $(MAC_LDFLAGS)
|
||||
|
||||
build-env:
|
||||
build-env: build-x64-env build-arm64-env
|
||||
|
||||
build-x64-env:
|
||||
docker buildx build --platform linux/amd64 -t $(CONTAINER) -f Dockerfile.dev .
|
||||
|
||||
build-arm64-env:
|
||||
docker buildx build --platform linux/arm64 -t $(CONTAINER) -f Dockerfile.dev .
|
||||
|
||||
build-docker:
|
||||
docker build -t gor-dev -f Dockerfile .
|
||||
|
||||
profile:
|
||||
go build && ./$(BIN_NAME) --output-http="http://localhost:9000" --input-dummy 0 --input-raw :9000 --input-http :9000 --memprofile=./mem.out --cpuprofile=./cpu.out --stats --output-http-stats --output-http-timeout 100ms
|
||||
|
||||
|
||||
+80
-3
@@ -22,6 +22,10 @@ import (
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var stats *expvar.Map
|
||||
@@ -206,14 +210,81 @@ func (l *Listener) ListenBackground(ctx context.Context) chan error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Allowed format:
|
||||
// [namespace/]pod/[pod_name]
|
||||
// [namespace/]deployment/[deployment_name]
|
||||
// [namespace/]daemonset/[daemonset_name]
|
||||
// [namespace/]labelSelector/[selector]
|
||||
// [namespace/]fieldSelector/[selector]
|
||||
func k8sIPs(addr string) []string {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
// creates the clientset
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
sections := strings.Split(addr, "/")
|
||||
|
||||
if len(sections) < 2 {
|
||||
panic("Not supported k8s scheme. Allowed values: [namespace/]pod/[pod_name], [namespace/]deployment/[deployment_name], [namespace/]daemonset/[daemonset_name], [namespace/]label/[label-name]/[label-value]")
|
||||
}
|
||||
|
||||
// If no namespace passed, assume it is ALL
|
||||
switch sections[0] {
|
||||
case "pod", "deployment", "daemonset", "labelSelector", "fieldSelector":
|
||||
sections = append([]string{""}, sections...)
|
||||
}
|
||||
|
||||
namespace, selectorType, selectorValue := sections[0], sections[1], sections[2]
|
||||
|
||||
labelSelector := ""
|
||||
fieldSelector := ""
|
||||
|
||||
switch selectorType {
|
||||
case "pod":
|
||||
fieldSelector = "metadata.name=" + selectorValue
|
||||
case "deployment":
|
||||
labelSelector = "app=" + selectorValue
|
||||
case "daemonset":
|
||||
labelSelector = "pod-template-generation=1,name=" + selectorValue
|
||||
case "labelSelector":
|
||||
labelSelector = selectorValue
|
||||
case "fieldSelector":
|
||||
fieldSelector = selectorValue
|
||||
}
|
||||
|
||||
pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector, FieldSelector: fieldSelector})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
var podIPs []string
|
||||
for _, pod := range pods.Items {
|
||||
for _, podIP := range pod.Status.PodIPs {
|
||||
podIPs = append(podIPs, podIP.IP)
|
||||
}
|
||||
}
|
||||
return podIPs
|
||||
}
|
||||
|
||||
// Filter returns automatic filter applied by goreplay
|
||||
// to a pcap handle of a specific interface
|
||||
func (l *Listener) Filter(ifi pcap.Interface) (filter string) {
|
||||
// https://www.tcpdump.org/manpages/pcap-filter.7.html
|
||||
|
||||
hosts := []string{l.host}
|
||||
if listenAll(l.host) || isDevice(l.host, ifi) {
|
||||
hosts = interfaceAddresses(ifi)
|
||||
|
||||
if strings.HasPrefix(l.host, "k8s://") {
|
||||
hosts = k8sIPs(l.host[6:])
|
||||
} else {
|
||||
if listenAll(l.host) || isDevice(l.host, ifi) {
|
||||
hosts = interfaceAddresses(ifi)
|
||||
}
|
||||
}
|
||||
|
||||
filter = portsFilter(l.Transport, "dst", l.ports)
|
||||
@@ -357,7 +428,7 @@ func http1EndHint(m *tcp.Message) bool {
|
||||
if m.MissingChunk() {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
req, res := http1StartHint(m.Packets()[0])
|
||||
return proto.HasFullPayload(m, m.PacketData()...) && (req || res)
|
||||
}
|
||||
@@ -575,6 +646,12 @@ func (l *Listener) setInterfaces() (err error) {
|
||||
}
|
||||
|
||||
for _, pi := range pifis {
|
||||
if strings.HasPrefix(l.host, "k8s://") {
|
||||
if !strings.HasPrefix(pi.Name, "veth") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if isDevice(l.host, pi) {
|
||||
l.Interfaces = []pcap.Interface{pi}
|
||||
return
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// +build linux
|
||||
//go:build linux && !arm64
|
||||
// +build linux,!arm64
|
||||
|
||||
package capture
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// +build !linux
|
||||
//go:build (!linux && ignore) || arm64 || darwin
|
||||
// +build !linux,ignore arm64 darwin
|
||||
|
||||
package capture
|
||||
|
||||
|
||||
@@ -7,14 +7,19 @@ require (
|
||||
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61 // indirect
|
||||
github.com/aws/aws-sdk-go v1.33.2
|
||||
github.com/bitly/go-hostpool v0.1.0 // indirect
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
github.com/containerd/containerd v1.5.9 // indirect
|
||||
github.com/docker/distribution v2.8.0+incompatible // indirect
|
||||
github.com/docker/docker v20.10.12+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/google/gopacket v1.1.20-0.20210429153827-3eaba0894325
|
||||
github.com/klauspost/compress v1.10.10 // indirect
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e
|
||||
k8s.io/apimachinery v0.23.3
|
||||
k8s.io/client-go v0.23.3
|
||||
)
|
||||
|
||||
@@ -56,6 +56,10 @@ func NewRAWInput(address string, config RAWInputConfig) (i *RAWInput) {
|
||||
host = address
|
||||
_ports = "0"
|
||||
err = nil
|
||||
} else if strings.HasPrefix(address, "k8s://") {
|
||||
portIndex := strings.LastIndex(address, ":")
|
||||
host = address[:portIndex]
|
||||
_ports = address[portIndex+1:]
|
||||
} else {
|
||||
log.Fatalf("input-raw: error while parsing address: %s", err)
|
||||
}
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Native k8s integration
|
||||
|
||||
After following steps below, you will be able to capture traffic inside k8s like this:
|
||||
|
||||
```
|
||||
gor --input-raw k8s://namespace/deployment/app:80 --output-http http://replay.com
|
||||
```
|
||||
|
||||
GoReplay will running as a daemonset (e.g. on each phisical k8s node.
|
||||
It will also require giving required permission to have read access to K8s APIs, so it can dynamically filter traffic for a specific pods.
|
||||
|
||||
Supported format for filtering required pods:
|
||||
|
||||
```
|
||||
k8s://[namespace/]pod/[pod_name] - k8s://default/pod/nginx-7848d4b86f-5nxz8
|
||||
k8s://[namespace/]deployment/[deployment_name] - k8s://default/deployment/nginx
|
||||
k8s://[namespace/]daemonset/[daemonset_name] - k8s://default/daemonset/nginx
|
||||
k8s://[namespace/]labelSelector/[selector] - k8s://default/labelSelector/app=nginx
|
||||
k8s://[namespace/]fieldSelector/[selector] - k8s://default/fieldSelector/metadata.name=nginx-7848d4b86f-5nxz8
|
||||
```
|
||||
`namespace` is optional, omit to use all namespaces: `k8s://labelSelector/app=replay`
|
||||
|
||||
## 1. Create a namespace
|
||||
`kubectl create namespace goreplay`
|
||||
|
||||
## 2. Create the Kubernetes service account in the namespace:
|
||||
|
||||
`kubectl create serviceaccount goreplay --namespace goreplay`
|
||||
|
||||
## 3. Create Cluster Role which gives read-only access to the pods:
|
||||
`kubectl -n goreplay -f clusterrole.yaml apply`
|
||||
|
||||
```yaml
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: pod-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["daemonset"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
```
|
||||
|
||||
## 4. Attach role to goreplay service account
|
||||
`kubectl -n goreplay -f rolebinding.yaml apply`
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: goreplay-reader-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: goreplay
|
||||
namespace: goreplay
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: pod-reader
|
||||
apiGroup: ""
|
||||
```
|
||||
|
||||
## 5. Start goreplay daemonset
|
||||
|
||||
`kubectl -n goreplay -f goreplay.yaml apply`
|
||||
|
||||
In arguments, specify which service you want to capture.
|
||||
Following format supported:
|
||||
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: goreplay-daemon
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
hostNetwork: true
|
||||
serviceAccountName: goreplay
|
||||
containers:
|
||||
- name: goreplay
|
||||
image: buger/goreplay:latest
|
||||
command:
|
||||
- "--input-raw k8s://deployments/nginx:80"
|
||||
- "--output-stdout"
|
||||
```
|
||||
|
||||
## 6. Create a simple http service (Optionally)
|
||||
|
||||
`kubectl -n default -f nginx.yaml apply`
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ngnix-service
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: NodePort
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 7. Verify installation
|
||||
|
||||
Find url for your service using `kubectl get svc` or `minikube service --url ngnix-service -n http`, and make a call to it.
|
||||
|
||||
Get GoReplay logs, and check if it capture traffic of your service.
|
||||
`kubectl logs -n goreplay -l name=goreplay-daemon --all-containers`
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: pod-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["daemonsets"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: goreplay-daemon
|
||||
namespace: goreplay
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: goreplay-daemon
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: goreplay-daemon
|
||||
spec:
|
||||
hostNetwork: true
|
||||
serviceAccountName: goreplay
|
||||
containers:
|
||||
- name: goreplay-daemon
|
||||
image: nicolaka/netshoot
|
||||
command: [ "/bin/bash", "-c", "--" ]
|
||||
args: [ "while true; do sleep 30; done;" ]
|
||||
@@ -0,0 +1,36 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ngnix-service
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: NodePort
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: goreplay-reader-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: goreplay
|
||||
namespace: goreplay
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: pod-reader
|
||||
apiGroup: ""
|
||||
Reference in New Issue
Block a user