Module: Vivarium

Defined in:
lib/vivarium.rb,
lib/vivarium/logger.rb,
lib/vivarium/version.rb

Defined Under Namespace

Classes: Daemon, Error, Event, Logger, MapStore, ObservationSession

Constant Summary collapse

PIN_DIR =
ENV.fetch("VIVARIUM_BPF_PIN_DIR", "/sys/fs/bpf/vivarium")
CONFIG_ROOT_TARGETS_PIN =
File.join(PIN_DIR, "config_root_targets")
CONFIG_SPAWNED_TARGETS_PIN =
File.join(PIN_DIR, "config_spawned_targets")
CONFIG_TARGETS_PIN =
CONFIG_ROOT_TARGETS_PIN
EVENT_INVOKED_PIN =
File.join(PIN_DIR, "event_invoked")
EVENT_WRITE_POS_PIN =
File.join(PIN_DIR, "event_write_pos")
EVENT_NAME_SIZE =
16
EVENT_PAYLOAD_SIZE =
256
EVENT_TS_SIZE =
8
EVENT_STRUCT_SIZE =
288
EVENT_TS_OFFSET =
0
EVENT_PID_OFFSET =
8
EVENT_NAME_OFFSET =
12
EVENT_PAYLOAD_OFFSET =
28
EVENT_CAPACITY =
1024
VERSION =
"0.1.2"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.bpf_pin_dirObject



37
38
39
# File 'lib/vivarium.rb', line 37

def bpf_pin_dir
  @bpf_pin_dir || PIN_DIR
end

Class Method Details

.build_observe_tracepoint(store, logger) ⇒ Object



836
837
838
839
840
841
842
843
844
845
# File 'lib/vivarium.rb', line 836

def self.build_observe_tracepoint(store, logger)
  TracePoint.new(:return, :c_return) do |tp|
    events = store.drain_events
    next if events.empty?

    stack = caller_locations(2, 16)
    stack = stack.reject { |loc| loc.path.to_s.include?("vivarium") } if filter_internal_frames?
    logger.log(events, tp, stack)
  end
end

.decode_bad_socket_payload(raw_payload) ⇒ Object



142
143
144
# File 'lib/vivarium.rb', line 142

def self.decode_bad_socket_payload(raw_payload)
  decode_odd_socket_payload(raw_payload)
end

.decode_dns_qname(raw_payload) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/vivarium.rb', line 73

def self.decode_dns_qname(raw_payload)
  bytes = raw_payload.to_s.b.bytes
  labels = []
  idx = 0

  while idx < bytes.length
    length = bytes[idx]
    break if length.nil? || length.zero?
    break if length > 63

    idx += 1
    break if (idx + length) > bytes.length

    label = bytes[idx, length].pack("C*")
    labels << label
    idx += length
  end

  return "" if labels.empty?

  labels.join(".")
end

.decode_odd_socket_payload(raw_payload) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/vivarium.rb', line 117

def self.decode_odd_socket_payload(raw_payload)
  bytes = raw_payload.to_s.b
  return "" if bytes.bytesize < 6

  family = bytes[0, 2].unpack1("S<")
  type = bytes[2, 2].unpack1("S<")
  protocol = bytes[4, 2].unpack1("S<")
  family_name = socket_const_name("AF_", family)
  type_name = socket_const_name("SOCK_", type)
  protocol_name = socket_const_name("IPPROTO_", protocol)
  "family=#{family}(#{family_name}) type=#{type}(#{type_name}) protocol=#{protocol}(#{protocol_name})"
end

.decode_sock_connect_payload(raw_payload) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/vivarium.rb', line 96

def self.decode_sock_connect_payload(raw_payload)
  bytes = raw_payload.to_s.b
  return "" if bytes.bytesize < 20

  family = bytes[0, 2].unpack1("S<")
  port = bytes[2, 2].unpack1("n")
  addr = bytes[4, 16]

  case family
  when 2 # AF_INET
    ipv4 = addr[0, 4].bytes.join(".")
    "#{ipv4}:#{port} (#{socket_const_name("AF_", family)})"
  when 10 # AF_INET6
    words = addr.unpack("n8")
    ipv6 = words.map { |w| format("%x", w) }.join(":")
    "[#{ipv6}]:#{port} (#{socket_const_name("AF_", family)})"
  else
    "family=#{family}(#{socket_const_name("AF_", family)}) port=#{port}"
  end
end

.filter_internal_frames?Boolean

Returns:

  • (Boolean)


847
848
849
850
851
852
# File 'lib/vivarium.rb', line 847

def self.filter_internal_frames?
  value = ENV["VIVARIUM_FILTER_INTERNAL_FRAMES"]
  return true if value.nil?

  !%w[0 false off no].include?(value.strip.downcase)
end

.observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human) ⇒ Object



799
800
801
802
803
# File 'lib/vivarium.rb', line 799

def self.observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human)
  return scoped_observe(pin_dir: pin_dir, logger: logger, dest: dest, format: format) { yield } if block_given?

  top_observe(pin_dir: pin_dir, logger: logger, dest: dest, format: format)
end

.render_event_payload(event) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/vivarium.rb', line 146

def self.render_event_payload(event)
  case event.event_name
  when "dns_req"
    decoded = decode_dns_qname(event.payload)
    decoded.empty? ? event.payload.inspect : decoded
  when "sock_connect"
    decoded = decode_sock_connect_payload(event.payload)
    decoded.empty? ? event.payload.inspect : decoded
  when "odd_socket"
    decoded = decode_odd_socket_payload(event.payload)
    decoded.empty? ? event.payload.inspect : decoded
  else
    event.payload.inspect
  end
end

.run_daemon!(argv = ARGV) ⇒ Object



854
855
856
857
858
859
860
861
862
# File 'lib/vivarium.rb', line 854

def self.run_daemon!(argv = ARGV)
  options = { pin_dir: bpf_pin_dir }
  OptionParser.new do |opts|
    opts.banner = "Usage: vivariumd [--pin-dir PATH]"
    opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
  end.parse!(argv)

  Daemon.new(pin_dir: options[:pin_dir]).run
end

.scoped_observe(pin_dir:, logger:, dest:, format:) ⇒ Object



820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
# File 'lib/vivarium.rb', line 820

def self.scoped_observe(pin_dir:, logger:, dest:, format:)
  logger ||= Logger.new(dest: dest, format: format)
  store = MapStore.new(pin_dir: pin_dir)
  pid = Process.pid
  store.register_pid(pid)
  logger.info("scoped observing with pid=#{pid}")

  tracer = build_observe_tracepoint(store, logger)
  tracer.enable

  yield
ensure
  tracer&.disable
  store&.unregister_pid(pid)
end

.socket_const_name(prefix, value) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/vivarium.rb', line 130

def self.socket_const_name(prefix, value)
  return "UNKNOWN" unless defined?(Socket)

  key = Socket.constants.find do |name|
    name.to_s.start_with?(prefix) && Socket.const_get(name) == value
  rescue NameError
    false
  end

  key ? key.to_s : "UNKNOWN"
end

.top_observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human) ⇒ Object



805
806
807
808
809
810
811
812
813
814
815
816
817
818
# File 'lib/vivarium.rb', line 805

def self.top_observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human)
  logger ||= Logger.new(dest: dest, format: format)
  store = MapStore.new(pin_dir: pin_dir)
  pid = Process.pid
  store.register_pid(pid)
  logger.info("top-level observing with pid=#{pid}")

  tracer = build_observe_tracepoint(store, logger)
  tracer.enable

  session = ObservationSession.new(store: store, pid: pid, tracer: tracer)
  at_exit { session.stop }
  session
end