mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
implement decision logic
This commit is contained in:
+33
-15
@@ -14,23 +14,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type bpfEvent struct {
|
type bpfEvent struct {
|
||||||
_ structs.HostLayout
|
_ structs.HostLayout
|
||||||
Pid uint32
|
Pid uint32
|
||||||
Uid uint32
|
Uid uint32
|
||||||
Daddr uint32
|
Daddr uint32
|
||||||
Dport uint16
|
Dport uint16
|
||||||
Proto uint8
|
Proto uint8
|
||||||
Comm [16]uint8
|
Action uint8
|
||||||
_ [1]byte
|
Comm [16]uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type bpfTarget struct {
|
||||||
|
_ structs.HostLayout
|
||||||
|
Ip uint32
|
||||||
|
Port uint16
|
||||||
|
_ [2]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// Names of all BPF objects in the ELF.
|
// Names of all BPF objects in the ELF.
|
||||||
//
|
//
|
||||||
// Used for safe lookups in a Collection or CollectionSpec.
|
// Used for safe lookups in a Collection or CollectionSpec.
|
||||||
const (
|
const (
|
||||||
bpfMapEvents = "events"
|
bpfMapEvents = "events"
|
||||||
bpfProgConnect4 = "connect4"
|
bpfMapExemptMap = "exempt_map"
|
||||||
bpfVarUnusedEvent = "unused_event"
|
bpfMapTargetMap = "target_map"
|
||||||
|
bpfProgConnect4 = "connect4"
|
||||||
|
bpfVarUnusedEvent = "unused_event"
|
||||||
|
bpfVarUnusedTarget = "unused_target"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadBpf returns the embedded CollectionSpec for bpf.
|
// loadBpf returns the embedded CollectionSpec for bpf.
|
||||||
@@ -82,14 +92,17 @@ type bpfProgramSpecs struct {
|
|||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfMapSpecs struct {
|
type bpfMapSpecs struct {
|
||||||
Events *ebpf.MapSpec `ebpf:"events"`
|
Events *ebpf.MapSpec `ebpf:"events"`
|
||||||
|
ExemptMap *ebpf.MapSpec `ebpf:"exempt_map"`
|
||||||
|
TargetMap *ebpf.MapSpec `ebpf:"target_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfVariableSpecs contains global variables before they are loaded into the kernel.
|
// bpfVariableSpecs contains global variables before they are loaded into the kernel.
|
||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfVariableSpecs struct {
|
type bpfVariableSpecs struct {
|
||||||
UnusedEvent *ebpf.VariableSpec `ebpf:"unused_event"`
|
UnusedEvent *ebpf.VariableSpec `ebpf:"unused_event"`
|
||||||
|
UnusedTarget *ebpf.VariableSpec `ebpf:"unused_target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfObjects contains all objects after they have been loaded into the kernel.
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
@@ -112,12 +125,16 @@ func (o *bpfObjects) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfMaps struct {
|
type bpfMaps struct {
|
||||||
Events *ebpf.Map `ebpf:"events"`
|
Events *ebpf.Map `ebpf:"events"`
|
||||||
|
ExemptMap *ebpf.Map `ebpf:"exempt_map"`
|
||||||
|
TargetMap *ebpf.Map `ebpf:"target_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *bpfMaps) Close() error {
|
func (m *bpfMaps) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
m.Events,
|
m.Events,
|
||||||
|
m.ExemptMap,
|
||||||
|
m.TargetMap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +142,8 @@ func (m *bpfMaps) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfVariables struct {
|
type bpfVariables struct {
|
||||||
UnusedEvent *ebpf.Variable `ebpf:"unused_event"`
|
UnusedEvent *ebpf.Variable `ebpf:"unused_event"`
|
||||||
|
UnusedTarget *ebpf.Variable `ebpf:"unused_target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfPrograms contains all programs after they have been loaded into the kernel.
|
// bpfPrograms contains all programs after they have been loaded into the kernel.
|
||||||
|
|||||||
Binary file not shown.
+33
-15
@@ -14,23 +14,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type bpfEvent struct {
|
type bpfEvent struct {
|
||||||
_ structs.HostLayout
|
_ structs.HostLayout
|
||||||
Pid uint32
|
Pid uint32
|
||||||
Uid uint32
|
Uid uint32
|
||||||
Daddr uint32
|
Daddr uint32
|
||||||
Dport uint16
|
Dport uint16
|
||||||
Proto uint8
|
Proto uint8
|
||||||
Comm [16]uint8
|
Action uint8
|
||||||
_ [1]byte
|
Comm [16]uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type bpfTarget struct {
|
||||||
|
_ structs.HostLayout
|
||||||
|
Ip uint32
|
||||||
|
Port uint16
|
||||||
|
_ [2]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// Names of all BPF objects in the ELF.
|
// Names of all BPF objects in the ELF.
|
||||||
//
|
//
|
||||||
// Used for safe lookups in a Collection or CollectionSpec.
|
// Used for safe lookups in a Collection or CollectionSpec.
|
||||||
const (
|
const (
|
||||||
bpfMapEvents = "events"
|
bpfMapEvents = "events"
|
||||||
bpfProgConnect4 = "connect4"
|
bpfMapExemptMap = "exempt_map"
|
||||||
bpfVarUnusedEvent = "unused_event"
|
bpfMapTargetMap = "target_map"
|
||||||
|
bpfProgConnect4 = "connect4"
|
||||||
|
bpfVarUnusedEvent = "unused_event"
|
||||||
|
bpfVarUnusedTarget = "unused_target"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadBpf returns the embedded CollectionSpec for bpf.
|
// loadBpf returns the embedded CollectionSpec for bpf.
|
||||||
@@ -82,14 +92,17 @@ type bpfProgramSpecs struct {
|
|||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfMapSpecs struct {
|
type bpfMapSpecs struct {
|
||||||
Events *ebpf.MapSpec `ebpf:"events"`
|
Events *ebpf.MapSpec `ebpf:"events"`
|
||||||
|
ExemptMap *ebpf.MapSpec `ebpf:"exempt_map"`
|
||||||
|
TargetMap *ebpf.MapSpec `ebpf:"target_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfVariableSpecs contains global variables before they are loaded into the kernel.
|
// bpfVariableSpecs contains global variables before they are loaded into the kernel.
|
||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfVariableSpecs struct {
|
type bpfVariableSpecs struct {
|
||||||
UnusedEvent *ebpf.VariableSpec `ebpf:"unused_event"`
|
UnusedEvent *ebpf.VariableSpec `ebpf:"unused_event"`
|
||||||
|
UnusedTarget *ebpf.VariableSpec `ebpf:"unused_target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfObjects contains all objects after they have been loaded into the kernel.
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
@@ -112,12 +125,16 @@ func (o *bpfObjects) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfMaps struct {
|
type bpfMaps struct {
|
||||||
Events *ebpf.Map `ebpf:"events"`
|
Events *ebpf.Map `ebpf:"events"`
|
||||||
|
ExemptMap *ebpf.Map `ebpf:"exempt_map"`
|
||||||
|
TargetMap *ebpf.Map `ebpf:"target_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *bpfMaps) Close() error {
|
func (m *bpfMaps) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
m.Events,
|
m.Events,
|
||||||
|
m.ExemptMap,
|
||||||
|
m.TargetMap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +142,8 @@ func (m *bpfMaps) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfVariables struct {
|
type bpfVariables struct {
|
||||||
UnusedEvent *ebpf.Variable `ebpf:"unused_event"`
|
UnusedEvent *ebpf.Variable `ebpf:"unused_event"`
|
||||||
|
UnusedTarget *ebpf.Variable `ebpf:"unused_target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfPrograms contains all programs after they have been loaded into the kernel.
|
// bpfPrograms contains all programs after they have been loaded into the kernel.
|
||||||
|
|||||||
Binary file not shown.
+83
-10
@@ -5,35 +5,108 @@
|
|||||||
|
|
||||||
char __license[] SEC("license") = "Dual MIT/GPL";
|
char __license[] SEC("license") = "Dual MIT/GPL";
|
||||||
|
|
||||||
|
#define IPPROTO_TCP 6
|
||||||
|
|
||||||
|
// Only HTTPS is a candidate for redirect. Every other destination port is left
|
||||||
|
// alone, otherwise ssh, dns, postgres and everything else on the host would be
|
||||||
|
// steered into a proxy that cannot speak their protocol.
|
||||||
|
#define REDIRECT_DPORT 443
|
||||||
|
|
||||||
|
// What the hook decided. Recorded on every event so the ladder can be checked
|
||||||
|
// against real traffic before any rewrite is switched on.
|
||||||
|
#define ACTION_REDIRECT 0
|
||||||
|
#define ACTION_SKIP_PROTO 1
|
||||||
|
#define ACTION_SKIP_LOOPBACK 2
|
||||||
|
#define ACTION_SKIP_EXEMPT 3
|
||||||
|
#define ACTION_SKIP_DPORT 4
|
||||||
|
#define ACTION_SKIP_NOTARGET 5
|
||||||
|
|
||||||
struct event {
|
struct event {
|
||||||
__u32 pid;
|
__u32 pid;
|
||||||
__u32 uid;
|
__u32 uid;
|
||||||
__u32 daddr;
|
__u32 daddr;
|
||||||
__u16 dport;
|
__u16 dport;
|
||||||
__u8 proto;
|
__u8 proto;
|
||||||
|
__u8 action;
|
||||||
__u8 comm[16]; // command
|
__u8 comm[16]; // command
|
||||||
};
|
};
|
||||||
|
|
||||||
struct event *unused_event __attribute__((unused));
|
struct event *unused_event __attribute__((unused));
|
||||||
|
|
||||||
|
// target is the redirect destination, written from userspace once the proxy is
|
||||||
|
// listening. Keeping it in a map means a proxy restart on a different port is
|
||||||
|
// one map update rather than a reload of the program.
|
||||||
|
struct target {
|
||||||
|
__u32 ip; // network byte order, same layout as ctx->user_ip4
|
||||||
|
__u16 port; // host byte order, converted when written back to ctx
|
||||||
|
};
|
||||||
|
|
||||||
|
struct target *unused_target __attribute__((unused));
|
||||||
|
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_ARRAY);
|
||||||
|
__uint(max_entries, 1);
|
||||||
|
__type(key, __u32);
|
||||||
|
__type(value, struct target);
|
||||||
|
} target_map SEC(".maps");
|
||||||
|
|
||||||
|
// Uids whose traffic is never redirected. The proxy's own upstream fetches live
|
||||||
|
// here. Without this the rewrite sends the proxy back into itself forever.
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_HASH);
|
||||||
|
__uint(max_entries, 8);
|
||||||
|
__type(key, __u32);
|
||||||
|
__type(value, __u8);
|
||||||
|
} exempt_map SEC(".maps");
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||||
__uint(max_entries, 1 << 24);
|
__uint(max_entries, 1 << 24);
|
||||||
} events SEC(".maps");
|
} events SEC(".maps");
|
||||||
|
|
||||||
|
static __always_inline __u8 decide(struct bpf_sock_addr *ctx, __u32 uid, __u16 dport) {
|
||||||
|
if (ctx->protocol != IPPROTO_TCP)
|
||||||
|
return ACTION_SKIP_PROTO;
|
||||||
|
|
||||||
|
// Loopback is never registry traffic, and this also covers clients already
|
||||||
|
// configured to reach the proxy directly.
|
||||||
|
if ((bpf_ntohl(ctx->user_ip4) >> 24) == 127)
|
||||||
|
return ACTION_SKIP_LOOPBACK;
|
||||||
|
|
||||||
|
if (bpf_map_lookup_elem(&exempt_map, &uid))
|
||||||
|
return ACTION_SKIP_EXEMPT;
|
||||||
|
|
||||||
|
if (dport != REDIRECT_DPORT)
|
||||||
|
return ACTION_SKIP_DPORT;
|
||||||
|
|
||||||
|
__u32 key = 0;
|
||||||
|
struct target *t = bpf_map_lookup_elem(&target_map, &key);
|
||||||
|
if (!t || t->port == 0)
|
||||||
|
return ACTION_SKIP_NOTARGET;
|
||||||
|
|
||||||
|
return ACTION_REDIRECT;
|
||||||
|
}
|
||||||
|
|
||||||
SEC("cgroup/connect4")
|
SEC("cgroup/connect4")
|
||||||
int connect4(struct bpf_sock_addr *ctx) {
|
int connect4(struct bpf_sock_addr *ctx) {
|
||||||
|
__u32 uid = bpf_get_current_uid_gid();
|
||||||
|
__u16 dport = bpf_ntohs(ctx->user_port);
|
||||||
|
__u8 action = decide(ctx, uid, dport);
|
||||||
|
|
||||||
|
// A full ring buffer costs an event, never a change in behaviour.
|
||||||
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
|
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
|
||||||
if (!e)
|
if (e) {
|
||||||
return 1;
|
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||||
|
e->uid = uid;
|
||||||
|
e->daddr = ctx->user_ip4;
|
||||||
|
e->dport = dport;
|
||||||
|
e->proto = ctx->protocol;
|
||||||
|
e->action = action;
|
||||||
|
bpf_get_current_comm(&e->comm, sizeof(e->comm));
|
||||||
|
bpf_ringbuf_submit(e, 0);
|
||||||
|
}
|
||||||
|
|
||||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
// Shadow mode. The decision is recorded and never acted on. Turning
|
||||||
e->uid = bpf_get_current_uid_gid();
|
// ACTION_REDIRECT into a rewrite of user_ip4 and user_port is the next step.
|
||||||
e->daddr = ctx->user_ip4;
|
|
||||||
e->dport = bpf_ntohs(ctx->user_port);
|
|
||||||
e-> proto = ctx->protocol;
|
|
||||||
bpf_get_current_comm(&e->comm, sizeof(e->comm));
|
|
||||||
|
|
||||||
bpf_ringbuf_submit(e, 0);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -2,7 +2,6 @@ module ebpf-poc
|
|||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require github.com/cilium/ebpf v0.22.0
|
||||||
github.com/cilium/ebpf v0.22.0 // indirect
|
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
require golang.org/x/sys v0.43.0 // indirect
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY=
|
github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY=
|
||||||
github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4=
|
github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4=
|
||||||
|
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
|
||||||
|
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
|
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
|
||||||
|
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
|
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||||
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
|||||||
+161
-41
@@ -1,4 +1,4 @@
|
|||||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event bpf connect.c -- -I/usr/include/aarch64-linux-gnu
|
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event -type target bpf connect.c -- -I/usr/include/aarch64-linux-gnu
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -7,10 +7,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/cilium/ebpf"
|
"github.com/cilium/ebpf"
|
||||||
@@ -19,46 +22,65 @@ import (
|
|||||||
"github.com/cilium/ebpf/rlimit"
|
"github.com/cilium/ebpf/rlimit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Mirrors the ACTION_* constants in connect.c.
|
||||||
|
var actionNames = map[uint8]string{
|
||||||
|
0: "REDIRECT",
|
||||||
|
1: "skip/proto",
|
||||||
|
2: "skip/loopback",
|
||||||
|
3: "skip/exempt",
|
||||||
|
4: "skip/dport",
|
||||||
|
5: "skip/no-target",
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := rlimit.RemoveMemlock()
|
proxyFlag := flag.String("proxy", "", "redirect target as host:port, e.g. 127.0.0.1:8443")
|
||||||
if err != nil {
|
exemptFlag := flag.String("exempt-uid", "", "comma separated uids that are never redirected")
|
||||||
fmt.Printf("Failed to remove mem lock: %v\n", err)
|
tcpOnlyFlag := flag.Bool("tcp-only", false, "only print TCP events, hiding DNS and route probe noise")
|
||||||
return
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := run(*proxyFlag, *exemptFlag, *tcpOnlyFlag); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "pmgwatch: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(proxyAddr, exemptUIDs string, tcpOnly bool) error {
|
||||||
|
if err := rlimit.RemoveMemlock(); err != nil {
|
||||||
|
return fmt.Errorf("remove memlock: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var objs bpfObjects
|
var objs bpfObjects
|
||||||
err = loadBpfObjects(&objs, nil)
|
if err := loadBpfObjects(&objs, nil); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("load bpf objects: %w", err)
|
||||||
fmt.Printf("Failed to load bpf objects: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer objs.Close()
|
defer objs.Close()
|
||||||
|
|
||||||
rootCgroup := "/sys/fs/cgroup"
|
if err := configureTarget(&objs, proxyAddr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := configureExempt(&objs, exemptUIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
l, err := link.AttachCgroup(link.CgroupOptions{
|
l, err := link.AttachCgroup(link.CgroupOptions{
|
||||||
Path: rootCgroup,
|
Path: "/sys/fs/cgroup",
|
||||||
Attach: ebpf.AttachCGroupInet4Connect,
|
Attach: ebpf.AttachCGroupInet4Connect,
|
||||||
Program: objs.Connect4,
|
Program: objs.Connect4,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to attach cgroup: %v\n", err)
|
return fmt.Errorf("attach cgroup: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
|
|
||||||
rd, err := ringbuf.NewReader(objs.Events)
|
rd, err := ringbuf.NewReader(objs.Events)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to create ringbuf reader: %v\n", err)
|
return fmt.Errorf("open ringbuf: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer rd.Close()
|
defer rd.Close()
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop() // Cleans up resources allocated by the signal package
|
defer stop()
|
||||||
|
|
||||||
fmt.Println("Application started. Press Ctrl+C to exit.")
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -66,41 +88,139 @@ func main() {
|
|||||||
rd.Close()
|
rd.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Shadow mode: decisions are recorded, nothing is redirected. Ctrl+C to exit.")
|
||||||
|
fmt.Printf("%-15s %-17s %-7s %-8s %-22s %s\n", "ACTION", "COMMAND", "UID", "PID", "DESTINATION", "PROTO")
|
||||||
|
|
||||||
|
return drain(rd, tcpOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drain(rd *ringbuf.Reader, tcpOnly bool) error {
|
||||||
var e bpfEvent
|
var e bpfEvent
|
||||||
|
|
||||||
for {
|
for {
|
||||||
rec, err := rd.Read()
|
rec, err := rd.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ringbuf.ErrClosed) {
|
if errors.Is(err, ringbuf.ErrClosed) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf("failed to read raw event: %v\n", err)
|
|
||||||
|
fmt.Fprintf(os.Stderr, "read event: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e)
|
if err := binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e); err != nil {
|
||||||
if err != nil {
|
fmt.Fprintf(os.Stderr, "decode event: %v\n", err)
|
||||||
fmt.Printf("failed to read event: %v\n", err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := make(net.IP, 4)
|
if tcpOnly && e.Proto != syscall.IPPROTO_TCP {
|
||||||
binary.LittleEndian.PutUint32(ip, e.Daddr)
|
continue
|
||||||
|
|
||||||
name := e.Comm[:]
|
|
||||||
if i := bytes.IndexByte(name, 0); i != -1 {
|
|
||||||
name = name[:i] // keep bytes before the first NUL
|
|
||||||
}
|
|
||||||
comm := string(name)
|
|
||||||
|
|
||||||
proto := ""
|
|
||||||
switch e.Proto {
|
|
||||||
case syscall.IPPROTO_TCP:
|
|
||||||
proto = "TCP"
|
|
||||||
case syscall.IPPROTO_UDP:
|
|
||||||
proto = "UDP"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
printEvent(&e)
|
||||||
fmt.Printf("PID: %v\nUID: %v\ndAddr: %v\ndPort: %v\nProto: %v\nCommand: %v\n", e.Pid, e.Uid, ip, e.Dport, proto, comm)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printEvent(e *bpfEvent) {
|
||||||
|
ip := make(net.IP, 4)
|
||||||
|
binary.LittleEndian.PutUint32(ip, e.Daddr)
|
||||||
|
|
||||||
|
action, ok := actionNames[e.Action]
|
||||||
|
if !ok {
|
||||||
|
action = fmt.Sprintf("action/%d", e.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%-15s %-17s %-7d %-8d %-22s %s\n",
|
||||||
|
action, comm(e.Comm[:]), e.Uid, e.Pid,
|
||||||
|
net.JoinHostPort(ip.String(), strconv.Itoa(int(e.Dport))), proto(e.Proto))
|
||||||
|
}
|
||||||
|
|
||||||
|
// comm is a fixed 16 byte buffer, NUL terminated when the name is shorter.
|
||||||
|
func comm(raw []byte) string {
|
||||||
|
if i := bytes.IndexByte(raw, 0); i != -1 {
|
||||||
|
raw = raw[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func proto(p uint8) string {
|
||||||
|
switch p {
|
||||||
|
case syscall.IPPROTO_TCP:
|
||||||
|
return "TCP"
|
||||||
|
case syscall.IPPROTO_UDP:
|
||||||
|
return "UDP"
|
||||||
|
default:
|
||||||
|
return strconv.Itoa(int(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureTarget writes the redirect destination the hook will use. Leaving it
|
||||||
|
// unset is valid: every otherwise eligible connection then reports
|
||||||
|
// skip/no-target, which is how shadow mode runs before a proxy exists.
|
||||||
|
func configureTarget(objs *bpfObjects, addr string) error {
|
||||||
|
if addr == "" {
|
||||||
|
fmt.Println("No redirect target set, eligible connections will report skip/no-target")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
host, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse proxy address %q: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil || ip.To4() == nil {
|
||||||
|
return fmt.Errorf("proxy address %q must be an IPv4 address", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse proxy port %q: %w", portStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The kernel holds the address in network byte order, which is what reading
|
||||||
|
// the four octets little endian reproduces.
|
||||||
|
target := bpfTarget{
|
||||||
|
Ip: binary.LittleEndian.Uint32(ip.To4()),
|
||||||
|
Port: uint16(port),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := objs.TargetMap.Put(uint32(0), target); err != nil {
|
||||||
|
return fmt.Errorf("write redirect target: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Redirect target: %s\n", addr)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureExempt(objs *bpfObjects, list string) error {
|
||||||
|
if list == "" {
|
||||||
|
fmt.Println("No exempt uids set, nothing bypasses the ladder")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var applied []string
|
||||||
|
for _, field := range strings.Split(list, ",") {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if field == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := strconv.ParseUint(field, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse exempt uid %q: %w", field, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := objs.ExemptMap.Put(uint32(uid), uint8(1)); err != nil {
|
||||||
|
return fmt.Errorf("write exempt uid %d: %w", uid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
applied = append(applied, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Exempt uids: %s\n", strings.Join(applied, ", "))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user