mirror of
https://github.com/daeuniverse/dae.git
synced 2024-12-22 16:14:40 +07:00
feat: dae trace (#435)
Co-authored-by: Sumire (菫) <151038614+sumire88@users.noreply.github.com>
This commit is contained in:
parent
e04b16fdea
commit
5f3249bcb3
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ outline.json
|
|||||||
go-mod/
|
go-mod/
|
||||||
node_modules/
|
node_modules/
|
||||||
*.log
|
*.log
|
||||||
|
.build_tags
|
||||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,6 @@
|
|||||||
[submodule "control/kern/headers"]
|
[submodule "control/kern/headers"]
|
||||||
path = control/kern/headers
|
path = control/kern/headers
|
||||||
url = https://github.com/daeuniverse/dae_bpf_headers
|
url = https://github.com/daeuniverse/dae_bpf_headers
|
||||||
|
[submodule "trace/kern/headers"]
|
||||||
|
path = trace/kern/headers
|
||||||
|
url = https://github.com/daeuniverse/dae_bpf_headers
|
||||||
|
@ -1 +1,2 @@
|
|||||||
submodule_paths=control/kern/headers
|
submodule_paths=control/kern/headers
|
||||||
|
submodule_paths=trace/kern/headers
|
||||||
|
10
Makefile
10
Makefile
@ -15,6 +15,7 @@ MAX_MATCH_SET_LEN ?= 64
|
|||||||
CFLAGS := -DMAX_MATCH_SET_LEN=$(MAX_MATCH_SET_LEN) $(CFLAGS)
|
CFLAGS := -DMAX_MATCH_SET_LEN=$(MAX_MATCH_SET_LEN) $(CFLAGS)
|
||||||
NOSTRIP ?= n
|
NOSTRIP ?= n
|
||||||
STRIP_PATH := $(shell command -v $(STRIP) 2>/dev/null)
|
STRIP_PATH := $(shell command -v $(STRIP) 2>/dev/null)
|
||||||
|
BUILD_TAGS_FILE := .build_tags
|
||||||
ifeq ($(strip $(NOSTRIP)),y)
|
ifeq ($(strip $(NOSTRIP)),y)
|
||||||
STRIP_FLAG := -no-strip
|
STRIP_FLAG := -no-strip
|
||||||
else ifeq ($(wildcard $(STRIP_PATH)),)
|
else ifeq ($(wildcard $(STRIP_PATH)),)
|
||||||
@ -47,7 +48,7 @@ dae: export CGO_ENABLED=0
|
|||||||
endif
|
endif
|
||||||
dae: ebpf
|
dae: ebpf
|
||||||
@echo $(CFLAGS)
|
@echo $(CFLAGS)
|
||||||
go build -o $(OUTPUT) $(BUILD_ARGS) .
|
go build -tags=$(shell cat $(BUILD_TAGS_FILE)) -o $(OUTPUT) $(BUILD_ARGS) .
|
||||||
## End Dae Build
|
## End Dae Build
|
||||||
|
|
||||||
## Begin Git Submodules
|
## Begin Git Submodules
|
||||||
@ -74,6 +75,8 @@ submodule submodules: $(submodule_paths)
|
|||||||
clean-ebpf:
|
clean-ebpf:
|
||||||
@rm -f control/bpf_bpf*.go && \
|
@rm -f control/bpf_bpf*.go && \
|
||||||
rm -f control/bpf_bpf*.o
|
rm -f control/bpf_bpf*.o
|
||||||
|
@rm -f trace/bpf_bpf*.go && \
|
||||||
|
rm -f trace/bpf_bpf*.o
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|
||||||
@ -82,10 +85,13 @@ ebpf: export BPF_CLANG := $(CLANG)
|
|||||||
ebpf: export BPF_STRIP_FLAG := $(STRIP_FLAG)
|
ebpf: export BPF_STRIP_FLAG := $(STRIP_FLAG)
|
||||||
ebpf: export BPF_CFLAGS := $(CFLAGS)
|
ebpf: export BPF_CFLAGS := $(CFLAGS)
|
||||||
ebpf: export BPF_TARGET := $(TARGET)
|
ebpf: export BPF_TARGET := $(TARGET)
|
||||||
|
ebpf: export BPF_TRACE_TARGET := $(GOARCH)
|
||||||
ebpf: submodule clean-ebpf
|
ebpf: submodule clean-ebpf
|
||||||
@unset GOOS && \
|
@unset GOOS && \
|
||||||
unset GOARCH && \
|
unset GOARCH && \
|
||||||
unset GOARM && \
|
unset GOARM && \
|
||||||
echo $(STRIP_FLAG) && \
|
echo $(STRIP_FLAG) && \
|
||||||
go generate ./control/control.go
|
go generate ./control/control.go && \
|
||||||
|
go generate ./trace/trace.go && echo trace > $(BUILD_TAGS_FILE) || echo > $(BUILD_TAGS_FILE)
|
||||||
|
|
||||||
## End Ebpf
|
## End Ebpf
|
||||||
|
72
cmd/trace.go
Normal file
72
cmd/trace.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
//go:build trace
|
||||||
|
// +build trace
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/daeuniverse/dae/cmd/internal"
|
||||||
|
"github.com/daeuniverse/dae/trace"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IPv4, IPv6 bool
|
||||||
|
L4Proto string
|
||||||
|
Port int
|
||||||
|
OutputFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
traceCmd := &cobra.Command{
|
||||||
|
Use: "trace",
|
||||||
|
Short: "To trace traffic",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
internal.AutoSu()
|
||||||
|
|
||||||
|
if IPv4 && IPv6 {
|
||||||
|
logrus.Fatalln("IPv4 and IPv6 cannot be set at the same time")
|
||||||
|
}
|
||||||
|
if !IPv4 && !IPv6 {
|
||||||
|
IPv4 = true
|
||||||
|
}
|
||||||
|
IPVersion := 4
|
||||||
|
if IPv6 {
|
||||||
|
IPVersion = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
var L4ProtoNo uint16
|
||||||
|
switch L4Proto {
|
||||||
|
case "tcp":
|
||||||
|
L4ProtoNo = syscall.IPPROTO_TCP
|
||||||
|
case "udp":
|
||||||
|
L4ProtoNo = syscall.IPPROTO_UDP
|
||||||
|
default:
|
||||||
|
logrus.Fatalf("Unknown L4 protocol: %s\n", L4Proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
if err := trace.StartTrace(ctx, IPVersion, L4ProtoNo, Port, OutputFile); err != nil {
|
||||||
|
logrus.Fatalln(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
traceCmd.PersistentFlags().BoolVarP(&IPv4, "ipv4", "4", false, "Capture IPv4 traffic")
|
||||||
|
traceCmd.PersistentFlags().BoolVarP(&IPv6, "ipv6", "6", false, "Capture IPv6 traffic")
|
||||||
|
traceCmd.PersistentFlags().StringVarP(&L4Proto, "l4-proto", "p", "tcp", "Layer 4 protocol")
|
||||||
|
traceCmd.PersistentFlags().IntVarP(&Port, "port", "P", 80, "Port")
|
||||||
|
traceCmd.PersistentFlags().StringVarP(&OutputFile, "output", "o", "/dev/stdout", "Output file")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(traceCmd)
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit d72c67ed8f5a7d11774b5cd88734e2ffe6847721
|
Subproject commit e4da1c9601e1c3797d02c481a462b66588477495
|
4
go.mod
4
go.mod
@ -8,7 +8,7 @@ require (
|
|||||||
github.com/adrg/xdg v0.4.0
|
github.com/adrg/xdg v0.4.0
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df
|
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df
|
||||||
github.com/bits-and-blooms/bloom/v3 v3.5.0
|
github.com/bits-and-blooms/bloom/v3 v3.5.0
|
||||||
github.com/cilium/ebpf v0.11.0
|
github.com/cilium/ebpf v0.12.3
|
||||||
github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d
|
github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d
|
||||||
github.com/daeuniverse/outbound v0.0.0-20240101085641-7932e7df927d
|
github.com/daeuniverse/outbound v0.0.0-20240101085641-7932e7df927d
|
||||||
github.com/daeuniverse/softwind v0.0.0-20231230065827-eed67f20d2c1
|
github.com/daeuniverse/softwind v0.0.0-20231230065827-eed67f20d2c1
|
||||||
@ -24,7 +24,7 @@ require (
|
|||||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||||
golang.org/x/crypto v0.12.0
|
golang.org/x/crypto v0.12.0
|
||||||
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
|
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
|
||||||
golang.org/x/sys v0.11.0
|
golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -10,6 +10,8 @@ github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWk
|
|||||||
github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs=
|
github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs=
|
||||||
github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y=
|
github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y=
|
||||||
github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs=
|
github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs=
|
||||||
|
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
|
||||||
|
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d h1:hnC39MjR7xt5kZjrKlef7DXKFDkiX8MIcDXYC/6Jf9Q=
|
github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d h1:hnC39MjR7xt5kZjrKlef7DXKFDkiX8MIcDXYC/6Jf9Q=
|
||||||
github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d/go.mod h1:VGWGgv7pCP5WGyHGUyb9+nq/gW0yBm+i/GfCNATOJ1M=
|
github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d/go.mod h1:VGWGgv7pCP5WGyHGUyb9+nq/gW0yBm+i/GfCNATOJ1M=
|
||||||
@ -200,6 +202,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c h1:3kC/TjQ+xzIblQv39bCOyRk8fbEeJcDHwbyxPUU2BpA=
|
||||||
|
golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
71
trace/kallsyms.go
Normal file
71
trace/kallsyms.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Symbol struct {
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
Addr uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
var kallsyms []Symbol
|
||||||
|
var kallsymsByName map[string]Symbol = make(map[string]Symbol)
|
||||||
|
var kallsymsByAddr map[uint64]Symbol = make(map[uint64]Symbol)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
readKallsyms()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKallsyms() {
|
||||||
|
file, err := os.Open("/proc/kallsyms")
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("failed to open /proc/kallsyms: %v", err)
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, err := strconv.ParseUint(parts[0], 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ, name := parts[1], parts[2]
|
||||||
|
kallsyms = append(kallsyms, Symbol{typ, name, addr})
|
||||||
|
kallsymsByName[name] = Symbol{typ, name, addr}
|
||||||
|
kallsymsByAddr[addr] = Symbol{typ, name, addr}
|
||||||
|
}
|
||||||
|
sort.Slice(kallsyms, func(i, j int) bool {
|
||||||
|
return kallsyms[i].Addr < kallsyms[j].Addr
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NearestSymbol(addr uint64) Symbol {
|
||||||
|
idx, _ := slices.BinarySearchFunc(kallsyms, addr, func(x Symbol, addr uint64) int { return int(x.Addr - addr) })
|
||||||
|
if idx == len(kallsyms) {
|
||||||
|
return kallsyms[idx-1]
|
||||||
|
}
|
||||||
|
if kallsyms[idx].Addr == addr {
|
||||||
|
return kallsyms[idx]
|
||||||
|
}
|
||||||
|
if idx == 0 {
|
||||||
|
return kallsyms[0]
|
||||||
|
}
|
||||||
|
return kallsyms[idx-1]
|
||||||
|
}
|
1
trace/kern/headers
Submodule
1
trace/kern/headers
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit e4da1c9601e1c3797d02c481a462b66588477495
|
241
trace/kern/trace.c
Normal file
241
trace/kern/trace.c
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
#include "headers/if_ether_defs.h"
|
||||||
|
#include "headers/vmlinux.h"
|
||||||
|
|
||||||
|
#include "headers/bpf_core_read.h"
|
||||||
|
#include "headers/bpf_endian.h"
|
||||||
|
#include "headers/bpf_helpers.h"
|
||||||
|
#include "headers/bpf_tracing.h"
|
||||||
|
|
||||||
|
#define IFNAMSIZ 16
|
||||||
|
#define PNAME_LEN 32
|
||||||
|
|
||||||
|
static const bool TRUE = true;
|
||||||
|
|
||||||
|
union addr {
|
||||||
|
u32 v4addr;
|
||||||
|
struct {
|
||||||
|
u64 d1;
|
||||||
|
u64 d2;
|
||||||
|
} v6addr;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
struct meta {
|
||||||
|
u64 pc;
|
||||||
|
u64 skb;
|
||||||
|
u64 second_param;
|
||||||
|
u32 mark;
|
||||||
|
u32 netns;
|
||||||
|
u32 ifindex;
|
||||||
|
u32 pid;
|
||||||
|
unsigned char ifname[IFNAMSIZ];
|
||||||
|
unsigned char pname[PNAME_LEN];
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
struct tuple {
|
||||||
|
union addr saddr;
|
||||||
|
union addr daddr;
|
||||||
|
u16 sport;
|
||||||
|
u16 dport;
|
||||||
|
u16 l3_proto;
|
||||||
|
u8 l4_proto;
|
||||||
|
u8 tcp_flags;
|
||||||
|
u16 payload_len;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
struct event {
|
||||||
|
struct meta meta;
|
||||||
|
struct tuple tuple;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
const struct event *_ __attribute__((unused));
|
||||||
|
|
||||||
|
struct tracing_config {
|
||||||
|
u16 port;
|
||||||
|
u16 l4_proto;
|
||||||
|
u8 ip_vsn;
|
||||||
|
};
|
||||||
|
|
||||||
|
static volatile const struct tracing_config tracing_cfg;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_HASH);
|
||||||
|
__type(key, __u64);
|
||||||
|
__type(value, bool);
|
||||||
|
__uint(max_entries, 1024);
|
||||||
|
} skb_addresses SEC(".maps");
|
||||||
|
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||||
|
__uint(max_entries, 1<<29);
|
||||||
|
} events SEC(".maps");
|
||||||
|
|
||||||
|
static __always_inline u32
|
||||||
|
get_netns(struct sk_buff *skb)
|
||||||
|
{
|
||||||
|
u32 netns = BPF_CORE_READ(skb, dev, nd_net.net, ns.inum);
|
||||||
|
|
||||||
|
// if skb->dev is not initialized, try to get ns from sk->__sk_common.skc_net.net->ns.inum
|
||||||
|
if (netns == 0) {
|
||||||
|
struct sock *sk = BPF_CORE_READ(skb, sk);
|
||||||
|
if (sk != NULL)
|
||||||
|
netns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
|
||||||
|
}
|
||||||
|
|
||||||
|
return netns;
|
||||||
|
}
|
||||||
|
|
||||||
|
static __always_inline bool
|
||||||
|
filter_l3_and_l4(struct sk_buff *skb)
|
||||||
|
{
|
||||||
|
void *skb_head = BPF_CORE_READ(skb, head);
|
||||||
|
u16 l3_off = BPF_CORE_READ(skb, network_header);
|
||||||
|
u16 l4_off = BPF_CORE_READ(skb, transport_header);
|
||||||
|
|
||||||
|
struct iphdr *l3_hdr = (struct iphdr *) (skb_head + l3_off);
|
||||||
|
u8 ip_vsn = BPF_CORE_READ_BITFIELD_PROBED(l3_hdr, version);
|
||||||
|
if (ip_vsn != tracing_cfg.ip_vsn)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
u16 l4_proto;
|
||||||
|
if (ip_vsn == 4) {
|
||||||
|
struct iphdr *ip4 = (struct iphdr *) l3_hdr;
|
||||||
|
l4_proto = BPF_CORE_READ(ip4, protocol);
|
||||||
|
} else if (ip_vsn == 6) {
|
||||||
|
struct ipv6hdr *ip6 = (struct ipv6hdr *) l3_hdr;
|
||||||
|
l4_proto = BPF_CORE_READ(ip6, nexthdr);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l4_proto != tracing_cfg.l4_proto)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
u16 sport, dport;
|
||||||
|
if (l4_proto == IPPROTO_TCP) {
|
||||||
|
struct tcphdr *tcp = (struct tcphdr *) (skb_head + l4_off);
|
||||||
|
sport = BPF_CORE_READ(tcp, source);
|
||||||
|
dport = BPF_CORE_READ(tcp, dest);
|
||||||
|
} else if (l4_proto == IPPROTO_UDP) {
|
||||||
|
struct udphdr *udp = (struct udphdr *) (skb_head + l4_off);
|
||||||
|
sport = BPF_CORE_READ(udp, source);
|
||||||
|
dport = BPF_CORE_READ(udp, dest);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dport != tracing_cfg.port && sport != tracing_cfg.port)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static __always_inline void
|
||||||
|
set_meta(struct meta *meta, struct sk_buff *skb, struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
meta->pc = BPF_CORE_READ(ctx, ip);
|
||||||
|
meta->skb = (__u64)skb;
|
||||||
|
meta->second_param = PT_REGS_PARM2(ctx);
|
||||||
|
meta->mark = BPF_CORE_READ(skb, mark);
|
||||||
|
meta->netns = get_netns(skb);
|
||||||
|
meta->ifindex = BPF_CORE_READ(skb, dev, ifindex);
|
||||||
|
BPF_CORE_READ_STR_INTO(&meta->ifname, skb, dev, name);
|
||||||
|
|
||||||
|
struct task_struct *current = (void *)bpf_get_current_task();
|
||||||
|
meta->pid = BPF_CORE_READ(current, pid);
|
||||||
|
u64 arg_start = BPF_CORE_READ(current, mm, arg_start);
|
||||||
|
bpf_probe_read_user_str(&meta->pname, PNAME_LEN, (void *)arg_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
static __always_inline void
|
||||||
|
set_tuple(struct tuple *tpl, struct sk_buff *skb)
|
||||||
|
{
|
||||||
|
void *skb_head = BPF_CORE_READ(skb, head);
|
||||||
|
u16 l3_off = BPF_CORE_READ(skb, network_header);
|
||||||
|
u16 l4_off = BPF_CORE_READ(skb, transport_header);
|
||||||
|
|
||||||
|
struct iphdr *l3_hdr = (struct iphdr *) (skb_head + l3_off);
|
||||||
|
u8 ip_vsn = BPF_CORE_READ_BITFIELD_PROBED(l3_hdr, version);
|
||||||
|
|
||||||
|
u16 l3_total_len;
|
||||||
|
if (ip_vsn == 4) {
|
||||||
|
struct iphdr *ip4 = (struct iphdr *) l3_hdr;
|
||||||
|
BPF_CORE_READ_INTO(&tpl->saddr, ip4, saddr);
|
||||||
|
BPF_CORE_READ_INTO(&tpl->daddr, ip4, daddr);
|
||||||
|
tpl->l4_proto = BPF_CORE_READ(ip4, protocol);
|
||||||
|
tpl->l3_proto = ETH_P_IP;
|
||||||
|
l3_total_len = bpf_ntohs(BPF_CORE_READ(ip4, tot_len));
|
||||||
|
} else if (ip_vsn == 6) {
|
||||||
|
struct ipv6hdr *ip6 = (struct ipv6hdr *) l3_hdr;
|
||||||
|
BPF_CORE_READ_INTO(&tpl->saddr, ip6, saddr);
|
||||||
|
BPF_CORE_READ_INTO(&tpl->daddr, ip6, daddr);
|
||||||
|
tpl->l4_proto = BPF_CORE_READ(ip6, nexthdr);
|
||||||
|
tpl->l3_proto = ETH_P_IPV6;
|
||||||
|
l3_total_len = bpf_ntohs(BPF_CORE_READ(ip6, payload_len));
|
||||||
|
}
|
||||||
|
u16 l3_hdr_len = l4_off - l3_off;
|
||||||
|
|
||||||
|
u16 l4_hdr_len;
|
||||||
|
if (tpl->l4_proto == IPPROTO_TCP) {
|
||||||
|
struct tcphdr *tcp = (struct tcphdr *) (skb_head + l4_off);
|
||||||
|
tpl->sport= BPF_CORE_READ(tcp, source);
|
||||||
|
tpl->dport= BPF_CORE_READ(tcp, dest);
|
||||||
|
bpf_probe_read_kernel(&tpl->tcp_flags, sizeof(tpl->tcp_flags),
|
||||||
|
(void *)tcp + offsetof(struct tcphdr, ack_seq) + 5);
|
||||||
|
l4_hdr_len = BPF_CORE_READ_BITFIELD_PROBED(tcp, doff) * 4;
|
||||||
|
tpl->payload_len = l3_total_len - l3_hdr_len - l4_hdr_len;
|
||||||
|
} else if (tpl->l4_proto == IPPROTO_UDP) {
|
||||||
|
struct udphdr *udp = (struct udphdr *) (skb_head + l4_off);
|
||||||
|
tpl->sport= BPF_CORE_READ(udp, source);
|
||||||
|
tpl->dport= BPF_CORE_READ(udp, dest);
|
||||||
|
tpl->payload_len = bpf_ntohs(BPF_CORE_READ(udp, len)) - sizeof(struct udphdr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static __always_inline int
|
||||||
|
handle_skb(struct sk_buff *skb, struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
bool tracked = false;
|
||||||
|
u64 skb_addr = (u64) skb;
|
||||||
|
struct event ev = {};
|
||||||
|
if (bpf_map_lookup_elem(&skb_addresses, &skb_addr)) {
|
||||||
|
tracked = true;
|
||||||
|
goto cont;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_l3_and_l4(skb))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (!tracked)
|
||||||
|
bpf_map_update_elem(&skb_addresses, &skb_addr, &TRUE, BPF_ANY);
|
||||||
|
|
||||||
|
cont:
|
||||||
|
set_meta(&ev.meta, skb, ctx);
|
||||||
|
set_tuple(&ev.tuple, skb);
|
||||||
|
|
||||||
|
bpf_ringbuf_output(&events, &ev, sizeof(ev), 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define KPROBE_SKB_AT(X) \
|
||||||
|
SEC("kprobe/skb-" #X) \
|
||||||
|
int kprobe_skb_##X(struct pt_regs *ctx) \
|
||||||
|
{ \
|
||||||
|
struct sk_buff *skb = (struct sk_buff *) PT_REGS_PARM##X(ctx); \
|
||||||
|
return handle_skb(skb, ctx); \
|
||||||
|
}
|
||||||
|
|
||||||
|
KPROBE_SKB_AT(1)
|
||||||
|
KPROBE_SKB_AT(2)
|
||||||
|
KPROBE_SKB_AT(3)
|
||||||
|
KPROBE_SKB_AT(4)
|
||||||
|
KPROBE_SKB_AT(5)
|
||||||
|
|
||||||
|
SEC("kprobe/skb_lifetime_termination")
|
||||||
|
int kprobe_skb_lifetime_termination(struct pt_regs *ctx)
|
||||||
|
{
|
||||||
|
u64 skb = (u64) PT_REGS_PARM1(ctx);
|
||||||
|
bpf_map_delete_elem(&skb_addresses, &skb);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SEC("license") const char __license[] = "Dual BSD/GPL";
|
283
trace/trace.go
Normal file
283
trace/trace.go
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/cilium/ebpf"
|
||||||
|
"github.com/cilium/ebpf/btf"
|
||||||
|
"github.com/cilium/ebpf/link"
|
||||||
|
"github.com/cilium/ebpf/ringbuf"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run -mod=mod github.com/cilium/ebpf/cmd/bpf2go -cc "$BPF_CLANG" "$BPF_STRIP_FLAG" -cflags "$BPF_CFLAGS" -target "$BPF_TRACE_TARGET" -type event bpf kern/trace.c -- -I./headers
|
||||||
|
|
||||||
|
var nativeEndian binary.ByteOrder
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
buf := [2]byte{}
|
||||||
|
*(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xABCD)
|
||||||
|
|
||||||
|
switch buf {
|
||||||
|
case [2]byte{0xCD, 0xAB}:
|
||||||
|
nativeEndian = binary.LittleEndian
|
||||||
|
case [2]byte{0xAB, 0xCD}:
|
||||||
|
nativeEndian = binary.BigEndian
|
||||||
|
default:
|
||||||
|
panic("Could not determine native endianness.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartTrace(ctx context.Context, ipVersion int, l4ProtoNo uint16, port int, outputFile string) (err error) {
|
||||||
|
objs, err := rewriteAndLoadBpf(ipVersion, l4ProtoNo, port)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer objs.Close()
|
||||||
|
|
||||||
|
targets, kfreeSkbReasons, err := searchAvailableTargets()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
links, err := attachBpfToTargets(objs, targets)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
i := 0
|
||||||
|
fmt.Printf("\n")
|
||||||
|
for _, link := range links {
|
||||||
|
i++
|
||||||
|
fmt.Printf("detaching kprobes: %04d/%04d\r", i, len(links))
|
||||||
|
link.Close()
|
||||||
|
}
|
||||||
|
fmt.Printf("\n")
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("\nstart tracing\n")
|
||||||
|
if err = handleEvents(ctx, objs, outputFile, kfreeSkbReasons); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func rewriteAndLoadBpf(ipVersion int, l4ProtoNo uint16, port int) (_ *bpfObjects, err error) {
|
||||||
|
spec, err := loadBpf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load BPF: %+v\n", err)
|
||||||
|
}
|
||||||
|
if err := spec.RewriteConstants(map[string]interface{}{
|
||||||
|
"tracing_cfg": struct {
|
||||||
|
port uint16
|
||||||
|
l4Proto uint16
|
||||||
|
ipVersion uint8
|
||||||
|
pad uint8
|
||||||
|
}{
|
||||||
|
port: Htons(uint16(port)),
|
||||||
|
l4Proto: uint16(l4ProtoNo),
|
||||||
|
ipVersion: uint8(ipVersion),
|
||||||
|
pad: 0,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to rewrite constants: %+v\n", err)
|
||||||
|
}
|
||||||
|
var opts ebpf.CollectionOptions
|
||||||
|
opts.Programs.LogLevel = ebpf.LogLevelInstruction
|
||||||
|
opts.Programs.LogSize = ebpf.DefaultVerifierLogSize * 100
|
||||||
|
objs := bpfObjects{}
|
||||||
|
if err := spec.LoadAndAssign(&objs, &opts); err != nil {
|
||||||
|
var (
|
||||||
|
ve *ebpf.VerifierError
|
||||||
|
verifierLog string
|
||||||
|
)
|
||||||
|
if errors.As(err, &ve) {
|
||||||
|
verifierLog = fmt.Sprintf("Verifier error: %+v\n", ve)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to load BPF: %+v\n%s", err, verifierLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &objs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchAvailableTargets() (targets map[string]int, kfreeSkbReasons map[uint64]string, err error) {
|
||||||
|
targets = map[string]int{}
|
||||||
|
|
||||||
|
btfSpec, err := btf.LoadKernelSpec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to load kernel BTF: %+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if kfreeSkbReasons, err = getKFreeSKBReasons(btfSpec); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iter := btfSpec.Iterate()
|
||||||
|
for iter.Next() {
|
||||||
|
typ := iter.Type
|
||||||
|
fn, ok := typ.(*btf.Func)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fnName := string(fn.Name)
|
||||||
|
|
||||||
|
fnProto := fn.Type.(*btf.FuncProto)
|
||||||
|
i := 1
|
||||||
|
for _, p := range fnProto.Params {
|
||||||
|
if ptr, ok := p.Type.(*btf.Pointer); ok {
|
||||||
|
if strct, ok := ptr.Target.(*btf.Struct); ok {
|
||||||
|
if strct.Name == "sk_buff" && i <= 5 {
|
||||||
|
name := fnName
|
||||||
|
targets[name] = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets, kfreeSkbReasons, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKFreeSKBReasons(spec *btf.Spec) (map[uint64]string, error) {
|
||||||
|
if _, err := spec.AnyTypeByName("kfree_skb_reason"); err != nil {
|
||||||
|
// Kernel is too old to have kfree_skb_reason
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dropReasonsEnum *btf.Enum
|
||||||
|
if err := spec.TypeByName("skb_drop_reason", &dropReasonsEnum); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find 'skb_drop_reason' enum: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := map[uint64]string{}
|
||||||
|
for _, val := range dropReasonsEnum.Values {
|
||||||
|
ret[uint64(val.Value)] = val.Name
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func attachBpfToTargets(objs *bpfObjects, targets map[string]int) (links []link.Link, err error) {
|
||||||
|
kp, err := link.Kprobe("kfree_skbmem", objs.KprobeSkbLifetimeTermination, nil)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("failed to attach kprobe to kfree_skbmem: %+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for fn, pos := range targets {
|
||||||
|
i++
|
||||||
|
fmt.Printf("attaching kprobes: %04d/%04d\r", i, len(targets))
|
||||||
|
var kp link.Link
|
||||||
|
switch pos {
|
||||||
|
case 1:
|
||||||
|
kp, err = link.Kprobe(fn, objs.KprobeSkb1, nil)
|
||||||
|
case 2:
|
||||||
|
kp, err = link.Kprobe(fn, objs.KprobeSkb2, nil)
|
||||||
|
case 3:
|
||||||
|
kp, err = link.Kprobe(fn, objs.KprobeSkb3, nil)
|
||||||
|
case 4:
|
||||||
|
kp, err = link.Kprobe(fn, objs.KprobeSkb4, nil)
|
||||||
|
case 5:
|
||||||
|
kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("failed to attach kprobe to %s: %+v\n", fn, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
links = append(links, kp)
|
||||||
|
}
|
||||||
|
if len(links) == 0 {
|
||||||
|
err = fmt.Errorf("failed to attach kprobes to any target")
|
||||||
|
}
|
||||||
|
links = append(links, kp)
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEvents(ctx context.Context, objs *bpfObjects, outputFile string, kfreeSkbReasons map[uint64]string) (err error) {
|
||||||
|
writer, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsReader, err := ringbuf.NewReader(objs.Events)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create ringbuf reader: %+v\n", err)
|
||||||
|
}
|
||||||
|
defer eventsReader.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
eventsReader.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
type bpfEvent struct {
|
||||||
|
Pc uint64
|
||||||
|
Skb uint64
|
||||||
|
SecondParam uint64
|
||||||
|
Mark uint32
|
||||||
|
Netns uint32
|
||||||
|
Ifindex uint32
|
||||||
|
Pid uint32
|
||||||
|
Ifname [16]uint8
|
||||||
|
Pname [32]uint8
|
||||||
|
Saddr [16]byte
|
||||||
|
Daddr [16]byte
|
||||||
|
Sport uint16
|
||||||
|
Dport uint16
|
||||||
|
L3Proto uint16
|
||||||
|
L4Proto uint8
|
||||||
|
TcpFlags uint8
|
||||||
|
PayloadLen uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
rec, err := eventsReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ringbuf.ErrClosed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logrus.Debugf("failed to read ringbuf: %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var event bpfEvent
|
||||||
|
if err = binary.Read(bytes.NewBuffer(rec.RawSample), nativeEndian, &event); err != nil {
|
||||||
|
logrus.Debugf("failed to parse ringbuf event: %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(writer, "%x mark=%x netns=%010d if=%d(%s) proc=%d(%s) ", event.Skb, event.Mark, event.Netns, event.Ifindex, TrimNull(string(event.Ifname[:])), event.Pid, TrimNull(string(event.Pname[:])))
|
||||||
|
if event.L3Proto == syscall.ETH_P_IP {
|
||||||
|
fmt.Fprintf(writer, "%s:%d > %s:%d ", net.IP(event.Saddr[:4]).String(), Ntohs(event.Sport), net.IP(event.Daddr[:4]).String(), Ntohs(event.Dport))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(writer, "[%s]:%d > [%s]:%d ", net.IP(event.Saddr[:]).String(), Ntohs(event.Sport), net.IP(event.Daddr[:]).String(), Ntohs(event.Dport))
|
||||||
|
}
|
||||||
|
if event.L4Proto == syscall.IPPROTO_TCP {
|
||||||
|
fmt.Fprintf(writer, "tcp_flags=%s ", TcpFlags(event.TcpFlags))
|
||||||
|
}
|
||||||
|
fmt.Fprintf(writer, "payload_len=%d ", event.PayloadLen)
|
||||||
|
sym := NearestSymbol(event.Pc)
|
||||||
|
fmt.Fprintf(writer, "%s", sym.Name)
|
||||||
|
if sym.Name == "kfree_skb_reason" {
|
||||||
|
fmt.Fprintf(writer, "(%s)", kfreeSkbReasons[event.SecondParam])
|
||||||
|
}
|
||||||
|
fmt.Fprintf(writer, "\n")
|
||||||
|
}
|
||||||
|
}
|
50
trace/utils.go
Normal file
50
trace/utils.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Htons(x uint16) uint16 {
|
||||||
|
data := make([]byte, 2)
|
||||||
|
nativeEndian.PutUint16(data, x)
|
||||||
|
return binary.BigEndian.Uint16(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ntohs(x uint16) uint16 {
|
||||||
|
data := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(data, x)
|
||||||
|
return nativeEndian.Uint16(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TrimNull(s string) string {
|
||||||
|
return strings.TrimRight(s, "\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TcpFlags(data uint8) string {
|
||||||
|
flags := []string{}
|
||||||
|
if data&0b00100000 != 0 {
|
||||||
|
flags = append(flags, "U")
|
||||||
|
}
|
||||||
|
if data&0b00010000 != 0 {
|
||||||
|
flags = append(flags, ".")
|
||||||
|
}
|
||||||
|
if data&0b00001000 != 0 {
|
||||||
|
flags = append(flags, "P")
|
||||||
|
}
|
||||||
|
if data&0b00000100 != 0 {
|
||||||
|
flags = append(flags, "R")
|
||||||
|
}
|
||||||
|
if data&0b00000010 != 0 {
|
||||||
|
flags = append(flags, "S")
|
||||||
|
}
|
||||||
|
if data&0b00000001 != 0 {
|
||||||
|
flags = append(flags, "F")
|
||||||
|
}
|
||||||
|
return strings.Join(flags, "")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user