Created
March 5, 2020 15:03
-
-
Save ilyash-b/d7f03561235941f59d3eec921b06346f to your computer and use it in GitHub Desktop.
Converge to iptables rules given in CSV files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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