Class: Reyes::GroupManager

Inherits:
Object
  • Object
show all
Includes:
Chalk::Log
Defined in:
lib/reyes/group_manager.rb

Overview

TODO: use a more precise name

Constant Summary collapse

ReyesInputChain =
'reyes-input'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(fake_aws, region, instance_id, generation = nil) ⇒ GroupManager

Returns a new instance of GroupManager.

Parameters:

  • fake_aws (Reyes::FakeAws)
  • region (String)
  • instance_id (String)

    A String instance ID of the target instance.



18
19
20
21
22
23
24
25
# File 'lib/reyes/group_manager.rb', line 18

def initialize(fake_aws, region, instance_id, generation=nil)
  log.info("Initializing #{self.class.name} for #{region} #{instance_id}")

  @fake_aws = fake_aws
  @generation = generation || RunGeneration.new
  @region = region
  @instance_id = instance_id
end

Instance Attribute Details

#fake_awsObject (readonly)

Returns the value of attribute fake_aws.



12
13
14
# File 'lib/reyes/group_manager.rb', line 12

def fake_aws
  @fake_aws
end

#instance_idObject (readonly)

Returns the value of attribute instance_id.



12
13
14
# File 'lib/reyes/group_manager.rb', line 12

def instance_id
  @instance_id
end

Class Method Details

.parse_ipset_name(name) ⇒ Hash

Returns the parsed names, or nil in case of error.

Returns:

  • (Hash)

    the parsed names, or nil in case of error



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/reyes/group_manager.rb', line 191

def self.parse_ipset_name(name)
  parts = name.split(":")
  return nil unless parts.length == 4

  generation, region, group_id, group_name = parts
  return {
    generation: Integer(generation),
    region: region,
    group_id: group_id,
    group_name: group_name
  }
end

Instance Method Details

#addresses_for_group(region, group_id) ⇒ Array<String>

Look up addresses for given group in remote VPCs.

Parameters:

  • region (String)
  • group_id (String)

Returns:

  • (Array<String>)

    A list of private instance IP addresses



211
212
213
# File 'lib/reyes/group_manager.rb', line 211

def addresses_for_group(region, group_id)
  fake_aws.addresses_for_security_group(region, group_id).reject(&:nil?)
end

#foreign_groups_by_name(name) ⇒ Hash

Look up remote VPC / EC2 classic security groups by name.

Parameters:

  • name (String)

Returns:

  • (Hash)

See Also:



223
224
225
# File 'lib/reyes/group_manager.rb', line 223

def foreign_groups_by_name(name)
  fake_aws.foreign_groups_by_name(name, vpc_id)
end

#generate_ipsets(data) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/reyes/group_manager.rb', line 151

def generate_ipsets(data)
  data.fetch(:ipsets).map do |name, hosts|
    log.info "Creating IPSet for #{name.inspect}"
    builder = IPSetBuilder.new(name)
    hosts.each do |host|
      log.info "  - #{host.inspect}"
      builder << host
    end

    builder
  end
end

#generate_iptables_script(data, options = {}) ⇒ String

Parameters:

  • data (Hash)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • log_drop (Boolean) — default: true
  • log_accept (Boolean) — default: false

Returns:

  • (String)


263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/reyes/group_manager.rb', line 263

def generate_iptables_script(data, options={})
  log.info("Generating script for iptables-restore")

  options = {
    log_drop: true,
    log_accept: false,
  }.merge(options)

  log_drop = options.fetch(:log_drop)
  log_accept = options.fetch(:log_accept)

  lines = []

  lines << "# Generated by Reyes v#{Reyes::VERSION} at #{Time.now.to_s}"

  # we use the default filter table
  lines << '*filter'

  lines << ':INPUT ACCEPT'
  lines << ':FORWARD ACCEPT'
  lines << ':OUTPUT ACCEPT'
  lines << ''

  lines << ":#{ReyesInputChain} -"
  lines << ':reyes-accept -'
  lines << ':reyes-drop -'
  lines << ':reyes-log-accept -'
  lines << ':reyes-log-drop -'

  # add rules to direct appropriate traffic into reyes
  lines << ''
  lines << '## input chain rules'
  lines.concat(input_chain_rules)

  lines << ''
  lines << '-A reyes-accept -j reyes-log-accept' if log_accept
  lines << '-A reyes-accept -j ACCEPT'
  lines << '-A reyes-drop -j reyes-log-drop' if log_drop
  lines << '-A reyes-drop -j DROP'

  lines << ''
  lines << '## static global rules'

  # allow normal ICMP traffic
  IPTables.innocuous_icmp_rules(ReyesInputChain).each do |r|
    lines << r.join(' ')
  end

  # allow established connections without logging
  lines << "-A #{ReyesInputChain} -m conntrack --ctstate " \
           "RELATED,ESTABLISHED -j ACCEPT"

  # drop invalid connections
  lines << "-A #{ReyesInputChain} -m conntrack --ctstate " \
           "INVALID -j reyes-drop"

  # add dynamic rules
  lines << ''
  lines << '## dynamic rules from security groups'
  dynamic_rules_from_data(data).each do |rule|
    lines << rule
  end

  # add reyes-drop to very end of reyes-input chain
  lines << ''
  lines << '## default drop'
  lines << "-A #{ReyesInputChain} -j reyes-drop"

  lines << ''
  lines << '## log rules'
  lines << IPTables.log_rule_string('reyes-log-accept', 'REYES ACCEPT')
  lines << IPTables.log_rule_string('reyes-log-drop', 'REYES BLOCK')

  # commit the filter table
  lines << ''
  lines << 'COMMIT'

  text = lines.join("\n")
  text << "\n"

  text
end

#generate_rulesObject

Given our instance ID and security group rules, generate IPTables rules needed to emulate security group behavior for foreign VPCs.

Look up the set of instance IP addresses in any remote VPC security groups referenced by our rules, and add them to a list of ipsets to create.

Also create a list of IPTables rules that will reference these ipsets.

This method generates the data and returns it as a hash without making any changes to the system.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/reyes/group_manager.rb', line 96

def generate_rules
  log.info("Generating rules for generation #{run_generation}")

  data = generate_rules_empty

  needed_groups = {}

  our_groups.each_pair do |group_id, group|
    group_key = "#{group_id}:#{group.fetch('name')}"
    data[:groups][group_key] = []

    # we only work with ingress permissions
    group.fetch('ingress_ip_permissions').each do |perm|
      perm_hash = {
        protocol: perm.fetch('protocol'),
        port: [perm.fetch('port_start'), perm.fetch('port_end')],
        remote_addrs: [],
        remote_sets: [],
      }

      case perm.fetch('protocol').to_s
      when 'icmp'
        log.info("Skipping ICMP rule")
        next
      when 'tcp', 'udp'

        # append IP ranges
        perm_hash[:remote_addrs] += perm.fetch('ip_ranges')

        # list the security groups we'll need to look up
        perm.fetch('group_names').each do |g_name|
          foreign_groups_by_name(g_name).each_pair do |fg_id, fg_data|
            needed_groups[fg_id] = fg_data
            perm_hash[:remote_sets] << ipset_name_for_group_hash(fg_data)
          end
        end

      else
        raise NotImplementedError.new(
          "Unexpected protocol #{perm['protocol'].inspect} in #{group_id}")
      end

      data[:groups][group_key] << perm_hash
    end
  end

  data[:ipsets] = {}
  needed_groups.each_pair do |group_id, group_data|
    data[:ipsets][ipset_name_for_group_hash(group_data)] = \
      addresses_for_group(group_data.fetch('region'), group_id)
  end

  data
end

#generate_rules_emptyObject



72
73
74
75
76
77
# File 'lib/reyes/group_manager.rb', line 72

def generate_rules_empty
  {
    :groups => {},
    :ipsets => {},
  }
end

#input_chain_rulesArray<String>

Generate a list of IPTables script lines that will inject traffic into the Reyes processing chain.

In EC2 classic, traffic relevant to Reyes will be arriving directly through IPsec, so these rules will filter all IPsec traffic.

In VPC, traffic relevant to Reyes may be forwarded by VPN servers and arrive from VPC CIDR blocks or EC2 classic CIDR blocks. All of this CIDR block information will be fetched from FakeAws data.

Returns:

  • (Array<String>)


239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/reyes/group_manager.rb', line 239

def input_chain_rules
  if vpc?
    # filter all remote CIDR blocks through reyes
    fake_aws.remote_cidr_blocks(vpc_id).map do |cidr|

      check_cidr_ok_for_reyes(cidr)

      "-A INPUT -s #{cidr} -j #{ReyesInputChain}"
    end
  else
    # filter all ipsec tunneled traffic through reyes
    ["-A INPUT -m policy --pol ipsec --dir in -j #{ReyesInputChain}"]
  end
end

#ipset_name_for_group_hash(group_hash) ⇒ String

Returns A string title, at most 31 characters long.

Parameters:

  • group_hash (Hash)

Returns:

  • (String)

    A string title, at most 31 characters long



183
184
185
186
187
188
# File 'lib/reyes/group_manager.rb', line 183

def ipset_name_for_group_hash(group_hash)
  [
    run_generation.to_s,
    group_hash.fetch('ipset_suffix'),
  ].join(':')[0...31]
end

#load_from_s3(aws, config) ⇒ Object



79
80
81
82
# File 'lib/reyes/group_manager.rb', line 79

def load_from_s3(aws, config)
  s3 = S3Loader.new(aws, config)
  s3.latest
end

#local_ipv4_addressesArray<IPAddr>

List IPv4 addresses of the current host.

Returns:

  • (Array<IPAddr>)


55
56
57
58
59
# File 'lib/reyes/group_manager.rb', line 55

def local_ipv4_addresses
  Socket.ip_address_list.find_all(&:ipv4?).map {|a|
    IPAddr.new(a.ip_address)
  }
end

#our_groups(skip_excluded = true) ⇒ Hash

Returns:

  • (Hash)


62
63
64
65
66
67
68
69
70
# File 'lib/reyes/group_manager.rb', line 62

def our_groups(skip_excluded=true)
  data = fake_aws.security_groups_for_instance(@region, @instance_id)
  if skip_excluded
    exclude = fake_aws.excluded_group_names.to_set
    data.reject {|g_id, g_data| exclude.include?(g_data.fetch('name')) }
  else
    data
  end
end

#our_instanceHash

Look up data for this instance from FakeAws data.

Returns:

  • (Hash)


47
48
49
# File 'lib/reyes/group_manager.rb', line 47

def our_instance
  fake_aws.instance(@region, @instance_id)
end

#run_generationInteger

Returns:

  • (Integer)


165
166
167
# File 'lib/reyes/group_manager.rb', line 165

def run_generation
  @generation.value
end

#run_generation_increment!Object

Increment the run generation and persist it to disk



175
176
177
# File 'lib/reyes/group_manager.rb', line 175

def run_generation_increment!
  @generation.increment!
end

#run_generation_timeTime

Returns:

  • (Time)


170
171
172
# File 'lib/reyes/group_manager.rb', line 170

def run_generation_time
  @generation.mtime
end

#vpc?Boolean

Whether the self EC2 instance is in VPC.

Returns:

  • (Boolean)


31
32
33
# File 'lib/reyes/group_manager.rb', line 31

def vpc?
  !!vpc_id
end

#vpc_idString?

The VPC ID (or nil) of the self EC2 instance.

Returns:

  • (String, nil)


39
40
41
# File 'lib/reyes/group_manager.rb', line 39

def vpc_id
  our_instance.fetch('vpc')
end