├── pkg ├── event │ ├── event.go │ ├── call │ │ ├── mincore_test.go │ │ ├── seccomp_test.go │ │ ├── setsockopt.go │ │ ├── getsockopt.go │ │ ├── close_test.go │ │ ├── setuid_test.go │ │ ├── exit_test.go │ │ ├── alarm_test.go │ │ ├── brk_test.go │ │ ├── rmdir_test.go │ │ ├── acct_test.go │ │ ├── chdir_test.go │ │ ├── chroot_test.go │ │ ├── kill_test.go │ │ ├── brk.go │ │ ├── close.go │ │ ├── exit.go │ │ ├── getcwd_test.go │ │ ├── setuid.go │ │ ├── creat_test.go │ │ ├── rename_test.go │ │ ├── futex_text.go │ │ ├── pivot_root_test.go │ │ ├── alarm.go │ │ ├── link_test.go │ │ ├── fstat_test.go │ │ ├── acct.go │ │ ├── chdir.go │ │ ├── rmdir.go │ │ ├── symlink_test.go │ │ ├── umount2_test.go │ │ ├── unlink_test.go │ │ ├── chroot.go │ │ ├── setns_test.go │ │ ├── unlink.go │ │ ├── execve_test.go │ │ ├── init_module_test.go │ │ ├── mkdir_test.go │ │ ├── listen_test.go │ │ ├── access_test.go │ │ ├── ftruncate_test.go │ │ ├── ptrace_test.go │ │ ├── read_test.go │ │ ├── syslog_test.go │ │ ├── mount_test.go │ │ ├── kill.go │ │ ├── timerfd_create_test.go │ │ ├── write_test.go │ │ ├── mprotect_test.go │ │ ├── faccessat_test.go │ │ ├── clone_test.go │ │ ├── socket_test.go │ │ ├── getcwd.go │ │ ├── open_test.go │ │ ├── openat_test.go │ │ ├── connect.go │ │ ├── readlinkat_test.go │ │ ├── nanosleep_test.go │ │ ├── nanosleep.go │ │ ├── link.go │ │ ├── listen.go │ │ ├── ftruncate.go │ │ ├── fstat.go │ │ ├── bind.go │ │ ├── creat.go │ │ ├── rename.go │ │ ├── access.go │ │ ├── inotify_add_watch_test.go │ │ ├── symlink.go │ │ ├── getpeername.go │ │ ├── getsockname.go │ │ ├── prlimit64_test.go │ │ ├── statfs.go │ │ ├── umount2.go │ │ ├── mkdir.go │ │ ├── recvfrom_test.go │ │ ├── sockopt_test.go │ │ ├── stat.go │ │ ├── pivot_root.go │ │ ├── timerfd_create.go │ │ ├── futex.go │ │ ├── bind_test.go │ │ ├── connect_test.go │ │ ├── setsockopt_test.go │ │ ├── syslog.go │ │ ├── getsockopt_test.go │ │ ├── mincore.go │ │ ├── getpeername_test.go │ │ ├── seccomp.go │ │ ├── getsockname_test.go │ │ ├── init_module.go │ │ ├── openat.go │ │ ├── sendto_test.go │ │ ├── execve.go │ │ ├── open.go │ │ ├── setns.go │ │ ├── read.go │ │ ├── write.go │ │ ├── readlink.go │ │ ├── faccessat.go │ │ ├── timerfd_settime.go │ │ ├── stat_test.go │ │ ├── statfs_test.go │ │ ├── timerfd_settime_test.go │ │ ├── accept_test.go │ │ ├── sendto.go │ │ ├── readlinkat.go │ │ ├── clone.go │ │ ├── recvfrom.go │ │ ├── mprotect.go │ │ ├── function.go │ │ └── accept.go │ ├── reader │ │ ├── reader.go │ │ ├── reader_test.go │ │ └── kernel.go │ └── types.go ├── types │ ├── flags.go │ ├── buffer.go │ ├── fd.go │ ├── dirfd.go │ ├── xmode.go │ ├── c.go │ ├── umountflags.go │ ├── container.go │ ├── cloneflags.go │ ├── msgflags.go │ ├── clocktypes.go │ ├── sockaddr.go │ └── fileflags.go ├── kernel │ ├── fixtures │ │ ├── dummy_probe.o │ │ ├── Makefile │ │ └── dummy_probe.c │ ├── doc.go │ ├── assets │ │ └── loader.go │ ├── probe_test.go │ ├── ross │ │ └── bob.go │ ├── metrics │ │ ├── handler.go │ │ └── metrics.go │ └── offsets.go ├── topology │ ├── consts.go │ ├── hash.go │ ├── kubernetes_test.go │ ├── ns.go │ ├── doc.go │ └── hub_test.go └── syscalls │ └── syscalls_test.go ├── chart ├── templates │ ├── NOTES.txt │ ├── server-service-account.yaml │ ├── controller-service-account.yaml │ ├── server-cluster-role.yaml │ ├── server-cluster-rolebinding.yaml │ ├── controller-cluster-rolebinding.yaml │ ├── controller-rolebinding.yaml │ ├── controller-role.yaml │ ├── server-service.yaml │ ├── controller-cluster-role.yaml │ ├── controller-deployment.yaml │ ├── server-daemonset.yaml │ └── _helpers.tpl ├── Chart.yaml ├── .helmignore └── values.yaml ├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── webhook │ ├── kustomization.yaml │ ├── service.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── kustomization.yaml │ ├── patches │ │ ├── cainjection_in_traces.yaml │ │ └── webhook_in_traces.yaml │ └── kustomizeconfig.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── trace_viewer_role.yaml │ ├── trace_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── default │ ├── kustomization.yaml │ ├── manager_webhook_patch.yaml │ ├── webhookcainjection_patch.yaml │ └── manager_auth_proxy_patch.yaml └── samples │ └── tools_v1alpha1_trace.yaml ├── media ├── logo.png ├── charts-ss.png ├── swoll-top.png ├── swoll-banner.png └── running-a-trace.gif ├── internal ├── bpf │ ├── entrypoint.sh │ ├── Dockerfile │ ├── asm_goto_workaround.h │ └── Makefile ├── Makefile ├── deploy │ ├── manifests │ │ ├── service.yaml │ │ ├── rbac.yaml │ │ └── probe.yaml │ └── config.yaml └── pkg │ └── alert │ ├── parser.go │ ├── alert.go │ ├── prometheus.go │ └── parser_test.go ├── main.go ├── examples ├── kubernetes-basic │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── deploy.yaml │ └── main.go ├── kubernetes-hub │ ├── Dockerfile │ ├── Makefile │ └── deploy.yaml └── basic-trace │ └── main.go ├── .whitesource ├── hack ├── tools │ ├── tools.go │ └── pretty_logs.go └── boilerplate.go.txt ├── .gitignore ├── Dockerfile ├── .github └── workflows │ ├── create-release.yaml │ ├── lint-and-test.yaml │ └── push-image.yaml ├── cmd ├── loader.go ├── offsetter.go ├── prometheus.go └── root.go ├── go.mod ├── api └── v1alpha1 │ └── groupversion_info.go └── .goreleaser.yml /pkg/event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | -------------------------------------------------------------------------------- /pkg/types/flags.go: -------------------------------------------------------------------------------- 1 | package types 2 | -------------------------------------------------------------------------------- /chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | swoll stuff 2 | -------------------------------------------------------------------------------- /pkg/event/call/mincore_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/criticalstack/swoll/HEAD/media/logo.png -------------------------------------------------------------------------------- /internal/bpf/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | cd /bpf 4 | KERNELDIR=/kernel make 5 | -------------------------------------------------------------------------------- /media/charts-ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/criticalstack/swoll/HEAD/media/charts-ss.png -------------------------------------------------------------------------------- /media/swoll-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/criticalstack/swoll/HEAD/media/swoll-top.png -------------------------------------------------------------------------------- /media/swoll-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/criticalstack/swoll/HEAD/media/swoll-banner.png -------------------------------------------------------------------------------- /media/running-a-trace.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/criticalstack/swoll/HEAD/media/running-a-trace.gif -------------------------------------------------------------------------------- /pkg/event/call/seccomp_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import "testing" 4 | 5 | func TestSeccomp(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /pkg/kernel/fixtures/dummy_probe.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/criticalstack/swoll/HEAD/pkg/kernel/fixtures/dummy_probe.o -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /pkg/kernel/doc.go: -------------------------------------------------------------------------------- 1 | // The kernel package contains APIs to create and communicate with the running BPF. 2 | package kernel 3 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /internal/Makefile: -------------------------------------------------------------------------------- 1 | all: bpf 2 | 3 | bpf: 4 | $(MAKE) -C bpf/ 5 | 6 | clean: 7 | $(MAKE) -C bpf/ clean 8 | 9 | .PHONY: all bpf 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/criticalstack/swoll/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /examples/kubernetes-basic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | WORKDIR /root/ 3 | RUN apk add --no-cache binutils 4 | COPY kube-trace ./ 5 | CMD ["./kube-trace"] 6 | -------------------------------------------------------------------------------- /examples/kubernetes-hub/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | WORKDIR /root/ 3 | RUN apk add --no-cache binutils 4 | COPY kubernetes-hub ./ 5 | CMD ["./kubernetes-hub"] 6 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/tools.swoll.criticalstack.com_traces.yaml 3 | 4 | patchesStrategicMerge: 5 | 6 | configurations: 7 | - kustomizeconfig.yaml 8 | -------------------------------------------------------------------------------- /pkg/types/buffer.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Buffer []byte 4 | 5 | /* 6 | // this was dumb.. 7 | func (b Buffer) String() string { 8 | return "\n" + hex.Dump(b) 9 | } 10 | */ 11 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: controller 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: swoll 2 | namePrefix: swoll- 3 | 4 | bases: 5 | - ../crd 6 | - ../rbac 7 | - ../manager 8 | 9 | patchesStrategicMerge: 10 | - manager_auth_proxy_patch.yaml 11 | 12 | vars: 13 | -------------------------------------------------------------------------------- /pkg/topology/consts.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | const ( 4 | // The prefix for where job-specific events are sent 5 | swJobStream = "job" 6 | // The prefix for where non-job-specific events (pathed) are sent 7 | swNsStream = "ns" 8 | ) 9 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: swoll 3 | description: an experimental suite of applications and APIs for monitoring kernel-level activity on a live Kubernetes cluster 4 | type: application 5 | version: 0.1.0 6 | appVersion: 1.16.0 7 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /chart/templates/server-service-account.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.server.enabled -}} 2 | {{- if .Values.server.serviceAccount.create }} 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: {{ template "swoll-server.serviceAccountName" . }} 7 | {{- end }} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "MEDIUM" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pkg/types/fd.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // used to mark a file-descriptor as an INPUT type, such as: 4 | // bind(, ...) 5 | type InputFD int 6 | 7 | // used to mark a file-descriptor as an OUTPUT type, such as: 8 | // = socket(...) 9 | type OutputFD int 10 | -------------------------------------------------------------------------------- /chart/templates/controller-service-account.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.controller.enabled -}} 2 | {{- if .Values.controller.serviceAccount.create }} 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: {{ template "swoll-controller.serviceAccountName" . }} 7 | {{- end }} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /pkg/event/call/setsockopt.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | type Setsockopt struct { 4 | Sockopt 5 | } 6 | 7 | func (s *Setsockopt) CallName() string { return "setsockopt" } 8 | func (s *Setsockopt) Return() *Argument { return nil } 9 | 10 | // other interface handlers are defined by Sockopt 11 | -------------------------------------------------------------------------------- /hack/tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | // This package imports things required by build scripts, to force `go mod` to see them as dependencies 4 | package tools 5 | 6 | import ( 7 | _ "github.com/go-bindata/go-bindata/go-bindata" 8 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/event/call/getsockopt.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | type Getsockopt struct { 4 | Sockopt `json:"sockopt"` 5 | } 6 | 7 | func (g *Getsockopt) CallName() string { return "getsockopt" } 8 | func (g *Getsockopt) Return() *Argument { return nil } 9 | 10 | // other interface handlers are defined by Sockopt 11 | -------------------------------------------------------------------------------- /internal/deploy/manifests/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: swoll-probe 5 | namespace: swoll 6 | spec: 7 | selector: 8 | app: swoll-probe 9 | type: NodePort 10 | ports: 11 | - port: 9095 12 | targetPort: 9095 13 | nodePort: 32095 14 | -------------------------------------------------------------------------------- /chart/templates/server-cluster-role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.server.enabled -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ template "swoll.fullname" . }}-server 6 | rules: 7 | - apiGroups: [""] 8 | resources: ["pods"] 9 | verbs: ["get", "watch", "list"] 10 | {{- end -}} 11 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /internal/bpf/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:unstable 2 | 3 | RUN apt-get update \ 4 | && apt-get dist-upgrade -y \ 5 | && apt-get install -y --no-install-recommends \ 6 | clang \ 7 | gcc \ 8 | libelf-dev \ 9 | libelf1 \ 10 | llvm \ 11 | make \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | COPY entrypoint.sh / 15 | 16 | ENTRYPOINT ["/entrypoint.sh"] 17 | -------------------------------------------------------------------------------- /pkg/types/dirfd.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type DirFD int 8 | 9 | func (d DirFD) String() string { 10 | if d == -100 { 11 | return "AT_FDCWD" 12 | } 13 | 14 | return fmt.Sprintf("%d", d) 15 | } 16 | 17 | /* 18 | func (d DirFD) MarshalJSON() ([]byte, error) { 19 | return json.Marshal(d.String()) 20 | } 21 | */ 22 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/samples/tools_v1alpha1_trace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tools.swoll.criticalstack.com/v1alpha1 2 | kind: Trace 3 | metadata: 4 | name: monitor-nginx 5 | spec: 6 | syscalls: 7 | - execve 8 | - openat 9 | labelSelector: 10 | matchLabels: 11 | app: "nginx" 12 | fieldSelector: 13 | matchLabels: 14 | status.phase: "Running" 15 | -------------------------------------------------------------------------------- /pkg/kernel/assets/loader.go: -------------------------------------------------------------------------------- 1 | // +build !nobindata 2 | 3 | package assets 4 | 5 | import ( 6 | "bytes" 7 | ) 8 | 9 | const ( 10 | defaultBPFObject = "internal/bpf/probe.o" 11 | ) 12 | 13 | func LoadBPFReader() *bytes.Reader { 14 | return bytes.NewReader(LoadBPF()) 15 | } 16 | 17 | func LoadBPF() []byte { 18 | bpf, _ := Asset(defaultBPFObject) 19 | return bpf 20 | } 21 | -------------------------------------------------------------------------------- /pkg/kernel/probe_test.go: -------------------------------------------------------------------------------- 1 | package kernel 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestProbe(t *testing.T) { 10 | bpf, err := ioutil.ReadFile("fixtures/dummy_probe.o") 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | p, err := NewProbe(bytes.NewReader(bpf), nil) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | _ = p 20 | } 21 | -------------------------------------------------------------------------------- /internal/pkg/alert/parser.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import "io" 4 | 5 | // Parser is an abstraction interface around parsing raw alert data. 6 | type Parser interface { 7 | ParseAlerts(r io.Reader) ([]*Alert, error) 8 | } 9 | 10 | // ParseAlerts runs the parser `p` against the data in `r` 11 | func ParseAlerts(p Parser, r io.Reader) ([]*Alert, error) { 12 | return p.ParseAlerts(r) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | internal/bpf/.built-in.a.cmd 2 | internal/bpf/built-in.a 3 | internal/bpf/Module.symvers 4 | internal/bpf/modules.order 5 | internal/bpf/probe.ll 6 | internal/bpf/probe.o 7 | internal/bpf/probe_metrics.ll 8 | internal/bpf/probe_metrics.o 9 | internal/bpf/probe_tracer.ll 10 | internal/bpf/probe_tracer.o 11 | 12 | bin/* 13 | data/* 14 | *.pem 15 | probe_tmp/ 16 | dist/ 17 | hack/tools/bin 18 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_traces.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: traces.tools.swoll.criticalstack.com 9 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | #- auth_proxy_service.yaml 10 | #- auth_proxy_role.yaml 11 | #- auth_proxy_role_binding.yaml 12 | #- auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | criEndpoint: /run/containerd/containerd.sock 2 | image: 3 | repository: criticalstack/swoll 4 | tag: latest 5 | pullPolicy: Always 6 | 7 | server: 8 | enabled: true 9 | service: 10 | type: NodePort 11 | port: 9095 12 | targetPort: 9095 13 | nodePort: 32095 14 | enablePrometheus: true 15 | serviceAccount: 16 | create: true 17 | 18 | controller: 19 | enabled: true 20 | serviceAccount: 21 | create: true 22 | -------------------------------------------------------------------------------- /config/rbac/trace_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view traces. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: trace-viewer-role 6 | rules: 7 | - apiGroups: 8 | - tools.swoll.criticalstack.com 9 | resources: 10 | - traces 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - tools.swoll.criticalstack.com 17 | resources: 18 | - traces/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /chart/templates/server-cluster-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.server.enabled -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ template "swoll.fullname" . }}-server 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: {{ template "swoll.fullname" . }}-server 10 | subjects: 11 | - kind: ServiceAccount 12 | name: {{ template "swoll-server.serviceAccountName" . }} 13 | namespace: {{ .Release.Namespace }} 14 | {{- end -}} 15 | -------------------------------------------------------------------------------- /config/rbac/trace_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit traces. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: trace-editor-role 6 | rules: 7 | - apiGroups: 8 | - tools.swoll.criticalstack.com 9 | resources: 10 | - traces 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - tools.swoll.criticalstack.com 21 | resources: 22 | - traces/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /internal/deploy/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cinder.crit.sh/v1alpha1 2 | kind: ClusterConfiguration 3 | featureGates: 4 | LocalRegistry: true 5 | postCritCommands: 6 | - | 7 | kubectl create namespace swoll 8 | /swoll/bin/swoll selftest -b /swoll/bpf/probe.o -r /run/containerd/containerd.sock -k /etc/kubernetes/admin.conf --fix 9 | extraMounts: 10 | - hostPath: internal/bpf 11 | containerPath: /swoll/bpf 12 | readOnly: true 13 | - hostPath: bin 14 | containerPath: /swoll/bin 15 | readOnly: true 16 | 17 | -------------------------------------------------------------------------------- /pkg/topology/hash.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import "hash/crc64" 4 | 5 | var crcTable = crc64.MakeTable(crc64.ECMA) 6 | 7 | func hash(s string) uint64 { 8 | h := crc64.Checksum([]byte(s), crcTable) 9 | 10 | if h == 0 { 11 | return 1 12 | } 13 | 14 | return h 15 | } 16 | 17 | func hashPath(paths ...string) []uint64 { 18 | ret := []uint64{hash("/")} 19 | 20 | for _, path := range paths { 21 | if path == "" { 22 | return ret 23 | } 24 | 25 | ret = append(ret, hash(path)) 26 | } 27 | 28 | return ret 29 | } 30 | -------------------------------------------------------------------------------- /chart/templates/controller-cluster-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.controller.enabled -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ template "swoll.fullname" . }}-controller 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: {{ template "swoll.fullname" . }}-controller 10 | subjects: 11 | - kind: ServiceAccount 12 | name: {{ template "swoll-controller.serviceAccountName" . }} 13 | namespace: {{ .Release.Namespace }} 14 | {{- end -}} 15 | -------------------------------------------------------------------------------- /pkg/event/call/close_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestClose(t *testing.T) { 10 | s := &Close{ 11 | FD: 1, 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | var c Close 19 | if err := json.Unmarshal(j, &c); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if !reflect.DeepEqual(s.Arguments(), c.Arguments()) { 24 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), c.Arguments()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /chart/templates/controller-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.controller.enabled -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ template "swoll.fullname" . }}-controller 6 | namespace: {{ .Release.Namespace }} 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: Role 10 | name: swoll-leader-election-role 11 | subjects: 12 | - kind: ServiceAccount 13 | name: {{ template "swoll-controller.serviceAccountName" . }} 14 | namespace: {{ .Release.Namespace }} 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /pkg/event/call/setuid_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestSetuid(t *testing.T) { 10 | s := &Setuid{ 11 | UID: 5, 12 | } 13 | j, err := json.Marshal(s) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | var a Setuid 19 | if err := json.Unmarshal(j, &a); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 24 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/kubernetes-basic/Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY=docker.io 2 | USERNAME=errzey 3 | APP_NAME=swoll-kube-test 4 | 5 | binary: 6 | go build -ldflags="-extldflags=-static" -o kube-trace 7 | 8 | build: binary 9 | docker build -t ${USERNAME}/${APP_NAME} . 10 | 11 | push: 12 | docker push ${USERNAME}/${APP_NAME} 13 | @echo "run: make deploy" 14 | 15 | all: build binary 16 | @echo "run: make push" 17 | 18 | deploy: 19 | kubectl apply -f deploy.yaml 20 | 21 | uninstall: 22 | kubectl delete -f deploy.yaml 23 | 24 | clean: 25 | rm ./kube-trace 26 | -------------------------------------------------------------------------------- /pkg/event/call/exit_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestExit(t *testing.T) { 10 | s := &Exit{ 11 | Code: 1, 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var e Exit 20 | if err := json.Unmarshal(j, &e); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if !reflect.DeepEqual(s.Arguments(), e.Arguments()) { 25 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), e.Arguments()) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /examples/kubernetes-hub/Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY=docker.io 2 | USERNAME=errzey 3 | APP_NAME=swoll-hub-test 4 | 5 | binary: 6 | go build -ldflags="-extldflags=-static" -o kubernetes-hub 7 | 8 | build: binary 9 | docker build -t ${USERNAME}/${APP_NAME} . 10 | 11 | push: 12 | docker push ${USERNAME}/${APP_NAME} 13 | @echo "run: make deploy" 14 | 15 | all: build binary 16 | @echo "run: make push" 17 | 18 | deploy: 19 | kubectl apply -f deploy.yaml 20 | 21 | uninstall: 22 | kubectl delete -f deploy.yaml 23 | 24 | clean: 25 | rm ./kubernetes-hub 26 | -------------------------------------------------------------------------------- /pkg/event/call/alarm_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestAlarm(t *testing.T) { 10 | s := &Alarm{ 11 | Seconds: 10, 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var a Alarm 20 | 21 | if err := json.Unmarshal(j, &a); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 26 | t.Errorf("Was expecting %v but got %v\n", s.Arguments(), a.Arguments()) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/brk_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestBrk(t *testing.T) { 10 | s := &Brk{ 11 | Addr: 0x02000000, 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var b Brk 20 | 21 | if err := json.Unmarshal(j, &b); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), b.Arguments()) { 26 | t.Errorf("Was expecting this is %v, but got %v", s.Arguments(), b.Arguments()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/rmdir_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestRmdir(t *testing.T) { 10 | s := &Rmdir{ 11 | Pathname: "/tmp/output.log", 12 | } 13 | j, err := json.Marshal(s) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | var a Rmdir 19 | if err = json.Unmarshal(j, &a); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 24 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/event/call/acct_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestAcct(t *testing.T) { 10 | s := &Acct{ 11 | Pathname: "/var/log/wtmp", 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var a Acct 20 | if err := json.Unmarshal(j, &a); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 25 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/chdir_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestChdir(t *testing.T) { 10 | s := &Chdir{ 11 | Filename: "/var/log", 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var c Chdir 20 | 21 | if err := json.Unmarshal(j, &c); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), c.Arguments()) { 26 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), c.Arguments()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/chroot_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestChroot(t *testing.T) { 10 | s := &Chroot{ 11 | Filename: "/root", 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var c Chroot 20 | 21 | if err := json.Unmarshal(j, &c); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), c.Arguments()) { 26 | t.Errorf("Was expecting %v, but got %v\b", s.Arguments(), c.Arguments()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/kill_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestKill(t *testing.T) { 10 | s := &Kill{ 11 | Pid: 209, 12 | Sig: 57, 13 | } 14 | 15 | j, err := json.Marshal(s) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | var k Kill 21 | if err = json.Unmarshal(j, &k); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), k.Arguments()) { 26 | t.Errorf("was expecting %v, but got %v\n", s.Arguments(), k.Arguments()) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /pkg/event/call/brk.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Brk struct { 10 | Addr uint64 `json:"addr"` 11 | } 12 | 13 | func (b *Brk) CallName() string { return "brk" } 14 | func (b *Brk) Return() *Argument { return nil } 15 | func (b *Brk) DecodeArguments(data []*byte, arglen int) error { 16 | b.Addr = types.MakeCU64(unsafe.Pointer(data[0])) 17 | return nil 18 | } 19 | 20 | func (b *Brk) Arguments() Arguments { 21 | return Arguments{ 22 | {"addr", "void *", b.Addr}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/event/call/close.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Close struct { 10 | FD types.InputFD 11 | } 12 | 13 | func (c *Close) CallName() string { return "close" } 14 | func (c *Close) Return() *Argument { return nil } 15 | func (c *Close) DecodeArguments(data []*byte, arglen int) error { 16 | c.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 17 | return nil 18 | } 19 | func (c *Close) Arguments() Arguments { 20 | return Arguments{ 21 | {"fd", "int", c.FD}, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/event/call/exit.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Exit struct { 10 | Code int `json:"code"` 11 | } 12 | 13 | func (e *Exit) CallName() string { return "exit" } 14 | func (e *Exit) Return() *Argument { return nil } 15 | func (e *Exit) DecodeArguments(data []*byte, arglen int) error { 16 | e.Code = int(types.MakeC32(unsafe.Pointer(data[0]))) 17 | return nil 18 | } 19 | 20 | func (e *Exit) Arguments() Arguments { 21 | return Arguments{ 22 | {"status", "int", e.Code}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/event/call/getcwd_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestGetcwd(t *testing.T) { 10 | s := &Getcwd{ 11 | Buf: "/tmp", 12 | Size: 5, 13 | } 14 | 15 | j, err := json.Marshal(s) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | var g Getcwd 21 | if err = json.Unmarshal(j, &g); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), g.Arguments()) { 26 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), g.Arguments()) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/setuid.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Setuid struct { 10 | UID int `json:"uid"` 11 | } 12 | 13 | func (s *Setuid) CallName() string { return "setuid" } 14 | func (s *Setuid) Return() *Argument { return nil } 15 | func (s *Setuid) DecodeArguments(data []*byte, arglen int) error { 16 | s.UID = int(types.MakeCU32(unsafe.Pointer(data[0]))) 17 | return nil 18 | } 19 | 20 | func (s *Setuid) Arguments() Arguments { 21 | return Arguments{ 22 | {"uid", "uid_t", s.UID}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/deploy/manifests/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: swoll-probe-pods-reader 6 | rules: 7 | - apiGroups: [""] 8 | resources: ["pods"] 9 | verbs: ["get", "watch", "list"] 10 | --- 11 | apiVersion: rbac.authorization.k8s.io/v1 12 | kind: ClusterRoleBinding 13 | metadata: 14 | name: swoll-probe-rolebinding 15 | roleRef: 16 | apiGroup: rbac.authorization.k8s.io 17 | kind: ClusterRole 18 | name: swoll-probe-pods-reader 19 | subjects: 20 | - kind: ServiceAccount 21 | name: default 22 | namespace: swoll 23 | -------------------------------------------------------------------------------- /pkg/event/call/creat_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestCreat(t *testing.T) { 10 | s := &Creat{ 11 | Pathname: "/var/log/messages", 12 | Mode: 0777, 13 | } 14 | 15 | j, err := json.Marshal(s) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | var c Creat 20 | if err := json.Unmarshal(j, &c); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if !reflect.DeepEqual(s.Arguments(), c.Arguments()) { 25 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), c.Arguments()) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/rename_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestRename(t *testing.T) { 10 | s := &Rename{ 11 | OldName: "/tmp/master.o", 12 | NewName: "/tmp/kop.o", 13 | } 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var a Rename 20 | if err = json.Unmarshal(j, &a); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 25 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /pkg/event/call/futex_text.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestFutex(t *testing.T) { 10 | s := &Futex{ 11 | Uaddr: 0x4323940, 12 | Op: 0, 13 | Val: 45, 14 | } 15 | 16 | j, err := json.Marshal(s) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | var w Write 22 | if err = json.Unmarshal(j, &w); err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if !reflect.DeepEqual(s.Arguments(), w.Arguments()) { 27 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), w.Arguments()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /examples/kubernetes-basic/README.md: -------------------------------------------------------------------------------- 1 | # What 2 | 3 | A simple application which uses the Swoll API to trace system calls on a 4 | kubernetes cluster. If a kernel event was associated with a host inside 5 | kuberentes, that information will be displayed. 6 | 7 | If an event did not come from kubernetes, (e.g., local operations) it will be 8 | marked with "-.-.-". 9 | 10 | # building 11 | 12 | 1. Modify `Makefile` and change the docker repo info. 13 | 2. Modify `deploy.yaml`'s `image` to the repository you created 14 | 3. Run: 15 | 16 | ``` 17 | make all 18 | make push 19 | make deploy 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /pkg/event/call/pivot_root_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestPivotroot(t *testing.T) { 10 | s := &PivotRoot{ 11 | NewRoot: "/tmp/newroot", 12 | OldRoot: "/mp/oldroot", 13 | } 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var a PivotRoot 20 | if err = json.Unmarshal(j, &a); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 25 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/alarm.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Alarm struct { 10 | Seconds uint32 `json:"seconds"` 11 | } 12 | 13 | func (a *Alarm) CallName() string { return "alarm" } 14 | func (a *Alarm) Return() *Argument { return nil } 15 | func (a *Alarm) DecodeArguments(data []*byte, arglen int) error { 16 | a.Seconds = types.MakeCU32(unsafe.Pointer(data[0])) 17 | return nil 18 | } 19 | 20 | func (a *Alarm) Arguments() Arguments { 21 | return Arguments{ 22 | {"seconds", "unsigned int", a.Seconds}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/event/call/link_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestLink(t *testing.T) { 10 | s := &Link{ 11 | OldName: "/var/tmp/systmd-private", 12 | NewName: "/tmp/systemd-read", 13 | } 14 | 15 | j, err := json.Marshal(s) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | var l Link 21 | if err = json.Unmarshal(j, &l); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), l.Arguments()) { 26 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), l.Arguments()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/fstat_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestFstat(t *testing.T) { 11 | s := &Fstat{ 12 | FD: 1, 13 | StatBuf: &syscall.Stat_t{ 14 | Dev: 1, 15 | }, 16 | } 17 | 18 | j, err := json.Marshal(s) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | var f Fstat 23 | if err := json.Unmarshal(j, &f); err != nil { 24 | t.Fatal(err) 25 | } 26 | if !reflect.DeepEqual(s.Arguments(), f.Arguments()) { 27 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), f.Arguments()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/acct.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Acct struct { 10 | Pathname string `json:"pathname"` 11 | } 12 | 13 | func (a *Acct) CallName() string { return "acct" } 14 | func (a *Acct) Return() *Argument { return nil } 15 | func (a *Acct) DecodeArguments(data []*byte, arglen int) error { 16 | a.Pathname = types.MakeCString(unsafe.Pointer(data[0]), arglen) 17 | return nil 18 | } 19 | 20 | func (a *Acct) Arguments() Arguments { 21 | return Arguments{ 22 | {"pathname", "const char *", a.Pathname}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/event/call/chdir.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Chdir struct { 10 | Filename string `json:"filename"` 11 | } 12 | 13 | func (c *Chdir) CallName() string { return "chdir" } 14 | func (c *Chdir) Return() *Argument { return nil } 15 | func (c *Chdir) DecodeArguments(data []*byte, arglen int) error { 16 | c.Filename = types.MakeCString(unsafe.Pointer(data[0]), arglen) 17 | return nil 18 | } 19 | 20 | func (c *Chdir) Arguments() Arguments { 21 | return Arguments{ 22 | {"path", "const char *", c.Filename}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/event/call/rmdir.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Rmdir struct { 10 | Pathname string `json:"pathname"` 11 | } 12 | 13 | func (r *Rmdir) CallName() string { return "rmdir" } 14 | func (r *Rmdir) Return() *Argument { return nil } 15 | func (r *Rmdir) DecodeArguments(data []*byte, arglen int) error { 16 | r.Pathname = types.MakeCString(unsafe.Pointer(data[0]), arglen) 17 | return nil 18 | } 19 | 20 | func (r *Rmdir) Arguments() Arguments { 21 | return Arguments{ 22 | {"pathname", "const char *", r.Pathname}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/event/call/symlink_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestSymlink(t *testing.T) { 10 | s := &Symlink{ 11 | Target: "/tmp/socket_link", 12 | Linkpath: "/usr/app/socket_link", 13 | } 14 | 15 | j, err := json.Marshal(s) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | var a Symlink 21 | 22 | if err := json.Unmarshal(j, &a); err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 27 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/umount2_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestUmount2(t *testing.T) { 11 | s := &Umount2{ 12 | Target: "/dev/hda2", 13 | Flags: syscall.MNT_FORCE, 14 | } 15 | 16 | j, err := json.Marshal(s) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | var u Umount2 22 | if err := json.Unmarshal(j, &u); err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if !reflect.DeepEqual(s.Arguments(), u.Arguments()) { 27 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), u.Arguments()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/unlink_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestUnlink(t *testing.T) { 10 | s := &Unlink{ 11 | Pathname: "/tmp/unlink.txt", 12 | } 13 | 14 | j, err := json.Marshal(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | var u Unlink 20 | if err := json.Unmarshal(j, &u); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | //fmt.Println(string(j), u.Arguments()) 25 | if !reflect.DeepEqual(s.Arguments(), u.Arguments()) { 26 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), u.Arguments()) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/chroot.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Chroot struct { 10 | Filename string `json:"filename"` 11 | } 12 | 13 | func (c *Chroot) CallName() string { return "chroot" } 14 | func (c *Chroot) Return() *Argument { return nil } 15 | func (c *Chroot) DecodeArguments(data []*byte, arglen int) error { 16 | c.Filename = types.MakeCString(unsafe.Pointer(data[0]), arglen) 17 | return nil 18 | } 19 | 20 | func (c *Chroot) Arguments() Arguments { 21 | return Arguments{ 22 | {"path", "const char *", c.Filename}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/event/call/setns_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestSetns(t *testing.T) { 11 | 12 | s := &Setns{ 13 | FD: 3478, 14 | NSType: syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, 15 | } 16 | j, err := json.Marshal(s) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | var a Setns 22 | if err = json.Unmarshal(j, &a); err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 27 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/unlink.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Unlink struct { 10 | Pathname string `json:"pathname"` 11 | } 12 | 13 | func (u *Unlink) CallName() string { return "unlink" } 14 | func (u *Unlink) Return() *Argument { return nil } 15 | func (u *Unlink) DecodeArguments(data []*byte, arglen int) error { 16 | u.Pathname = types.MakeCString(unsafe.Pointer(data[0]), arglen) 17 | return nil 18 | } 19 | 20 | func (u *Unlink) Arguments() Arguments { 21 | return Arguments{ 22 | {"pathname", "const char *", u.Pathname}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/topology/kubernetes_test.go: -------------------------------------------------------------------------------- 1 | package topology_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/criticalstack/swoll/pkg/topology" 7 | ) 8 | 9 | func ExampleNewKubernetes() { 10 | observer, err := topology.NewKubernetes( 11 | topology.WithKubernetesConfig("/root/.kube/config"), 12 | topology.WithKubernetesNamespace("kube-system"), 13 | topology.WithKubernetesCRI("/run/containerd/containerd.sock"), 14 | topology.WithKubernetesLabelSelector("app=nginx"), 15 | topology.WithKubernetesFieldSelector("status.phase=Running")) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | fmt.Println(observer) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/event/call/execve_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestExecve(t *testing.T) { 10 | s := &Execve{ 11 | Filename: "/bin/swoll", 12 | Argv: [4]string{"arg1", "arg2", "arg3", "arg4"}, 13 | } 14 | 15 | j, err := json.Marshal(s) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | var e Execve 21 | if err := json.Unmarshal(j, &e); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if !reflect.DeepEqual(s.Arguments(), e.Arguments()) { 26 | t.Errorf("Was expecting %v, but got this %v\n", s.Arguments(), e.Arguments()) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/init_module_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestInitModule(t *testing.T) { 10 | 11 | s := &InitModule{ 12 | Name: "drm", 13 | Len: 491520, 14 | Params: "edid_firmware='1'", 15 | } 16 | 17 | j, err := json.Marshal(s) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | var i InitModule 23 | if err = json.Unmarshal(j, &i); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if !reflect.DeepEqual(s.Arguments(), i.Arguments()) { 28 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), i.Arguments()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/mkdir_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "encoding/json" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func TestMkdir(t *testing.T) { 13 | s := &Mkdir{ 14 | Pathname: "/usr/local/bpf", 15 | Mode: unix.S_ISVTX, 16 | } 17 | 18 | j, err := json.Marshal(s) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | var m Mkdir 24 | if err = json.Unmarshal(j, &m); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | if !reflect.DeepEqual(s.Arguments(), m.Arguments()) { 29 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), m.Arguments()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Critical Stack, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /pkg/event/call/listen_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | ) 10 | 11 | func TestListen(t *testing.T) { 12 | s := &Listen{ 13 | Sock: types.InputFD(48), 14 | Backlog: 5, 15 | } 16 | 17 | j, err := json.Marshal(s) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | var l Listen 23 | if err = json.Unmarshal(j, &l); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if !reflect.DeepEqual(s.Arguments(), l.Arguments()) { 28 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), l.Arguments()) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /pkg/event/call/access_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func TestAccess(t *testing.T) { 12 | s := &Access{ 13 | Pathname: "/var/log/messages", 14 | Mode: unix.W_OK | unix.X_OK, 15 | } 16 | 17 | j, err := json.Marshal(s) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | var a Access 23 | if err := json.Unmarshal(j, &a); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 28 | t.Errorf("Was expecting %v but got %v\n", s.Arguments(), a.Arguments()) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine3.12 as builder 2 | 3 | RUN apk add --no-cache gcc musl-dev linux-headers binutils git 4 | WORKDIR /workspace 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | ARG GOPROXY 8 | ARG GOSUMDB 9 | RUN go mod download 10 | 11 | COPY api api/ 12 | COPY cmd cmd/ 13 | COPY config config/ 14 | COPY internal internal/ 15 | COPY pkg pkg/ 16 | COPY main.go main.go 17 | 18 | RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o swoll main.go 19 | 20 | FROM alpine:3.12 21 | 22 | COPY --from=builder /workspace/swoll /usr/local/bin 23 | RUN apk add --no-cache binutils 24 | 25 | ENTRYPOINT ["/usr/local/bin/swoll"] 26 | -------------------------------------------------------------------------------- /pkg/event/call/ftruncate_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | ) 10 | 11 | func TestFtruncate(t *testing.T) { 12 | s := &Ftruncate{ 13 | FD: types.InputFD(456), 14 | Length: 556657, 15 | } 16 | 17 | j, err := json.Marshal(s) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | var f Ftruncate 23 | if err = json.Unmarshal(j, &f); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if !reflect.DeepEqual(s.Arguments(), f.Arguments()) { 28 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), f.Arguments()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/ptrace_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestPtrace(t *testing.T) { 11 | s := &Ptrace{ 12 | Request: syscall.PTRACE_CONT, 13 | PID: 45678, 14 | Addr: 0x500000, 15 | Data: PtraceData(23), 16 | } 17 | j, err := json.Marshal(s) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | var a Ptrace 23 | if err = json.Unmarshal(j, &a); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 28 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /chart/templates/controller-role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.controller.enabled -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: swoll-leader-election-role 6 | namespace: {{ .Release.Namespace }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - configmaps 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | - create 17 | - update 18 | - patch 19 | - delete 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - configmaps/status 24 | verbs: 25 | - get 26 | - update 27 | - patch 28 | - apiGroups: 29 | - "" 30 | resources: 31 | - events 32 | verbs: 33 | - create 34 | {{- end -}} 35 | -------------------------------------------------------------------------------- /chart/templates/server-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.server.enabled -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ template "swoll.fullname" . }}-server 6 | namespace: "{{ .Release.Namespace }}" 7 | spec: 8 | selector: 9 | {{- include "swoll.labels" . | nindent 4 }} 10 | app.kubernetes.io/component: server 11 | type: {{ .Values.server.service.type }} 12 | ports: 13 | - port: {{ .Values.server.service.port }} 14 | {{- if eq .Values.server.service.type "NodePort" }} 15 | nodePort: {{ .Values.server.service.nodePort }} 16 | {{- end }} 17 | targetPort: {{ .Values.server.service.targetPort }} 18 | {{- end -}} 19 | -------------------------------------------------------------------------------- /pkg/event/call/read_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | ) 10 | 11 | func TestRead(t *testing.T) { 12 | s := &Read{ 13 | FD: 74, 14 | Buf: (types.Buffer)([]byte("/tmp/virt.txt")), 15 | Count: 203, 16 | } 17 | 18 | j, err := json.Marshal(s) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | var a Read 24 | if err = json.Unmarshal(j, &a); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 29 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/event/call/syslog_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "encoding/json" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func TestSyslog(t *testing.T) { 13 | s := &Syslog{ 14 | Type: unix.SYSLOG_ACTION_OPEN, 15 | Buf: "bpf kernel error", 16 | Len: 18, 17 | } 18 | 19 | j, err := json.Marshal(s) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | var y Syslog 25 | if err := json.Unmarshal(j, &y); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if !reflect.DeepEqual(s.Arguments(), y.Arguments()) { 30 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), y.Arguments()) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yaml: -------------------------------------------------------------------------------- 1 | name: Create GitHub release for Swoll 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | build-publish-release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Run goreleaser 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | run: | 22 | export GOPATH=$(go env GOPATH) 23 | curl -sL https://git.io/goreleaser | bash 24 | -------------------------------------------------------------------------------- /pkg/event/call/mount_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestMount(t *testing.T) { 11 | s := &Mount{ 12 | Device: "ext4", 13 | Directory: "/home", 14 | Type: "tmpfs", 15 | Flags: syscall.MS_SLAVE | syscall.MSG_SYN, 16 | } 17 | 18 | j, err := json.Marshal(s) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | var m Mount 24 | if err = json.Unmarshal(j, &m); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | if !reflect.DeepEqual(s.Arguments(), m.Arguments()) { 29 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), m.Arguments()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/event/call/kill.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Kill struct { 10 | Pid int32 `json:"pid"` 11 | Sig int32 `json:"sig"` 12 | } 13 | 14 | func (k *Kill) CallName() string { return "kill" } 15 | func (k *Kill) Return() *Argument { return nil } 16 | func (k *Kill) DecodeArguments(data []*byte, arglen int) error { 17 | k.Pid = types.MakeC32(unsafe.Pointer(data[0])) 18 | k.Sig = types.MakeC32(unsafe.Pointer(data[1])) 19 | 20 | return nil 21 | } 22 | 23 | func (k *Kill) Arguments() Arguments { 24 | return Arguments{ 25 | {"pid", "pid_t", k.Pid}, 26 | {"sig", "int", k.Sig}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/timerfd_create_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "encoding/json" 8 | 9 | "github.com/criticalstack/swoll/pkg/types" 10 | ) 11 | 12 | func TestTimerFDCreat(t *testing.T) { 13 | s := &TimerFDCreate{ 14 | Clock: types.TFDClock(45), 15 | Flags: types.TFD_CLOEXEC, 16 | } 17 | 18 | j, err := json.Marshal(s) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | var a TimerFDCreate 24 | if err := json.Unmarshal(j, &a); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 29 | t.Errorf("was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/event/call/write_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "encoding/json" 8 | 9 | "github.com/criticalstack/swoll/pkg/types" 10 | ) 11 | 12 | func TestWrite(t *testing.T) { 13 | 14 | s := &Write{ 15 | FD: types.InputFD(1), 16 | Buf: types.Buffer([]byte("write test")), 17 | Count: 12, 18 | } 19 | 20 | j, err := json.Marshal(s) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | var w Write 26 | if err = json.Unmarshal(j, &w); err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | if !reflect.DeepEqual(s.Arguments(), w.Arguments()) { 31 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), w.Arguments()) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /pkg/event/call/mprotect_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func TestMprotect(t *testing.T) { 12 | s := &Mprotect{ 13 | Addr: 0x02000000, 14 | Len: 256, 15 | Prot: unix.PROT_READ, 16 | AddrData: []byte("DADTDADFADSDF=="), 17 | } 18 | 19 | j, err := json.Marshal(s) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | var m Mprotect 25 | if err = json.Unmarshal(j, &m); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if !reflect.DeepEqual(s.Arguments(), m.Arguments()) { 30 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), m.Arguments()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/kernel/ross/bob.go: -------------------------------------------------------------------------------- 1 | package ross 2 | 3 | import ( 4 | "math/rand" 5 | "syscall" 6 | "time" 7 | 8 | "golang.org/x/crypto/ssh/terminal" 9 | ) 10 | 11 | func Paint() string { 12 | if terminal.IsTerminal(syscall.Stdin) { 13 | isms := []string{ 14 | "We don't make mistakes, just happy little accidents.", 15 | "Talent is a pursued interest.", 16 | "There's nothing wrong with having a tree as a friend.", 17 | "Let's get crazy.", 18 | "Believe that you can do it cause you can do it.", 19 | "On AWS there are no mistakes, only happy little catastrophes", 20 | } 21 | 22 | rand.Seed(time.Now().Unix()) 23 | return isms[rand.Int()%len(isms)] 24 | } 25 | 26 | return "" 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/faccessat_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func TestFaccessat(t *testing.T) { 12 | s := &Faccessat{ 13 | FD: -100, 14 | Pathname: "/var/log/dmesg", 15 | Mode: 0777, 16 | Flags: unix.AT_SYMLINK_NOFOLLOW, 17 | } 18 | 19 | j, err := json.Marshal(s) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | var f Faccessat 25 | if err := json.Unmarshal(j, &f); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if !reflect.DeepEqual(s.Arguments(), f.Arguments()) { 30 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), f.Arguments()) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /pkg/event/call/clone_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestClone(t *testing.T) { 11 | s := &Clone{ 12 | Flags: syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS, 13 | NewStack: 0x31337, 14 | ChildStack: 0x31338, 15 | ParentStack: 0x0, 16 | Tls: 0, 17 | } 18 | 19 | j, err := json.Marshal(s) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | var c Clone 25 | if err := json.Unmarshal(j, &c); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if !reflect.DeepEqual(s.Arguments(), c.Arguments()) { 30 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), c.Arguments()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/event/call/socket_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func TestSocket(t *testing.T) { 13 | s := &Socket{ 14 | Family: SockFamily(syscall.AF_INET), 15 | Type: SockType(unix.SOCK_STREAM), 16 | Protocol: SockProtocol(0), 17 | } 18 | 19 | j, err := json.Marshal(s) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | var a Socket 25 | 26 | if err := json.Unmarshal(j, &a); err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 31 | t.Errorf("Was expecting %v but got %v\n", s.Arguments(), a.Arguments()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /pkg/event/call/getcwd.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Getcwd struct { 10 | Buf string `json:"buf"` 11 | Size int `json:"size"` 12 | } 13 | 14 | func (g *Getcwd) CallName() string { return "getcwd" } 15 | func (g *Getcwd) Return() *Argument { return nil } 16 | func (g *Getcwd) DecodeArguments(data []*byte, arglen int) error { 17 | g.Buf = types.MakeCString(unsafe.Pointer(data[0]), arglen) 18 | g.Size = int(types.MakeCU64(unsafe.Pointer(data[1]))) 19 | return nil 20 | } 21 | 22 | func (g *Getcwd) Arguments() Arguments { 23 | return Arguments{ 24 | {"buf", "char *", g.Buf}, 25 | {"size", "size_t", g.Size}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/open_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | 9 | "github.com/criticalstack/swoll/pkg/types" 10 | ) 11 | 12 | func TestOpen(t *testing.T) { 13 | 14 | s := Open{ 15 | Filename: "/tmp/output.txt", 16 | Flags: 0, 17 | Mode: syscall.S_IRUSR, 18 | Ret: types.OutputFD(12), 19 | } 20 | 21 | j, err := json.Marshal(s) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | var o Open 27 | if err = json.Unmarshal(j, &o); err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | if !reflect.DeepEqual(s.Arguments(), o.Arguments()) { 32 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), o.Arguments()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/event/call/openat_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | 9 | "github.com/criticalstack/swoll/pkg/types" 10 | ) 11 | 12 | func TestOpenat(t *testing.T) { 13 | 14 | s := &Openat{ 15 | DirFD: types.DirFD(3846), 16 | Pathname: "/tmp/output.txt", 17 | Flags: syscall.O_RDWR | syscall.O_SYNC, 18 | } 19 | 20 | j, err := json.Marshal(s) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | var o Openat 26 | if err = json.Unmarshal(j, &o); err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | if !reflect.DeepEqual(s.Arguments(), o.Arguments()) { 31 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), o.Arguments()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_traces.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: traces.tools.swoll.criticalstack.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /pkg/event/call/connect.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Connect struct { 10 | FD types.InputFD `json:"fd"` 11 | Saddr *types.SockAddr 12 | } 13 | 14 | func (c *Connect) CallName() string { return "connect" } 15 | func (c *Connect) Return() *Argument { return nil } 16 | func (c *Connect) DecodeArguments(data []*byte, arglen int) error { 17 | c.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 18 | c.Saddr = (*types.SockAddr)(unsafe.Pointer(data[1])) 19 | return nil 20 | } 21 | 22 | func (c *Connect) Arguments() Arguments { 23 | return Arguments{ 24 | {"sockfd", "int", c.FD}, 25 | {"addr", "struct sockaddr *", c.Saddr}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/readlinkat_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | ) 10 | 11 | func TestReadlinkat(t *testing.T) { 12 | s := &Readlinkat{ 13 | DirFD: 754, 14 | Pathname: "var/sys/block/dm-0", 15 | Buf: (types.Buffer)([]byte("readlink works")), 16 | Bufsize: 15, 17 | } 18 | 19 | j, err := json.Marshal(s) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | var a Readlinkat 25 | if err = json.Unmarshal(j, &a); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 30 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/event/call/nanosleep_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | ) 10 | 11 | func TestNanosleep(t *testing.T) { 12 | s := &Nanosleep{ 13 | Req: types.Timespec{ 14 | Sec: 1250, 15 | Nsec: 0, 16 | }, 17 | Rem: types.Timespec{ 18 | Sec: 9954, 19 | Nsec: 3456, 20 | }, 21 | } 22 | 23 | j, err := json.Marshal(s) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | var n Nanosleep 29 | if err = json.Unmarshal(j, &n); err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if !reflect.DeepEqual(s.Arguments(), n.Arguments()) { 34 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), n.Arguments()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/event/call/nanosleep.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "C" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | import "unsafe" 9 | 10 | type Nanosleep struct { 11 | Req types.Timespec `json:"req"` 12 | Rem types.Timespec `json:"rem"` 13 | } 14 | 15 | func (n *Nanosleep) CallName() string { return "nanosleep" } 16 | func (n *Nanosleep) Return() *Argument { return nil } 17 | func (n *Nanosleep) DecodeArguments(data []*byte, arglen int) error { 18 | n.Req = *(*types.Timespec)(unsafe.Pointer(&data[0])) 19 | n.Rem = *(*types.Timespec)(unsafe.Pointer(&data[1])) 20 | return nil 21 | } 22 | func (n *Nanosleep) Arguments() Arguments { 23 | return Arguments{ 24 | {"req", "void *", n.Req}, 25 | {"rem", "void *", n.Rem}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/link.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Link struct { 10 | OldName string `json:"old_name"` 11 | NewName string `json:"new_name"` 12 | } 13 | 14 | func (l *Link) CallName() string { return "link" } 15 | func (l *Link) Return() *Argument { return nil } 16 | func (l *Link) DecodeArguments(data []*byte, arglen int) error { 17 | l.OldName = types.MakeCString(unsafe.Pointer(data[0]), arglen) 18 | l.NewName = types.MakeCString(unsafe.Pointer(data[1]), arglen) 19 | return nil 20 | } 21 | 22 | func (l *Link) Arguments() Arguments { 23 | return Arguments{ 24 | {"oldpath", "const char *", l.OldName}, 25 | {"newpath", "const char *", l.NewName}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/listen.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Listen struct { 10 | Sock types.InputFD `json:"sock"` 11 | Backlog int `json:"backlog"` 12 | } 13 | 14 | func (l *Listen) CallName() string { return "listen" } 15 | func (l *Listen) Return() *Argument { return nil } 16 | func (l *Listen) DecodeArguments(data []*byte, arglen int) error { 17 | l.Sock = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 18 | l.Backlog = int(types.MakeC32(unsafe.Pointer(data[1]))) 19 | 20 | return nil 21 | } 22 | 23 | func (l *Listen) Arguments() Arguments { 24 | return Arguments{ 25 | {"sockfd", "int", l.Sock}, 26 | {"backlog", "int", l.Backlog}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/criticalstack/swoll/pkg/kernel" 7 | ) 8 | 9 | type EventReader interface { 10 | Read() chan interface{} 11 | Run(ctx context.Context) error 12 | } 13 | 14 | type EmptyReader struct{} 15 | 16 | func (e *EmptyReader) Read() chan interface{} { return nil } 17 | func (e *EmptyReader) Run(ctx context.Context) error { return nil } 18 | 19 | // NewEventReader attempts to create an EventReader of a known type 20 | // Currently only knows: *kernel.Probe, *redis.PubSub 21 | func NewEventReader(src interface{}) EventReader { 22 | switch src := src.(type) { 23 | case *kernel.Probe: 24 | return NewKernelReader(src) 25 | } 26 | 27 | return &EmptyReader{} 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/ftruncate.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Ftruncate struct { 10 | FD types.InputFD `json:"fd"` 11 | Length int `json:"length"` 12 | } 13 | 14 | func (f *Ftruncate) CallName() string { return "ftruncate" } 15 | func (f *Ftruncate) Return() *Argument { return nil } 16 | func (f *Ftruncate) DecodeArguments(data []*byte, arglen int) error { 17 | f.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 18 | f.Length = int(types.MakeC32(unsafe.Pointer(data[1]))) 19 | 20 | return nil 21 | } 22 | 23 | func (f *Ftruncate) Arguments() Arguments { 24 | return Arguments{ 25 | {"fd", "int", f.FD}, 26 | {"length", "off_t", f.Length}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/fstat.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | type Fstat struct { 11 | FD types.InputFD `json:"fd"` 12 | StatBuf *syscall.Stat_t `json:"stat_buf"` 13 | } 14 | 15 | func (f *Fstat) CallName() string { return "fstat" } 16 | func (f *Fstat) Return() *Argument { return nil } 17 | func (f *Fstat) DecodeArguments(data []*byte, arglen int) error { 18 | f.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 19 | f.StatBuf = (*syscall.Stat_t)(unsafe.Pointer(data[1])) 20 | 21 | return nil 22 | } 23 | 24 | func (f *Fstat) Arguments() Arguments { 25 | return Arguments{ 26 | {"fd", "int", f.FD}, 27 | {"statbuf", "struct stat *", f.StatBuf}, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/bind.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Bind struct { 10 | FD types.InputFD 11 | Ret types.OutputFD 12 | Saddr *types.SockAddr 13 | } 14 | 15 | func (b *Bind) CallName() string { return "bind" } 16 | func (b *Bind) Return() *Argument { return &Argument{"out_fd", "int", b.Ret} } 17 | 18 | func (b *Bind) DecodeArguments(data []*byte, arglen int) error { 19 | b.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 20 | b.Saddr = (*types.SockAddr)(unsafe.Pointer(data[1])) 21 | 22 | return nil 23 | } 24 | 25 | func (b *Bind) Arguments() Arguments { 26 | return Arguments{ 27 | {"sockfd", "int", b.FD}, 28 | {"addr", "struck sockaddr *", b.Saddr}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/creat.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "os" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | type Creat struct { 11 | Pathname string `json:"pathname"` 12 | Mode os.FileMode `json:"mode"` 13 | } 14 | 15 | func (c *Creat) CallName() string { return "creat" } 16 | func (c *Creat) Return() *Argument { return nil } 17 | func (c *Creat) DecodeArguments(data []*byte, arglen int) error { 18 | c.Pathname = types.MakeCString(unsafe.Pointer(data[0]), arglen) 19 | c.Mode = os.FileMode(types.MakeC32(unsafe.Pointer(data[1]))) 20 | return nil 21 | } 22 | 23 | func (c *Creat) Arguments() Arguments { 24 | return Arguments{ 25 | {"pathname", "const char *", c.Pathname}, 26 | {"mode", "mode_t", c.Mode}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/rename.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Rename struct { 10 | OldName string `json:"old_name"` 11 | NewName string `json:"new_name"` 12 | } 13 | 14 | func (r *Rename) CallName() string { return "rename" } 15 | func (r *Rename) Return() *Argument { return nil } 16 | func (r *Rename) DecodeArguments(data []*byte, arglen int) error { 17 | r.OldName = types.MakeCString(unsafe.Pointer(data[0]), arglen) 18 | r.NewName = types.MakeCString(unsafe.Pointer(data[1]), arglen) 19 | return nil 20 | } 21 | 22 | func (r *Rename) Arguments() Arguments { 23 | return Arguments{ 24 | {"oldpath", "const char *", r.OldName}, 25 | {"newpath", "const char *", r.NewName}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/access.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Access struct { 10 | Pathname string `json:"pathname"` 11 | Mode types.XmodeFlags `json:"mode"` 12 | } 13 | 14 | func (a *Access) CallName() string { return "access" } 15 | func (a *Access) Return() *Argument { return nil } 16 | func (a *Access) DecodeArguments(data []*byte, arglen int) error { 17 | a.Pathname = types.MakeCString(unsafe.Pointer(data[0]), arglen) 18 | a.Mode = types.XmodeFlags(types.MakeC32(unsafe.Pointer(data[1]))) 19 | return nil 20 | } 21 | 22 | func (a *Access) Arguments() Arguments { 23 | return Arguments{ 24 | {"pathname", "const char *", a.Pathname}, 25 | {"mode", "int", a.Mode}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/inotify_add_watch_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func TestINotifyAddWatch(t *testing.T) { 13 | s := &INotifyAddWatch{ 14 | FD: types.InputFD(29), 15 | Pathname: "/usr/unix_listen.log", 16 | Mask: unix.IN_ACCESS | unix.IN_CLOSE, 17 | } 18 | 19 | j, err := json.Marshal(s) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | var i INotifyAddWatch 25 | if err := json.Unmarshal(j, &i); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if !reflect.DeepEqual(s.Arguments(), i.Arguments()) { 30 | t.Errorf("Was expecting %v, but got\n %v\n", s.Arguments(), i.Arguments()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/event/call/symlink.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Symlink struct { 10 | Target string `json:"target"` 11 | Linkpath string `json:"linkpath"` 12 | } 13 | 14 | func (s *Symlink) CallName() string { return "symlink" } 15 | func (s *Symlink) Return() *Argument { return nil } 16 | func (s *Symlink) DecodeArguments(data []*byte, arglen int) error { 17 | s.Target = types.MakeCString(unsafe.Pointer(data[0]), arglen) 18 | s.Linkpath = types.MakeCString(unsafe.Pointer(data[1]), arglen) 19 | return nil 20 | } 21 | 22 | func (s *Symlink) Arguments() Arguments { 23 | return Arguments{ 24 | {"target", "const char *", s.Target}, 25 | {"linkpath", "const char *", s.Linkpath}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/getpeername.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Getpeername struct { 10 | FD types.InputFD `json:"fd"` 11 | Saddr *types.SockAddr `json:"saddr"` 12 | } 13 | 14 | func (g *Getpeername) CallName() string { return "getpeername" } 15 | func (g *Getpeername) Return() *Argument { return nil } 16 | func (g *Getpeername) DecodeArguments(data []*byte, arglen int) error { 17 | g.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 18 | g.Saddr = (*types.SockAddr)(unsafe.Pointer(data[1])) 19 | return nil 20 | } 21 | 22 | func (g *Getpeername) Arguments() Arguments { 23 | return Arguments{ 24 | {"sockfd", "int", g.FD}, 25 | {"addr", "struct sockaddr *", g.Saddr}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/getsockname.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Getsockname struct { 10 | FD types.InputFD `json:"fd"` 11 | Saddr *types.SockAddr `json:"saddr"` 12 | } 13 | 14 | func (g *Getsockname) CallName() string { return "getsockname" } 15 | func (g *Getsockname) Return() *Argument { return nil } 16 | func (g *Getsockname) DecodeArguments(data []*byte, arglen int) error { 17 | g.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 18 | g.Saddr = (*types.SockAddr)(unsafe.Pointer(data[1])) 19 | return nil 20 | } 21 | 22 | func (g *Getsockname) Arguments() Arguments { 23 | return Arguments{ 24 | {"sockfd", "int", g.FD}, 25 | {"addr", "struct sockaddr *", g.Saddr}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/prlimit64_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestPrlimit64(t *testing.T) { 11 | s := &Prlimit64{ 12 | Pid: 40623, 13 | Resource: syscall.RLIMIT_CPU, 14 | New: (Rlimit)(syscall.Rlimit{ 15 | Cur: 39, 16 | Max: 40004, 17 | }), 18 | Old: (Rlimit)(syscall.Rlimit{ 19 | Cur: 394, 20 | Max: 45637, 21 | }), 22 | } 23 | 24 | j, err := json.Marshal(s) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | var a Prlimit64 30 | if err = json.Unmarshal(j, &a); err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 35 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/event/call/statfs.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | type Statfs struct { 11 | Path string `json:"path"` 12 | StatBuf syscall.Stat_t `json:"stat_buf"` 13 | } 14 | 15 | func (s *Statfs) CallName() string { return "statfs" } 16 | func (s *Statfs) Return() *Argument { return nil } 17 | func (s *Statfs) DecodeArguments(data []*byte, arglen int) error { 18 | s.Path = types.MakeCString(unsafe.Pointer(data[0]), arglen) 19 | s.StatBuf = *(*syscall.Stat_t)(unsafe.Pointer(data[1])) 20 | return nil 21 | } 22 | 23 | func (s *Statfs) Arguments() Arguments { 24 | return Arguments{ 25 | {"path", "const char *", s.Path}, 26 | {"buf", "struct statfs *", s.StatBuf}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/event/call/umount2.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Umount2 struct { 10 | Target string `json:"target"` 11 | Flags types.UmountFlags `json:"flags"` 12 | } 13 | 14 | func (u *Umount2) CallName() string { return "umount" } 15 | func (u *Umount2) Return() *Argument { return nil } 16 | func (u *Umount2) DecodeArguments(data []*byte, arglen int) error { 17 | u.Target = types.MakeCString(unsafe.Pointer(data[0]), arglen) 18 | u.Flags = types.UmountFlags(types.MakeC32(unsafe.Pointer(data[1]))) 19 | return nil 20 | } 21 | 22 | func (u *Umount2) Arguments() Arguments { 23 | return Arguments{ 24 | {"target", "const char *", u.Target}, 25 | {"flags", "int", u.Flags}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/mkdir.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "os" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | // Mkdir ... 11 | type Mkdir struct { 12 | Pathname string `json:"pathname"` 13 | Mode os.FileMode `json:"mode"` 14 | } 15 | 16 | func (m *Mkdir) CallName() string { return "mkdir" } 17 | func (m *Mkdir) Return() *Argument { return nil } 18 | func (m *Mkdir) DecodeArguments(data []*byte, arglen int) error { 19 | m.Pathname = types.MakeCString(unsafe.Pointer(data[0]), arglen) 20 | m.Mode = os.FileMode(types.MakeC32(unsafe.Pointer(data[1]))) 21 | return nil 22 | } 23 | 24 | func (m *Mkdir) Arguments() Arguments { 25 | return Arguments{ 26 | {"pathname", "const char *", m.Pathname}, 27 | {"mode", "mode_t", m.Mode}, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/event/call/recvfrom_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | 9 | "github.com/criticalstack/swoll/pkg/types" 10 | ) 11 | 12 | func TestRecvfrom(t *testing.T) { 13 | s := &Recvfrom{ 14 | FD: types.InputFD(55), 15 | Ubuf: types.Buffer([]byte("core dump")), 16 | Size: 10, 17 | OSize: 11, 18 | Flags: (types.MsgFlags)(syscall.MSG_PEEK | syscall.MSG_OOB), 19 | } 20 | 21 | j, err := json.Marshal(s) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | var a Recvfrom 27 | if err = json.Unmarshal(j, &a); err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 32 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/event/call/sockopt_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | 9 | "encoding/json" 10 | ) 11 | 12 | func TestSockopt(t *testing.T) { 13 | sockN := &SockoptName{ 14 | Tp: 1, 15 | Lv: 1, 16 | } 17 | 18 | s := &Sockopt{ 19 | FD: types.InputFD(2), 20 | Level: SockoptLevel(1), 21 | Name: sockN, 22 | Val: []byte("sock"), 23 | Len: 4578, 24 | } 25 | 26 | j, err := json.Marshal(s) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | var a Sockopt 32 | if err = json.Unmarshal(j, &a); err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 37 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/event/call/stat.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | type Stat struct { 11 | Filename string `json:"filename"` 12 | StatBuf syscall.Stat_t `json:"stat_buf"` 13 | } 14 | 15 | func (s *Stat) CallName() string { return "stat" } 16 | func (s *Stat) Return() *Argument { return nil } 17 | func (s *Stat) DecodeArguments(data []*byte, arglen int) error { 18 | s.Filename = types.MakeCString(unsafe.Pointer(data[0]), arglen) 19 | s.StatBuf = *(*syscall.Stat_t)(unsafe.Pointer(data[1])) 20 | 21 | return nil 22 | } 23 | 24 | func (s *Stat) Arguments() Arguments { 25 | return Arguments{ 26 | {"pathname", "const char *", s.Filename}, 27 | {"statbuf", "struct stat *", s.StatBuf}, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/kernel/fixtures/Makefile: -------------------------------------------------------------------------------- 1 | KERNELDIR ?= /lib/modules/$(shell uname -r)/build 2 | 3 | always += dummy_probe.o 4 | 5 | all: dummy_probe 6 | 7 | dummy_probe: 8 | $(MAKE) -C $(KERNELDIR) M=$$PWD 9 | 10 | $(obj)/dummy_probe.o: $(src)/dummy_probe.c 11 | clang $(LINUXINCLUDE) $(KBUILD_CPPFLAGS) $(DEBUG) -D__KERNEL__ -D__BPF_TRACING__ \ 12 | -Wno-gnu-variable-sized-type-not-at-end -Wno-address-of-packed-member -fno-jump-tables \ 13 | -Wno-tautological-compare -O3 -g -emit-llvm -c $< -o $(patsubst %.o,%.ll,$@) 14 | llc -march=bpf -filetype=obj -o $@ $(patsubst %.o,%.ll,$@) 15 | 16 | clean: 17 | rm -f *~ 18 | rm -f dummy_probe.ll 19 | rm -f Modules.symvers 20 | rm -f modules.order 21 | rm -f Module.symvers 22 | rm -f .cache.mk 23 | 24 | clean_all: clean 25 | rm -rf dummy_probe.o 26 | 27 | -------------------------------------------------------------------------------- /pkg/event/types.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/criticalstack/swoll/pkg/types" 5 | "github.com/go-redis/redis" 6 | ) 7 | 8 | // KernelEvent is a raw event from the kernel, used by kernel event reader 9 | type KernelEvent []byte 10 | 11 | // KernelLostEvent is a raw lost event counter from the kernel, used by kernel event reader 12 | type KernelLostEvent uint64 13 | 14 | // ContainerAddEvent is an event sourced from the Topology api on 15 | // container-entry 16 | type ContainerAddEvent struct{ *types.Container } 17 | 18 | // ContainerDelEvent is an event sourced from the Topology api upon 19 | // container-exit 20 | type ContainerDelEvent struct{ *types.Container } 21 | 22 | // RedisEvent is an event containing a message from a redis query 23 | type RedisEvent *redis.Message 24 | -------------------------------------------------------------------------------- /pkg/event/call/pivot_root.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type PivotRoot struct { 10 | NewRoot string `json:"new_root"` 11 | OldRoot string `json:"old_root"` 12 | } 13 | 14 | func (p *PivotRoot) CallName() string { return "pivot_root" } 15 | func (p *PivotRoot) Return() *Argument { return nil } 16 | func (p *PivotRoot) DecodeArguments(data []*byte, arglen int) error { 17 | p.NewRoot = types.MakeCString(unsafe.Pointer(data[0]), arglen) 18 | p.OldRoot = types.MakeCString(unsafe.Pointer(data[1]), arglen) 19 | 20 | return nil 21 | } 22 | 23 | func (p *PivotRoot) Arguments() Arguments { 24 | return Arguments{ 25 | {"new_root", "const char *", p.NewRoot}, 26 | {"put_old", "const char *", p.OldRoot}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/types/xmode.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strings" 5 | 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | type XmodeFlags int 10 | 11 | var xModeMasks = map[int]string{ 12 | unix.R_OK: "R_OK", 13 | unix.W_OK: "W_OK", 14 | unix.X_OK: "X_OK", 15 | } 16 | 17 | func (flags XmodeFlags) Parse() []string { 18 | if flags == 0 { 19 | return []string{"F_OK"} 20 | } 21 | 22 | ret := []string{} 23 | fint := int(flags) 24 | for flag, fstr := range xModeMasks { 25 | if flag&fint != 0 { 26 | ret = append(ret, fstr) 27 | } 28 | } 29 | 30 | return ret 31 | } 32 | 33 | func (flags XmodeFlags) String() string { 34 | return strings.Join(flags.Parse(), "|") 35 | } 36 | 37 | /* 38 | func (flags XmodeFlags) MarshalJSON() ([]byte, error) { 39 | return json.Marshal(flags.Parse()) 40 | } 41 | */ 42 | -------------------------------------------------------------------------------- /pkg/event/call/timerfd_create.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type TimerFDCreate struct { 10 | Clock types.TFDClock `json:"clock"` 11 | Flags types.TimerFlags `json:"flags"` 12 | } 13 | 14 | func (t *TimerFDCreate) CallName() string { return "timerfd_create" } 15 | func (t *TimerFDCreate) Return() *Argument { return nil } 16 | func (t *TimerFDCreate) DecodeArguments(data []*byte, arglen int) error { 17 | t.Clock = types.TFDClock(types.MakeC32(unsafe.Pointer(data[0]))) 18 | t.Flags = types.TimerFlags(types.MakeC32(unsafe.Pointer(data[1]))) 19 | return nil 20 | } 21 | 22 | func (t *TimerFDCreate) Arguments() Arguments { 23 | return Arguments{ 24 | {"clockid", "int", t.Clock}, 25 | {"flags", "int", t.Flags}, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/event/call/futex.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Futex struct { 10 | Uaddr uint32 `json:"uaddr"` 11 | Op int `json:"op"` 12 | Val uint32 `json:"val"` 13 | } 14 | 15 | func (f *Futex) CallName() string { return "futex" } 16 | func (f *Futex) Return() *Argument { return nil } 17 | func (f *Futex) DecodeArguments(data []*byte, arglen int) error { 18 | f.Uaddr = types.MakeCU32(unsafe.Pointer(data[0])) 19 | f.Op = int(types.MakeC32(unsafe.Pointer(data[1]))) 20 | f.Val = types.MakeCU32(unsafe.Pointer(data[2])) 21 | return nil 22 | } 23 | 24 | func (f *Futex) Arguments() Arguments { 25 | return Arguments{ 26 | {"uaddr", "int *", f.Uaddr}, 27 | {"futex_op", "int", f.Op}, 28 | {"val", "int", f.Val}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/topology/ns.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "syscall" 7 | ) 8 | 9 | // getNamespaceIno returns the numerical kernel-namespace for the specified 10 | // namespace-type `t` for the process-id `pid` in the proc-root directory `root` 11 | func getNamespaceIno(root string, pid int, t string) (int, error) { 12 | var stat syscall.Stat_t 13 | 14 | pfile := path.Join(root, "/proc", fmt.Sprintf("%d", pid), "ns", t) 15 | 16 | if err := syscall.Stat(pfile, &stat); err != nil { 17 | return -1, err 18 | } 19 | 20 | return int(stat.Ino), nil 21 | } 22 | 23 | // getPidNamespace is a wrapper around getNamespaceIno to fetch a tasks kernel 24 | // pid-namespace. 25 | func getPidNamespace(root string, pid int) (int, error) { 26 | return getNamespaceIno(root, pid, "pid_for_children") 27 | } 28 | -------------------------------------------------------------------------------- /pkg/syscalls/syscalls_test.go: -------------------------------------------------------------------------------- 1 | package syscalls 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func (dst *Syscall) equals(src *Syscall) bool { 8 | return dst.Nr == src.Nr && 9 | dst.Name == src.Name && 10 | dst.Class == src.Class && 11 | dst.Group == src.Group 12 | } 13 | 14 | func TestSyscalls(t *testing.T) { 15 | s, err := New() 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | 20 | wants := []Syscall{ 21 | {0, "sys_read", "FileSystem", "ReadWrite"}, 22 | } 23 | 24 | for _, want := range wants { 25 | has1 := s.Lookup(want.Nr) 26 | has2 := s.Lookup(want.Name) 27 | 28 | if neq := has1.equals(has2); !neq { 29 | t.Errorf("lookups did not match for same value: %v/=%v", has1, has2) 30 | } 31 | 32 | if neq := want.equals(has1); !neq { 33 | t.Errorf("%v/=%v", want, has1) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/event/call/bind_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/criticalstack/swoll/pkg/types" 11 | ) 12 | 13 | func TestBind(t *testing.T) { 14 | sa := &syscall.RawSockaddrInet4{ 15 | Family: syscall.AF_INET, 16 | Addr: [4]byte{172, 19, 31, 254}, 17 | Port: htons(80), 18 | } 19 | 20 | s := &Bind{ 21 | FD: 1, 22 | Saddr: (*types.SockAddr)(unsafe.Pointer(sa)), 23 | } 24 | 25 | j, err := json.Marshal(s) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | var b Bind 31 | 32 | if err := json.Unmarshal(j, &b); err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | if !reflect.DeepEqual(s.Arguments(), b.Arguments()) { 37 | t.Errorf("Was expecting %v but got %v\n", s.Arguments(), b.Arguments()) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /pkg/event/call/connect_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/criticalstack/swoll/pkg/types" 11 | ) 12 | 13 | func TestConnect(t *testing.T) { 14 | sa := &syscall.RawSockaddrInet4{ 15 | Family: syscall.AF_INET, 16 | Addr: [4]byte{172, 19, 31, 254}, 17 | Port: htons(80), 18 | } 19 | 20 | s := &Connect{ 21 | FD: 1, 22 | Saddr: (*types.SockAddr)(unsafe.Pointer(sa)), 23 | } 24 | 25 | j, err := json.Marshal(s) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | var c Connect 31 | if err := json.Unmarshal(j, &c); err != nil { 32 | t.Fatal(err) 33 | } 34 | if !reflect.DeepEqual(s.Arguments(), c.Arguments()) { 35 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), c.Arguments()) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /pkg/event/call/setsockopt_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | ) 10 | 11 | func TestSetsocketopt(t *testing.T) { 12 | sockN := &SockoptName{ 13 | Tp: 1, 14 | Lv: 1, 15 | } 16 | 17 | s := &Setsockopt{ 18 | Sockopt{ 19 | FD: types.InputFD(2), 20 | Level: SockoptLevel(1), 21 | Name: sockN, 22 | Val: []byte("sock"), 23 | Len: 4578, 24 | }, 25 | } 26 | 27 | j, err := json.Marshal(s) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | var a Setsockopt 33 | if err = json.Unmarshal(j, &a); err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 38 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /pkg/event/call/syslog.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Syslog struct { 10 | Type int `json:"type"` 11 | Buf string `json:"buf"` 12 | Len int `json:"len"` 13 | } 14 | 15 | func (s *Syslog) CallName() string { return "syslog" } 16 | func (s *Syslog) Return() *Argument { return nil } 17 | func (s *Syslog) DecodeArguments(data []*byte, arglen int) error { 18 | s.Type = int(types.MakeC32(unsafe.Pointer(data[0]))) 19 | s.Buf = types.MakeCString(unsafe.Pointer(data[1]), arglen) 20 | s.Len = int(types.MakeC32(unsafe.Pointer(data[2]))) 21 | return nil 22 | } 23 | 24 | func (s *Syslog) Arguments() Arguments { 25 | return Arguments{ 26 | {"type", "int", s.Type}, 27 | {"bufp", "char *", s.Buf}, 28 | {"len", "int", s.Len}, 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /pkg/event/reader/reader_test.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck 2 | package reader 3 | 4 | import ( 5 | "context" 6 | "testing" 7 | ) 8 | 9 | type fakeReader struct { 10 | ch chan interface{} 11 | } 12 | 13 | type fakeReaderEvent int 14 | 15 | func (f *fakeReader) Read() chan interface{} { 16 | return f.ch 17 | } 18 | 19 | func (f *fakeReader) Run(ctx context.Context) error { 20 | f.ch <- fakeReaderEvent(1) 21 | return nil 22 | } 23 | 24 | func newfakereader() EventReader { 25 | r := &fakeReader{make(chan interface{})} 26 | 27 | return r 28 | } 29 | 30 | func TestFakeEventReader(t *testing.T) { 31 | readr := newfakereader() 32 | go readr.Run(context.TODO()) 33 | 34 | msg := <-readr.Read() 35 | 36 | want := 1 37 | have := msg.(fakeReaderEvent) 38 | 39 | if want != int(have) { 40 | t.Errorf("want %v, have %v", want, have) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/event/call/getsockopt_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func TestGetSockopt(t *testing.T) { 13 | s := &Getsockopt{ 14 | Sockopt: Sockopt{ 15 | FD: 1, 16 | Level: SockoptLevel(int(syscall.SOL_SOCKET)), 17 | Name: &SockoptName{ 18 | Tp: unix.SO_REUSEPORT, 19 | Lv: syscall.SOL_SOCKET, 20 | }, 21 | Val: []byte{1, 0, 0, 0}, 22 | Len: 1, 23 | }, 24 | } 25 | 26 | j, err := json.Marshal(s) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | var a Getsockopt 32 | if err := json.Unmarshal(j, &a); err != nil { 33 | t.Error(err) 34 | } 35 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 36 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/event/call/mincore.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Mincore struct { 10 | Addr uintptr `json:"addr"` 11 | Len int `json:"len"` 12 | Vec uintptr `json:"vec"` 13 | } 14 | 15 | func (m *Mincore) CallName() string { return "mincore" } 16 | func (m *Mincore) Return() *Argument { return nil } 17 | func (m *Mincore) DecodeArguments(data []*byte, arglen int) error { 18 | m.Addr = uintptr(types.MakeCU64(unsafe.Pointer(data[0]))) 19 | m.Len = int(types.MakeCU64(unsafe.Pointer(data[1]))) 20 | m.Vec = uintptr(types.MakeCU64(unsafe.Pointer(data[2]))) 21 | return nil 22 | } 23 | 24 | func (m *Mincore) Arguments() Arguments { 25 | return Arguments{ 26 | {"addr", "void *", m.Addr}, 27 | {"len", "size_t", m.Len}, 28 | {"vec", "unsigned char *", m.Vec}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/getpeername_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/criticalstack/swoll/pkg/types" 11 | ) 12 | 13 | func TestGetpeername(t *testing.T) { 14 | sa := &syscall.RawSockaddrInet4{ 15 | Family: syscall.AF_INET, 16 | Addr: [4]byte{172, 19, 31, 254}, 17 | Port: htons(80), 18 | } 19 | 20 | s := &Getpeername{ 21 | FD: types.InputFD(20), 22 | Saddr: (*types.SockAddr)(unsafe.Pointer(sa)), 23 | } 24 | 25 | j, err := json.Marshal(s) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | var g Getpeername 31 | if err := json.Unmarshal(j, &g); err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | if !reflect.DeepEqual(s.Arguments(), g.Arguments()) { 36 | t.Errorf("Was expecting %v but got %v\n", s.Arguments(), g.Arguments()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/event/call/seccomp.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Seccomp struct { 10 | Op int `json:"op"` 11 | Flags int `json:"flags"` 12 | Args string `json:"args"` 13 | } 14 | 15 | func (s *Seccomp) CallName() string { return "seccomp" } 16 | func (s *Seccomp) Return() *Argument { return nil } 17 | func (s *Seccomp) DecodeArguments(data []*byte, arglen int) error { 18 | s.Op = int(types.MakeCU32(unsafe.Pointer(data[0]))) 19 | s.Flags = int(types.MakeCU32(unsafe.Pointer(data[1]))) 20 | s.Args = types.MakeCString(unsafe.Pointer(data[2]), arglen) 21 | return nil 22 | } 23 | 24 | func (s *Seccomp) Arguments() Arguments { 25 | return Arguments{ 26 | {"operation", "unsigned int", s.Op}, 27 | {"flags", "unsigned int", s.Flags}, 28 | {"args", "void *", s.Args}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/getsockname_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/criticalstack/swoll/pkg/types" 11 | ) 12 | 13 | func TestGetsockname(t *testing.T) { 14 | 15 | sa := &syscall.RawSockaddrInet4{ 16 | Family: syscall.AF_INET, 17 | Addr: [4]byte{172, 19, 31, 254}, 18 | Port: htons(80), 19 | } 20 | 21 | s := &Getsockname{ 22 | FD: types.InputFD(20), 23 | Saddr: (*types.SockAddr)(unsafe.Pointer(sa)), 24 | } 25 | 26 | j, err := json.Marshal(s) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | var g Getsockname 32 | if err := json.Unmarshal(j, &g); err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | if !reflect.DeepEqual(s.Arguments(), g.Arguments()) { 37 | t.Errorf("Was expecting %v but got %v\n", s.Arguments(), g.Arguments()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /pkg/event/call/init_module.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type InitModule struct { 10 | Name string `json:"name"` 11 | Len int `json:"len"` 12 | Params string `json:"params"` 13 | } 14 | 15 | func (i *InitModule) CallName() string { return "init_module" } 16 | func (i *InitModule) Return() *Argument { return nil } 17 | func (i *InitModule) DecodeArguments(data []*byte, arglen int) error { 18 | i.Name = types.MakeCString(unsafe.Pointer(data[0]), arglen) 19 | i.Len = int(types.MakeCU64(unsafe.Pointer(data[1]))) 20 | i.Params = types.MakeCString(unsafe.Pointer(data[2]), arglen) 21 | return nil 22 | } 23 | 24 | func (i *InitModule) Arguments() Arguments { 25 | return Arguments{ 26 | {"module_image", "void *", i.Name}, 27 | {"len", "unsigned long", i.Len}, 28 | {"param_values", "const char *", i.Params}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/openat.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Openat struct { 10 | DirFD types.DirFD `json:"dir_fd"` 11 | Pathname string `json:"pathname"` 12 | Flags types.FileFlags `json:"flags"` 13 | } 14 | 15 | func (o *Openat) CallName() string { return "openat" } 16 | func (o *Openat) Return() *Argument { return nil } 17 | 18 | func (o *Openat) DecodeArguments(data []*byte, arglen int) error { 19 | o.DirFD = types.DirFD(types.MakeC32(unsafe.Pointer(data[0]))) 20 | o.Pathname = types.MakeCString(unsafe.Pointer(data[1]), arglen) 21 | o.Flags = types.FileFlags(types.MakeC32(unsafe.Pointer(data[2]))) 22 | return nil 23 | } 24 | 25 | func (o *Openat) Arguments() Arguments { 26 | return Arguments{ 27 | {"dirfd", "int", o.DirFD}, 28 | {"pathname", "const char *", o.Pathname}, 29 | {"flags", "int", o.Flags}, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/event/call/sendto_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/criticalstack/swoll/pkg/types" 11 | ) 12 | 13 | func TestSendto(t *testing.T) { 14 | sa := &syscall.RawSockaddrInet4{ 15 | Family: syscall.AF_INET, 16 | Addr: [4]byte{172, 19, 31, 254}, 17 | Port: htons(80), 18 | } 19 | 20 | s := &Sendto{ 21 | FD: types.InputFD(956), 22 | Ubuf: types.Buffer([]byte("test")), 23 | Size: 5, 24 | Flags: syscall.MSG_OOB | syscall.MSG_TRUNC, 25 | Saddr: (*types.SockAddr)(unsafe.Pointer(sa)), 26 | } 27 | j, err := json.Marshal(s) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | var w Sendto 33 | if err = json.Unmarshal(j, &w); err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | if !reflect.DeepEqual(s.Arguments(), w.Arguments()) { 38 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), w.Arguments()) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /pkg/event/call/execve.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | type Execve struct { 11 | Filename string `json:"filename"` 12 | Argv [4]string `json:"argv"` 13 | } 14 | 15 | func (e *Execve) CallName() string { return "execve" } 16 | func (e *Execve) Return() *Argument { return nil } 17 | func (e *Execve) DecodeArguments(data []*byte, arglen int) error { 18 | e.Filename = types.MakeCString(unsafe.Pointer(data[0]), arglen) 19 | e.Argv[0] = types.MakeCString(unsafe.Pointer(data[1]), arglen) 20 | e.Argv[1] = types.MakeCString(unsafe.Pointer(data[2]), arglen) 21 | e.Argv[2] = types.MakeCString(unsafe.Pointer(data[3]), arglen) 22 | return nil 23 | } 24 | 25 | func (e *Execve) Arguments() Arguments { 26 | return Arguments{ 27 | {"filename", "const char *", e.Filename}, 28 | {"argv[]", "char * const", fmt.Sprintf("%s %s %s %s", e.Argv[0], e.Argv[1], e.Argv[2], e.Argv[3])}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/event/call/open.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Open struct { 10 | Filename string `json:"filename"` 11 | Flags types.FileFlags `json:"flags"` 12 | Mode int `json:"mode"` 13 | Ret types.OutputFD `json:"ret"` 14 | } 15 | 16 | func (o *Open) CallName() string { return "open" } 17 | func (o *Open) Return() *Argument { return &Argument{"out_fd", "int", o.Ret} } 18 | 19 | func (o *Open) DecodeArguments(data []*byte, arglen int) error { 20 | o.Filename = types.MakeCString(unsafe.Pointer(data[0]), arglen) 21 | o.Flags = types.FileFlags(types.MakeC32(unsafe.Pointer(data[1]))) 22 | o.Mode = int(types.MakeC32(unsafe.Pointer(data[2]))) 23 | 24 | return nil 25 | } 26 | 27 | func (o *Open) Arguments() Arguments { 28 | return Arguments{ 29 | {"pathname", "const char *", o.Filename}, 30 | {"flags", "int", o.Flags}, 31 | {"mode", "int", o.Mode}, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/event/call/setns.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Setns struct { 10 | FD types.InputFD `json:"fd"` 11 | NSType types.CloneFlags `json:"ns_type"` 12 | } 13 | 14 | const ( 15 | CLONE_NEWCGROUP = 0x02000000 16 | CLONE_NEWUTS = 0x04000000 17 | CLONE_NEWIPC = 0x08000000 18 | CLONE_NEWUSER = 0x10000000 19 | CLONE_NEWPID = 0x20000000 20 | CLONE_NEWNET = 0x40000000 21 | CLONE_NEWNS = 0x00020000 22 | ) 23 | 24 | func (s *Setns) CallName() string { return "setns" } 25 | func (s *Setns) Return() *Argument { return nil } 26 | func (s *Setns) DecodeArguments(data []*byte, arglen int) error { 27 | s.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 28 | s.NSType = types.CloneFlags(types.MakeC32(unsafe.Pointer(data[1]))) 29 | 30 | return nil 31 | } 32 | 33 | func (s *Setns) Arguments() Arguments { 34 | return Arguments{ 35 | {"fd", "int", s.FD}, 36 | {"nstype", "int", s.NSType}, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /chart/templates/controller-cluster-role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.controller.enabled -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: {{ template "swoll.fullname" . }}-controller 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - nodes 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | - apiGroups: 17 | - batch 18 | resources: 19 | - jobs 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - batch 30 | resources: 31 | - jobs/status 32 | verbs: 33 | - get 34 | - apiGroups: 35 | - tools.swoll.criticalstack.com 36 | resources: 37 | - traces 38 | verbs: 39 | - create 40 | - delete 41 | - get 42 | - list 43 | - patch 44 | - update 45 | - watch 46 | - apiGroups: 47 | - tools.swoll.criticalstack.com 48 | resources: 49 | - traces/status 50 | verbs: 51 | - get 52 | - patch 53 | - update 54 | {{- end -}} 55 | -------------------------------------------------------------------------------- /pkg/event/call/read.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Read struct { 10 | FD types.InputFD `json:"fd"` 11 | Buf types.Buffer `json:"buf"` 12 | Count int `json:"count"` 13 | } 14 | 15 | func (r *Read) CallName() string { return "read" } 16 | func (r *Read) Return() *Argument { return &Argument{"count", "int", r.Count} } 17 | func (r *Read) DecodeArguments(data []*byte, arglen int) error { 18 | r.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 19 | r.Count = int(types.MakeC64(unsafe.Pointer(data[2]))) 20 | 21 | var blen int 22 | 23 | if r.Count > arglen { 24 | blen = arglen 25 | } else { 26 | blen = r.Count 27 | } 28 | 29 | r.Buf = types.Buffer(types.MakeCBytes(unsafe.Pointer(data[1]), blen)) 30 | 31 | return nil 32 | } 33 | 34 | func (r *Read) Arguments() Arguments { 35 | return Arguments{ 36 | {"fd", "int", r.FD}, 37 | {"buf", "void *", r.Buf}, 38 | {"count", "ssize_t", r.Count}, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | containers: 26 | - command: 27 | - swoll 28 | - controller 29 | - --enable-leader-election 30 | - --image 31 | - criticalstack/swoll:latest 32 | image: criticalstack/swoll:latest 33 | name: manager 34 | resources: 35 | limits: 36 | cpu: 100m 37 | memory: 30Mi 38 | requests: 39 | cpu: 100m 40 | memory: 20Mi 41 | terminationGracePeriodSeconds: 10 42 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - nodes 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - apiGroups: 18 | - batch 19 | resources: 20 | - jobs 21 | verbs: 22 | - create 23 | - delete 24 | - get 25 | - list 26 | - patch 27 | - update 28 | - watch 29 | - apiGroups: 30 | - batch 31 | resources: 32 | - jobs/status 33 | verbs: 34 | - get 35 | - apiGroups: 36 | - "" 37 | resources: 38 | - configmaps 39 | verbs: 40 | - get 41 | - list 42 | - watch 43 | - apiGroups: 44 | - tools.swoll.criticalstack.com 45 | resources: 46 | - traces 47 | verbs: 48 | - create 49 | - delete 50 | - get 51 | - list 52 | - patch 53 | - update 54 | - watch 55 | - apiGroups: 56 | - tools.swoll.criticalstack.com 57 | resources: 58 | - traces/status 59 | verbs: 60 | - get 61 | - patch 62 | - update 63 | -------------------------------------------------------------------------------- /pkg/event/call/write.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Write struct { 10 | FD types.InputFD `json:"fd"` 11 | Buf types.Buffer `json:"buf"` 12 | Count int `json:"count"` 13 | } 14 | 15 | func (w *Write) CallName() string { return "write" } 16 | func (w *Write) Return() *Argument { return &Argument{"count", "ssize_t", w.Count} } 17 | func (w *Write) DecodeArguments(data []*byte, arglen int) error { 18 | w.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 19 | w.Count = int(types.MakeC64(unsafe.Pointer(data[2]))) 20 | 21 | var blen int 22 | 23 | if w.Count > arglen { 24 | blen = arglen 25 | } else { 26 | blen = w.Count 27 | } 28 | 29 | w.Buf = types.Buffer(types.MakeCBytes(unsafe.Pointer(data[1]), blen)) 30 | 31 | return nil 32 | } 33 | 34 | func (w *Write) Arguments() Arguments { 35 | return Arguments{ 36 | {"fd", "int", w.FD}, 37 | {"buf", "const void *", w.Buf}, 38 | {"count", "ssize_t", w.Count}, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and test Swoll 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install golang 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: '^1.13.1' # The Go version to download (if necessary) and use. 22 | 23 | - name: Run linter 24 | run: | 25 | make test 26 | 27 | test: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v2 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Install golang 36 | uses: actions/setup-go@v2 37 | with: 38 | go-version: '^1.13.1' # The Go version to download (if necessary) and use. 39 | 40 | - name: Run tests 41 | run: | 42 | make test 43 | -------------------------------------------------------------------------------- /pkg/event/call/readlink.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Readlink struct { 10 | Pathname string `json:"pathname"` 11 | Buf types.Buffer `json:"buf"` 12 | Bufsize int `json:"size"` 13 | } 14 | 15 | func (r *Readlink) CallName() string { return "readlink" } 16 | func (r *Readlink) Return() *Argument { return nil } 17 | func (r *Readlink) DecodeArguments(data []*byte, arglen int) error { 18 | r.Pathname = types.MakeCString(unsafe.Pointer(data[0]), arglen) 19 | r.Bufsize = int(types.MakeC64(unsafe.Pointer(data[2]))) 20 | 21 | var bsz int 22 | 23 | if r.Bufsize < arglen { 24 | bsz = r.Bufsize 25 | } else { 26 | bsz = arglen 27 | } 28 | 29 | r.Buf = types.MakeCBytes(unsafe.Pointer(data[1]), bsz) 30 | 31 | return nil 32 | } 33 | 34 | func (r *Readlink) Arguments() Arguments { 35 | return Arguments{ 36 | {"pathname", "const char *", r.Pathname}, 37 | {"buf", "char *", r.Buf}, 38 | {"bufsiz", "size_t", r.Bufsize}, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/event/call/faccessat.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "os" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | type Faccessat struct { 11 | FD types.DirFD `json:"fd"` 12 | Pathname string `json:"pathname"` 13 | Mode os.FileMode `json:"mode"` 14 | Flags int `json:"flags"` 15 | } 16 | 17 | func (f *Faccessat) CallName() string { return "faccessat" } 18 | func (f *Faccessat) Return() *Argument { return nil } 19 | func (f *Faccessat) DecodeArguments(data []*byte, arglen int) error { 20 | f.FD = types.DirFD(types.MakeC32(unsafe.Pointer(data[0]))) 21 | f.Pathname = types.MakeCString(unsafe.Pointer(data[1]), arglen) 22 | f.Mode = os.FileMode(types.MakeC32(unsafe.Pointer(data[2]))) 23 | f.Flags = int(types.MakeC32(unsafe.Pointer(data[3]))) 24 | return nil 25 | } 26 | 27 | func (f *Faccessat) Arguments() Arguments { 28 | return Arguments{ 29 | {"dirfd", "int", f.FD}, 30 | {"pathname", "const char *", f.Pathname}, 31 | {"mode", "int", f.Mode}, 32 | {"flags", "int", f.Flags}, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/types/c.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/binary" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | func MakeCBytes(data unsafe.Pointer, len int) []byte { 10 | var bytes []byte 11 | 12 | shdr := (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) 13 | shdr.Cap = int(len) 14 | shdr.Len = int(len) 15 | shdr.Data = uintptr(data) 16 | 17 | return bytes 18 | } 19 | 20 | func MakeCString(data unsafe.Pointer, len int) string { 21 | bytes := MakeCBytes(data, len) 22 | count := 0 23 | 24 | for _, b := range bytes { 25 | if b == 0 { 26 | break 27 | } 28 | 29 | count++ 30 | } 31 | 32 | return string(bytes[:count]) 33 | } 34 | 35 | func MakeCU64(data unsafe.Pointer) uint64 { 36 | return binary.LittleEndian.Uint64(MakeCBytes(data, 8)) 37 | } 38 | 39 | func MakeC64(data unsafe.Pointer) int64 { 40 | return int64(MakeCU64(data)) 41 | } 42 | 43 | func MakeCU32(data unsafe.Pointer) uint32 { 44 | return binary.LittleEndian.Uint32(MakeCBytes(data, 4)) 45 | } 46 | 47 | func MakeC32(data unsafe.Pointer) int32 { 48 | return int32(MakeCU32(data)) 49 | } 50 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /internal/bpf/asm_goto_workaround.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This is literally ripped right ouf of the kernel. All rights go to these 3 | * people. 4 | * 5 | * SPDX-License-Identifier: GPL-2.0 6 | * Copyright (c) 2019 Facebook 7 | */ 8 | #ifndef __ASM_GOTO_WORKAROUND_H 9 | #define __ASM_GOTO_WORKAROUND_H 10 | 11 | /* 12 | * This will bring in asm_volatile_goto and asm_inline macro definitions 13 | * if enabled by compiler and config options. 14 | */ 15 | #include 16 | 17 | #ifdef asm_volatile_goto 18 | #undef asm_volatile_goto 19 | #define asm_volatile_goto(x ...) asm volatile ("invalid use of asm_volatile_goto") 20 | #endif 21 | 22 | /* 23 | * asm_inline is defined as asm __inline in "include/linux/compiler_types.h" 24 | * if supported by the kernel's CC (i.e CONFIG_CC_HAS_ASM_INLINE) which is not 25 | * supported by CLANG. 26 | */ 27 | #ifdef asm_inline 28 | #undef asm_inline 29 | #define asm_inline asm 30 | #endif 31 | 32 | #define volatile(x ...) volatile ("") 33 | 34 | #ifndef __kernel_time_t 35 | #define __kernel_time_t __kernel_timer_t 36 | #endif 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /pkg/event/call/timerfd_settime.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type TimerFDSettime struct { 10 | FD types.InputFD `json:"fd"` 11 | Flags types.TimerFlags `json:"flags"` 12 | New types.Itimerspec `json:"new"` 13 | Old types.Itimerspec `json:"old"` 14 | } 15 | 16 | func (f *TimerFDSettime) CallName() string { return "timerfd_settime" } 17 | func (f *TimerFDSettime) Return() *Argument { return nil } 18 | func (f *TimerFDSettime) DecodeArguments(data []*byte, arglen int) error { 19 | f.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 20 | f.Flags = types.TimerFlags(types.MakeC32(unsafe.Pointer(data[1]))) 21 | f.New = *(*types.Itimerspec)(unsafe.Pointer(data[2])) 22 | f.Old = *(*types.Itimerspec)(unsafe.Pointer(data[3])) 23 | 24 | return nil 25 | } 26 | func (f *TimerFDSettime) Arguments() Arguments { 27 | return Arguments{ 28 | {"fd", "int", f.FD}, 29 | {"flags", "int", f.Flags}, 30 | {"new_value", "const struct itimerspec *", f.New}, 31 | {"old_value", "struct itimerspec *", f.Old}, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/event/call/stat_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestStat(t *testing.T) { 11 | s := &Stat{ 12 | Filename: "/tmp/tmux.log", 13 | StatBuf: syscall.Stat_t{ 14 | Dev: 16, 15 | Ino: 4, 16 | Nlink: 20, 17 | Mode: 0, 18 | Uid: 2, 19 | Gid: 5, 20 | X__pad0: 0, 21 | Rdev: 16, 22 | Size: 45679, 23 | Blksize: 512, 24 | Blocks: 256, 25 | Atim: syscall.Timespec{ 26 | Sec: 0, 27 | Nsec: 0, 28 | }, 29 | Mtim: syscall.Timespec{ 30 | Sec: 45, 31 | Nsec: 0, 32 | }, 33 | Ctim: syscall.Timespec{ 34 | Sec: 22, 35 | Nsec: 9830002, 36 | }, 37 | X__unused: [3]int64{28, 98, 72}, 38 | }, 39 | } 40 | 41 | j, err := json.Marshal(s) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | var a Stat 47 | if err := json.Unmarshal(j, &a); err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 52 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/event/call/statfs_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestStatfs(t *testing.T) { 11 | s := &Statfs{ 12 | Path: "/tmp/vibe.log", 13 | StatBuf: syscall.Stat_t{ 14 | Dev: 16, 15 | Ino: 4, 16 | Nlink: 20, 17 | Mode: 0, 18 | Uid: 2, 19 | Gid: 5, 20 | X__pad0: 0, 21 | Rdev: 16, 22 | Size: 45679, 23 | Blksize: 512, 24 | Blocks: 256, 25 | Atim: syscall.Timespec{ 26 | Sec: 0, 27 | Nsec: 0, 28 | }, 29 | Mtim: syscall.Timespec{ 30 | Sec: 45, 31 | Nsec: 0, 32 | }, 33 | Ctim: syscall.Timespec{ 34 | Sec: 22, 35 | Nsec: 9830002, 36 | }, 37 | X__unused: [3]int64{28, 98, 72}, 38 | }, 39 | } 40 | 41 | j, err := json.Marshal(s) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | var a Statfs 47 | if err := json.Unmarshal(j, &a); err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 52 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), s.Arguments()) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /pkg/event/call/timerfd_settime_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "encoding/json" 8 | 9 | "github.com/criticalstack/swoll/pkg/types" 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func TestTimerFDSettime(t *testing.T) { 14 | 15 | n := types.Itimerspec{ 16 | Interval: types.Timespec{ 17 | Sec: 1250, 18 | Nsec: 0, 19 | }, 20 | Value: types.Timespec{ 21 | Sec: 1250, 22 | Nsec: 0, 23 | }, 24 | } 25 | 26 | o := types.Itimerspec{ 27 | Interval: types.Timespec{ 28 | Sec: 2450, 29 | Nsec: 0, 30 | }, 31 | Value: types.Timespec{ 32 | Sec: 2350, 33 | Nsec: 0, 34 | }, 35 | } 36 | 37 | s := &TimerFDSettime{ 38 | FD: types.InputFD(98), 39 | Flags: types.TimerFlags(unix.CLOCK_REALTIME), 40 | New: n, 41 | Old: o, 42 | } 43 | j, err := json.Marshal(s) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | var a TimerFDSettime 49 | if err = json.Unmarshal(j, &a); err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | if !reflect.DeepEqual(s.Arguments(), a.Arguments()) { 54 | t.Errorf("Was expecting %v, but got %v\n", s.Arguments(), a.Arguments()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/basic-trace/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/criticalstack/swoll/pkg/event" 9 | "github.com/criticalstack/swoll/pkg/kernel" 10 | "github.com/criticalstack/swoll/pkg/kernel/assets" 11 | ) 12 | 13 | func dumpTextEvent(ev *event.TraceEvent) { 14 | fn := ev.Argv 15 | 16 | fmt.Printf("[%s/%v] (%s) %s(", ev.Comm, ev.Pid, ev.Error, fn.CallName()) 17 | for _, arg := range fn.Arguments() { 18 | fmt.Printf("(%s)%s=%v ", arg.Type, arg.Name, arg.Value) 19 | } 20 | fmt.Println(")") 21 | } 22 | 23 | func main() { 24 | probe, err := kernel.NewProbe(assets.LoadBPFReader(), nil) 25 | if err != nil { 26 | log.Fatalf("Unable to load static BPF asset: %v", err) 27 | } 28 | 29 | if err := probe.InitProbe(kernel.WithOffsetDetection(), kernel.WithDefaultFilter()); err != nil { 30 | log.Fatalf("Unable to initialize probe: %v", err) 31 | } 32 | 33 | event := new(event.TraceEvent) 34 | 35 | probe.Run(context.Background(), func(msg []byte, lost uint64) error { 36 | parsed, err := event.Ingest(msg) 37 | if err != nil { 38 | return nil 39 | } 40 | 41 | dumpTextEvent(parsed) 42 | 43 | return nil 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/event/call/accept_test.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/criticalstack/swoll/pkg/types" 11 | ) 12 | 13 | func htons(port uint16) uint16 { 14 | var ( 15 | lowbyte uint8 = uint8(port) 16 | highbyte uint8 = uint8(port << 8) 17 | ret uint16 = uint16(lowbyte)<<8 + uint16(highbyte) 18 | ) 19 | return ret 20 | } 21 | 22 | func TestAcceptMarshal(t *testing.T) { 23 | } 24 | 25 | func TestAccept4Marshal(t *testing.T) { 26 | sa := &syscall.RawSockaddrInet4{ 27 | Family: syscall.AF_INET, 28 | Addr: [4]byte{172, 19, 31, 254}, 29 | Port: htons(80), 30 | } 31 | 32 | s := &Accept4{ 33 | FD: 1, 34 | Saddr: (*types.SockAddr)(unsafe.Pointer(sa)), 35 | Flags: syscall.SOCK_CLOEXEC | syscall.SOCK_NONBLOCK, 36 | } 37 | 38 | j, err := json.Marshal(s) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | var a4 Accept4 44 | if err := json.Unmarshal(j, &a4); err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | if !reflect.DeepEqual(s.Arguments(), a4.Arguments()) { 49 | t.Errorf("Was expecting %v but got %v\n", s.Arguments(), a4.Arguments()) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /pkg/event/call/sendto.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Sendto struct { 10 | FD types.InputFD `json:"fd"` 11 | Ubuf types.Buffer `json:"ubuf"` 12 | Size int `json:"size"` 13 | Flags types.MsgFlags `json:"flags"` 14 | Saddr *types.SockAddr `json:"saddr"` 15 | } 16 | 17 | func (s *Sendto) CallName() string { return "sendto" } 18 | func (s *Sendto) Return() *Argument { return nil } 19 | func (s *Sendto) DecodeArguments(data []*byte, arglen int) error { 20 | s.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 21 | s.Ubuf = types.Buffer(types.MakeCBytes(unsafe.Pointer(data[1]), arglen)) 22 | s.Size = int(types.MakeC64(unsafe.Pointer(data[2]))) 23 | s.Flags = types.MsgFlags(types.MakeC32(unsafe.Pointer(data[3]))) 24 | s.Saddr = (*types.SockAddr)(unsafe.Pointer(data[4])) 25 | 26 | return nil 27 | } 28 | 29 | func (s *Sendto) Arguments() Arguments { 30 | return Arguments{ 31 | {"sockfd", "int", s.FD}, 32 | {"buf", "const void *", s.Ubuf}, 33 | {"len", "size_t", s.Size}, 34 | {"flags", "int", s.Flags}, 35 | {"dest_addr", "const struct sockaddr *", s.Saddr}, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/event/call/readlinkat.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Readlinkat struct { 10 | DirFD types.DirFD `json:"dir_fd"` 11 | Pathname string `json:"pathname"` 12 | Buf types.Buffer `json:"buf"` 13 | Bufsize int `json:"size"` 14 | } 15 | 16 | func (r *Readlinkat) CallName() string { return "readlinkat" } 17 | func (r *Readlinkat) Return() *Argument { return nil } 18 | func (r *Readlinkat) DecodeArguments(data []*byte, arglen int) error { 19 | r.DirFD = types.DirFD(types.MakeC32(unsafe.Pointer(data[0]))) 20 | r.Pathname = types.MakeCString(unsafe.Pointer(data[1]), arglen) 21 | r.Bufsize = int(types.MakeC64(unsafe.Pointer(data[3]))) 22 | 23 | var bsz int 24 | 25 | if r.Bufsize < arglen { 26 | bsz = r.Bufsize 27 | } else { 28 | bsz = arglen 29 | } 30 | 31 | r.Buf = types.MakeCBytes(unsafe.Pointer(data[2]), bsz) 32 | 33 | return nil 34 | } 35 | 36 | func (r *Readlinkat) Arguments() Arguments { 37 | return Arguments{ 38 | {"dirfd", "int", r.DirFD}, 39 | {"pathname", "const char *", r.Pathname}, 40 | {"buf", "char *", r.Buf}, 41 | {"bufsiz", "size_t", r.Bufsize}, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/types/umountflags.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "syscall" 7 | ) 8 | 9 | type UmountFlags int 10 | 11 | var umountFlags = map[int]string{ 12 | syscall.MNT_FORCE: "MNT_FORCE", 13 | syscall.MNT_DETACH: "MNT_DETACH", 14 | syscall.MNT_EXPIRE: "MNT_EXPIRE", 15 | } 16 | 17 | func (flags UmountFlags) Parse() []string { 18 | ret := []string{} 19 | fint := int(flags) 20 | 21 | for flag, fstr := range umountFlags { 22 | if fint&flag != 0 { 23 | ret = append(ret, fstr) 24 | } 25 | } 26 | 27 | return ret 28 | } 29 | 30 | func (flags UmountFlags) String() string { 31 | return strings.Join(flags.Parse(), "|") 32 | } 33 | 34 | func (flags UmountFlags) MarshalJSON() ([]byte, error) { 35 | return json.Marshal(flags.Parse()) 36 | } 37 | 38 | func (f *UmountFlags) UnmarshalJSON(data []byte) error { 39 | var a []string 40 | 41 | if err := json.Unmarshal(data, &a); err != nil { 42 | return err 43 | } 44 | 45 | for _, v := range a { 46 | switch v { 47 | case "MNT_FORCE": 48 | *f |= syscall.MNT_FORCE 49 | case "MNT_DETACH": 50 | *f |= syscall.MNT_DETACH 51 | case "MNT_EXPIRE": 52 | *f |= syscall.MNT_EXPIRE 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/event/call/clone.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | 7 | "github.com/criticalstack/swoll/pkg/types" 8 | ) 9 | 10 | type Clone struct { 11 | Flags types.CloneFlags `json:"flags"` 12 | NewStack uint64 `json:"new_stack"` 13 | ChildStack uint64 `json:"child_stack"` 14 | ParentStack uint64 `json:"parent_stack"` 15 | Tls uint64 `json:"tls"` 16 | } 17 | 18 | func (c *Clone) CallName() string { return "clone" } 19 | func (c *Clone) Return() *Argument { return nil } 20 | func (c *Clone) DecodeArguments(data []*byte, arglen int) error { 21 | c.Flags = types.CloneFlags(types.MakeCU64(unsafe.Pointer(data[0]))) 22 | c.NewStack = uint64(types.MakeCU64(unsafe.Pointer(data[1]))) 23 | c.ChildStack = uint64(types.MakeCU64(unsafe.Pointer(data[2]))) 24 | c.ParentStack = uint64(types.MakeCU64(unsafe.Pointer(data[3]))) 25 | c.Tls = uint64(types.MakeCU64(unsafe.Pointer(data[4]))) 26 | 27 | return nil 28 | } 29 | 30 | func (c *Clone) Arguments() Arguments { 31 | return Arguments{ 32 | {"fn", "int (*)(void *)", fmt.Sprintf("0x%08x", c.NewStack)}, 33 | {"child_stack", "void *", fmt.Sprintf("0x%08x", c.ChildStack)}, 34 | {"flags", "int", c.Flags}, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/pkg/alert/alert.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | const ( 4 | // StatusUnknown is status of this alert is unknown 5 | StatusUnknown = iota 6 | //StatusFiring is the status of this alert is currently firing 7 | StatusFiring 8 | // StatusResolved is the status of this alert has been resovled 9 | StatusResolved 10 | ) 11 | 12 | // Source contains all the relevant information about an entity that triggered 13 | // this alert. 14 | type Source struct { 15 | Namespace string // the k8s namespace 16 | Pod string // the k8s pod name 17 | Container string // the name of the k8s container 18 | } 19 | 20 | // Status is a reference to the state of which this alert is currently in. 21 | type Status int 22 | 23 | // Info contains specific information about an alert as it pertains to an AlertSource 24 | type Info struct { 25 | Status Status // unknown|firing|resolved 26 | Name string // Name of the alert 27 | Hash string // A unique identifier for this specific alert 28 | Syscall string // the name of the syscall that triggered the alert. 29 | URL string // GeneratorURL 30 | } 31 | 32 | // Alert contains both informational fields, and the source of 33 | // an alert. 34 | type Alert struct { 35 | Info Info 36 | Source Source 37 | } 38 | -------------------------------------------------------------------------------- /pkg/types/container.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "fmt" 4 | 5 | type Container struct { 6 | ID string `json:"id,omitempty"` 7 | Pod string `json:"pod,omitempty"` 8 | PodSandboxID string `json:"sandbox-id,omitempty"` 9 | Name string `json:"name,omitempty"` 10 | Image string `json:"image,omitempty"` 11 | Namespace string `json:"namespace,omitempty"` 12 | Labels map[string]string `json:"labels,omitempty"` 13 | Pid int `json:"pid,omitempty"` 14 | PidNamespace int `json:"pid-namespace,omitempty"` 15 | } 16 | 17 | func (c *Container) FQDN() string { 18 | if c == nil { 19 | return "-.-.-" 20 | } 21 | 22 | return fmt.Sprintf("%s.%s.%s", c.Name, c.Pod, c.Namespace) 23 | } 24 | 25 | func (c *Container) Copy() *Container { 26 | n := &Container{ 27 | ID: c.ID, 28 | Pod: c.Pod, 29 | PodSandboxID: c.PodSandboxID, 30 | Name: c.Name, 31 | Image: c.Image, 32 | Namespace: c.Namespace, 33 | Labels: make(map[string]string), 34 | Pid: c.Pid, 35 | PidNamespace: c.PidNamespace, 36 | } 37 | 38 | for k, v := range c.Labels { 39 | n.Labels[k] = v 40 | } 41 | 42 | return n 43 | } 44 | -------------------------------------------------------------------------------- /cmd/loader.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/criticalstack/swoll/pkg/kernel/assets" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // loadBPFargs will attempt to find the BPF object file via the commandline, 12 | // If the argument is empty (default), we check the local environment, and if 13 | // that fails, we attempt to load the go-bindata generated asset. 14 | func loadBPFargs(cmd *cobra.Command, args []string) ([]byte, error) { 15 | var ( 16 | bpf []byte 17 | err error 18 | ) 19 | 20 | // first check to see if the bpf object was defined at the commandline 21 | bpfFile, err = cmd.Flags().GetString("bpf") 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | if bpfFile == "" { 27 | // not found on the command-line, now try environment 28 | bpfFile = os.Getenv("SWOLL_BPFOBJECT") 29 | } 30 | 31 | if bpfFile != "" { 32 | // attempt to read the bpf object file if defined 33 | bpf, err = ioutil.ReadFile(bpfFile) 34 | if err != nil && !os.IsNotExist(err) { 35 | // only error if the error is *NOT* of type "file not found" 36 | return nil, err 37 | } 38 | } 39 | 40 | if len(bpf) == 0 { 41 | // we've tried all sorts of ways to load this file, by default 42 | // it attempts to use the go-bindata generated asset resource. 43 | bpf = assets.LoadBPF() 44 | } 45 | 46 | return bpf, err 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/criticalstack/swoll 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fatih/color v1.9.0 7 | github.com/gizak/termui/v3 v3.1.0 8 | github.com/go-bindata/go-bindata v3.1.2+incompatible 9 | github.com/go-echarts/go-echarts v1.0.0 10 | github.com/go-logr/logr v0.1.0 11 | github.com/go-redis/redis v6.15.6+incompatible 12 | github.com/golangci/golangci-lint v1.32.0 13 | github.com/google/uuid v1.1.2 14 | github.com/gorilla/handlers v1.4.2 15 | github.com/gorilla/mux v1.7.4 16 | github.com/gorilla/websocket v1.4.2 17 | github.com/iovisor/gobpf v0.0.0-20191219090757-e72091e3c5e6 18 | github.com/pkg/errors v0.9.1 19 | github.com/prometheus/alertmanager v0.21.0 20 | github.com/prometheus/client_golang v1.6.0 21 | github.com/prometheus/common v0.10.0 22 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 23 | github.com/sirupsen/logrus v1.7.0 24 | github.com/spf13/cobra v1.1.1 25 | github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 26 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 27 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 28 | google.golang.org/grpc v1.26.0 29 | k8s.io/api v0.18.4 30 | k8s.io/apimachinery v0.18.10 31 | k8s.io/client-go v0.18.4 32 | k8s.io/cri-api v0.0.0-20190828121515-24ae4d4e8b03 33 | sigs.k8s.io/controller-runtime v0.6.1 34 | sigs.k8s.io/yaml v1.2.0 35 | 36 | ) 37 | -------------------------------------------------------------------------------- /pkg/event/call/recvfrom.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/criticalstack/swoll/pkg/types" 7 | ) 8 | 9 | type Recvfrom struct { 10 | FD types.InputFD `json:"fd"` 11 | Ubuf types.Buffer `json:"ubuf"` 12 | Size int `json:"size"` 13 | OSize int `json:"o_size"` 14 | Flags types.MsgFlags `json:"flags"` 15 | Saddr *types.SockAddr `json:"saddr"` 16 | } 17 | 18 | func (r *Recvfrom) CallName() string { return "recvfrom" } 19 | func (r *Recvfrom) Return() *Argument { return nil } 20 | func (r *Recvfrom) DecodeArguments(data []*byte, arglen int) error { 21 | r.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 22 | // XXX[lz]: technically, the length of this buffer will be the RETURN VALUE 23 | // of this function, but for now, we just copy the whole buffer from the 24 | // kernel, and let the caller deal with the truncation. 25 | r.Ubuf = types.Buffer(types.MakeCBytes(unsafe.Pointer(data[1]), arglen)) 26 | r.Flags = types.MsgFlags(types.MakeCU32(unsafe.Pointer(data[3]))) 27 | r.Saddr = (*types.SockAddr)(unsafe.Pointer(data[4])) 28 | 29 | return nil 30 | } 31 | 32 | func (r *Recvfrom) Arguments() Arguments { 33 | return Arguments{ 34 | {"sockfd", "int", r.FD}, 35 | {"buf", "void *", r.Ubuf}, 36 | {"len", "size_t", r.Size}, 37 | {"flags", "int", r.Flags}, 38 | {"src_addr", "struct sockaddr *", r.Saddr}, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the tools v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=tools.swoll.criticalstack.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "tools.swoll.criticalstack.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /hack/tools/pretty_logs.go: -------------------------------------------------------------------------------- 1 | // quick tool to read from stdin, expecting JSON client.StreamMessage logs 2 | // e.g., `kubectl logs -l sw-job= -f | go run hack/tools/pretty_logs.go` 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | 12 | "github.com/criticalstack/swoll/pkg/client" 13 | "github.com/fatih/color" 14 | ) 15 | 16 | func main() { 17 | scanner := bufio.NewScanner(os.Stdin) 18 | 19 | for scanner.Scan() { 20 | var event client.StreamMessage 21 | 22 | b := scanner.Bytes() 23 | 24 | if len(b) == 0 || b[0] == '\n' || b[1] == '\r' { 25 | continue 26 | } 27 | 28 | err := json.Unmarshal(b, &event) 29 | if err != nil { 30 | continue 31 | } 32 | 33 | fn := event.Data.Argv 34 | args := fn.Arguments() 35 | 36 | var errno string 37 | 38 | if event.Data.Error == 0 { 39 | errno = color.GreenString("OK") 40 | } else { 41 | errno = color.RedString(event.Data.Error.String()) 42 | } 43 | 44 | bold := color.New(color.Bold).SprintFunc() 45 | cyan := color.New(color.FgCyan).SprintFunc() 46 | green := color.New(color.FgGreen).SprintFunc() 47 | 48 | fmt.Printf("%35s: [%8s] (%11s) %s(", bold(green(event.Data.Container.FQDN())), event.Data.Comm, errno, bold(cyan(fn.CallName()))) 49 | 50 | for x, arg := range args { 51 | fmt.Printf("(%s)%s=%v", bold(arg.Type), arg.Name, bold(arg.Value)) 52 | 53 | if x < len(args)-1 { 54 | fmt.Printf(", ") 55 | } 56 | } 57 | 58 | fmt.Printf(")\n") 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /pkg/event/reader/kernel.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/criticalstack/swoll/pkg/event" 8 | "github.com/criticalstack/swoll/pkg/kernel" 9 | ) 10 | 11 | // KernelReader handles callbacks from the kernel and implements a higher level 12 | // channel-based emission to clients 13 | type KernelReader struct { 14 | probe *kernel.Probe 15 | backlog chan interface{} 16 | } 17 | 18 | // NewKernelReader creates a kernel event reader from a `kernel.Probe` context 19 | func NewKernelReader(probe *kernel.Probe) EventReader { 20 | return &KernelReader{probe, make(chan interface{})} 21 | } 22 | 23 | // Read returns the channel of processed events. 24 | func (k *KernelReader) Read() chan interface{} { 25 | return k.backlog 26 | } 27 | 28 | // handler converts the raw bytes into a KernelEvent and 29 | // writes it to the backlog. We assume here that if msg is nil, 30 | // that this is a lost msg and emit it as so. 31 | func (k *KernelReader) handler(msg []byte, lost uint64) error { 32 | if msg != nil { 33 | k.backlog <- event.KernelEvent(msg) 34 | } 35 | return nil 36 | } 37 | 38 | // Run starts up the kernel reader in its own goroutine, 39 | // then simply reads the raw messages in, and pipes them 40 | // to the KernelEvent channel. 41 | func (k *KernelReader) Run(ctx context.Context) error { 42 | if k == nil { 43 | return errors.New("nil context") 44 | } 45 | 46 | //nolint:errcheck 47 | go k.probe.Run(ctx, k.handler) 48 | <-ctx.Done() 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/kernel/metrics/handler.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "sync" 5 | "unsafe" 6 | 7 | "github.com/iovisor/gobpf/elf" 8 | ) 9 | 10 | type Handler struct { 11 | module *elf.Module 12 | table *elf.Map 13 | sync.Mutex 14 | } 15 | 16 | func NewHandler(mod *elf.Module) *Handler { 17 | return &Handler{ 18 | module: mod, 19 | table: mod.Map("swoll_metrics"), 20 | } 21 | } 22 | 23 | func (h *Handler) PruneNamespace(ns int) (int, error) { 24 | k := &key{} 25 | v := &val{} 26 | n := &key{} 27 | toDelete := make([]*key, 0) 28 | 29 | h.Lock() 30 | defer h.Unlock() 31 | 32 | for { 33 | more, _ := h.module.LookupNextElement(h.table, 34 | unsafe.Pointer(k), 35 | unsafe.Pointer(n), 36 | unsafe.Pointer(v)) 37 | if !more { 38 | break 39 | } 40 | 41 | k = n 42 | 43 | if k.pidNs == uint32(ns) { 44 | toDelete = append(toDelete, k.copy()) 45 | } 46 | 47 | } 48 | 49 | for _, ent := range toDelete { 50 | h.module.DeleteElement(h.table, unsafe.Pointer(ent)) 51 | } 52 | 53 | return len(toDelete), nil 54 | } 55 | 56 | func (h *Handler) QueryAll() Metrics { 57 | ret := Metrics{} 58 | kkey := &key{} 59 | kval := &val{} 60 | next := &key{} 61 | 62 | h.Lock() 63 | defer h.Unlock() 64 | 65 | for { 66 | more, _ := h.module.LookupNextElement(h.table, 67 | unsafe.Pointer(kkey), 68 | unsafe.Pointer(next), 69 | unsafe.Pointer(kval)) 70 | if !more { 71 | break 72 | } 73 | 74 | kkey = next 75 | ret = append(ret, Metric{k: kkey.copy(), v: kval.copy()}) 76 | } 77 | 78 | return ret 79 | } 80 | -------------------------------------------------------------------------------- /examples/kubernetes-basic/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: swoll-test 6 | --- 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | metadata: 10 | name: swoll-kube-test 11 | namespace: swoll-test 12 | spec: 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | app: swoll-kube-test 17 | template: 18 | metadata: 19 | labels: 20 | app: swoll-kube-test 21 | spec: 22 | hostPID: true 23 | volumes: 24 | - name: sys 25 | hostPath: 26 | path: /sys 27 | - name: containerd 28 | hostPath: 29 | path: /run/containerd/containerd.sock 30 | containers: 31 | - name: swoll-kube-test 32 | image: errzey/swoll-kube-test:latest 33 | imagePullPolicy: Always 34 | securityContext: 35 | privileged: true 36 | volumeMounts: 37 | - mountPath: /run/containerd/containerd.sock 38 | name: containerd 39 | - mountPath: /sys 40 | name: sys 41 | --- 42 | apiVersion: rbac.authorization.k8s.io/v1 43 | kind: ClusterRole 44 | metadata: 45 | name: swoll-kube-test-reader 46 | rules: 47 | - apiGroups: [""] 48 | resources: ["pods"] 49 | verbs: ["get", "watch", "list"] 50 | --- 51 | apiVersion: rbac.authorization.k8s.io/v1 52 | kind: ClusterRoleBinding 53 | metadata: 54 | name: swoll-kube-test-rolebinding 55 | roleRef: 56 | apiGroup: rbac.authorization.k8s.io 57 | kind: ClusterRole 58 | name: swoll-kube-test-reader 59 | subjects: 60 | - kind: ServiceAccount 61 | name: default 62 | namespace: swoll-test 63 | -------------------------------------------------------------------------------- /internal/bpf/Makefile: -------------------------------------------------------------------------------- 1 | KERNELDIR ?= /lib/modules/$(shell uname -r)/build 2 | 3 | define BASEFLAGS 4 | -I$(KERNELDIR)/arch/x86/include \ 5 | -I$(KERNELDIR)/arch/x86/include/generated \ 6 | -I$(KERNELDIR)/include \ 7 | -I$(KERNELDIR)/include/uapi \ 8 | -include $(KERNELDIR)/include/linux/kconfig.h \ 9 | -include asm_goto_workaround.h \ 10 | -D__KERNEL__ \ 11 | -D__BPF_TRACING__ \ 12 | -Wno-gnu-variable-sized-type-not-at-end \ 13 | -Wno-address-of-packed-member \ 14 | -fno-jump-tables \ 15 | -Wno-pointer-sign \ 16 | -Wno-tautological-compare 17 | endef 18 | 19 | all: probe.o 20 | 21 | clean: 22 | rm -f *~ 23 | rm -f .built-in.a.cmd 24 | rm -f built-in.a 25 | rm -f probe*.o 26 | rm -f probe*.ll 27 | rm -f Modules.symvers 28 | rm -f modules.order 29 | rm -f Module.symvers 30 | rm -f .cache.mk 31 | 32 | probe.o: probe.c 33 | @echo "Building Probe..." 34 | clang ${BASEFLAGS} -O2 -emit-llvm -c $< -o $(patsubst %.o,%.ll,$@) 35 | llc -march=bpf -filetype=obj -o $@ $(patsubst %.o,%.ll,$@) 36 | 37 | NAME=probe 38 | IMAGE=probe-builder:latest 39 | TMP_DIR ?= $(shell pwd)/probe_tmp 40 | 41 | build: ## Build kernel probe /w temp container 42 | rm -rf $(TMP_DIR) 43 | mkdir -p $(TMP_DIR) 44 | cp -Lr $(KERNELDIR)/* $(TMP_DIR) 45 | docker build . -t probe-builder 46 | -docker rm -f -v $(NAME) 47 | docker run --name=$(NAME) \ 48 | -v $$(pwd):/bpf \ 49 | -v $(TMP_DIR):/kernel \ 50 | $(IMAGE) 51 | docker rm -f -v $(NAME) 52 | rm -rf $(TMP_DIR) 53 | -------------------------------------------------------------------------------- /chart/templates/controller-deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.controller.enabled -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | name: {{ template "swoll.fullname" . }}-controller 8 | namespace: {{ .Release.Namespace }} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | {{- include "swoll.labels" . | nindent 6 }} 14 | app.kubernetes.io/component: controller 15 | template: 16 | metadata: 17 | labels: 18 | {{- include "swoll.labels" . | nindent 8 }} 19 | app.kubernetes.io/component: controller 20 | spec: 21 | serviceAccountName: {{ include "swoll-controller.serviceAccountName" .}} 22 | containers: 23 | - args: 24 | - --secure-listen-address=0.0.0.0:8443 25 | - --upstream=http://127.0.0.1:8080/ 26 | - --logtostderr=true 27 | - --v=10 28 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 29 | name: kube-rbac-proxy 30 | ports: 31 | - containerPort: 8443 32 | name: https 33 | - args: 34 | - --metrics-addr=127.0.0.1:8080 35 | - --enable-leader-election 36 | command: 37 | - swoll 38 | - controller 39 | - --enable-leader-election 40 | - --image 41 | - "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 42 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 43 | name: manager 44 | resources: 45 | limits: 46 | cpu: 100m 47 | memory: 30Mi 48 | requests: 49 | cpu: 100m 50 | memory: 20Mi 51 | terminationGracePeriodSeconds: 10 52 | {{- end -}} 53 | -------------------------------------------------------------------------------- /internal/deploy/manifests/probe.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: swoll-probe 5 | namespace: swoll 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: swoll-probe 10 | template: 11 | metadata: 12 | name: swoll-probe 13 | labels: 14 | app: "swoll-probe" 15 | swoll: "false" 16 | annotations: 17 | prometheus.io/scrape: "true" 18 | prometheus.io/port: "9095" 19 | spec: 20 | hostPID: true 21 | hostNetwork: true 22 | dnsPolicy: ClusterFirstWithHostNet 23 | tolerations: 24 | - effect: NoSchedule 25 | operator: Exists 26 | containers: 27 | - command: 28 | - swoll 29 | - server 30 | - -r 31 | - /run/containerd/containerd.sock 32 | - --log 33 | - trace 34 | name: swoll-probe 35 | image: 'criticalstack/swoll:latest' 36 | imagePullPolicy: Always 37 | securityContext: 38 | privileged: true 39 | volumeMounts: 40 | - mountPath: /output 41 | name: kubeconf 42 | - mountPath: /run/containerd/containerd.sock 43 | name: containerd 44 | - mountPath: /swoll/probe.o 45 | name: swoll-probe 46 | - mountPath: /sys 47 | name: sys 48 | volumes: 49 | - name: kubeconf 50 | hostPath: 51 | path: /output 52 | - name: sys 53 | hostPath: 54 | path: /sys 55 | - name: containerd 56 | hostPath: 57 | path: /run/containerd/containerd.sock 58 | - name: swoll-probe 59 | hostPath: 60 | path: /swoll/bpf/probe.o 61 | 62 | -------------------------------------------------------------------------------- /pkg/kernel/fixtures/dummy_probe.c: -------------------------------------------------------------------------------- 1 | /* this file contains the base maps and functions needed in order to 2 | * pass any test that involves loading the BPF object. 3 | */ 4 | 5 | #undef asm_volatile_goto 6 | #define asm_volatile_goto(...) asm volatile ("stuff") 7 | 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "../../../internal/bpf/bpf.h" 18 | 19 | 20 | #define DUMMYVAL \ 21 | { \ 22 | .type = BPF_MAP_TYPE_HASH, \ 23 | .key_size = 1, \ 24 | .value_size = 1, \ 25 | .max_entries = 1, \ 26 | } 27 | 28 | struct bpf_map_def SEC("maps/sk_metrics") sk_metrics = DUMMYVAL; 29 | struct bpf_map_def SEC("maps/sk_state_map") sk_state_map = DUMMYVAL; 30 | struct bpf_map_def SEC("maps/sk_event_scratch") sk_event_scratch = DUMMYVAL; 31 | struct bpf_map_def SEC("maps/sk_filter_config") sk_filter_config = DUMMYVAL; 32 | struct bpf_map_def SEC("maps/sk_filter") sk_filter = DUMMYVAL; 33 | 34 | struct bpf_map_def 35 | SEC("maps/sk_perf_output") sk_perf_output = 36 | { 37 | .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, 38 | .key_size = sizeof(int), 39 | .value_size = sizeof(int), 40 | .max_entries = 1024, 41 | }; 42 | 43 | SEC("tracepoint/raw_syscalls/sys_enter") int 44 | __senter(void * ctx) 45 | { 46 | return 0; 47 | } 48 | 49 | SEC("tracepoint/raw_syscalls/sys_exit") int 50 | __sexit(void * ctx) 51 | { 52 | return 0; 53 | } 54 | 55 | SEC("tracepoint/syscalls/sys_enter_execve") int 56 | __sexecve(void * ctx) 57 | { 58 | return 0; 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/alert/prometheus.go: -------------------------------------------------------------------------------- 1 | // Prometheus Alert-Manager alert parser 2 | 3 | package alert 4 | 5 | import ( 6 | "encoding/json" 7 | "io" 8 | 9 | "github.com/prometheus/alertmanager/template" 10 | "github.com/prometheus/common/model" 11 | ) 12 | 13 | // Prometheus is our parser for prometheus-sourced alerts 14 | type Prometheus struct { 15 | Parser 16 | } 17 | 18 | // parseStatus converts the prometheus-alert status to our 19 | // internal status name. 20 | func parseStatus(in model.AlertStatus) Status { 21 | switch in { 22 | case model.AlertFiring: 23 | return StatusFiring 24 | case model.AlertResolved: 25 | return StatusResolved 26 | default: 27 | return StatusUnknown 28 | } 29 | } 30 | 31 | // parseSingleAlert will parse a single alert within a group 32 | // of alerts from prometheus. 33 | func parseSingleAlert(in template.Alert) (*Alert, error) { 34 | return &Alert{ 35 | Info: Info{ 36 | Status: parseStatus(model.AlertStatus(in.Status)), 37 | Name: in.Labels["alertname"], 38 | Syscall: in.Labels["syscall"], 39 | Hash: in.Fingerprint, 40 | URL: in.GeneratorURL, 41 | }, 42 | Source: Source{ 43 | Namespace: in.Labels["namespace"], 44 | Pod: in.Labels["pod"], 45 | Container: in.Labels["container"], 46 | }, 47 | }, nil 48 | } 49 | 50 | // ParseAlerts parses prometheus-formatted data from `r` 51 | func (p *Prometheus) ParseAlerts(r io.Reader) ([]*Alert, error) { 52 | data := template.Data{} 53 | 54 | if err := json.NewDecoder(r).Decode(&data); err != nil { 55 | return nil, err 56 | } 57 | 58 | ret := make([]*Alert, 0) 59 | 60 | for _, rawAlert := range data.Alerts { 61 | alert, err := parseSingleAlert(rawAlert) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | ret = append(ret, alert) 67 | } 68 | 69 | return ret, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/types/cloneflags.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strings" 5 | "syscall" 6 | ) 7 | 8 | type CloneFlags int 9 | 10 | const CLONE_NEWCGROUP = 0x02000000 11 | 12 | var cloneFlagMasks = map[int]string{ 13 | syscall.CLONE_CHILD_CLEARTID: "CLONE_CHILD_CLEARTID", 14 | syscall.CLONE_CHILD_SETTID: "CLONE_CHILD_SETTID", 15 | syscall.CLONE_FILES: "CLONE_FILES", 16 | syscall.CLONE_FS: "CLONE_FS", 17 | syscall.CLONE_IO: "CLONE_IO", 18 | CLONE_NEWCGROUP: "CLONE_NEWCGROUP", 19 | syscall.CLONE_NEWIPC: "CLONE_NEWIPC", 20 | syscall.CLONE_NEWNET: "CLONE_NEWNET", 21 | syscall.CLONE_NEWNS: "CLONE_NEWNS", 22 | syscall.CLONE_NEWPID: "CLONE_NEWPID", 23 | syscall.CLONE_NEWUSER: "CLONE_NEWUSER", 24 | syscall.CLONE_NEWUTS: "CLONE_NEWUTS", 25 | syscall.CLONE_PARENT: "CLONE_PARENT", 26 | syscall.CLONE_PARENT_SETTID: "CLONE_PARENT_SETTID", 27 | syscall.CLONE_PTRACE: "CLONE_PTRACE", 28 | syscall.CLONE_SETTLS: "CLONE_SETTLS", 29 | syscall.CLONE_SIGHAND: "CLONE_SIGHAND", 30 | syscall.CLONE_SYSVSEM: "CLONE_SYSVSEM", 31 | syscall.CLONE_THREAD: "CLONE_THREAD", 32 | syscall.CLONE_UNTRACED: "CLONE_UNTRACED", 33 | syscall.CLONE_VFORK: "CLONE_VFORK", 34 | syscall.CLONE_VM: "CLONE_VM", 35 | } 36 | 37 | func (flags CloneFlags) Parse() []string { 38 | ret := []string{} 39 | fint := int(flags) 40 | 41 | for m, fstr := range cloneFlagMasks { 42 | if fint&m != 0 { 43 | ret = append(ret, fstr) 44 | } 45 | } 46 | return ret 47 | } 48 | 49 | func (flags CloneFlags) String() string { 50 | return strings.Join(flags.Parse(), "|") 51 | } 52 | 53 | /* 54 | func (flags CloneFlags) MarshalJSON() ([]byte, error) { 55 | return json.Marshal(flags.Parse()) 56 | } 57 | */ 58 | -------------------------------------------------------------------------------- /cmd/offsetter.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/criticalstack/swoll/pkg/kernel" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func SetOffsetsFromArgs(probe *kernel.Probe, cmd *cobra.Command, args []string) error { 13 | nodetect, err := cmd.Flags().GetBool("no-detect-offsets") 14 | if err != nil { 15 | return err 16 | } 17 | 18 | if !nodetect { 19 | if err := probe.DetectAndSetOffsets(); err != nil { 20 | return err 21 | } 22 | } else { 23 | offset, err := cmd.Flags().GetString("nsproxy-offset") 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if offset != "" { 29 | offset = strings.TrimPrefix(offset, "0x") 30 | offset, err := strconv.ParseInt(offset, 16, 64) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | setter, err := kernel.NewOffsetter(probe.Module()) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | log.Infof("Setting task_struct->nsproxy offset to: %x\n", offset) 41 | 42 | if err := setter.Set("nsproxy", kernel.OffsetValue(offset)); err != nil { 43 | return err 44 | } 45 | } 46 | 47 | offset, err = cmd.Flags().GetString("pidns-offset") 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if offset != "" { 53 | offset = strings.TrimPrefix(offset, "0x") 54 | offset, err := strconv.ParseInt(offset, 16, 64) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | setter, err := kernel.NewOffsetter(probe.Module()) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | log.Infof("Setting pid_namespace->ns offset to: %x\n", offset) 65 | 66 | if err := setter.Set("pid_ns_common", kernel.OffsetValue(offset)); err != nil { 67 | return err 68 | } 69 | 70 | } 71 | 72 | } 73 | 74 | return nil 75 | 76 | } 77 | -------------------------------------------------------------------------------- /pkg/types/msgflags.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "syscall" 8 | ) 9 | 10 | type MsgFlags int 11 | 12 | var msgFlagMasks = map[int]string{ 13 | syscall.MSG_CMSG_CLOEXEC: "MSG_CMSG_CLOEXEC", 14 | syscall.MSG_DONTWAIT: "MSG_DONTWAIT", 15 | syscall.MSG_ERRQUEUE: "MSG_ERRQUEUE", 16 | syscall.MSG_OOB: "MSG_OOB", 17 | syscall.MSG_PEEK: "MSG_PEEK", 18 | syscall.MSG_TRUNC: "MSG_TRUNC", 19 | syscall.MSG_WAITALL: "MSG_WAITALL", 20 | syscall.MSG_NOSIGNAL: "MSG_NOSIGNAL", 21 | } 22 | 23 | func (flags MsgFlags) Parse() []string { 24 | ret := []string{} 25 | fint := int(flags) 26 | 27 | for flag, fstr := range msgFlagMasks { 28 | if flag&fint != 0 { 29 | ret = append(ret, fstr) 30 | } 31 | } 32 | 33 | return ret 34 | } 35 | 36 | func (flags MsgFlags) String() string { 37 | parsed := flags.Parse() 38 | 39 | if len(parsed) == 0 { 40 | return fmt.Sprintf("%d", flags) 41 | } 42 | 43 | return strings.Join(parsed, "|") 44 | } 45 | 46 | func (flags MsgFlags) MarshalJSON() ([]byte, error) { 47 | return json.Marshal(flags.Parse()) 48 | } 49 | 50 | func (f *MsgFlags) UnmarshalJSON(data []byte) error { 51 | var a []string 52 | 53 | if err := json.Unmarshal(data, &a); err != nil { 54 | return err 55 | } 56 | 57 | for _, v := range a { 58 | switch v { 59 | case "MSG_CMSG_CLOEXEC": 60 | *f |= syscall.MSG_CMSG_CLOEXEC 61 | case "MSG_DONTWAIT": 62 | *f |= syscall.MSG_DONTWAIT 63 | case "MSG_ERRQUEUE": 64 | *f |= syscall.MSG_ERRQUEUE 65 | case "MSG_OOB": 66 | *f |= syscall.MSG_OOB 67 | case "MSG_PEEK": 68 | *f |= syscall.MSG_PEEK 69 | case "MSG_TRUNC": 70 | *f |= syscall.MSG_TRUNC 71 | case "MSG_WAITALL": 72 | *f |= syscall.MSG_WAITALL 73 | case "MSG_NOSIGNAL": 74 | *f |= syscall.MSG_NOSIGNAL 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | # Ensure that any go.mod modifications due to linters, go generate, etc are 4 | # removed. 5 | - go mod tidy 6 | # Using Go modules intrinsically causes modifications to these files that are 7 | # unfortunately unavoidable. Newer patch versions available to the CI will 8 | # satisfy the same module requirements and cause additions to go.sum. 9 | - git checkout -- go.sum 10 | builds: 11 | - id: swoll 12 | env: 13 | - CGO_ENABLED=1 14 | - GO111MODULE=on 15 | main: ./main.go 16 | binary: swoll 17 | goos: 18 | - linux 19 | goarch: 20 | - amd64 21 | gcflags: 22 | - all=-trimpath={{.Env.GOPATH}} 23 | asmflags: 24 | - all=-trimpath={{.Env.GOPATH}} 25 | flags: 26 | - -a 27 | - -tags=netgo,osusergo 28 | ldflags: 29 | - -linkmode external 30 | - -extldflags '-static' 31 | - -s -w 32 | - -X "github.com/criticalstack/swoll/cmd.buildTime={{.Date}}" 33 | - -X "github.com/criticalstack/swoll/cmd.gitCommit={{.ShortCommit}}" 34 | - -X "github.com/criticalstack/swoll/cmd.version={{.Tag}}" 35 | archives: 36 | - replacements: 37 | darwin: Darwin 38 | linux: Linux 39 | windows: Windows 40 | 386: i386 41 | amd64: x86_64 42 | nfpms: 43 | - file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 44 | package_name: swoll 45 | homepage: https://github.com/criticalstack/swoll 46 | description: Swoll Engine 47 | maintainer: criticalstack 48 | license: Apache-2.0 49 | vendor: criticalstack 50 | bindir: "/usr/local/bin" 51 | replacements: 52 | amd64: x86_64 53 | formats: 54 | - deb 55 | - rpm 56 | checksum: 57 | name_template: 'checksums.txt' 58 | snapshot: 59 | name_template: "{{ .Tag }}-next" 60 | changelog: 61 | sort: asc 62 | filters: 63 | exclude: 64 | - '^docs:' 65 | - '^test:' 66 | -------------------------------------------------------------------------------- /internal/pkg/alert/parser_test.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestPrometheusAlertParser(t *testing.T) { 10 | input := ` 11 | { 12 | "alerts":[ 13 | { 14 | "status":"firing", 15 | "labels":{ 16 | "alertname":"a1", 17 | "container":"c1", 18 | "namespace":"n1", 19 | "pod":"p1", 20 | "syscall":"sys_connect" 21 | }, 22 | "fingerprint":"f5dd88504b52de0a" 23 | }, 24 | { 25 | "status":"firing", 26 | "labels":{ 27 | "alertname":"a1", 28 | "container":"c1", 29 | "namespace":"n1", 30 | "pod":"p1", 31 | "syscall":"sys_execve" 32 | }, 33 | "fingerprint":"8c19e2515f784e90" 34 | } 35 | ] 36 | } 37 | ` 38 | wantData := []*Alert{ 39 | { 40 | Info: Info{ 41 | Status: StatusFiring, 42 | Name: "a1", 43 | Hash: "f5dd88504b52de0a", 44 | Syscall: "sys_connect", 45 | }, 46 | Source: Source{ 47 | Namespace: "n1", 48 | Pod: "p1", 49 | Container: "c1", 50 | }, 51 | }, 52 | { 53 | Info: Info{ 54 | Status: StatusFiring, 55 | Name: "a1", 56 | Hash: "8c19e2515f784e90", 57 | Syscall: "sys_execve", 58 | }, 59 | Source: Source{ 60 | Namespace: "n1", 61 | Pod: "p1", 62 | Container: "c1", 63 | }, 64 | }, 65 | } 66 | 67 | alerts, err := ParseAlerts(new(Prometheus), strings.NewReader(input)) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | 72 | if alerts == nil { 73 | t.Errorf("could not parse alerts") 74 | } 75 | 76 | if len(alerts) != len(wantData) { 77 | t.Error("len(want) != len(have)") 78 | } 79 | 80 | for idx, w := range wantData { 81 | want := w 82 | have := alerts[idx] 83 | 84 | if want.Info != have.Info { 85 | t.Errorf("want %v, have %v", want.Info, have.Info) 86 | } 87 | 88 | if want.Source != have.Source { 89 | t.Errorf("want %v, have %v", want.Source, have.Source) 90 | } 91 | } 92 | 93 | fmt.Println(alerts) 94 | } 95 | -------------------------------------------------------------------------------- /chart/templates/server-daemonset.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.server.enabled -}} 2 | apiVersion: {{ template "daemonset.apiVersion" . }} 3 | kind: DaemonSet 4 | metadata: 5 | name: {{ template "swoll.fullname" . }}-server 6 | namespace: "{{ .Release.Namespace }}" 7 | spec: 8 | selector: 9 | matchLabels: 10 | {{- include "swoll.labels" . | nindent 6 }} 11 | app.kubernetes.io/component: server 12 | template: 13 | metadata: 14 | name: {{ template "swoll.fullname" . }}-server 15 | labels: 16 | {{- include "swoll.labels" . | nindent 8 }} 17 | app.kubernetes.io/component: server 18 | swoll: "false" 19 | annotations: 20 | {{- if .Values.server.enablePrometheus }} 21 | prometheus.io/scrape: "true" 22 | prometheus.io/port: "{{ .Values.server.service.targetPort }}" 23 | {{- end }} 24 | spec: 25 | hostPID: true 26 | hostNetwork: true 27 | dnsPolicy: ClusterFirstWithHostNet 28 | tolerations: 29 | - effect: NoSchedule 30 | operator: Exists 31 | serviceAccountName: {{ include "swoll-server.serviceAccountName" . }} 32 | containers: 33 | - command: 34 | - swoll 35 | - server 36 | - -r 37 | - {{ .Values.criEndpoint }} 38 | - --listen-addr 39 | - ":{{ .Values.server.service.targetPort }}" 40 | name: server 41 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 42 | imagePullPolicy: "{{ .Values.image.pullPolicy }}" 43 | securityContext: 44 | privileged: true 45 | volumeMounts: 46 | - mountPath: {{ .Values.criEndpoint }} 47 | name: containerd 48 | - mountPath: /sys 49 | name: sys 50 | volumes: 51 | - name: sys 52 | hostPath: 53 | path: /sys 54 | - name: containerd 55 | hostPath: 56 | path: {{ .Values.criEndpoint }} 57 | {{- end -}} 58 | -------------------------------------------------------------------------------- /examples/kubernetes-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/criticalstack/swoll/pkg/event" 9 | "github.com/criticalstack/swoll/pkg/kernel" 10 | "github.com/criticalstack/swoll/pkg/kernel/assets" 11 | "github.com/criticalstack/swoll/pkg/topology" 12 | "github.com/criticalstack/swoll/pkg/types" 13 | ) 14 | 15 | func dumpTextEvent(ev *event.TraceEvent) { 16 | fmt.Printf("%s: [%s/%v] (%s) %s(", ev.Container.FQDN(), ev.Comm, ev.Pid, ev.Error, ev.Argv.CallName()) 17 | for _, arg := range ev.Argv.Arguments() { 18 | fmt.Printf("(%s)%s=%v ", arg.Type, arg.Name, arg.Value) 19 | } 20 | fmt.Println(")") 21 | } 22 | 23 | func main() { 24 | probe, err := kernel.NewProbe(assets.LoadBPFReader(), nil) 25 | if err != nil { 26 | log.Fatalf("Unable to load static BPF asset: %v", err) 27 | } 28 | 29 | if err := probe.InitProbe(kernel.WithOffsetDetection()); err != nil { 30 | log.Fatalf("Unable to initialize probe: %v", err) 31 | } 32 | 33 | filter := kernel.NewFilter(probe.Module()) 34 | 35 | filter.FilterSelf() 36 | filter.AddSyscall("execve", -1) 37 | filter.AddSyscall("openat", -1) 38 | filter.AddSyscall("accept4", -1) 39 | filter.AddSyscall("connect", -1) 40 | 41 | observer, err := topology.NewKubernetes(topology.WithKubernetesCRI("/run/containerd/containerd.sock")) 42 | if err != nil { 43 | log.Fatalf("Unable to create topology context: %v", err) 44 | } 45 | 46 | ctx := context.Background() 47 | topo := topology.NewTopology(observer) 48 | event := event.NewTraceEvent().WithContainerLookup( 49 | func(ns int) (*types.Container, error) { 50 | return topo.LookupContainer(ctx, ns) 51 | }) 52 | 53 | go topo.Run(ctx, func(tp topology.EventType, c *types.Container) { 54 | fmt.Printf("eventType=%v, container=%v\n", tp, c.FQDN()) 55 | }) 56 | 57 | probe.Run(ctx, func(msg []byte, lost uint64) error { 58 | parsed, err := event.Ingest(msg) 59 | if err != nil { 60 | return nil 61 | } 62 | 63 | dumpTextEvent(parsed) 64 | 65 | return nil 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/types/clocktypes.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | type TFDClock int 11 | type TimerFlags int 12 | 13 | const ( 14 | TFD_CLOEXEC = unix.O_CLOEXEC 15 | TFD_NONBLOCK = unix.O_NONBLOCK 16 | TFD_TIMER_ABSTIME = 1 << 0 17 | TFD_TIMER_CANCEL_ON_SET = 1 << 1 18 | ) 19 | 20 | var setTimerFlags = map[int]string{ 21 | TFD_CLOEXEC: "TFD_CLOEXEC", 22 | TFD_NONBLOCK: "TFD_NONBLOCK", 23 | TFD_TIMER_ABSTIME: "TFD_TIMER_ABSTIME", 24 | TFD_TIMER_CANCEL_ON_SET: "TFD_TIMER_CANCEL_ON_SET", 25 | } 26 | 27 | /* 28 | var clockTypes = map[int]string{ 29 | unix.CLOCK_REALTIME: "CLOCK_REALTIME", 30 | unix.CLOCK_MONOTONIC: "CLOCK_MONOTONIC", 31 | unix.CLOCK_BOOTTIME: "CLOCK_BOOTTIME", 32 | unix.CLOCK_REALTIME_ALARM: "CLOCK_REALTIME_ALARM", 33 | unix.CLOCK_BOOTTIME_ALARM: "CLOCK_BOOTTIME_ALARM", 34 | } 35 | */ 36 | 37 | type Timespec struct { 38 | Sec int64 39 | Nsec int64 40 | } 41 | 42 | type Itimerspec struct { 43 | Interval Timespec 44 | Value Timespec 45 | } 46 | 47 | func (f TimerFlags) Parse() []string { 48 | ret := make([]string, 0) 49 | fint := int(f) 50 | 51 | for mode, fstr := range setTimerFlags { 52 | if fint&mode != 0 { 53 | ret = append(ret, fstr) 54 | } 55 | } 56 | 57 | return ret 58 | } 59 | 60 | func (f TimerFlags) String() string { 61 | return strings.Join(f.Parse(), "|") 62 | } 63 | 64 | func (f TimerFlags) MarshalJSON() ([]byte, error) { 65 | return json.Marshal(f.Parse()) 66 | } 67 | 68 | func (f *TimerFlags) UnmarshalJSON(data []byte) error { 69 | var a []string 70 | 71 | if err := json.Unmarshal(data, &a); err != nil { 72 | return err 73 | } 74 | 75 | for _, val := range a { 76 | switch val { 77 | case "TFD_CLOEXEC": 78 | *f |= TFD_CLOEXEC 79 | case "TFD_NONBLOCK": 80 | *f |= TFD_NONBLOCK 81 | case "TFD_TIMER_ABSTIME": 82 | *f |= TFD_TIMER_ABSTIME 83 | case "TFD_TIMER_CANCEL_ON_SET": 84 | *f |= TFD_TIMER_CANCEL_ON_SET 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/event/call/mprotect.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "strings" 5 | "syscall" 6 | "unsafe" 7 | 8 | "github.com/criticalstack/swoll/pkg/types" 9 | ) 10 | 11 | type ProtFlags int 12 | type Mprotect struct { 13 | Addr uintptr `json:"addr"` 14 | Len int `json:"len"` 15 | Prot ProtFlags `json:"prot"` 16 | AddrData types.Buffer `json:"addr_data"` 17 | } 18 | 19 | func (m *Mprotect) CallName() string { return "mprotect" } 20 | func (m *Mprotect) Return() *Argument { return nil } 21 | func (m *Mprotect) DecodeArguments(data []*byte, arglen int) error { 22 | m.Addr = uintptr(types.MakeCU64(unsafe.Pointer(data[0]))) 23 | m.Len = int(types.MakeCU64(unsafe.Pointer(data[1]))) 24 | m.Prot = ProtFlags(types.MakeC32(unsafe.Pointer(data[2]))) 25 | 26 | if arglen < m.Len { 27 | arglen = m.Len 28 | } 29 | 30 | // this is a special case from our probe which will dereference that memory 31 | // region at `Addr` 32 | m.AddrData = types.MakeCBytes(unsafe.Pointer(data[3]), arglen) 33 | return nil 34 | } 35 | 36 | func (m *Mprotect) Arguments() Arguments { 37 | return Arguments{ 38 | {"addr", "void *", m.Addr}, 39 | {"len", "size_t", m.Len}, 40 | {"prot", "int", m.Prot}, 41 | } 42 | } 43 | 44 | const PROT_SEM = 0x8 45 | 46 | var protMasks = map[int]string{ 47 | syscall.PROT_NONE: "PROT_NONE", 48 | syscall.PROT_READ: "PROT_READ", 49 | syscall.PROT_WRITE: "PROT_WRITE", 50 | syscall.PROT_EXEC: "PROT_EXEC", 51 | PROT_SEM: "PROT_SEM", 52 | syscall.PROT_GROWSUP: "PROT_GROWSUP", 53 | syscall.PROT_GROWSDOWN: "PROT_GROWSDOWN", 54 | } 55 | 56 | func (flags ProtFlags) Parse() []string { 57 | if flags == 0 { 58 | return []string{"PROT_NONE"} 59 | } 60 | 61 | ret := []string{} 62 | fint := int(flags) 63 | 64 | for flag, fstr := range protMasks { 65 | if fint&flag != 0 { 66 | ret = append(ret, fstr) 67 | } 68 | } 69 | 70 | return ret 71 | } 72 | 73 | func (flags ProtFlags) String() string { 74 | return strings.Join(flags.Parse(), "|") 75 | } 76 | 77 | /* 78 | func (flags ProtFlags) MarshalJSON() ([]byte, error) { 79 | return json.Marshal(flags.Parse()) 80 | } 81 | */ 82 | -------------------------------------------------------------------------------- /pkg/types/sockaddr.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "fmt" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | // SockAddr our local version of RawSockadddr that we can manipulate 12 | type SockAddr syscall.RawSockaddr 13 | 14 | func ntohs(port uint16) uint16 { 15 | buf := make([]byte, 2) 16 | binary.BigEndian.PutUint16(buf, port) 17 | return binary.LittleEndian.Uint16(buf) 18 | } 19 | 20 | func parseSockAddr(s *SockAddr) (string, int) { 21 | switch s.Family { 22 | case syscall.AF_INET: 23 | pp := (*syscall.RawSockaddrInet4)(unsafe.Pointer(s)) 24 | 25 | port := ntohs(pp.Port) 26 | addr := fmt.Sprintf("ipv4:%d.%d.%d.%d", pp.Addr[0], 27 | pp.Addr[1], pp.Addr[2], pp.Addr[3]) 28 | 29 | return addr, int(port) 30 | case syscall.AF_INET6: 31 | pp := (*syscall.RawSockaddrInet6)(unsafe.Pointer(s)) 32 | 33 | port := pp.Port 34 | addr := fmt.Sprintf("ipv6:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x", 35 | pp.Addr[0], 36 | pp.Addr[1], 37 | pp.Addr[2], 38 | pp.Addr[3], 39 | pp.Addr[4], 40 | pp.Addr[5], 41 | pp.Addr[6], 42 | pp.Addr[7], 43 | pp.Addr[8], 44 | pp.Addr[9], 45 | pp.Addr[10], 46 | pp.Addr[11], 47 | pp.Addr[12], 48 | pp.Addr[13], 49 | pp.Addr[14], 50 | pp.Addr[15]) 51 | 52 | return addr, int(port) 53 | case syscall.AF_UNIX: 54 | pp := (*syscall.RawSockaddrUnix)(unsafe.Pointer(s)) 55 | 56 | return fmt.Sprintf("unix:%v", MakeCString(unsafe.Pointer(&pp.Path), len(pp.Path))), 0 //C.GoString((*C.char)(unsafe.Pointer(&pp.Path)))), 0 57 | case syscall.AF_UNSPEC: 58 | return fmt.Sprintf("unspec:%v", s.Data), 0 59 | 60 | } 61 | 62 | return fmt.Sprintf("%d", s.Family), 0 63 | } 64 | 65 | func (s *SockAddr) String() string { 66 | addr, port := parseSockAddr(s) 67 | 68 | if addr != "" { 69 | return fmt.Sprintf("%v:%v", addr, port) 70 | } 71 | 72 | return "" 73 | } 74 | 75 | func (s *SockAddr) MarshalJSON() ([]byte, error) { 76 | addr, port := parseSockAddr(s) 77 | 78 | type Alias SockAddr 79 | n := &struct { 80 | *Alias 81 | Decoded string `json:"decoded,omitempty"` 82 | }{ 83 | Alias: (*Alias)(s), 84 | Decoded: fmt.Sprintf("%s:%d", addr, port), 85 | } 86 | 87 | return json.Marshal(n) 88 | } 89 | -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "swoll.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "swoll.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "swoll.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 32 | {{- end }} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "swoll.labels" -}} 38 | helm.sh/chart: {{ include "swoll.chart" . }} 39 | app.kubernetes.io/name: {{ include "swoll.name" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end }} 46 | 47 | {{- define "daemonset.apiVersion" -}} 48 | {{- if semverCompare "<1.9-0" .Capabilities.KubeVersion.GitVersion -}} 49 | {{- print "extensions/v1beta1" -}} 50 | {{- else -}} 51 | {{- print "apps/v1" -}} 52 | {{- end -}} 53 | {{- end -}} 54 | 55 | {{/* 56 | Create the names of the service accounts to use 57 | */}} 58 | {{- define "swoll-server.serviceAccountName" -}} 59 | {{- if .Values.server.serviceAccount.create -}} 60 | {{ default (include "swoll.fullname" . | printf "%s-server") .Values.server.serviceAccount.name }} 61 | {{- else -}} 62 | {{ default "default" .Values.server.serviceAccount.name }} 63 | {{- end -}} 64 | {{- end -}} 65 | 66 | {{- define "swoll-controller.serviceAccountName" -}} 67 | {{- if .Values.controller.serviceAccount.create -}} 68 | {{ default (include "swoll.fullname" . | printf "%s-controller") .Values.controller.serviceAccount.name }} 69 | {{- else -}} 70 | {{ default "default" .Values.controller.serviceAccount.name }} 71 | {{- end -}} 72 | {{- end -}} 73 | -------------------------------------------------------------------------------- /examples/kubernetes-hub/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is the temporary test namespace we use for this example 3 | apiVersion: v1 4 | kind: Namespace 5 | metadata: 6 | name: swoll-hub-test 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: swoll-hub 12 | # make sure this deployment sits in our test namespace 13 | namespace: swoll-hub-test 14 | spec: 15 | replicas: 1 16 | selector: 17 | matchLabels: 18 | app: swoll-hub 19 | template: 20 | metadata: 21 | labels: 22 | app: swoll-hub 23 | # tell swoll not to monitor ourselves 24 | noSwoll: 'true' 25 | spec: 26 | hostPID: true 27 | volumes: 28 | - name: sys 29 | hostPath: 30 | path: /sys 31 | - name: containerd 32 | hostPath: 33 | path: /run/containerd/containerd.sock 34 | containers: 35 | - name: swoll-hub-test 36 | # change this to your own personal repo if you modify the code 37 | image: errzey/swoll-hub-test:latest 38 | imagePullPolicy: Always 39 | securityContext: 40 | privileged: true 41 | volumeMounts: 42 | - mountPath: /run/containerd/containerd.sock 43 | name: containerd 44 | - mountPath: /sys 45 | name: sys 46 | --- 47 | apiVersion: rbac.authorization.k8s.io/v1 48 | kind: ClusterRole 49 | metadata: 50 | name: swoll-hub-test-reader 51 | rules: 52 | - apiGroups: [""] 53 | resources: ["pods"] 54 | verbs: ["get", "watch", "list"] 55 | --- 56 | apiVersion: rbac.authorization.k8s.io/v1 57 | kind: ClusterRoleBinding 58 | metadata: 59 | name: swoll-hub-test-rolebinding 60 | roleRef: 61 | apiGroup: rbac.authorization.k8s.io 62 | kind: ClusterRole 63 | name: swoll-hub-test-reader 64 | subjects: 65 | - kind: ServiceAccount 66 | name: default 67 | namespace: swoll-hub-test 68 | --- 69 | # Spin up some test containers which this example will monitor 70 | apiVersion: v1 71 | kind: Pod 72 | metadata: 73 | name: nginx-reader-writer 74 | namespace: swoll-hub-test 75 | labels: 76 | app: nginx 77 | spec: 78 | volumes: 79 | - name: html 80 | emptyDir: {} 81 | containers: 82 | - name: webserver 83 | image: nginx 84 | volumeMounts: 85 | - name: html 86 | mountPath: /usr/share/nginx/html 87 | - name: indexwriter 88 | image: debian 89 | volumeMounts: 90 | - name: html 91 | mountPath: /html 92 | command: ["/bin/sh", "-c"] 93 | args: 94 | - while true; do 95 | date >> /html/index․html; 96 | sleep 5; 97 | done 98 | -------------------------------------------------------------------------------- /pkg/kernel/offsets.go: -------------------------------------------------------------------------------- 1 | package kernel 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | 7 | "github.com/iovisor/gobpf/elf" 8 | ) 9 | 10 | type OffsetType uint8 11 | type OffsetValue uint32 12 | 13 | const ( 14 | // ebpf configuration for the offset to task_struct's `nsproxy` member 15 | OffsetNSProxy OffsetType = 1 16 | // ebpf configuration for the offset to pid_namespace's `ns` member 17 | OffsetPidNSCommon OffsetType = 2 18 | ) 19 | 20 | // The Offsetter class holds all the pertinent information to store offsets in 21 | // the running kernel's offset lookup table. 22 | type Offsetter struct { 23 | module *elf.Module 24 | configMap *elf.Map 25 | offsets map[OffsetType]*Offset 26 | } 27 | 28 | // Offset is a structure that represents a single offset configuration entry in 29 | // the ebpf. 30 | type Offset struct { 31 | Type OffsetType 32 | Value OffsetValue 33 | } 34 | 35 | // NewOffsetter creates and initializes a new Offsetter context from the ebpf 36 | // module. 37 | func NewOffsetter(mod *elf.Module) (*Offsetter, error) { 38 | if smap := mod.Map("swoll_offsets_config"); smap != nil { 39 | return &Offsetter{ 40 | module: mod, 41 | configMap: smap, 42 | offsets: make(map[OffsetType]*Offset), 43 | }, nil 44 | } 45 | return nil, fmt.Errorf("swoll_offsets_config nil map") 46 | } 47 | 48 | // NewOffset creates a new offset context 49 | func NewOffset(t OffsetType, offs OffsetValue) *Offset { 50 | switch t { 51 | case OffsetNSProxy: 52 | break 53 | case OffsetPidNSCommon: 54 | break 55 | default: 56 | return nil 57 | } 58 | 59 | return &Offset{ 60 | Type: t, 61 | Value: offs, 62 | } 63 | } 64 | 65 | // Set will set the bpf offset configuration based on the type `t`. `t` can 66 | // either be a string (nsproxy, pid_ns_common), or its native OffsetType. 67 | // The value of which is the offset where this structure member lives. 68 | func (o *Offsetter) Set(t interface{}, offset OffsetValue) error { 69 | var realType OffsetType 70 | 71 | switch t := t.(type) { 72 | case string: 73 | switch t { 74 | case "nsproxy": 75 | realType = OffsetNSProxy 76 | case "pid_ns_common": 77 | realType = OffsetPidNSCommon 78 | } 79 | case int: 80 | realType = OffsetType(t) 81 | case OffsetType: 82 | realType = t 83 | } 84 | 85 | if offs := NewOffset(realType, offset); offs != nil { 86 | o.offsets[realType] = offs 87 | } else { 88 | return fmt.Errorf("unknown offset type") 89 | } 90 | 91 | // set the value of this member in the bpf map. 92 | return o.module.UpdateElement(o.configMap, 93 | unsafe.Pointer(&realType), 94 | unsafe.Pointer(&offset), 0) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/topology/doc.go: -------------------------------------------------------------------------------- 1 | // Package topology is the preferred method for creating and supervising system 2 | // traces when using the Swoll API on modern container management and 3 | // orchestration systems such as Kubernetes. 4 | // 5 | // To better understand what this package does, it is best to start with 6 | // learning a little bit about how Swoll creates, captures, filters, and 7 | // emits data from the kernel back into our code. 8 | // 9 | // The Swoll BPF has a very simple userspace-configurable filtering mechanism 10 | // which allows us to either white-list or black-list what syscalls we want to 11 | // monitor. Optionally, each call we want to monitor can also be associated with 12 | // a specific kernel namespace. So, for example, a user can request to only see 13 | // events which made the sytem call "open" in the kernel PID-Namespace `31337`. 14 | // Any events that do not match this specific rule will be silently dropped by 15 | // the kernel. 16 | // 17 | // Furthermore, each filter can optionally maintain a basic sample-rate 18 | // configuration, giving the developer the option to gain insight into high-load 19 | // system-calls such as `sys_read` without impacting performance too much. 20 | // 21 | // Since each container within a `Pod` gets its own unique (or derived if 22 | // shared) namespace, swoll exploits the above ns+syscall filter feature by 23 | // maintaining the relations between Kubernetes and the container-runtime by 24 | // dynamically updating and tuning the filters in real-time. 25 | // 26 | // In short (using Kubernetes as an example), when we request Swoll to monitor 27 | // syscall events for the Pod "foobar", we connect to the kube-api-server, watch 28 | // for Pod events that match "foobar", and when matched, utilizes the Container 29 | // Runtime Interface to find process details for that Pod. Once we have obtained 30 | // the init PID from the CRI, we can render the PID namespace we need to use to 31 | // set the filter in the kernel. 32 | // 33 | // In theory this sounds simple, but in practice things are not as easy. Swoll 34 | // strives to run as lean-and-mean as possible, and in doing so, the goal 35 | // of which is "One BPF Context To Mon Them All", and still without sacrificing 36 | // performance for flexibility or vice-versa. 37 | // 38 | // And the Topology API is exactly that. It "observes" events from Kubernetes 39 | // and CRI (see: topology.Observer), runs one or more v1alpha1.Trace specifications as a 40 | // topology.Job, which in-turn dynamically updates, de-duplicates, 41 | // and prunes the kernel filter inside a single BPF context, better known as the 42 | // topology.Hub. 43 | package topology 44 | -------------------------------------------------------------------------------- /pkg/topology/hub_test.go: -------------------------------------------------------------------------------- 1 | package topology_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | "github.com/criticalstack/swoll/api/v1alpha1" 8 | "github.com/criticalstack/swoll/pkg/event" 9 | "github.com/criticalstack/swoll/pkg/topology" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | // Running the Hub 14 | func ExampleHub_Run() { 15 | obs, err := topology.NewKubernetes() 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | var bpf *bytes.Reader 21 | 22 | hub, err := topology.NewHub(bpf, obs) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | ctx := context.Background() 28 | go hub.Run(ctx) 29 | <-ctx.Done() 30 | } 31 | 32 | // A short example showing how to use the RunTrace call 33 | func ExampleHub_RunTrace() { 34 | var ( 35 | bpf *bytes.Reader 36 | observer topology.Observer 37 | ) 38 | 39 | hub, err := topology.NewHub(bpf, observer) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | ctx := context.Background() 45 | 46 | // Run the Hub. 47 | go hub.MustRun(ctx) 48 | 49 | // Monitor execve and openat in the kubernetes-namespace 'kube-system' and 50 | // name the job "foo-bar". 51 | go hub.RunTrace(ctx, &v1alpha1.Trace{ 52 | ObjectMeta: metav1.ObjectMeta{ 53 | Namespace: "kube-system", 54 | }, 55 | Spec: v1alpha1.TraceSpec{ 56 | Syscalls: []string{"execve", "openat"}, 57 | }, 58 | Status: v1alpha1.TraceStatus{ 59 | JobID: "foo-bar", 60 | }, 61 | }) 62 | 63 | // trace is now running inside the Hub, you must attach to it to recv events 64 | <-ctx.Done() 65 | 66 | } 67 | 68 | // Simple example to show how to use the AttachTrace method, this assumes the 69 | // topology.Hub is already running with an Observer. 70 | func ExampleHub_AttachTrace() { 71 | var hub *topology.Hub 72 | 73 | trace := &v1alpha1.Trace{ 74 | ObjectMeta: metav1.ObjectMeta{ 75 | Namespace: "kube-system", 76 | }, 77 | Spec: v1alpha1.TraceSpec{ 78 | Syscalls: []string{"execve", "openat"}, 79 | }, 80 | Status: v1alpha1.TraceStatus{ 81 | JobID: "foo-bar", 82 | }, 83 | } 84 | 85 | go hub.RunTrace(context.TODO(), trace) 86 | hub.AttachTrace(trace, func(name string, ev *event.TraceEvent) {}) 87 | } 88 | 89 | // In this example we use AttachPath to "subscribe" to a subset of events being 90 | // sent to a running Job output. 91 | func ExampleHub_AttachPath() { 92 | var hub *topology.Hub 93 | // Assumes there is a job that has matches namespace=kube-system, 94 | // pod=foo-pod, and a container named "boo" 95 | unsub := hub.AttachPath("example", []string{"kube-system", "foo-pod", "boo"}, 96 | func(name string, ev *event.TraceEvent) {}) 97 | defer unsub() 98 | } 99 | -------------------------------------------------------------------------------- /pkg/event/call/function.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Argument represents a single value within a function call. 10 | // function(arg=(int)N <- name="arg", type="int", value="N" 11 | type Argument struct { 12 | // the name of the Value 13 | Name string `json:"name"` 14 | // the Type of the Value 15 | Type string `json:"type"` 16 | // THE ACTUAL VALUE OF THE VALUE! 17 | Value interface{} `json:"val"` 18 | } 19 | 20 | type Arguments []*Argument 21 | 22 | // Function represents a function call of some sort, 23 | // as of the time of writing, that's just syscalls. 24 | type Function interface { 25 | // The name of the function 26 | CallName() string 27 | // The return argument of the function 28 | Return() *Argument 29 | // A slight abstraction around function arguments and how they can 30 | // be semi-serialized. This must return an array of EventArg's 31 | Arguments() Arguments 32 | // TODO[lz]: uhh, maybe one day, this just seems like 33 | // more work than needed, and little to no use, right? 34 | // Marshal() ([]byte, error) 35 | } 36 | 37 | // FunctionDecoder api's should be able to read in an array of byte pointers 38 | // (basically void argv[][]), and fill in information about itself. 39 | type FunctionDecoder interface { 40 | DecodeArguments([]*byte, int) error 41 | } 42 | 43 | // Functionhandler api's define a function they are authoratative for, and a 44 | // decoder for the function. 45 | type FunctionHandler interface { 46 | Function 47 | FunctionDecoder 48 | } 49 | 50 | // FunctionHandle allows for abstract readers/writers on handlers. 51 | type FunctionHandle struct { 52 | FunctionHandler 53 | } 54 | 55 | func (a Argument) String() string { 56 | return fmt.Sprintf("%s=(%s)%v", a.Name, a.Type, a.Value) 57 | } 58 | 59 | func (a Argument) MarshalJSON() ([]byte, error) { 60 | return json.Marshal(map[string]interface{}{ 61 | "name": a.Name, 62 | "type": a.Type, 63 | "val": a.Value}, 64 | ) 65 | } 66 | 67 | func (a Arguments) String() string { 68 | args := make([]string, 0, len(a)) 69 | 70 | for _, arg := range a { 71 | args = append(args, arg.String()) 72 | } 73 | 74 | return strings.Join(args, ", ") 75 | } 76 | 77 | func (h *FunctionHandle) String() string { 78 | if ret := h.Return(); ret != nil { 79 | return fmt.Sprintf("%s(%s)=%v", h.CallName(), h.Arguments(), ret) 80 | } 81 | 82 | return fmt.Sprintf("%s(%s)", h.CallName(), h.Arguments()) 83 | } 84 | 85 | func (h *FunctionHandle) MarshalJSON() ([]byte, error) { 86 | return json.Marshal(map[string]interface{}{ 87 | "name": h.CallName(), 88 | "args": h.Arguments(), 89 | "ret": h.Return(), 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/event/call/accept.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "syscall" 7 | "unsafe" 8 | 9 | "github.com/criticalstack/swoll/pkg/types" 10 | ) 11 | 12 | type AcceptFlags int 13 | 14 | type Accept struct { 15 | FD types.InputFD `json:"fd"` 16 | Saddr *types.SockAddr `json:"saddr"` 17 | } 18 | 19 | type Accept4 struct { 20 | FD types.InputFD `json:"fd"` 21 | Saddr *types.SockAddr `json:"saddr"` 22 | Flags AcceptFlags `json:"flags"` 23 | } 24 | 25 | var masks = map[int]string{ 26 | syscall.SOCK_NONBLOCK: "SOCK_NONBLOCK", 27 | syscall.SOCK_CLOEXEC: "SOCK_CLOEXEC", 28 | } 29 | 30 | func (flags AcceptFlags) Parse() []string { 31 | ret := make([]string, 0) 32 | fint := int(flags) 33 | 34 | for flag, fstr := range masks { 35 | if fint&flag != 0 { 36 | ret = append(ret, fstr) 37 | } 38 | } 39 | 40 | return ret 41 | } 42 | 43 | func (flags AcceptFlags) String() string { 44 | return strings.Join(flags.Parse(), "|") 45 | } 46 | 47 | func (f AcceptFlags) MarshalJSON() ([]byte, error) { 48 | return json.Marshal(f.Parse()) 49 | } 50 | 51 | func (f *AcceptFlags) UnmarshalJSON(b []byte) error { 52 | a := make([]string, 0) 53 | if err := json.Unmarshal(b, &a); err != nil { 54 | return err 55 | } 56 | for _, val := range a { 57 | switch val { 58 | case "SOCK_NONBLOCK": 59 | *f |= syscall.SOCK_NONBLOCK 60 | case "SOCK_CLOEXEC": 61 | *f |= syscall.SOCK_CLOEXEC 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (e *Accept) CallName() string { return "accept" } 69 | func (e *Accept) Return() *Argument { return nil } 70 | func (e *Accept) DecodeArguments(data []*byte, arglen int) error { 71 | e.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 72 | e.Saddr = (*types.SockAddr)(unsafe.Pointer(data[1])) 73 | 74 | return nil 75 | } 76 | 77 | func (e *Accept) Arguments() Arguments { 78 | return Arguments{ 79 | {"sockfd", "int", e.FD}, 80 | {"addr", "struct sockaddr *", e.Saddr}, 81 | } 82 | } 83 | 84 | func (e *Accept4) CallName() string { return "accept4" } 85 | func (e *Accept4) Return() *Argument { return nil } 86 | func (e *Accept4) Arguments() Arguments { 87 | return Arguments{ 88 | {"sockfd", "int", e.FD}, 89 | {"addr", "struct sockaddr *", e.Saddr}, 90 | {"flags", "int", e.Flags}, 91 | } 92 | } 93 | 94 | func (e *Accept4) DecodeArguments(data []*byte, arglen int) error { 95 | e.FD = types.InputFD(types.MakeC32(unsafe.Pointer(data[0]))) 96 | e.Saddr = (*types.SockAddr)(unsafe.Pointer(data[1])) 97 | e.Flags = AcceptFlags(types.MakeC32(unsafe.Pointer(data[2]))) 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /cmd/prometheus.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/criticalstack/swoll/pkg/kernel/metrics" 8 | "github.com/criticalstack/swoll/pkg/syscalls" 9 | "github.com/criticalstack/swoll/pkg/topology" 10 | "github.com/criticalstack/swoll/pkg/types" 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | type prometheusWorker struct { 15 | handler *metrics.Handler 16 | topo *topology.Topology 17 | total *prometheus.Desc 18 | timeSpent *prometheus.Desc 19 | syscalls *syscalls.Syscalls 20 | } 21 | 22 | func newPrometheusWorker(handler *metrics.Handler, topo *topology.Topology) *prometheusWorker { 23 | labels := []string{"syscall", "pod", "container", "namespace", "class", "group", "err", "kns"} 24 | 25 | syscalls, _ := syscalls.New() 26 | 27 | return &prometheusWorker{ 28 | handler: handler, 29 | topo: topo, 30 | syscalls: syscalls, 31 | total: prometheus.NewDesc( 32 | prometheus.BuildFQName("swoll_node_metrics", "", "syscall_count"), 33 | "The total count for a given syscall.", 34 | labels, 35 | nil, 36 | ), 37 | timeSpent: prometheus.NewDesc( 38 | prometheus.BuildFQName("swoll_node_metrics", "", "syscall_time"), 39 | "The time in nanoseconds spent executing a given syscall.", 40 | labels, 41 | nil, 42 | ), 43 | } 44 | } 45 | 46 | func (w prometheusWorker) Describe(ch chan<- *prometheus.Desc) { 47 | ch <- w.total 48 | ch <- w.timeSpent 49 | } 50 | 51 | func (w prometheusWorker) Collect(ch chan<- prometheus.Metric) { 52 | if w.topo == nil { 53 | // if we don't have a valid topology context right now, just discard. 54 | // TODO[lz]: should we do non-resolved stats here? 55 | return 56 | } 57 | 58 | for _, metric := range w.handler.QueryAll() { 59 | container, err := w.topo.LookupContainer(context.TODO(), metric.PidNamespace()) 60 | if err != nil { 61 | continue 62 | } 63 | 64 | scnr := metric.Syscall() 65 | errno := metric.Errno() 66 | count := metric.Count() 67 | spent := metric.TimeSpent() 68 | syscall := w.syscalls.Lookup(int(scnr)) 69 | 70 | ch <- prometheus.MustNewConstMetric( 71 | w.total, 72 | prometheus.CounterValue, 73 | float64(count), 74 | syscall.Name, 75 | container.Pod, container.Name, container.Namespace, 76 | syscall.Class, 77 | syscall.Group, 78 | types.Errno(errno).String(), 79 | fmt.Sprintf("%v", metric.PidNamespace()), 80 | ) 81 | 82 | ch <- prometheus.MustNewConstMetric( 83 | w.timeSpent, 84 | prometheus.CounterValue, 85 | float64(spent), 86 | syscall.Name, 87 | container.Pod, container.Name, container.Namespace, 88 | syscall.Class, 89 | syscall.Group, 90 | types.Errno(errno).String(), 91 | fmt.Sprintf("%v", metric.PidNamespace()), 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/types/fileflags.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "syscall" 7 | ) 8 | 9 | type FileFlags int 10 | 11 | var fileCreationMasks = map[int]string{ 12 | syscall.O_RDWR: "O_RDWR", 13 | syscall.O_WRONLY: "O_WRONLY", 14 | syscall.O_RDONLY: "O_RDONLY", 15 | syscall.O_APPEND: "O_APPEND", 16 | syscall.O_ASYNC: "O_ASYNC", 17 | syscall.O_CLOEXEC: "O_CLOEXEC", 18 | syscall.O_CREAT: "O_CREAT", 19 | syscall.O_DIRECT: "O_DIRECT", 20 | syscall.O_DIRECTORY: "O_DIRECTORY", 21 | syscall.O_DSYNC: "O_DSYNC", 22 | syscall.O_EXCL: "O_EXCL", 23 | syscall.O_NOATIME: "O_NOATIME", 24 | syscall.O_NOCTTY: "O_NOCTTY", 25 | syscall.O_NOFOLLOW: "O_NOFOLLOW", 26 | syscall.O_NONBLOCK: "O_NONBLOCK", 27 | syscall.O_SYNC: "O_SYNC", 28 | syscall.O_TRUNC: "O_TRUNC", 29 | } 30 | 31 | func (flags FileFlags) Parse() []string { 32 | if flags == 0 { 33 | return []string{"O_RDONLY"} 34 | } 35 | 36 | ret := []string{} 37 | fint := int(flags) 38 | 39 | for flag, fstr := range fileCreationMasks { 40 | if flag&fint != 0 { 41 | ret = append(ret, fstr) 42 | } 43 | } 44 | 45 | return ret 46 | } 47 | 48 | func (flags FileFlags) String() string { 49 | return strings.Join(flags.Parse(), "|") 50 | } 51 | 52 | func (f FileFlags) MarshalJSON() ([]byte, error) { 53 | return json.Marshal(f.Parse()) 54 | } 55 | 56 | func (f *FileFlags) UnmarshalJSON(data []byte) error { 57 | var a []string 58 | 59 | *f = 0 60 | 61 | if err := json.Unmarshal(data, &a); err != nil { 62 | return err 63 | } 64 | 65 | for _, val := range a { 66 | switch val { 67 | case "O_RDWR": 68 | *f |= syscall.O_RDWR 69 | case "O_WRONLY": 70 | *f |= syscall.O_WRONLY 71 | case "O_RDONLY": 72 | *f |= syscall.O_RDONLY 73 | case "O_APPEND": 74 | *f |= syscall.O_APPEND 75 | case "O_ASYNC": 76 | *f |= syscall.O_ASYNC 77 | case "O_CLOEXEC": 78 | *f |= syscall.O_CLOEXEC 79 | case "O_CREAT": 80 | *f |= syscall.O_CREAT 81 | case "O_DIRECT": 82 | *f |= syscall.O_DIRECT 83 | case "O_DIRECTORY": 84 | *f |= syscall.O_DIRECTORY 85 | case "O_DSYNC": 86 | *f |= syscall.O_DSYNC 87 | case "O_EXCL": 88 | *f |= syscall.O_EXCL 89 | case "O_NOATIME": 90 | *f |= syscall.O_NOATIME 91 | case "O_NOCTTY": 92 | *f |= syscall.O_NOCTTY 93 | case "O_NOFOLLOW": 94 | *f |= syscall.O_NOFOLLOW 95 | case "O_NONBLOCK": 96 | *f |= syscall.O_NONBLOCK 97 | case "O_SYNC": 98 | *f |= syscall.O_SYNC 99 | case "O_TRUNC": 100 | *f |= syscall.O_TRUNC 101 | 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | 108 | /* 109 | func (flags FileFlags) MarshalJSON() ([]byte, error) { 110 | return json.Marshal(flags.Parse()) 111 | } 112 | */ 113 | -------------------------------------------------------------------------------- /pkg/kernel/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // key is the structure in kernel memory that represents the 9 | // key-value of a metric entry from the kernel. 10 | type key struct { 11 | pidNs uint32 12 | syscall uint32 13 | errno uint16 14 | pad uint16 15 | } 16 | 17 | // val is the structure that is the value of `key` as seen by 18 | // the kernel. 19 | type val struct { 20 | count uint64 21 | time uint64 22 | first uint64 23 | last uint64 24 | reserved uint64 25 | } 26 | 27 | // Metric is just an aggregate of the kernel key and value 28 | type Metric struct { 29 | k *key 30 | v *val 31 | } 32 | 33 | // Metrics is an array of metric entries 34 | type Metrics []Metric 35 | 36 | func (k *key) String() string { 37 | return fmt.Sprintf("pidns=%v, , sc=%v, errno=%v", k.pidNs, k.syscall, k.errno) 38 | } 39 | 40 | func (v *val) String() string { 41 | return fmt.Sprintf("count=%v, time=%v, first=%v, last=%v", v.count, v.time, v.first, v.last) 42 | } 43 | 44 | func (m Metric) String() string { 45 | return fmt.Sprintf("key={%v}, val={%v}", m.k, m.v) 46 | } 47 | 48 | // copy makes a copy of the key. 49 | func (k *key) copy() *key { 50 | return &key{ 51 | pidNs: k.pidNs, 52 | syscall: k.syscall, 53 | errno: k.errno, 54 | pad: k.pad, 55 | } 56 | } 57 | 58 | // copy makes a copy of the value 59 | func (v *val) copy() *val { 60 | return &val{ 61 | count: v.count, 62 | time: v.time, 63 | first: v.first, 64 | last: v.last, 65 | reserved: v.reserved, 66 | } 67 | } 68 | 69 | func (m *Metric) Count() uint64 { 70 | if m != nil && m.v != nil { 71 | return m.v.count 72 | } 73 | 74 | return 0 75 | } 76 | 77 | func (m *Metric) TimeSpent() uint64 { 78 | if m != nil && m.v != nil { 79 | return m.v.time 80 | } 81 | 82 | return 0 83 | } 84 | 85 | func (m *Metric) First() uint64 { 86 | if m != nil && m.v != nil { 87 | return m.v.first 88 | } 89 | 90 | return 0 91 | } 92 | 93 | func (m *Metric) Last() uint64 { 94 | if m != nil && m.v != nil { 95 | return m.v.last 96 | } 97 | 98 | return 0 99 | } 100 | 101 | func (m *Metric) PidNS() uint32 { 102 | if m != nil && m.k != nil { 103 | return m.k.pidNs 104 | } 105 | 106 | return 0 107 | } 108 | 109 | func (m *Metric) Syscall() uint32 { 110 | if m != nil && m.k != nil { 111 | return m.k.syscall 112 | } 113 | 114 | // if not found, since '0' can be seen as `read`, just return -1. 115 | return math.MaxUint32 116 | } 117 | 118 | func (m *Metric) Errno() uint16 { 119 | if m != nil && m.k != nil { 120 | return m.k.errno 121 | } 122 | 123 | return 0 124 | } 125 | 126 | // PidNamespace is for podmon.ResolverContext interface abstraction 127 | func (m Metric) PidNamespace() int { 128 | return int(m.PidNS()) 129 | } 130 | -------------------------------------------------------------------------------- /.github/workflows/push-image.yaml: -------------------------------------------------------------------------------- 1 | name: Build and push Swoll container image 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | push: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Prepare 19 | id: prep 20 | run: | 21 | DOCKER_IMAGE=${GITHUB_REPOSITORY} 22 | VERSION=noop 23 | if [ "${{ github.event_name }}" = "schedule" ]; then 24 | VERSION=nightly 25 | elif [[ $GITHUB_REF == refs/tags/* ]]; then 26 | VERSION=${GITHUB_REF#refs/tags/} 27 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 28 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') 29 | if [ "${{ github.event.repository.default_branch }}" = "$VERSION" ]; then 30 | VERSION=edge 31 | fi 32 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 33 | VERSION=pr-${{ github.event.number }} 34 | fi 35 | TAGS="${DOCKER_IMAGE}:${VERSION}" 36 | if [[ $VERSION =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 37 | MINOR=${VERSION%.*} 38 | MAJOR=${MINOR%.*} 39 | TAGS="$TAGS,${DOCKER_IMAGE}:${MINOR},${DOCKER_IMAGE}:${MAJOR},${DOCKER_IMAGE}:latest" 40 | elif [ "${{ github.event_name }}" = "push" ]; then 41 | TAGS="$TAGS,${DOCKER_IMAGE}:sha-${GITHUB_SHA::8}" 42 | fi 43 | echo ::set-output name=version::${VERSION} 44 | echo ::set-output name=tags::${TAGS} 45 | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') 46 | 47 | - name: Set up QEMU 48 | uses: docker/setup-qemu-action@v1 49 | 50 | - name: Set up Docker Buildx 51 | uses: docker/setup-buildx-action@v1 52 | 53 | - name: Cache Docker layers 54 | uses: actions/cache@v2 55 | with: 56 | path: /tmp/.buildx-cache 57 | key: ${{ runner.os }}-buildx-${{ github.sha }} 58 | restore-keys: | 59 | ${{ runner.os }}-buildx- 60 | 61 | - name: Login to DockerHub 62 | uses: docker/login-action@v1 63 | with: 64 | username: ${{ secrets.DOCKERHUB_USERNAME }} 65 | password: ${{ secrets.DOCKERHUB_TOKEN }} 66 | 67 | - name: Build and push 68 | id: docker_build 69 | uses: docker/build-push-action@v2 70 | with: 71 | context: . 72 | file: ./Dockerfile 73 | platforms: linux/amd64 74 | push: ${{ github.event_name != 'pull_request' }} 75 | tags: ${{ steps.prep.outputs.tags }} 76 | cache-from: type=local,src=/tmp/.buildx-cache 77 | cache-to: type=local,dest=/tmp/.buildx-cache 78 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/criticalstack/swoll/pkg/kernel/ross" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | gitCommit string 16 | buildTime string 17 | ) 18 | 19 | var ( 20 | bpfFile string 21 | 22 | rootCmd = &cobra.Command{ 23 | Use: "swoll", 24 | Short: "The kubernetes-aware eBPF tracing and metrics thingy.", 25 | Long: ` _ _ || :) 26 | _>\/\/(_)|| critical-stack(c) 27 | ` + ross.Paint(), 28 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 29 | log.SetOutput(os.Stderr) 30 | 31 | logcall, _ := cmd.Flags().GetBool("log-callers") 32 | if logcall { 33 | log.SetReportCaller(logcall) 34 | 35 | } 36 | log.SetFormatter(&log.TextFormatter{ 37 | ForceColors: true, 38 | FullTimestamp: true, 39 | PadLevelText: true, 40 | CallerPrettyfier: func(f *runtime.Frame) (string, string) { 41 | return f.Function + ": ", fmt.Sprintf("%s:%d", f.File, f.Line) 42 | }, 43 | }) 44 | 45 | switch cmd.Flag("log").Value.String() { 46 | case "debug": 47 | log.SetLevel(log.DebugLevel) 48 | case "trace": 49 | log.SetLevel(log.TraceLevel) 50 | case "info": 51 | log.SetLevel(log.InfoLevel) 52 | case "warn": 53 | log.SetLevel(log.WarnLevel) 54 | case "error": 55 | log.SetLevel(log.ErrorLevel) 56 | case "fatal": 57 | log.SetLevel(log.FatalLevel) 58 | case "panic": 59 | log.SetLevel(log.PanicLevel) 60 | default: 61 | return fmt.Errorf("unrecognized log output level") 62 | } 63 | 64 | return nil 65 | }, 66 | Run: func(cmd *cobra.Command, args []string) { 67 | ver, err := cmd.Flags().GetBool("version") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | if ver { 73 | log.Printf("Version: %s (compiled on: %s)\n", gitCommit, buildTime) 74 | } 75 | }, 76 | } 77 | ) 78 | 79 | // Execute ... 80 | func Execute() { 81 | if err := rootCmd.Execute(); err != nil { 82 | fmt.Println(err) 83 | os.Exit(1) 84 | } 85 | } 86 | 87 | func init() { 88 | 89 | rootCmd.PersistentFlags().StringP("cri", "r", "", "path to the local CRI unix socket") 90 | rootCmd.PersistentFlags().StringP("kubeconfig", "k", "", "path to the local k8s config instance (defaults to incluster config)") 91 | rootCmd.PersistentFlags().StringP("altroot", "A", "", "alternate root CWD") 92 | rootCmd.PersistentFlags().StringP("bpf", "b", "", "alternative external bpf object") 93 | rootCmd.PersistentFlags().BoolP("version", "V", false, "show version information") 94 | rootCmd.PersistentFlags().String("nsproxy-offset", "", "Offset (in hex) of task_struct->nsproxy") 95 | rootCmd.PersistentFlags().String("pidns-offset", "", "Offset (in hex) of pid_namespace->ns") 96 | rootCmd.PersistentFlags().Bool("no-detect-offsets", false, "Do not automatically determine task_struct member offsets (uses default offsets)") 97 | rootCmd.PersistentFlags().String("log", "info", "log level") 98 | rootCmd.PersistentFlags().Bool("log-callers", false, "log callers") 99 | } 100 | --------------------------------------------------------------------------------