rbbcc 0.10.0 → 0.11.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6550ee287fe7fb0e1e51112ed55345e2f7b314e9d581b8f28a166eec6f4542aa
4
- data.tar.gz: 9c826257aa21ad3ba3862eb7324683db36f9081d02b45b61a5c189ecb24f8836
3
+ metadata.gz: 0cafb82823139848b9c16a2182ef4ba3c86c391126700ee6cbb31ab0a427da85
4
+ data.tar.gz: 711be4b7c0bf09879445d02b7bf0379b9c51dc186f56129227c416a0515e00ff
5
5
  SHA512:
6
- metadata.gz: 50cb035a4d0db91343599437ef2183b9a60eb138ee156ee406f2dd505e9725f8289f7ebdc6c6e04a07264d3eb6095a7a1c8b09a6b509840e6af03b767c38179d
7
- data.tar.gz: bc64019b6a2aa9d3be9c255e9f49c15f6313984e504b910306eff4ad7c410d478b5b106d24dd6ae489848c23e7ae274537d8b38fd59773e0a2f14518e601610f
6
+ metadata.gz: 007c4273121411d6548b071fe48fda59c36af94a36c7de183a8fbacceba722c837115af8a2892ec79277957217b5152b58ec12ce4dd56c153293a2ac6fae9894
7
+ data.tar.gz: 94da55a1738a534d0bffc95ac2ebabcb431e5ea98785506a0006e1d249b83d602f8bbcaec47890124afbf42a9a11f394f986b9795b194db0acb101a8d536ca26
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbbcc (0.10.0)
4
+ rbbcc (0.11.0)
5
5
  fiddle
6
6
 
7
7
  GEM
@@ -23,6 +23,7 @@ GEM
23
23
  PLATFORMS
24
24
  aarch64-linux
25
25
  arm64-darwin-21
26
+ arm64-darwin-24
26
27
  arm64-darwin-25
27
28
 
28
29
  DEPENDENCIES
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # dns_blocker.rb Block DNS queries for a specified domain using TC/eBPF.
4
+ #
5
+ # Uses TC clsact qdisc and attaches a SCHED_CLS BPF program to the
6
+ # egress path. Because pyroute2 is unavailable in Ruby, the BPF
7
+ # program is pinned to /sys/fs/bpf and attached via the `tc` shell
8
+ # command.
9
+ #
10
+ # Usage (must be run as root):
11
+ # ruby dns_blocker.rb -i eth0 -d ruby-lang.org
12
+ #
13
+
14
+ require 'rbbcc'
15
+ require 'optparse'
16
+
17
+ include RbBCC
18
+
19
+ def domain_to_payload_check_code(domain)
20
+ # Convert a domain name to DNS wire format (length-prefixed labels)
21
+ # For example, "example.com" becomes "\\x07example\\x03com\\x00"
22
+ dns_expression = domain.split('.').map { |label| "#{label.length.chr}#{label}" }.join + "\x00"
23
+ c_check_code = dns_expression.chars.map.with_index { |c, i| "payload[offset+#{i}] == #{c.ord}" }.join(" &&\n ")
24
+ c_check_code
25
+ end
26
+
27
+ BPF_TEXT = ->(domain) {
28
+ <<~CLANG
29
+ // Some Hack :(
30
+ #define BPF_LOAD_ACQ -1
31
+ #define BPF_STORE_REL -2
32
+
33
+ #include <uapi/linux/bpf.h>
34
+ #include <uapi/linux/pkt_cls.h>
35
+ #include <linux/if_ether.h>
36
+ #include <linux/ip.h>
37
+ #include <linux/udp.h>
38
+
39
+ int block_dns(struct __sk_buff *skb) {
40
+ void *data = (void *)(long)skb->data;
41
+ void *data_end = (void *)(long)skb->data_end;
42
+
43
+ // Ethernet header check
44
+ struct ethhdr *eth = data;
45
+ if ((void *)(eth + 1) > data_end) return TC_ACT_OK;
46
+ if (eth->h_proto != bpf_htons(ETH_P_IP)) return TC_ACT_OK;
47
+
48
+ // IP header check
49
+ struct iphdr *ip = (void *)(eth + 1);
50
+ if ((void *)(ip + 1) > data_end) return TC_ACT_OK;
51
+ if (ip->protocol != IPPROTO_UDP) return TC_ACT_OK;
52
+
53
+ // UDP header check
54
+ struct udphdr *udp = (void *)ip + (ip->ihl * 4);
55
+ if ((void *)(udp + 1) > data_end) return TC_ACT_OK;
56
+
57
+ // Only care about port 53 (DNS) egress queries
58
+ if (udp->dest != bpf_htons(53)) return TC_ACT_OK;
59
+
60
+ // DNS payload boundary check: DNS header (12 bytes) + "example.com" wire format (13 bytes)
61
+ unsigned char *payload = (unsigned char *)(udp + 1);
62
+ if ((void *)(payload + 12 + 13) > data_end) return TC_ACT_OK;
63
+
64
+ // "example.com" in DNS wire format: \\x07example\\x03com\\x00
65
+ int offset = 12;
66
+ if (#{domain_to_payload_check_code(domain)}) {
67
+ bpf_trace_printk("Blocked DNS query for #{domain}\\n");
68
+ return TC_ACT_SHOT;
69
+ }
70
+
71
+ return TC_ACT_OK;
72
+ }
73
+ CLANG
74
+ }
75
+
76
+ PIN_PATH = "/sys/fs/bpf/dns_blocker_prog"
77
+
78
+ def setup_tc(interface)
79
+ # Run idempotently
80
+ system("sudo tc qdisc add dev #{interface} clsact 2>/dev/null")
81
+ end
82
+
83
+ def attach_tc(interface)
84
+ system(
85
+ "sudo tc filter add dev #{interface} egress" +
86
+ " bpf pinned #{PIN_PATH} da",
87
+ exception: true
88
+ )
89
+ end
90
+
91
+ def cleanup_tc(interface)
92
+ # Run idempotently
93
+ system("sudo tc qdisc del dev #{interface} clsact 2>/dev/null")
94
+ File.unlink(PIN_PATH) if File.exist?(PIN_PATH)
95
+ end
96
+
97
+ options = {domain: "example.com"}
98
+ OptionParser.new { |opts|
99
+ opts.banner = "Usage: #{$0} -i INTERFACE"
100
+ opts.on("-i", "--interface IFACE", "Network interface to monitor (e.g. eth0)") do |v|
101
+ options[:interface] = v
102
+ end
103
+ opts.on("-d", "--domain DOMAIN", "Domain to block (default: example.com)") do |v|
104
+ options[:domain] = v
105
+ end
106
+ }.parse!
107
+
108
+ iface = options[:interface] || abort("Error: Interface name is required")
109
+
110
+ # Clean up any leftover state from a previous run
111
+ cleanup_tc(iface)
112
+
113
+ puts "[*] Compiling BPF program..."
114
+ b = BCC.new(text: BPF_TEXT.call(options[:domain]))
115
+ fn = b.load_func("block_dns", BPF::SCHED_CLS)
116
+
117
+ # Pin the loaded BPF program so that `tc` can reference it by path
118
+ puts "[*] Pinning BPF program to #{PIN_PATH} ..."
119
+ BCC.pin!(fn, PIN_PATH)
120
+
121
+ # Set up clsact qdisc and attach the pinned program to egress
122
+ puts "[*] Attaching TC filter to #{iface} (egress) ..."
123
+ setup_tc(iface)
124
+ attach_tc(iface)
125
+
126
+ puts "[*] Blocking DNS queries for #{options[:domain]} on #{iface}. Press Ctrl+C to stop."
127
+ begin
128
+ b.trace_print
129
+ rescue Interrupt
130
+ puts "\n[*] Shutting down..."
131
+ ensure
132
+ cleanup_tc(iface)
133
+ puts "[*] Cleanup done."
134
+ end
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # lsm_sockblock.rb Monitor/block AF_ALG socket_create via BPF LSM.
4
+ #
5
+ # This example uses LSM_PROBE(socket_create) and a BPF_ARRAY map for mode:
6
+ # 0 = preview (log only)
7
+ # 1 = block (return -EPERM)
8
+ #
9
+ # The config map is pinned to bpffs so mode can be changed externally.
10
+ #
11
+ # Usage:
12
+ # sudo ruby examples/lsm_sockblock.rb
13
+ # sudo ruby examples/lsm_sockblock.rb --mode block
14
+ # sudo ruby examples/lsm_sockblock.rb --pin-path /sys/fs/bpf/my_config_map
15
+
16
+ require 'optparse'
17
+ require 'socket'
18
+ require 'rbbcc'
19
+
20
+ include RbBCC
21
+
22
+ PROGRAM = <<~CLANG
23
+ #include <linux/lsm_hooks.h>
24
+ #include <linux/socket.h>
25
+ #include <uapi/asm-generic/errno-base.h>
26
+
27
+ struct data_t {
28
+ u32 pid;
29
+ int family;
30
+ int type;
31
+ int is_warning;
32
+ int is_blocked;
33
+ char comm[16];
34
+ };
35
+
36
+ BPF_PERF_OUTPUT(events);
37
+ BPF_ARRAY(config_map, u32, 1);
38
+
39
+ LSM_PROBE(socket_create, int family, int type, int protocol, int kern)
40
+ {
41
+ u32 pid = bpf_get_current_pid_tgid() >> 32;
42
+ struct data_t data = {};
43
+
44
+ u32 key = 0;
45
+ u32 *mode = config_map.lookup(&key);
46
+ int is_block_mode = (mode && *mode == 1);
47
+
48
+ data.pid = pid;
49
+ data.family = family;
50
+ data.type = type;
51
+ bpf_get_current_comm(&data.comm, sizeof(data.comm));
52
+
53
+ if (family == AF_ALG) {
54
+ data.is_blocked = is_block_mode;
55
+ data.is_warning = 1;
56
+ events.perf_submit(ctx, &data, sizeof(data));
57
+
58
+ if (is_block_mode) {
59
+ return -EPERM;
60
+ }
61
+ } else {
62
+ data.is_blocked = 0;
63
+ data.is_warning = 0;
64
+ events.perf_submit(ctx, &data, sizeof(data));
65
+ }
66
+
67
+ return 0;
68
+ }
69
+ CLANG
70
+
71
+ options = {
72
+ mode: "preview",
73
+ pin_path: "/sys/fs/bpf/rbbcc_lsm_config_map"
74
+ }
75
+
76
+ OptionParser.new do |opts|
77
+ opts.banner = "Usage: #{$0} [--mode preview|block] [--pin-path PATH]"
78
+
79
+ opts.on("--mode MODE", ["preview", "block"], "Operation mode (default: preview)") do |v|
80
+ options[:mode] = v
81
+ end
82
+
83
+ opts.on("--pin-path PATH", "bpffs pin path (default: /sys/fs/bpf/rbbcc_lsm_config_map)") do |v|
84
+ options[:pin_path] = v
85
+ end
86
+ end.parse!
87
+
88
+ mode_value = (options[:mode] == "block") ? 1 : 0
89
+
90
+ families = Socket.constants.grep(/^AF_/).each_with_object({}) do |name, h|
91
+ begin
92
+ v = Socket.const_get(name)
93
+ h[v] = name.to_s if v.is_a?(Integer)
94
+ rescue NameError
95
+ next
96
+ end
97
+ end
98
+
99
+ types = Socket.constants.grep(/^SOCK_/).each_with_object({}) do |name, h|
100
+ begin
101
+ v = Socket.const_get(name)
102
+ h[v] = name.to_s if v.is_a?(Integer)
103
+ rescue NameError
104
+ next
105
+ end
106
+ end
107
+
108
+ begin
109
+ b = BCC.new(text: PROGRAM)
110
+
111
+ config = b["config_map"]
112
+ config[0] = mode_value
113
+
114
+ File.unlink(options[:pin_path]) if File.exist?(options[:pin_path])
115
+ BCC.pin!(config.map_fd, options[:pin_path])
116
+
117
+ puts "LSM BPF started in #{options[:mode].upcase} mode."
118
+ puts "Pinned config map: #{options[:pin_path]}"
119
+ puts "Tracing AF_ALG socket_create... Press Ctrl-C to exit."
120
+
121
+ b["events"].open_perf_buffer do |cpu, data, size|
122
+ event = b["events"].event(data)
123
+ family = families.fetch(event.family, "AF_UNKNOWN(#{event.family})")
124
+ stype = types.fetch(event.type, "SOCK_UNKNOWN(#{event.type})")
125
+
126
+ puts "PID: #{event.pid.to_s.ljust(7)} | COMM: #{event.comm.to_s.ljust(15)} | FAMILY: #{family.ljust(14)} | TYPE: #{stype}"
127
+
128
+ next if event.is_warning == 0
129
+
130
+ mode_str = (event.is_blocked == 1) ? "BLOCK" : "PREVIEW"
131
+ status = (event.is_blocked == 1) ? "REJECTED" : "WARNING"
132
+ puts "\e[1;31m[#{mode_str}] #{status}: PID #{event.pid} (#{event.comm}) tried AF_ALG socket creation.\e[0m"
133
+ end
134
+
135
+ loop do
136
+ b.perf_buffer_poll
137
+ end
138
+ rescue Interrupt
139
+ puts "\nStopping..."
140
+ ensure
141
+ File.unlink(options[:pin_path]) if File.exist?(options[:pin_path])
142
+ end
@@ -0,0 +1,119 @@
1
+ from bcc import BPF, lib
2
+ import socket
3
+ import ctypes
4
+ import os
5
+
6
+ # 1. C言語側のプログラム
7
+ program = r"""
8
+ #include <linux/lsm_hooks.h>
9
+ #include <linux/socket.h>
10
+ #include <uapi/asm-generic/errno-base.h>
11
+
12
+ struct data_t {
13
+ u32 pid;
14
+ int family;
15
+ int type;
16
+ int is_warning;
17
+ int is_blocked;
18
+ char comm[16];
19
+ };
20
+
21
+ BPF_PERF_OUTPUT(events);
22
+
23
+ // モード保存用マップ (Index 0 を使用)
24
+ // 1: blockモード, 0: previewモード
25
+ BPF_ARRAY(config_map, u32, 1);
26
+
27
+ LSM_PROBE(socket_create, int family, int type, int protocol, int kern)
28
+ {
29
+ u32 pid = bpf_get_current_pid_tgid() >> 32;
30
+ struct data_t data = {};
31
+
32
+ // マップから現在のモードを取得
33
+ u32 key = 0;
34
+ u32 *mode = config_map.lookup(&key);
35
+ int is_block_mode = (mode && *mode == 1);
36
+
37
+ data.pid = pid;
38
+ data.family = family;
39
+ data.type = type;
40
+ bpf_get_current_comm(&data.comm, sizeof(data.comm));
41
+
42
+ if (family == AF_ALG) {
43
+ data.is_blocked = is_block_mode;
44
+ data.is_warning = 1;
45
+
46
+ // ログデータを送信
47
+ events.perf_submit(ctx, &data, sizeof(data));
48
+
49
+ // blockモードならエラーを返してシステムコールを失敗させる
50
+ if (is_block_mode) {
51
+ return -EPERM; // -1 (Operation not permitted)
52
+ }
53
+ } else {
54
+ data.is_blocked = 0;
55
+ data.is_warning = 0;
56
+
57
+ // ログデータを送信
58
+ events.perf_submit(ctx, &data, sizeof(data));
59
+ }
60
+
61
+ return 0;
62
+ }
63
+ """
64
+
65
+ # 定数解決用
66
+ families = {getattr(socket, n): n for n in dir(socket) if n.startswith('AF_')}
67
+ types = {getattr(socket, n): n for n in dir(socket) if n.startswith('SOCK_')}
68
+
69
+ def print_event(cpu, data, size):
70
+ event = b["events"].event(data)
71
+
72
+ family_str = families.get(event.family, f"AF_UNKNOWN({event.family})")
73
+ type_str = types.get(event.type, f"SOCK_UNKNOWN({event.type})")
74
+
75
+ print(f"PID: {event.pid:<7} | COMM: {event.comm.decode('utf-8'):<15} | "
76
+ f"FAMILY: {family_str:<12} | TYPE: {type_str}")
77
+
78
+ if event.is_warning:
79
+ mode_str = "BLOCK" if event.is_blocked else "PREVIEW"
80
+ status = "!! REJECTED !!" if event.is_blocked else "WARNING"
81
+
82
+ print(f"[{mode_str}] {status}: PID {event.pid} ({event.comm.decode()}) "
83
+ f"tried to create AF_ALG socket.")
84
+
85
+ # 2. ロードと設定
86
+ try:
87
+ b = BPF(text=program)
88
+
89
+ # --- モード設定 ---
90
+ # 1 を書き込むと blockモード、0 だと previewモード
91
+ mode = 0
92
+ config_table = b.get_table("config_map")
93
+ config_table[ctypes.c_uint32(0)] = ctypes.c_uint32(mode)
94
+
95
+ # Mapの永続化 - ユーザ空間からモードを変更できるようにするため
96
+ map_path = "/sys/fs/bpf/my_config_map"
97
+ if os.path.exists(map_path):
98
+ os.remove(map_path)
99
+ print(f"Pinning config map to -> {map_path}")
100
+ map_fd = config_table.get_fd()
101
+ res = lib.bpf_obj_pin(map_fd, ctypes.c_char_p(map_path.encode()))
102
+ if res != 0:
103
+ raise Exception(f"Failed to pin map to {map_path}: {os.strerror(-res)}")
104
+
105
+ # -----------------
106
+
107
+ print(f"LSM BPF started in {'BLOCK' if mode == 1 else 'PREVIEW'} mode.")
108
+ print("Tracing AF_ALG creation... Press Ctrl-C to exit.")
109
+
110
+ b["events"].open_perf_buffer(print_event)
111
+
112
+ while True:
113
+ try:
114
+ b.perf_buffer_poll()
115
+ except KeyboardInterrupt:
116
+ exit()
117
+
118
+ except Exception as e:
119
+ print(f"Failed: {e}")
data/lib/rbbcc/bcc.rb CHANGED
@@ -180,7 +180,7 @@ module RbBCC
180
180
  end
181
181
  orig_name = c.inspect
182
182
  c.define_singleton_method :inspect do
183
- orig_name.sub /(?=>$)/, " original_desc=#{desc.inspect}" rescue super
183
+ orig_name.sub(/(?=>$)/, " original_desc=#{desc.inspect}") rescue super
184
184
  end
185
185
  c
186
186
  end
@@ -218,6 +218,23 @@ module RbBCC
218
218
  fn[:sock] = sock
219
219
  fn
220
220
  end
221
+
222
+ #: (Integer | Hash[Symbol, untyped] fd, String path) -> String
223
+ def pin!(fd, path)
224
+ fd = fd[:fd] if fd.is_a?(Hash)
225
+ unless fd.is_a?(Integer) && fd >= 0
226
+ raise ArgumentError, "fd must exist and be a non-negative Integer"
227
+ end
228
+ unless path.is_a?(String) && !path.empty?
229
+ raise ArgumentError, "path must be a non-empty String"
230
+ end
231
+
232
+ res = Clib.bpf_obj_pin(fd, path)
233
+ if res < 0
234
+ raise SystemCallError.new("Failed to pin BPF object to %s" % path, Fiddle.last_error)
235
+ end
236
+ path
237
+ end
221
238
  end
222
239
 
223
240
  def initialize(text: "", src_file: nil, hdr_file: nil, debug: 0, cflags: [], usdt_contexts: [], allow_rlimit: 0, dev_name: nil)
@@ -279,7 +296,6 @@ module RbBCC
279
296
 
280
297
  def gen_args_from_usdt
281
298
  ptr = Clib.bcc_usdt_genargs(@usdt_contexts.map(&:context).pack('J*'), @usdt_contexts.size)
282
- code = ""
283
299
  if !ptr || ptr.null?
284
300
  return nil
285
301
  end
@@ -627,6 +643,9 @@ module RbBCC
627
643
  tp: tp,
628
644
  fn_name: fn[:name]
629
645
  )
646
+ elsif func_name.start_with?("lsm__")
647
+ # LSM_PROBE programs are attached by libbcc while loading.
648
+ load_func(func_name, BPF::LSM)
630
649
  end
631
650
  end
632
651
  end
data/lib/rbbcc/clib.rb CHANGED
@@ -192,6 +192,7 @@ module RbBCC
192
192
 
193
193
  extern 'int bpf_open_raw_sock(const char *name)'
194
194
  extern 'int bpf_attach_socket(int sockfd, int progfd)'
195
+ extern 'int bpf_obj_pin(int fd, const char *pathname)'
195
196
  end
196
197
  end
197
198
 
data/lib/rbbcc/consts.rb CHANGED
@@ -19,5 +19,8 @@ module RbBCC
19
19
  SK_MSG = 16
20
20
  RAW_TRACEPOINT = 17
21
21
  CGROUP_SOCK_ADDR = 18
22
+ CGROUP_SOCKOPT = 25
23
+ TRACING = 26
24
+ LSM = 29
22
25
  end
23
26
  end
data/lib/rbbcc/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RbBCC
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbbcc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -84,6 +84,7 @@ files:
84
84
  - examples/collectsyscall.rb
85
85
  - examples/dddos.rb
86
86
  - examples/disksnoop.rb
87
+ - examples/dns_blocker.rb
87
88
  - examples/example.gif
88
89
  - examples/extract_arg.rb
89
90
  - examples/hello_fields.rb
@@ -91,9 +92,11 @@ files:
91
92
  - examples/hello_ring_buffer.rb
92
93
  - examples/hello_world.rb
93
94
  - examples/kvm_hypercall.rb
95
+ - examples/lsm_sockblock.rb
94
96
  - examples/mallocstack.rb
95
97
  - examples/networking/http_filter/http-parse-simple.c
96
98
  - examples/networking/http_filter/http-parse-simple.rb
99
+ - examples/py-orig/sockblock.py
97
100
  - examples/ruby_usdt.rb
98
101
  - examples/sbrk_trace.rb
99
102
  - examples/syscalluname.rb