summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/nat.py259
-rwxr-xr-xsrc/migration-scripts/nat/4-to-558
-rwxr-xr-xsrc/op_mode/show_nat_statistics.py63
-rwxr-xr-xsrc/op_mode/to_be_migrated/vyatta-nat-translations.pl267
-rwxr-xr-xsrc/validators/ip-protocol41
-rwxr-xr-xsrc/validators/ipv4-address-exclude7
-rwxr-xr-xsrc/validators/ipv4-prefix-exclude7
-rwxr-xr-xsrc/validators/ipv4-range33
-rwxr-xr-xsrc/validators/ipv4-range-exclude7
9 files changed, 742 insertions, 0 deletions
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
new file mode 100755
index 000000000..5cb1af1f1
--- /dev/null
+++ b/src/conf_mode/nat.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, cmd
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+
+default_config_data = {
+ 'deleted': False,
+ 'destination': [],
+ 'helper_functions': None,
+ 'pre_ct_helper': '',
+ 'pre_ct_conntrack': '',
+ 'out_ct_helper': '',
+ 'out_ct_conntrack': '',
+ 'source': []
+}
+
+iptables_nat_config = '/tmp/vyos-nat-rules.nft'
+
+def _check_kmod():
+ """ load required Kernel modules """
+ modules = ['nft_nat', 'nft_chain_nat_ipv4']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
+
+def get_handler(json, chain, target):
+ """ Get nftable rule handler number of given chain/target combination.
+ Handler is required when adding NAT/Conntrack helper targets """
+ for x in json:
+ if x['chain'] != chain:
+ continue
+ if x['target'] != target:
+ continue
+ return x['handle']
+
+ return None
+
+
+def verify_rule(rule, err_msg):
+ """ Common verify steps used for both source and destination NAT """
+ if rule['translation_port'] or rule['dest_port'] or rule['source_port']:
+ if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ proto = rule['protocol']
+ raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")')
+
+ if '/' in rule['translation_address']:
+ raise ConfigError(f'{err_msg}\n' \
+ 'Cannot use ports with an IPv4net type translation address as it\n' \
+ 'statically maps a whole network of addresses onto another\n' \
+ 'network of addresses')
+
+ if not rule['translation_address']:
+ raise ConfigError(f'{err_msg} translation address not specified')
+ else:
+ addr = rule['translation_address']
+ if addr != 'masquerade' and not is_addr_assigned(addr):
+ print(f'Warning: IP address {addr} does not exist on the system!')
+
+
+def parse_source_destination(conf, source_dest):
+ """ Common wrapper to read in both NAT source and destination CLI """
+ tmp = []
+ base_level = ['nat', source_dest]
+ conf.set_level(base_level)
+ for number in conf.list_nodes(['rule']):
+ rule = {
+ 'description': '',
+ 'dest_address': '',
+ 'dest_port': '',
+ 'disabled': False,
+ 'exclude': False,
+ 'interface_in': '',
+ 'interface_out': '',
+ 'log': False,
+ 'protocol': 'all',
+ 'number': number,
+ 'source_address': '',
+ 'source_port': '',
+ 'translation_address': '',
+ 'translation_port': ''
+ }
+ conf.set_level(base_level + ['rule', number])
+
+ if conf.exists(['description']):
+ rule['description'] = conf.return_value(['description'])
+
+ if conf.exists(['destination', 'address']):
+ rule['dest_address'] = conf.return_value(['destination', 'address'])
+
+ if conf.exists(['destination', 'port']):
+ rule['dest_port'] = conf.return_value(['destination', 'port'])
+
+ if conf.exists(['disable']):
+ rule['disabled'] = True
+
+ if conf.exists(['exclude']):
+ rule['exclude'] = True
+
+ if conf.exists(['inbound-interface']):
+ rule['interface_in'] = conf.return_value(['inbound-interface'])
+
+ if conf.exists(['outbound-interface']):
+ rule['interface_out'] = conf.return_value(['outbound-interface'])
+
+ if conf.exists(['log']):
+ rule['log'] = True
+
+ if conf.exists(['protocol']):
+ rule['protocol'] = conf.return_value(['protocol'])
+
+ if conf.exists(['source', 'address']):
+ rule['source_address'] = conf.return_value(['source', 'address'])
+
+ if conf.exists(['source', 'port']):
+ rule['source_port'] = conf.return_value(['source', 'port'])
+
+ if conf.exists(['translation', 'address']):
+ rule['translation_address'] = conf.return_value(['translation', 'address'])
+
+ if conf.exists(['translation', 'port']):
+ rule['translation_port'] = conf.return_value(['translation', 'port'])
+
+ tmp.append(rule)
+
+ return tmp
+
+def get_config():
+ nat = deepcopy(default_config_data)
+ conf = Config()
+
+ # read in current nftable (once) for further processing
+ tmp = cmd('nft -j list table raw')
+ nftable_json = json.loads(tmp)
+
+ # condense the full JSON table into a list with only relevand informations
+ pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
+ condensed_json = jmespath.search(pattern, nftable_json)
+
+ if not conf.exists(['nat']):
+ nat['helper_functions'] = 'remove'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+
+ nat['deleted'] = True
+
+ return nat
+
+ # check if NAT connection tracking helpers need to be set up - this has to
+ # be done only once
+ if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
+ nat['helper_functions'] = 'add'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+
+ # set config level for parsing in NAT configuration
+ conf.set_level(['nat'])
+
+ # use a common wrapper function to read in the source / destination
+ # tree from the config - thus we do not need to replicate almost the
+ # same code :-)
+ for tgt in ['source', 'destination']:
+ nat[tgt] = parse_source_destination(conf, tgt)
+
+ return nat
+
+def verify(nat):
+ if nat['deleted']:
+ # no need to verify the CLI as NAT is going to be deactivated
+ return None
+
+ if nat['helper_functions']:
+ if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']):
+ raise Exception('could not determine nftable ruleset handlers')
+
+ for rule in nat['source']:
+ interface = rule['interface_out']
+ err_msg = f"Source NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_out']:
+ raise ConfigError(f'{err_msg} outbound-interface not specified')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ for rule in nat['destination']:
+ interface = rule['interface_in']
+ err_msg = f"Destination NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_in']:
+ raise ConfigError(f'{err_msg} inbound-interface not specified')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ return None
+
+def generate(nat):
+ render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755)
+
+ return None
+
+def apply(nat):
+ cmd(f'{iptables_nat_config}')
+ if os.path.isfile(iptables_nat_config):
+ os.unlink(iptables_nat_config)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ _check_kmod()
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/migration-scripts/nat/4-to-5 b/src/migration-scripts/nat/4-to-5
new file mode 100755
index 000000000..dda191719
--- /dev/null
+++ b/src/migration-scripts/nat/4-to-5
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Drop the enable/disable from the nat "log" node. If log node is specified
+# it is "enabled"
+
+from sys import argv,exit
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+if not config.exists(['nat']):
+ # Nothing to do
+ exit(0)
+else:
+ for direction in ['source', 'destination']:
+ if not config.exists(['nat', direction]):
+ continue
+
+ for rule in config.list_nodes(['nat', direction, 'rule']):
+ base = ['nat', direction, 'rule', rule]
+
+ # Check if the log node exists and if log is enabled,
+ # migrate it to the new valueless 'log' node
+ if config.exists(base + ['log']):
+ tmp = config.return_value(base + ['log'])
+ config.delete(base + ['log'])
+ if tmp == 'enable':
+ config.set(base + ['log'])
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py
new file mode 100755
index 000000000..0b53112f2
--- /dev/null
+++ b/src/op_mode/show_nat_statistics.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+
+from argparse import ArgumentParser
+from jinja2 import Template
+from sys import exit
+from vyos.util import cmd
+
+OUT_TMPL_SRC="""
+rule pkts bytes interface
+---- ---- ----- ---------
+{% for r in output %}
+{%- if r.comment -%}
+{%- set packets = r.counter.packets -%}
+{%- set bytes = r.counter.bytes -%}
+{%- set interface = r.interface -%}
+{# remove rule comment prefix #}
+{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') -%}
+{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }}
+{%- endif %}
+{% endfor %}
+"""
+
+parser = ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true")
+group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true")
+args = parser.parse_args()
+
+if args.source or args.destination:
+ tmp = cmd('sudo nft -j list table nat')
+ tmp = json.loads(tmp)
+
+ source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ data = {
+ 'output' : jmespath.search(source if args.source else destination, tmp),
+ 'direction' : 'source' if args.source else 'destination'
+ }
+
+ tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True)
+ print(tmpl.render(data))
+ exit(0)
+else:
+ parser.print_help()
+ exit(1)
+
diff --git a/src/op_mode/to_be_migrated/vyatta-nat-translations.pl b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl
new file mode 100755
index 000000000..94ed74bad
--- /dev/null
+++ b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl
@@ -0,0 +1,267 @@
+#!/usr/bin/perl
+#
+# Module: vyatta-nat-translate.pl
+#
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Stig Thormodsrud
+# Date: July 2008
+# Description: Script to display nat translations
+#
+# **** End License ****
+#
+
+use Getopt::Long;
+use XML::Simple;
+use Data::Dumper;
+use POSIX;
+
+use warnings;
+use strict;
+
+my $dump = 0;
+my ($xml_file, $verbose, $proto, $stats, $ipaddr, $pipe);
+my $type;
+my $verbose_format = "%-20s %-18s %-20s %-18s\n";
+my $format = "%-20s %-20s %-4s %-8s";
+
+sub add_xml_root {
+ my $xml = shift;
+
+ $xml = '<data>' . $xml . '</data>';
+ return $xml;
+}
+
+
+sub read_xml_file {
+ my $file = shift;
+
+ local($/, *FD); # slurp mode
+ open FD, "<", $file or die "Couldn't open $file\n";
+ my $xml = <FD>;
+ close FD;
+ return $xml;
+}
+
+sub print_xml {
+ my $data = shift;
+ print Dumper($data);
+}
+
+sub guess_snat_dnat {
+ my ($src, $dst) = @_;
+
+ if ($src->{original} eq $dst->{reply}) {
+ return "dnat";
+ }
+ if ($dst->{original} eq $src->{reply}) {
+ return "snat";
+ }
+ return "unkn";
+}
+
+sub nat_print_xml {
+ my ($data, $type) = @_;
+
+ my $flow = 0;
+
+ my %flowh;
+ while (1) {
+ my $meta = 0;
+ last if ! defined $data->{flow}[$flow];
+ my $flow_ref = $data->{flow}[$flow];
+ my $flow_type = $flow_ref->{type};
+ my (%src, %dst, %sport, %dport, %proto);
+ my (%packets, %bytes);
+ my $timeout = undef;
+ my $uses = undef;
+ while (1) {
+ my $meta_ref = $flow_ref->{meta}[$meta];
+ last if ! defined $meta_ref;
+ my $dir = $meta_ref->{direction};
+ if ($dir eq 'original' or $dir eq 'reply') {
+ my $l3_ref = $meta_ref->{layer3}[0];
+ my $l4_ref = $meta_ref->{layer4}[0];
+ my $count_ref = $meta_ref->{counters}[0];
+ if (defined $l3_ref) {
+ $src{$dir} = $l3_ref->{src}[0];
+ $dst{$dir} = $l3_ref->{dst}[0];
+ if (defined $l4_ref) {
+ $sport{$dir} = $l4_ref->{sport}[0];
+ $dport{$dir} = $l4_ref->{dport}[0];
+ $proto{$dir} = $l4_ref->{protoname};
+ }
+ }
+ if (defined $stats and defined $count_ref) {
+ $packets{$dir} = $count_ref->{packets}[0];
+ $bytes{$dir} = $count_ref->{bytes}[0];
+ }
+ } elsif ($dir eq 'independent') {
+ $timeout = $meta_ref->{timeout}[0];
+ $uses = $meta_ref->{'use'}[0];
+ }
+ $meta++;
+ }
+ my ($proto, $in_src, $in_dst, $out_src, $out_dst);
+ $proto = $proto{original};
+ $in_src = "$src{original}";
+ $in_src .= ":$sport{original}" if defined $sport{original};
+ $in_dst = "$dst{original}";
+ $in_dst .= ":$dport{original}" if defined $dport{original};
+ $out_src = "$dst{reply}";
+ $out_src .= ":$dport{reply}" if defined $dport{reply};
+ $out_dst = "$src{reply}";
+ $out_dst .= ":$sport{reply}" if defined $sport{reply};
+ if (defined $verbose) {
+ printf($verbose_format, $in_src, $in_dst, $out_src, $out_dst);
+ }
+# if (! defined $type) {
+# $type = guess_snat_dnat(\%src, \%dst);
+# }
+ if (defined $type) {
+ my ($from, $to);
+ if ($type eq 'source') {
+ $from = "$src{original}";
+ $to = "$dst{reply}";
+ if (defined $sport{original} and defined $dport{reply}) {
+ if ($sport{original} ne $dport{reply}) {
+ $from .= ":$sport{original}";
+ $to .= ":$dport{reply}";
+ }
+ }
+ } else {
+ $from = "$dst{original}";
+ $to = "$src{reply}";
+ if (defined $dport{original} and defined $sport{reply}) {
+ if ($dport{original} ne $sport{reply}) {
+ $from .= ":$dport{original}";
+ $to .= ":$sport{reply}";
+ }
+ }
+ }
+ if (defined $verbose) {
+ print " $proto: $from ==> $to";
+ } else {
+ my $timeout2 = "";
+ if (defined $timeout) {
+ $timeout2 = $timeout;
+ }
+ printf($format, $from, $to, $proto, $timeout2);
+ print " $flow_type" if defined $flow_type;
+ print "\n";
+ }
+ }
+ if (defined $verbose) {
+ print " timeout: $timeout" if defined $timeout;
+ print " use: $uses " if defined $uses;
+ print " type: $flow_type" if defined $flow_type;
+ print "\n";
+ }
+ if (defined $stats) {
+ foreach my $dir ('original', 'reply') {
+ if (defined $packets{$dir}) {
+ printf(" %-8s: packets %s, bytes %s\n",
+ $dir, $packets{$dir}, $bytes{$dir});
+ }
+ }
+ }
+ $flow++;
+ }
+ return $flow;
+}
+
+
+#
+# main
+#
+GetOptions("verbose" => \$verbose,
+ "proto=s" => \$proto,
+ "file=s" => \$xml_file,
+ "stats" => \$stats,
+ "type=s" => \$type,
+ "ipaddr=s" => \$ipaddr,
+ "pipe" => \$pipe,
+);
+
+my $conntrack = '/usr/sbin/conntrack';
+if (! -f $conntrack) {
+ die "Package [conntrack] not installed";
+}
+
+die "Must specify NAT type!" if !defined($type);
+die "Unknown NAT type!" if (($type ne 'source') && ($type ne 'destination'));
+
+my $xs = XML::Simple->new(ForceArray => 1, KeepRoot => 0);
+my ($xml, $data);
+
+# flush stdout after every write for pipe mode
+$| = 1 if defined $pipe;
+
+if (defined $verbose) {
+ printf($verbose_format, 'Pre-NAT src', 'Pre-NAT dst',
+ 'Post-NAT src', 'Post-NAT dst');
+} else {
+ printf($format, 'Pre-NAT', 'Post-NAT', 'Prot', 'Timeout');
+ print " Type" if defined $pipe;
+ print "\n";
+}
+
+if (defined $xml_file) {
+ $xml = read_xml_file($xml_file);
+ $data = $xs->XMLin($xml);
+ if ($dump) {
+ print_xml($data);
+ exit;
+ }
+ nat_print_xml($data, 'snat');
+
+} elsif (defined $pipe) {
+ while ($xml = <STDIN>) {
+ $xml =~ s/\<\?xml version=\"1\.0\" encoding=\"utf-8\"\?\>//;
+ $xml =~ s/\<conntrack\>//;
+ $xml = add_xml_root($xml);
+ $data = $xs->XMLin($xml);
+ nat_print_xml($data, $type);
+ }
+} else {
+ if (defined $proto) {
+ $proto = "-p $proto"
+ } else {
+ $proto = "";
+ }
+ if ($type eq 'source') {
+ my $ipopt = "";
+ if (defined $ipaddr) {
+ $ipopt = "--orig-src $ipaddr";
+ }
+ $xml = `sudo $conntrack -L -n $ipopt -o xml $proto 2>/dev/null`;
+ chomp $xml;
+ $data = undef;
+ $data = $xs->XMLin($xml) if ! $xml eq '';
+ }
+ if ($type eq 'destination') {
+ my $ipopt = "";
+ if (defined $ipaddr) {
+ $ipopt = "--orig-dst $ipaddr";
+ }
+ $xml = `sudo $conntrack -L -g $ipopt -o xml $proto 2>/dev/null`;
+ chomp $xml;
+ $data = undef;
+ $data = $xs->XMLin($xml) if ! $xml eq '';
+ }
+ nat_print_xml($data, $type) if defined $data;
+}
+
+# end of file
diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol
new file mode 100755
index 000000000..078f8e319
--- /dev/null
+++ b/src/validators/ip-protocol
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+from sys import argv,exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ input = argv[1]
+ try:
+ # IP protocol can be in the range 0 - 255, thus the range must end with 256
+ if int(input) in range(0, 256):
+ exit(0)
+ except ValueError:
+ pass
+
+ pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \
+ "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \
+ "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \
+ "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \
+ "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \
+ "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b"
+ if re.match(pattern, input):
+ exit(0)
+
+ exit(1)
diff --git a/src/validators/ipv4-address-exclude b/src/validators/ipv4-address-exclude
new file mode 100755
index 000000000..80ad17d45
--- /dev/null
+++ b/src/validators/ipv4-address-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-address "${arg:1}"
diff --git a/src/validators/ipv4-prefix-exclude b/src/validators/ipv4-prefix-exclude
new file mode 100755
index 000000000..4f7de400a
--- /dev/null
+++ b/src/validators/ipv4-prefix-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-prefix "${arg:1}"
diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range
new file mode 100755
index 000000000..ae3f3f163
--- /dev/null
+++ b/src/validators/ipv4-range
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter
+ip2dec () {
+ local a b c d ip=$@
+ IFS=. read -r a b c d <<< "$ip"
+ printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"
+}
+
+# Only run this if there is a hypen present in $1
+if [[ "$1" =~ "-" ]]; then
+ # This only works with real bash (<<<) - split IP addresses into array with
+ # hyphen as delimiter
+ readarray -d - -t strarr <<< $1
+
+ ipaddrcheck --is-ipv4-single ${strarr[0]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ ipaddrcheck --is-ipv4-single ${strarr[1]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ start=$(ip2dec ${strarr[0]})
+ stop=$(ip2dec ${strarr[1]})
+ if [ $start -ge $stop ]; then
+ exit 1
+ fi
+fi
+
+exit 0
diff --git a/src/validators/ipv4-range-exclude b/src/validators/ipv4-range-exclude
new file mode 100755
index 000000000..3787b4dec
--- /dev/null
+++ b/src/validators/ipv4-range-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-range "${arg:1}"