vivarium 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +14 -3
- data/examples/network_client_demo.rb +76 -0
- data/lib/vivarium/logger.rb +2 -2
- data/lib/vivarium/version.rb +1 -1
- data/lib/vivarium.rb +419 -19
- data/sig/vivarium.rbs +11 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fb7901d1f200e910b88b75fd8ba54cbaa2d6cf2152c31e35b6f790e8b9150e5
|
|
4
|
+
data.tar.gz: 045c6ef47acb148605a7dd7cbbda5526d82d3548191135baedecaa5d09840f25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9042d44b860bde781d512e36367bfc178a74dd57c6ae6be49c5cea73ec4fbd8b621382ace5d0e1b18188e36bae41d3f5e7e273fd8e019c1ec88820f95653b255
|
|
7
|
+
data.tar.gz: 9c22285362cea0fe871af6c1f67a66eeb85556a52c5f6aabe93da2b20daa51cbf6528df9021ead0fbb2acdcb152bbee60a1a873c9d7a9b75b1edc8ea244c6016
|
data/README.md
CHANGED
|
@@ -18,10 +18,13 @@ The goal is to visualize which Ruby method context triggered low-level events.
|
|
|
18
18
|
Implemented in this repository:
|
|
19
19
|
|
|
20
20
|
- BPF LSM hook on `file_open`
|
|
21
|
+
- BPF LSM hook on `socket_create` (flags unusual socket creation as `odd_socket`)
|
|
22
|
+
- BPF LSM hook on `socket_connect` (captures destination family/address/port as `sock_connect`)
|
|
23
|
+
- BPF kprobe on `ip_local_out` (captures UDP/53 DNS QNAME raw bytes as `dns_req`)
|
|
21
24
|
- Shared pinned maps on bpffs
|
|
22
25
|
- `config_root_targets` (root PID -> 0/1)
|
|
23
26
|
- `config_spawned_targets` (spawned TID -> 0/1)
|
|
24
|
-
- `event_invoked` (array length
|
|
27
|
+
- `event_invoked` (array length 1024 with `event_t` records)
|
|
25
28
|
- `event_write_pos` (cursor for appending into `event_invoked`)
|
|
26
29
|
- Ruby API `Vivarium.observe do ... end`
|
|
27
30
|
- Registers current PID to `config_root_targets`
|
|
@@ -36,8 +39,8 @@ Implemented in this repository:
|
|
|
36
39
|
```c
|
|
37
40
|
struct event_t {
|
|
38
41
|
u32 pid;
|
|
39
|
-
char event_name[
|
|
40
|
-
char payload[
|
|
42
|
+
char event_name[16];
|
|
43
|
+
char payload[256];
|
|
41
44
|
};
|
|
42
45
|
```
|
|
43
46
|
|
|
@@ -81,6 +84,14 @@ Vivarium.observe do
|
|
|
81
84
|
end
|
|
82
85
|
```
|
|
83
86
|
|
|
87
|
+
3) Network monitoring demo client:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bundle exec ruby examples/network_client_demo.rb
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This demo intentionally triggers `sock_connect`, `dns_req`, and `odd_socket` events.
|
|
94
|
+
|
|
84
95
|
You can also start top-level observation without a block (it keeps observing until process exit):
|
|
85
96
|
|
|
86
97
|
```ruby
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "socket"
|
|
5
|
+
require "vivarium"
|
|
6
|
+
|
|
7
|
+
# Usage:
|
|
8
|
+
# 1) In another shell (root): sudo bundle exec vivariumd
|
|
9
|
+
# 2) Run this script: bundle exec ruby examples/network_client_demo.rb
|
|
10
|
+
|
|
11
|
+
def try_step(title)
|
|
12
|
+
puts "[client] #{title}"
|
|
13
|
+
yield
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
puts "[client] #{title} failed: #{e.class}: #{e.message}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Vivarium.observe do
|
|
19
|
+
# Likely emits sock_connect and dns_req via resolver traffic.
|
|
20
|
+
try_step("system: DNS lookup") do
|
|
21
|
+
system("getent hosts example.com >/dev/null 2>&1 || true")
|
|
22
|
+
system("getent hosts unknown.example.com >/dev/null 2>&1 || true")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Likely emits sock_connect through HTTPS connection attempts.
|
|
26
|
+
try_step("system: curl") do
|
|
27
|
+
system("curl -I https://example.com >/dev/null 2>&1 || true")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# ICMP example (may require CAP_NET_RAW / root depending on environment).
|
|
31
|
+
try_step("system: ping") do
|
|
32
|
+
system("ping -c 1 example.com >/dev/null 2>&1 || true")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Explicit connect path.
|
|
36
|
+
try_step("Ruby TCP connect") do
|
|
37
|
+
sock = TCPSocket.new("example.com", 80)
|
|
38
|
+
sock.close
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Raw DNS query payload, useful for dns_req decode testing.
|
|
42
|
+
try_step("Ruby UDP DNS query") do
|
|
43
|
+
dns_query = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" +
|
|
44
|
+
"\x09udp-query\x07example\x03com\x00" +
|
|
45
|
+
"\x00\x01\x00\x01"
|
|
46
|
+
|
|
47
|
+
udp = UDPSocket.new
|
|
48
|
+
begin
|
|
49
|
+
udp.connect("127.0.0.53", 53)
|
|
50
|
+
rescue StandardError
|
|
51
|
+
udp.connect("8.8.8.8", 53)
|
|
52
|
+
end
|
|
53
|
+
udp.send(dns_query, 0)
|
|
54
|
+
udp.close
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Explicit sendto path for DNS payload visibility.
|
|
58
|
+
try_step("Ruby UDP sendto DNS query") do
|
|
59
|
+
dns_query = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" +
|
|
60
|
+
"\x06sendto\x07example\x03com\x00" +
|
|
61
|
+
"\x00\x01\x00\x01"
|
|
62
|
+
|
|
63
|
+
udp = UDPSocket.new
|
|
64
|
+
udp.send(dns_query, 0, "127.0.0.53", 53)
|
|
65
|
+
udp.close
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Intentionally unusual socket type to trigger odd_socket.
|
|
69
|
+
try_step("Ruby odd socket attempt") do
|
|
70
|
+
af_packet = Socket.const_defined?(:AF_PACKET) ? Socket::AF_PACKET : 17
|
|
71
|
+
raw = Socket.new(af_packet, Socket::SOCK_RAW, 0)
|
|
72
|
+
raw.close
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts "[client] done"
|
data/lib/vivarium/logger.rb
CHANGED
|
@@ -45,7 +45,7 @@ module Vivarium
|
|
|
45
45
|
@io.puts "[vivarium] #{events.size} event(s) at #{tp.defined_class}##{tp.method_id} (#{tp.event})"
|
|
46
46
|
@io.puts " location: #{tp.path}:#{tp.lineno}"
|
|
47
47
|
events.each do |event|
|
|
48
|
-
@io.puts " pid=#{event.pid} #{event.event_name} payload=#{event
|
|
48
|
+
@io.puts " ktime_ns=#{event.ktime_ns} pid=#{event.pid} #{event.event_name} payload=#{Vivarium.render_event_payload(event)}"
|
|
49
49
|
end
|
|
50
50
|
@io.puts " stack:"
|
|
51
51
|
stack.each do |loc|
|
|
@@ -59,7 +59,7 @@ module Vivarium
|
|
|
59
59
|
event: tp.event.to_s,
|
|
60
60
|
path: tp.path,
|
|
61
61
|
lineno: tp.lineno,
|
|
62
|
-
events: events.map { |e| { pid: e.pid, event_name: e.event_name, payload: e
|
|
62
|
+
events: events.map { |e| { ktime_ns: e.ktime_ns, pid: e.pid, event_name: e.event_name, payload: Vivarium.render_event_payload(e) } },
|
|
63
63
|
stack: stack.map { |loc| "#{loc.path}:#{loc.lineno}:in #{loc.base_label}" }
|
|
64
64
|
}
|
|
65
65
|
@io.puts JSON.generate(entry)
|
data/lib/vivarium/version.rb
CHANGED
data/lib/vivarium.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
require "optparse"
|
|
6
6
|
require "pathname"
|
|
7
7
|
require "rbbcc"
|
|
8
|
+
require "socket"
|
|
8
9
|
require_relative "vivarium/version"
|
|
9
10
|
require_relative "vivarium/logger"
|
|
10
11
|
|
|
@@ -20,8 +21,13 @@ module Vivarium
|
|
|
20
21
|
|
|
21
22
|
EVENT_NAME_SIZE = 16
|
|
22
23
|
EVENT_PAYLOAD_SIZE = 256
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
EVENT_TS_SIZE = 8
|
|
25
|
+
EVENT_STRUCT_SIZE = 288
|
|
26
|
+
EVENT_TS_OFFSET = 0
|
|
27
|
+
EVENT_PID_OFFSET = 8
|
|
28
|
+
EVENT_NAME_OFFSET = 12
|
|
29
|
+
EVENT_PAYLOAD_OFFSET = 28
|
|
30
|
+
EVENT_CAPACITY = 1024
|
|
25
31
|
|
|
26
32
|
@bpf_pin_dir = PIN_DIR
|
|
27
33
|
|
|
@@ -33,20 +39,26 @@ module Vivarium
|
|
|
33
39
|
end
|
|
34
40
|
end
|
|
35
41
|
|
|
36
|
-
Event = Struct.new(:pid, :event_name, :payload, keyword_init: true) do
|
|
42
|
+
Event = Struct.new(:ktime_ns, :pid, :event_name, :payload, keyword_init: true) do
|
|
37
43
|
def empty?
|
|
38
|
-
pid.to_i.zero? && event_name.to_s.empty? && payload.to_s.empty?
|
|
44
|
+
ktime_ns.to_i.zero? && pid.to_i.zero? && event_name.to_s.empty? && payload.to_s.empty?
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def self.from_binary(raw)
|
|
42
48
|
bytes = raw.to_s.b
|
|
43
49
|
bytes = bytes.ljust(EVENT_STRUCT_SIZE, "\x00")
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
ktime_ns = bytes[EVENT_TS_OFFSET, EVENT_TS_SIZE].unpack1("Q<")
|
|
52
|
+
pid = bytes[EVENT_PID_OFFSET, 4].unpack1("L<")
|
|
53
|
+
event_name = c_string(bytes[EVENT_NAME_OFFSET, EVENT_NAME_SIZE])
|
|
54
|
+
raw_payload = bytes[EVENT_PAYLOAD_OFFSET, EVENT_PAYLOAD_SIZE]
|
|
55
|
+
payload = if %w[dns_req sock_connect odd_socket].include?(event_name)
|
|
56
|
+
raw_payload
|
|
57
|
+
else
|
|
58
|
+
c_string(raw_payload)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
new(ktime_ns: ktime_ns, pid: pid, event_name: event_name, payload: payload)
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
def self.c_string(bytes)
|
|
@@ -58,6 +70,95 @@ module Vivarium
|
|
|
58
70
|
end
|
|
59
71
|
end
|
|
60
72
|
|
|
73
|
+
def self.decode_dns_qname(raw_payload)
|
|
74
|
+
bytes = raw_payload.to_s.b.bytes
|
|
75
|
+
labels = []
|
|
76
|
+
idx = 0
|
|
77
|
+
|
|
78
|
+
while idx < bytes.length
|
|
79
|
+
length = bytes[idx]
|
|
80
|
+
break if length.nil? || length.zero?
|
|
81
|
+
break if length > 63
|
|
82
|
+
|
|
83
|
+
idx += 1
|
|
84
|
+
break if (idx + length) > bytes.length
|
|
85
|
+
|
|
86
|
+
label = bytes[idx, length].pack("C*")
|
|
87
|
+
labels << label
|
|
88
|
+
idx += length
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
return "" if labels.empty?
|
|
92
|
+
|
|
93
|
+
labels.join(".")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.decode_sock_connect_payload(raw_payload)
|
|
97
|
+
bytes = raw_payload.to_s.b
|
|
98
|
+
return "" if bytes.bytesize < 20
|
|
99
|
+
|
|
100
|
+
family = bytes[0, 2].unpack1("S<")
|
|
101
|
+
port = bytes[2, 2].unpack1("n")
|
|
102
|
+
addr = bytes[4, 16]
|
|
103
|
+
|
|
104
|
+
case family
|
|
105
|
+
when 2 # AF_INET
|
|
106
|
+
ipv4 = addr[0, 4].bytes.join(".")
|
|
107
|
+
"#{ipv4}:#{port} (#{socket_const_name("AF_", family)})"
|
|
108
|
+
when 10 # AF_INET6
|
|
109
|
+
words = addr.unpack("n8")
|
|
110
|
+
ipv6 = words.map { |w| format("%x", w) }.join(":")
|
|
111
|
+
"[#{ipv6}]:#{port} (#{socket_const_name("AF_", family)})"
|
|
112
|
+
else
|
|
113
|
+
"family=#{family}(#{socket_const_name("AF_", family)}) port=#{port}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.decode_odd_socket_payload(raw_payload)
|
|
118
|
+
bytes = raw_payload.to_s.b
|
|
119
|
+
return "" if bytes.bytesize < 6
|
|
120
|
+
|
|
121
|
+
family = bytes[0, 2].unpack1("S<")
|
|
122
|
+
type = bytes[2, 2].unpack1("S<")
|
|
123
|
+
protocol = bytes[4, 2].unpack1("S<")
|
|
124
|
+
family_name = socket_const_name("AF_", family)
|
|
125
|
+
type_name = socket_const_name("SOCK_", type)
|
|
126
|
+
protocol_name = socket_const_name("IPPROTO_", protocol)
|
|
127
|
+
"family=#{family}(#{family_name}) type=#{type}(#{type_name}) protocol=#{protocol}(#{protocol_name})"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.socket_const_name(prefix, value)
|
|
131
|
+
return "UNKNOWN" unless defined?(Socket)
|
|
132
|
+
|
|
133
|
+
key = Socket.constants.find do |name|
|
|
134
|
+
name.to_s.start_with?(prefix) && Socket.const_get(name) == value
|
|
135
|
+
rescue NameError
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
key ? key.to_s : "UNKNOWN"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.decode_bad_socket_payload(raw_payload)
|
|
143
|
+
decode_odd_socket_payload(raw_payload)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.render_event_payload(event)
|
|
147
|
+
case event.event_name
|
|
148
|
+
when "dns_req"
|
|
149
|
+
decoded = decode_dns_qname(event.payload)
|
|
150
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
151
|
+
when "sock_connect"
|
|
152
|
+
decoded = decode_sock_connect_payload(event.payload)
|
|
153
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
154
|
+
when "odd_socket"
|
|
155
|
+
decoded = decode_odd_socket_payload(event.payload)
|
|
156
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
157
|
+
else
|
|
158
|
+
event.payload.inspect
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
61
162
|
class MapStore
|
|
62
163
|
def initialize(pin_dir: Vivarium.bpf_pin_dir)
|
|
63
164
|
@pin_dir = pin_dir
|
|
@@ -132,6 +233,23 @@ module Vivarium
|
|
|
132
233
|
|
|
133
234
|
class Daemon
|
|
134
235
|
BPF_PROGRAM_TEMPLATE = <<~CLANG
|
|
236
|
+
#include <linux/socket.h>
|
|
237
|
+
#include <uapi/linux/in.h>
|
|
238
|
+
#include <uapi/linux/in6.h>
|
|
239
|
+
#include <uapi/linux/ip.h>
|
|
240
|
+
#include <uapi/linux/udp.h>
|
|
241
|
+
|
|
242
|
+
#ifndef SOCK_STREAM
|
|
243
|
+
#define SOCK_STREAM 1
|
|
244
|
+
#endif
|
|
245
|
+
#ifndef SOCK_DGRAM
|
|
246
|
+
#define SOCK_DGRAM 2
|
|
247
|
+
#endif
|
|
248
|
+
|
|
249
|
+
struct net;
|
|
250
|
+
struct sock;
|
|
251
|
+
struct sk_buff;
|
|
252
|
+
|
|
135
253
|
struct path {
|
|
136
254
|
void *mnt;
|
|
137
255
|
void *dentry;
|
|
@@ -141,7 +259,62 @@ module Vivarium
|
|
|
141
259
|
struct path f_path;
|
|
142
260
|
};
|
|
143
261
|
|
|
262
|
+
struct sockaddr_t {
|
|
263
|
+
u16 sa_family;
|
|
264
|
+
unsigned char sa_data[14];
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
struct sockaddr_in_t {
|
|
268
|
+
u16 sin_family;
|
|
269
|
+
u16 sin_port;
|
|
270
|
+
u32 sin_addr;
|
|
271
|
+
unsigned char pad[8];
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
struct sockaddr_in6_t {
|
|
275
|
+
u16 sin6_family;
|
|
276
|
+
u16 sin6_port;
|
|
277
|
+
u32 sin6_flowinfo;
|
|
278
|
+
unsigned char sin6_addr[16];
|
|
279
|
+
u32 sin6_scope_id;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
struct sockaddr_port_t {
|
|
283
|
+
u16 family;
|
|
284
|
+
u16 port;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
struct iovec_t {
|
|
288
|
+
void *iov_base;
|
|
289
|
+
unsigned long iov_len;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
struct user_msghdr_t {
|
|
293
|
+
void *msg_name;
|
|
294
|
+
int msg_namelen;
|
|
295
|
+
struct iovec_t *msg_iov;
|
|
296
|
+
unsigned long msg_iovlen;
|
|
297
|
+
void *msg_control;
|
|
298
|
+
unsigned long msg_controllen;
|
|
299
|
+
unsigned int msg_flags;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
struct mmsghdr_t {
|
|
303
|
+
struct user_msghdr_t msg_hdr;
|
|
304
|
+
unsigned int msg_len;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
struct sk_buff_t {
|
|
308
|
+
unsigned char *head;
|
|
309
|
+
unsigned char *data;
|
|
310
|
+
u32 len;
|
|
311
|
+
u16 mac_header;
|
|
312
|
+
u16 network_header;
|
|
313
|
+
u16 transport_header;
|
|
314
|
+
};
|
|
315
|
+
|
|
144
316
|
struct event_t {
|
|
317
|
+
u64 ktime_ns;
|
|
145
318
|
u32 pid;
|
|
146
319
|
char event_name[16];
|
|
147
320
|
char payload[#{EVENT_PAYLOAD_SIZE}];
|
|
@@ -149,7 +322,8 @@ module Vivarium
|
|
|
149
322
|
|
|
150
323
|
BPF_HASH(config_root_targets, u32, u8, 1024);
|
|
151
324
|
BPF_HASH(config_spawned_targets, u32, u8, 8192);
|
|
152
|
-
|
|
325
|
+
BPF_HASH(dns_connected_tids, u32, u8, 8192);
|
|
326
|
+
BPF_ARRAY(event_invoked, struct event_t, #{EVENT_CAPACITY});
|
|
153
327
|
BPF_ARRAY(event_write_pos, u32, 1);
|
|
154
328
|
|
|
155
329
|
static __always_inline int target_enabled(u32 pid, u32 tid)
|
|
@@ -167,6 +341,61 @@ module Vivarium
|
|
|
167
341
|
return 0;
|
|
168
342
|
}
|
|
169
343
|
|
|
344
|
+
static __always_inline void submit_event(struct event_t *ev)
|
|
345
|
+
{
|
|
346
|
+
u32 zero = 0;
|
|
347
|
+
u32 *write_pos = event_write_pos.lookup(&zero);
|
|
348
|
+
if (!write_pos) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
ev->ktime_ns = bpf_ktime_get_ns();
|
|
353
|
+
|
|
354
|
+
u32 idx = *write_pos % #{EVENT_CAPACITY};
|
|
355
|
+
__sync_fetch_and_add(write_pos, 1);
|
|
356
|
+
event_invoked.update(&idx, ev);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
static __always_inline int is_dns_destination(void *addr)
|
|
360
|
+
{
|
|
361
|
+
u16 family = 0;
|
|
362
|
+
bpf_probe_read_user(&family, sizeof(family), addr);
|
|
363
|
+
|
|
364
|
+
if (family == AF_INET) {
|
|
365
|
+
struct sockaddr_in_t sin = {};
|
|
366
|
+
bpf_probe_read_user(&sin, sizeof(sin), addr);
|
|
367
|
+
return sin.sin_port == __constant_htons(53);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (family == AF_INET6) {
|
|
371
|
+
struct sockaddr_in6_t sin6 = {};
|
|
372
|
+
bpf_probe_read_user(&sin6, sizeof(sin6), addr);
|
|
373
|
+
return sin6.sin6_port == __constant_htons(53);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
static __always_inline void submit_dns_req(u32 pid, unsigned char *payload, unsigned int payload_len)
|
|
380
|
+
{
|
|
381
|
+
unsigned int copy_len = payload_len;
|
|
382
|
+
|
|
383
|
+
if (copy_len <= 12) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
copy_len -= 12;
|
|
388
|
+
if (copy_len > 64) {
|
|
389
|
+
copy_len = 64;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
struct event_t ev = {};
|
|
393
|
+
ev.pid = pid;
|
|
394
|
+
__builtin_memcpy(ev.event_name, "dns_req", 8);
|
|
395
|
+
bpf_probe_read_user(&ev.payload[0], copy_len, payload + 12);
|
|
396
|
+
submit_event(&ev);
|
|
397
|
+
}
|
|
398
|
+
|
|
170
399
|
TRACEPOINT_PROBE(sched, sched_process_fork)
|
|
171
400
|
{
|
|
172
401
|
u32 parent = args->parent_pid;
|
|
@@ -191,6 +420,7 @@ module Vivarium
|
|
|
191
420
|
{
|
|
192
421
|
u32 tid = (u32)bpf_get_current_pid_tgid();
|
|
193
422
|
config_spawned_targets.delete(&tid);
|
|
423
|
+
dns_connected_tids.delete(&tid);
|
|
194
424
|
return 0;
|
|
195
425
|
}
|
|
196
426
|
|
|
@@ -204,14 +434,6 @@ module Vivarium
|
|
|
204
434
|
return 0;
|
|
205
435
|
}
|
|
206
436
|
|
|
207
|
-
u32 zero = 0;
|
|
208
|
-
u32 *write_pos = event_write_pos.lookup(&zero);
|
|
209
|
-
if (!write_pos) {
|
|
210
|
-
return 0;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
u32 idx = *write_pos & 1023;
|
|
214
|
-
__sync_fetch_and_add(write_pos, 1);
|
|
215
437
|
struct event_t ev = {};
|
|
216
438
|
int path_ret;
|
|
217
439
|
ev.pid = pid;
|
|
@@ -226,7 +448,185 @@ module Vivarium
|
|
|
226
448
|
}
|
|
227
449
|
|
|
228
450
|
bpf_trace_printk("vivarium: pid=%d path=%s\\n", pid, ev.payload);
|
|
229
|
-
|
|
451
|
+
submit_event(&ev);
|
|
452
|
+
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
LSM_PROBE(socket_create, int family, int type, int protocol, int kern)
|
|
457
|
+
{
|
|
458
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
459
|
+
u32 pid = pid_tgid >> 32;
|
|
460
|
+
u32 tid = (u32)pid_tgid;
|
|
461
|
+
|
|
462
|
+
if (!target_enabled(pid, tid)) {
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if ((family == AF_INET || family == AF_INET6) && (type == SOCK_STREAM || type == SOCK_DGRAM)) {
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
struct event_t ev = {};
|
|
471
|
+
u16 family16 = family;
|
|
472
|
+
u16 type16 = type;
|
|
473
|
+
u16 proto16 = protocol;
|
|
474
|
+
|
|
475
|
+
ev.pid = pid;
|
|
476
|
+
__builtin_memcpy(ev.event_name, "odd_socket", 11);
|
|
477
|
+
__builtin_memcpy(&ev.payload[0], &family16, sizeof(family16));
|
|
478
|
+
__builtin_memcpy(&ev.payload[2], &type16, sizeof(type16));
|
|
479
|
+
__builtin_memcpy(&ev.payload[4], &proto16, sizeof(proto16));
|
|
480
|
+
submit_event(&ev);
|
|
481
|
+
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
LSM_PROBE(socket_connect, struct socket *sock, struct sockaddr *address, int addrlen)
|
|
486
|
+
{
|
|
487
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
488
|
+
u32 pid = pid_tgid >> 32;
|
|
489
|
+
u32 tid = (u32)pid_tgid;
|
|
490
|
+
u16 family = 0;
|
|
491
|
+
u8 one = 1;
|
|
492
|
+
|
|
493
|
+
if (!target_enabled(pid, tid)) {
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!address) {
|
|
498
|
+
return 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
bpf_probe_read_kernel(&family, sizeof(family), address);
|
|
502
|
+
|
|
503
|
+
struct event_t ev = {};
|
|
504
|
+
ev.pid = pid;
|
|
505
|
+
__builtin_memcpy(ev.event_name, "sock_connect", 13);
|
|
506
|
+
__builtin_memcpy(&ev.payload[0], &family, sizeof(family));
|
|
507
|
+
|
|
508
|
+
if (family == AF_INET) {
|
|
509
|
+
struct sockaddr_in_t sin = {};
|
|
510
|
+
bpf_probe_read_kernel(&sin, sizeof(sin), address);
|
|
511
|
+
__builtin_memcpy(&ev.payload[2], &sin.sin_port, sizeof(sin.sin_port));
|
|
512
|
+
__builtin_memcpy(&ev.payload[4], &sin.sin_addr, sizeof(sin.sin_addr));
|
|
513
|
+
if (sin.sin_port == __constant_htons(53)) {
|
|
514
|
+
dns_connected_tids.update(&tid, &one);
|
|
515
|
+
}
|
|
516
|
+
} else if (family == AF_INET6) {
|
|
517
|
+
struct sockaddr_in6_t sin6 = {};
|
|
518
|
+
bpf_probe_read_kernel(&sin6, sizeof(sin6), address);
|
|
519
|
+
__builtin_memcpy(&ev.payload[2], &sin6.sin6_port, sizeof(sin6.sin6_port));
|
|
520
|
+
__builtin_memcpy(&ev.payload[4], &sin6.sin6_addr, sizeof(sin6.sin6_addr));
|
|
521
|
+
if (sin6.sin6_port == __constant_htons(53)) {
|
|
522
|
+
dns_connected_tids.update(&tid, &one);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
submit_event(&ev);
|
|
527
|
+
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
TRACEPOINT_PROBE(syscalls, sys_enter_sendmsg)
|
|
532
|
+
{
|
|
533
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
534
|
+
u32 pid = pid_tgid >> 32;
|
|
535
|
+
u32 tid = (u32)pid_tgid;
|
|
536
|
+
struct user_msghdr_t msg = {};
|
|
537
|
+
struct iovec_t iov = {};
|
|
538
|
+
|
|
539
|
+
if (!target_enabled(pid, tid)) {
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (!args->msg) {
|
|
544
|
+
return 0;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
bpf_probe_read_user(&msg, sizeof(msg), args->msg);
|
|
548
|
+
if (!msg.msg_iov || msg.msg_iovlen == 0) {
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (msg.msg_name && !is_dns_destination(msg.msg_name)) {
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
bpf_probe_read_user(&iov, sizeof(iov), msg.msg_iov);
|
|
557
|
+
if (!iov.iov_base) {
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
submit_dns_req(pid, (unsigned char *)iov.iov_base, (unsigned int)iov.iov_len);
|
|
562
|
+
|
|
563
|
+
return 0;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
TRACEPOINT_PROBE(syscalls, sys_enter_sendto)
|
|
567
|
+
{
|
|
568
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
569
|
+
u32 pid = pid_tgid >> 32;
|
|
570
|
+
u32 tid = (u32)pid_tgid;
|
|
571
|
+
unsigned char *buff = args->buff;
|
|
572
|
+
int dns_match = 0;
|
|
573
|
+
|
|
574
|
+
if (!target_enabled(pid, tid)) {
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!buff) {
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (args->addr) {
|
|
583
|
+
dns_match = is_dns_destination(args->addr);
|
|
584
|
+
} else {
|
|
585
|
+
u8 *connected = dns_connected_tids.lookup(&tid);
|
|
586
|
+
dns_match = connected && *connected == 1;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!dns_match) {
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
submit_dns_req(pid, buff, args->len);
|
|
594
|
+
dns_connected_tids.delete(&tid);
|
|
595
|
+
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
TRACEPOINT_PROBE(syscalls, sys_enter_sendmmsg)
|
|
600
|
+
{
|
|
601
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
602
|
+
u32 pid = pid_tgid >> 32;
|
|
603
|
+
u32 tid = (u32)pid_tgid;
|
|
604
|
+
struct mmsghdr_t mmsg = {};
|
|
605
|
+
struct iovec_t iov = {};
|
|
606
|
+
|
|
607
|
+
if (!target_enabled(pid, tid)) {
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (!args->mmsg) {
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
bpf_probe_read_user(&mmsg, sizeof(mmsg), args->mmsg);
|
|
616
|
+
if (mmsg.msg_hdr.msg_name && !is_dns_destination(mmsg.msg_hdr.msg_name)) {
|
|
617
|
+
return 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!mmsg.msg_hdr.msg_iov || mmsg.msg_hdr.msg_iovlen == 0) {
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
bpf_probe_read_user(&iov, sizeof(iov), mmsg.msg_hdr.msg_iov);
|
|
625
|
+
if (!iov.iov_base) {
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
submit_dns_req(pid, (unsigned char *)iov.iov_base, (unsigned int)iov.iov_len);
|
|
230
630
|
|
|
231
631
|
return 0;
|
|
232
632
|
}
|
data/sig/vivarium.rbs
CHANGED
|
@@ -5,6 +5,7 @@ module Vivarium
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
class Event < ::Struct
|
|
8
|
+
attr_accessor ktime_ns: Integer?
|
|
8
9
|
attr_accessor pid: Integer?
|
|
9
10
|
attr_accessor event_name: String?
|
|
10
11
|
attr_accessor payload: String?
|
|
@@ -39,11 +40,21 @@ module Vivarium
|
|
|
39
40
|
|
|
40
41
|
EVENT_NAME_SIZE: Integer
|
|
41
42
|
EVENT_PAYLOAD_SIZE: Integer
|
|
43
|
+
EVENT_TS_SIZE: Integer
|
|
42
44
|
EVENT_STRUCT_SIZE: Integer
|
|
45
|
+
EVENT_TS_OFFSET: Integer
|
|
46
|
+
EVENT_PID_OFFSET: Integer
|
|
47
|
+
EVENT_NAME_OFFSET: Integer
|
|
48
|
+
EVENT_PAYLOAD_OFFSET: Integer
|
|
43
49
|
EVENT_CAPACITY: Integer
|
|
44
50
|
|
|
45
51
|
def self.bpf_pin_dir: () -> String
|
|
46
52
|
def self.bpf_pin_dir=: (String dir) -> String
|
|
53
|
+
def self.decode_dns_qname: (String raw_payload) -> String
|
|
54
|
+
def self.decode_sock_connect_payload: (String raw_payload) -> String
|
|
55
|
+
def self.decode_odd_socket_payload: (String raw_payload) -> String
|
|
56
|
+
def self.decode_bad_socket_payload: (String raw_payload) -> String
|
|
57
|
+
def self.render_event_payload: (Event event) -> String
|
|
47
58
|
def self.observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) { () -> untyped } -> untyped
|
|
48
59
|
def self.top_observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) -> ObservationSession
|
|
49
60
|
def self.filter_internal_frames?: () -> bool
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vivarium
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Uchio Kondo
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.11.
|
|
18
|
+
version: 0.11.4
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.11.
|
|
25
|
+
version: 0.11.4
|
|
26
26
|
description: Vivarium visualizes low-level events such as file open paths and relates
|
|
27
27
|
them to Ruby method boundaries by combining RbBCC (eBPF LSM) and TracePoint.
|
|
28
28
|
email:
|
|
@@ -34,6 +34,7 @@ extra_rdoc_files: []
|
|
|
34
34
|
files:
|
|
35
35
|
- README.md
|
|
36
36
|
- Rakefile
|
|
37
|
+
- examples/network_client_demo.rb
|
|
37
38
|
- exe/vivariumd
|
|
38
39
|
- lib/vivarium.rb
|
|
39
40
|
- lib/vivarium/logger.rb
|