Skip to content

Instantly share code, notes, and snippets.

@ilyash-b
Created March 5, 2020 15:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ilyash-b/d7f03561235941f59d3eec921b06346f to your computer and use it in GitHub Desktop.
Save ilyash-b/d7f03561235941f59d3eec921b06346f to your computer and use it in GitHub Desktop.
Converge to iptables rules given in CSV files
#!/usr/bin/env ngs
# Turn rules parsing debugging with "DEBUG=rules" environment variable setting
COMMENT_PREFIX = 'aws-migration-'
INT_NUMBER_REGEX = /^[0-9]+$/
F is_ip(x:Str) {
octets = x.split('.')
not(octets.all(INT_NUMBER_REGEX)) returns false
octets .= map(Int)
octets.len() == 4 and octets.all(0..255)
}
VALIDATION = {
'rule_id': INT_NUMBER_REGEX
'customer_id': INT_NUMBER_REGEX
'customer_ip': is_ip
'customer_port': INT_NUMBER_REGEX
'make_iptables_rule': F(x) Result({decode(x, Bool)})
'nat_ip': is_ip
'nat_port': INT_NUMBER_REGEX
}
type IptablesRule
F init(r:IptablesRule, argv:Arr) {
r.argv = argv
section "Convert rule to hash/multiset" {
# In (argv): --switch1 value1a --switch1 value1a --switch2 value2
# Out (parsed_rule): {'--switch1': ['value1a', 'value1b'], '--switch2': ['value2']}
parsed_rule = {}
argv.each_chunk(2, F(chunk) {
k = chunk[0]
v = chunk[1]
parsed_rule.dflt(k, []).push(v)
})
}
r.rule = parsed_rule.sort().mapv(sort) # r.rule is a multiset
r.id = Str(r.rule.without('--comment'))
}
F Str(r:IptablesRule) "<${r.typeof().name} ${r.argv.join(' ')}>"
_created_by_this_script_pred = Pred({'rule': {'--comment': X.any(Pfx(COMMENT_PREFIX))}})
F created_by_this_script(r:IptablesRule) r._created_by_this_script_pred()
F cksum(log_comment:Str, data:Str) {
ret = `echo -n ${data} | line: md5sum`.split(' ')[0]
log("${log_comment} checksum: ${ret}")
ret
}
F parse_iptables_rules(s:Str) {
current_rules = s.lines()
# current_rules = current_rules.map(split(X, ' '))
iptables_table_name = null
iptables_chain_name = null
seen_commit = false
current_rules = collector for rule in current_rules {
debug("rules", "Parsing: ${rule}")
ematch rule {
Pfx('#') {
debug("rules", "Skipping comment ${rule}")
}
Pfx('*') {
iptables_table_name = rule[1..null]
debug("rules", "Parsed table name: ${iptables_table_name}")
}
Pfx(':') {
iptables_chain_name = rule[1..null].split(' ')[0]
debug("rules", "Parsed chain name: ${iptables_chain_name}")
}
Pfx('-A') {
assert('"' not in rule, "Can't deal with quotes in rules")
(['-t', iptables_table_name] + rule.split(' ')).IptablesRule().collect()
}
'COMMIT' {
seen_commit = true
debug("rules", "Parsed commit")
}
Any {
throw InvalidArgument("Don't know how to parse current iptables rule").set(rule=rule)
}
}
}
assert(seen_commit, "Current rules don't have COMMIT line")
debug("rules", "Current rules: ${current_rules}")
current_rules
}
F maybe_output_save() {
if ENV.get('DO_SAVE', 'no').decode(Bool) {
echo("service iptables-persistent save")
} else {
log("*** iptables rules were not saved permanently ***")
log("After confirming everything works as expected, please run: service iptables-persistent save")
log("Alternatively, run with DO_SAVE=yes environment variable")
}
}
F main(op:Str, definitions_file:Str) {
guard op == 'converge'
comment_inserted = Time().Str('%Y%m%d_%H%M%S')
section "Read input" {
definitions = read(definitions_file)
current_rules = read()
}
section "Calculate and log checksums" {
definitions_cksum = cksum("NAT definitions from file ${definitions_file}", definitions)
cksum("Current rules", current_rules)
}
section "Parse definitions into iptables rules" {
assert('"' not in definitions, "Not prepared to deal with quotes in definitions file")
definitions_rows = lines(definitions)
columns_names = definitions_rows[0].split(',')
definitions = definitions_rows[1..null].map(F(definition_row) {
definition_row_cells = definition_row.split(',')
Hash(columns_names, definition_row_cells)
})
definitions .= filter({'make_iptables_rule': decode(X, Bool) })
assert(definitions, "No definitions")
log("Active definitions (rule_ids): ${definitions.rule_id}")
desired_rules = definitions.map(F(definition) {
%[
-t nat
-A PREROUTING
-m tcp
-p tcp
-d "${definition.nat_ip}/32" --dport ${definition.nat_port}
-j DNAT --to-destination "${definition.customer_ip}:${definition.customer_port}"
-m comment
--comment "${COMMENT_PREFIX}rule-${definition.rule_id}-customer-${definition.customer_id}-inserted-${comment_inserted}-definitions_cksum-${definitions_cksum}"
].IptablesRule()
})
}
current_rules .= parse_iptables_rules()
section "Validate definitions" {
definitions.each(F(definition) {
VALIDATION.each(F(field_name, validation) {
field_name not in definition throws InvalidArgument("Missing field ${field_name} in definition ${definition}")
p = Pred(validation)
p(definition[field_name]).not() throws InvalidArgument("Field ${field_name} failed to validate in definition ${definition}")
})
})
}
section "Generate iptables commands" {
diff = Diff(current_rules, desired_rules, {A.id == B.id})
# Only remove rules that are marked with this script's comment
# Never touch rules that were added externally
diff.remove .= filter(created_by_this_script)
for k in %[add remove] {
rules = diff.(k)
log("To ${k} total - ${rules.len()}")
}
for rule in diff.add {
echo("iptables ${rule.argv.join(' ')}".tap(log))
}
for rule in diff.remove {
echo("iptables ${rule.argv.replace('-A', '-D').join(' ')}".tap(log))
}
if len(diff.add) + len(diff.remove) > 0 {
maybe_output_save()
}
}
}
F main(op:Str) {
guard op == 'clear'
current_rules = read()
cksum("Current rules", current_rules)
current_rules = parse_iptables_rules(current_rules).filter(created_by_this_script)
for rule in current_rules {
echo("iptables ${rule.argv.replace('-A', '-D').join(' ')}".tap(log))
}
if current_rules {
maybe_output_save()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment