Skip to content

Commit

Permalink
Merge branch 'udp_session_tracking'
Browse files Browse the repository at this point in the history
  • Loading branch information
piax93 committed Nov 5, 2020
2 parents 64a99a1 + 45356c8 commit ec973f2
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 106 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ username associated with the process.
- Listening port `port`
- Network protocol `protocol` (e.g. tcp)
- Configurable to also periodically provide snapshots of all listening processes
- Best effort tracking of UDP sessions with configurability and output
similar to the ones of TCP outbound connections.
- Optional plugin system for enriching events in userland
- Included `sourceipmap` plugin for mapping source address
- Included `loginuidmap` plugin for adding loginuid info to process tree
Expand Down Expand Up @@ -77,7 +79,7 @@ Pidtree-bcc implements a modular probe system which allows multiple eBPF program
to be compiled and run in parallel. Probe loading is handled via the top-level keys
in the configuration (see [`example_config.yml`](example_config.yml)).

Currently, this repository implements the `tcp_connect` and `net_listen` probes.
Currently, this repository implements the `tcp_connect`, `net_listen` and `udp_session` probes.
It is possible to extend this system with external packages via the `--extra-probe-path`
command line parameter.

Expand Down
38 changes: 21 additions & 17 deletions example_config.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
---
_net_filters: &net_filters
- subnet_name: 10
network: 10.0.0.0
network_mask: 255.0.0.0
description: "all RFC 1918 10/8"
- subnet_name: 17216
network: 172.16.0.0
network_mask: 255.240.0.0
description: "all RFC 1918 172.16/12"
- subnet_name: 169254
network: 169.254.0.0
network_mask: 255.255.0.0
description: "all 169.254/16 loopback"
- subnet_name: 127
network: 127.0.0.0
network_mask: 255.0.0.0
description: "all 127/8 loopback"

udp_session:
filters: *net_filters
tcp_connect:
filters:
- subnet_name: 10
network: 10.0.0.0
network_mask: 255.0.0.0
description: "all RFC 1918 10/8"
- subnet_name: 17216
network: 172.16.0.0
network_mask: 255.240.0.0
description: "all RFC 1918 172.16/12"
- subnet_name: 169254
network: 169.254.0.0
network_mask: 255.255.0.0
description: "all 169.254/16 loopback"
- subnet_name: 127
network: 127.0.0.0
network_mask: 255.0.0.0
description: "all 127/8 loopback"
filters: *net_filters
plugins:
sourceipmap:
enabled: True
Expand Down
78 changes: 41 additions & 37 deletions itest/example_config.yml
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
---
_net_filters: &net_filters
- subnet_name: 0_0_0_0__2
network: 0.0.0.0
network_mask: 192.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 64_0_0_0__3
network: 64.0.0.0
network_mask: 224.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 96_0_0_0__4
network: 96.0.0.0
network_mask: 240.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 112_0_0_0__5
network: 112.0.0.0
network_mask: 248.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 120_0_0_0__6
network: 120.0.0.0
network_mask: 252.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 124_0_0_0__7
network: 124.0.0.0
network_mask: 254.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 126_0_0_0__8
network: 126.0.0.0
network_mask: 255.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 128_0_0_0__1
network: 128.0.0.0
network_mask: 128.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 127_0_0_0__16
network: 127.0.0.0
network_mask: 255.255.0.0
description: "127.0/16 to get rid of the noise"

tcp_connect:
filters:
- subnet_name: 0_0_0_0__2
network: 0.0.0.0
network_mask: 192.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 64_0_0_0__3
network: 64.0.0.0
network_mask: 224.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 96_0_0_0__4
network: 96.0.0.0
network_mask: 240.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 112_0_0_0__5
network: 112.0.0.0
network_mask: 248.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 120_0_0_0__6
network: 120.0.0.0
network_mask: 252.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 124_0_0_0__7
network: 124.0.0.0
network_mask: 254.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 126_0_0_0__8
network: 126.0.0.0
network_mask: 255.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 128_0_0_0__1
network: 128.0.0.0
network_mask: 128.0.0.0
description: "Non-loopback subnet section"
- subnet_name: 127_0_0_0__16
network: 127.0.0.0
network_mask: 255.255.0.0
description: "127.0/16 to get rid of the noise"
filters: *net_filters
net_listen:
excludeports:
- 31337
udp_session:
filters: *net_filters
11 changes: 11 additions & 0 deletions itest/itest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ TIMEOUT=$(( SPIN_UP_TIME + 5 ))
TEST_CASES=(
"tcp_connect:create_connect_event:nc -w 1 127.1.33.7 $TEST_CONNECT_PORT"
"net_listen:create_listen_event:nc -w $TEST_LISTEN_TIMEOUT -lnp $TEST_LISTEN_PORT"
"udp_session:create_udp_event:nc -w 1 -u 127.1.33.7 $TEST_CONNECT_PORT"
)

function is_port_used {
Expand All @@ -42,6 +43,16 @@ function create_listen_event {
nc -w $TEST_LISTEN_TIMEOUT -lnp $TEST_LISTEN_PORT
}

function create_udp_event {
echo "Creating test UDP listener"
nc -u -w $TEST_LISTEN_TIMEOUT -l -p $TEST_CONNECT_PORT & > /dev/null
listener_pid=$!
sleep 1
echo "Making test UDP connection"
echo "Hello World!" | nc -w 1 -u 127.1.33.7 $TEST_CONNECT_PORT
wait $listener_pid
}

function cleanup {
echo "CLEANUP: Caught EXIT"
set +eE
Expand Down
8 changes: 6 additions & 2 deletions pidtree_bcc/probes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from typing import Mapping

from bcc import BPF
from jinja2 import Template
from jinja2 import Environment
from jinja2 import FileSystemLoader

from pidtree_bcc.plugins import load_plugins
from pidtree_bcc.utils import find_subclass
Expand Down Expand Up @@ -64,7 +65,8 @@ class variable defining a list of config fields.
template_config = {k: template_config[k] for k in self.TEMPLATE_VARS}
else:
template_config.pop('plugins', None)
self.expanded_bpf_text = Template(self.BPF_TEXT).render(**template_config)
jinja_env = Environment(loader=FileSystemLoader(os.path.dirname(module_src)))
self.expanded_bpf_text = jinja_env.from_string(self.BPF_TEXT).render(**template_config)

def _process_events(self, cpu: Any, data: Any, size: Any, from_bpf: bool = True):
""" BPF event callback
Expand All @@ -76,6 +78,8 @@ def _process_events(self, cpu: Any, data: Any, size: Any, from_bpf: bool = True)
"""
event = self.bpf['events'].event(data) if from_bpf else data
event = self.enrich_event(event)
if not event:
return
event['timestamp'] = datetime.utcnow().isoformat() + 'Z'
event['probe'] = self.probe_name
for event_plugin in self.plugins:
Expand Down
16 changes: 2 additions & 14 deletions pidtree_bcc/probes/net_listen.j2
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{%- import 'utils.j2' as utils -%}
#include <net/sock.h>
#include <bcc/proto.h>

Expand All @@ -11,20 +12,7 @@ struct listen_bind_t {
u8 protocol;
};

static u8 get_socket_protocol(struct sock *sk)
{
// I'd love to be the one to have figured this out, I'm not
// https://github.com/iovisor/bcc/blob/v0.16.0/tools/tcpaccept.py#L115
u8 protocol;
int gso_max_segs_offset = offsetof(struct sock, sk_gso_max_segs);
int sk_lingertime_offset = offsetof(struct sock, sk_lingertime);
if (sk_lingertime_offset - gso_max_segs_offset == 4) {
protocol = *(u8 *)((u64)&sk->sk_gso_max_segs - 3);
} else {
protocol = *(u8 *)((u64)&sk->sk_wmem_queued - 3);
}
return protocol;
}
{{ utils.get_proto_func() }}

static void net_listen_event(struct pt_regs *ctx)
{
Expand Down
38 changes: 3 additions & 35 deletions pidtree_bcc/probes/tcp_connect.j2
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
{%- import 'utils.j2' as utils -%}
#include <net/sock.h>
#include <bcc/proto.h>

// IPs and masks are given in integer notation with their dotted notation in the comment
{% for filter in filters %}
// {{ filter.get("description", filter["subnet_name"]) }}
#define subnet_{{ filter["subnet_name"] }} {{ ip_to_int(filter["network"]) }} // {{ filter["network"] }}
#define subnet_{{ filter["subnet_name"] }}_mask {{ ip_to_int(filter["network_mask"]) }} // {{ filter["network_mask"] }}
{% endfor %}
{{ utils.net_filter_masks(filters, ip_to_int) }}

BPF_HASH(currsock, u32, struct sock *);
BPF_PERF_OUTPUT(events);
Expand Down Expand Up @@ -44,35 +40,7 @@ int kretprobe__tcp_v4_connect(struct pt_regs *ctx)
u16 dport = 0;
bpf_probe_read(&daddr, sizeof(daddr), &skp->__sk_common.skc_daddr);
bpf_probe_read(&dport, sizeof(dport), &skp->__sk_common.skc_dport);
//
// For each filter, drop the packet iff
// - a filter's subnet matches AND
// - the port is not one of the filter's excepted ports AND
// - the port is one of the filter's included ports, if they exist
//
if (0 // for easier templating
{% for filter in filters -%}
|| ( (
subnet_{{ filter["subnet_name"] }}
& subnet_{{ filter["subnet_name"] }}_mask
) == (daddr & subnet_{{ filter["subnet_name"] }}_mask)
&& ( 1 == 1 // For easier templating
{% for port in filter.get('except_ports', []) -%}
&& ntohs({{ port }}) != dport
{% endfor -%}
)
&& (
{% if filter.get('include_ports') -%}
0
{% for port in filter.get('include_ports', []) -%}
|| ntohs({{ port }}) == dport
{% endfor -%}
{% else -%}
1
{% endif -%}
)
)
{% endfor %} ) {
{{ utils.net_filter_if_excluded(filters) | indent(4) }} {
currsock.delete(&pid);
return 0;
}
Expand Down
90 changes: 90 additions & 0 deletions pidtree_bcc/probes/udp_session.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{%- import 'utils.j2' as utils -%}
#include <net/sock.h>
#include <bcc/proto.h>

#define SESSION_START 1
#define SESSION_CONTINUE 2
#define SESSION_END 3

{{ utils.net_filter_masks(filters, ip_to_int) }}

struct udp_socket_tuple {
u32 pid;
u64 sock_pointer;
};

struct udp_session_event {
u8 type;
u32 pid;
u64 sock_pointer;
u32 daddr;
u16 dport;
};

BPF_PERF_OUTPUT(events);
BPF_HASH(tracing, struct udp_socket_tuple, u8);

{{ utils.get_proto_func() }}

// We probe only the entrypoint as looking at return codes doesn't have much value
// since UDP does not do any checks for successfull communications. The only errors
// which may arise from this function would be due to the kernel running out of memory,
// and you have bigger problems than precisely tracing UDP connections at that point.
int kprobe__udp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size)
{
if(sk->__sk_common.skc_family != AF_INET) return 0;

// Destination info will either be embedded in the socket if `connect`
// was called or specified in the message
struct sockaddr_in* sin = msg->msg_name;
u32 daddr = sin->sin_addr.s_addr ? sin->sin_addr.s_addr : sk->sk_daddr;
u16 dport = sin->sin_port ? sin->sin_port : sk->sk_dport;

{{ utils.net_filter_if_excluded(filters) | indent(4) }} {
return 0;
}

// Check if we are already tracing this session
u32 pid = bpf_get_current_pid_tgid();
struct udp_socket_tuple sock_tuple = {};
sock_tuple.pid = pid;
sock_tuple.sock_pointer = (u64) sk;
u8 trace_flag = tracing.lookup(&sock_tuple) != 0 ? SESSION_CONTINUE : SESSION_START;

struct udp_session_event session = {};
session.pid = pid;
session.type = trace_flag;
session.sock_pointer = sock_tuple.sock_pointer;
bpf_probe_read(&session.daddr, sizeof(u32), &daddr);
bpf_probe_read(&session.dport, sizeof(u16), &dport);
session.dport = ntohs(session.dport);
events.perf_submit(ctx, &session, sizeof(session));
if(trace_flag == SESSION_START) {
// We don't care about the actual value in the map
// any u8 var would be fine
tracing.update(&sock_tuple, &trace_flag);
}

return 0;
}

// Again, we don't care about the `close` call being successfull, we treat
// the invocation as the end of the session regardless
int kprobe__inet_release(struct pt_regs *ctx, struct socket *sock) {
u8 protocol = get_socket_protocol(sock->sk);
if(protocol != IPPROTO_UDP) return 0;

u32 pid = bpf_get_current_pid_tgid();
struct udp_socket_tuple sock_tuple = {};
sock_tuple.pid = pid;
sock_tuple.sock_pointer = (u64) sock->sk;
if(tracing.lookup(&sock_tuple) != 0) {
struct udp_session_event session = {};
session.pid = pid;
session.type = SESSION_END;
session.sock_pointer = sock_tuple.sock_pointer;
events.perf_submit(ctx, &session, sizeof(session));
tracing.delete(&sock_tuple);
}
return 0;
}
Loading

0 comments on commit ec973f2

Please sign in to comment.