#!/usr/bin/perl -w # $Id: metawall.pl,v 1.9 2002/09/22 22:49:25 ssnodgra Exp $ # metawall.pl - Generate firewall rules using a metalanguage # Copyright (C)2002 Steven R. Snodgrass # See the COPYRIGHT section in this file for full licensing details # # Initialization # # Directives use strict; # Modules use Getopt::Std; use NetAddr::IP; # Globals (initialized constants) my $COMMENT = '#'; my $EXEC; my $IPTABLES = '/sbin/iptables'; my $VERSION = '0.10'; # Supported backend targets # Possible future targets: ipf, ipfks, ios, iosre, iosfw my %TARGETS = ( ipt => 1, iptct => 1 ); # ICMP request/reply type pairs my %ICMPPAIR = ( 8 => 0, # echo 13 => 14, # timestamp 15 => 16, # information 17 => 18, # address mask 33 => 34, # IPv6 where-are-you 35 => 36, # mobile registration 37 => 38 # domain name ); # Initialize the network definitions table # This is a hash of NetAddr::IP arrays my %NetDef = ( RFC1918 => [ (new NetAddr::IP '10.0.0.0/8'), (new NetAddr::IP '172.16.0.0/12'), (new NetAddr::IP '192.168.0.0/16') ], ); # Initialize the protocol definitions table # TCP and UDP protocols have a pair of port ranges, ICMP has type and code my %ProDef = ( ip => ['ip'], icmp => ['icmp', undef, undef], tcp => ['tcp', undef, undef], udp => ['udp', undef, undef], ping => ['icmp', 8, undef], ftp => ['tcp', undef, [21, 21]], ssh => ['tcp', undef, [22, 22]], telnet => ['tcp', undef, [23, 23]], smtp => ['tcp', undef, [25, 25]], dns => ['udp', undef, [53, 53]], dnsxfr => ['tcp', undef, [53, 53]], http => ['tcp', undef, [80, 80]], pop3 => ['tcp', undef, [110, 110]], nntp => ['tcp', undef, [119, 119]], ntp => ['udp', undef, [123, 123]], imap => ['tcp', undef, [143, 143]], https => ['tcp', undef, [443, 443]], imaps => ['tcp', undef, [993, 993]], ); # Initialize the chains table # This keeps track of what chains have already been created my %Chain = (INPUT => 1, FORWARD => 1, OUTPUT => 1); # # Subroutines # # Convert a comma-separated list of CIDR blocks and/or defined macros into # a reference to an array of IP::NetAddr objects. Note that duplicate CIDR # blocks are eliminated and contiguous CIDR blocks merged when possible by # the NetAddr compact function. sub BuildIPList { my ($iparg) = @_; if (lc($iparg) eq 'any') { return [ new NetAddr::IP '0.0.0.0/0' ]; } my @iplist; foreach my $ippart (split /,/, $iparg) { if (defined $NetDef{$ippart}) { push @iplist, @{$NetDef{$ippart}}; } else { my $netaddr = new NetAddr::IP $ippart; die "Bad network address $ippart" unless (defined $netaddr && $netaddr == $netaddr->network()); push @iplist, $netaddr; } } NetAddr::IP::compactref(\@iplist); } # Verifies that defined and protocol symbols consists of only alphanumeric # and underscore characters beginning with an alpha. sub CheckSymbol { my ($symbol) = @_; die "Invalid symbol $symbol" unless $symbol =~ /^[a-zA-Z]\w*$/; die "Reserved symbol $symbol" if $symbol =~ /^ip|icmp|tcp|udp$/; } # Execute or print a backend command sub DoCommand { my ($rule) = @_; if ($EXEC) { my @rulelist = split /\s+/, $rule; system @rulelist; } else { print $rule, "\n"; } } # Create a new chain sub CreateChain { my ($chain, $backend) = @_; $Chain{$chain} = 1; DoCommand("$IPTABLES -N $chain"); DoCommand("$IPTABLES -F $chain"); } # Initialize the main chain with necessary rules sub InitChain { my ($name, $backend) = @_; if ($backend eq 'iptct') { DoCommand("$IPTABLES -A $name -m state --state ESTABLISHED,RELATED -j ACCEPT"); } } # Parse a TCP/UDP port range and return low and high values # Input: portnum or loport: or :hiport or loport:hiport # Output: [loport, hiport] sub ParsePort { my ($loport, $hiport) = split /:/, $_[0]; $hiport = $loport unless defined $hiport; $loport ||= 0; $hiport ||= 65535; die "Invalid port specification" unless (0 <= $loport && $loport <= $hiport && $hiport <= 65535); return [$loport, $hiport]; } # Parse a protocol definition and return a protocol descriptor # We take all the attributes from the existing definition for the specified # protocol, then override them with any user-supplied parameters sub ParseProto { my ($proto, @pdef) = @_; my ($ipproto, $type, $code, $sport, $dport); # Set up default options from the protocol definition table if (exists $ProDef{$proto}) { $ipproto = $ProDef{$proto}->[0]; if ($ipproto eq 'icmp') { $type = $ProDef{$proto}->[1]; $code = $ProDef{$proto}->[2]; } elsif ($ipproto eq 'tcp' || $ipproto eq 'udp') { $sport = $ProDef{$proto}->[1]; $dport = $ProDef{$proto}->[2]; } } else { die "Invalid protocol $proto" unless getprotobyname($proto); $ipproto = $proto; } # Parse the user specified options if ($ipproto eq 'icmp') { while (@pdef) { my $arg = shift @pdef; if ($arg eq 'type') { $type = shift @pdef; } elsif ($arg eq 'code') { $code = shift @pdef; } else { die "Invalid protocol option $arg"; } } die "Invalid protocol option @pdef" if @pdef; if (defined $code && !defined $type) { die "Cannot specify ICMP code without type"; } if (defined $type) { die "Invalid ICMP type" unless ($type >= 0 && $type <= 255); if (defined $code) { die "Invalid ICMP code" unless ($code >= 0 && $code <= 255); } } return [$ipproto, $type, $code]; } elsif ($ipproto eq 'tcp' || $ipproto eq 'udp') { while (@pdef) { my $arg = shift @pdef; if ($arg eq 'sport') { $sport = ParsePort(shift @pdef); } elsif ($arg eq 'dport') { $dport = ParsePort(shift @pdef); } else { die "Invalid protocol option $arg"; } } die "Invalid protocol option @pdef" if @pdef; return [$ipproto, $sport, $dport]; } else { die "Invalid protocol option @pdef" if @pdef; return [$ipproto]; } } # Put an IP block in iptables format # 'IP_address' for hosts # 'IP_address/mask' for blocks sub FormatIptIP { my ($ip) = @_; ($ip->masklen() == 32) ? $ip->addr() : "$ip"; } # Returns true if the port pair restricts the port range sub IsPortRange { my ($loport, $hiport) = @_; return ($loport != 0 || $hiport != 65535); } # Put a port specification in iptables format sub FormatIptPort { my ($loport, $hiport) = @{$_[0]}; if ($loport == $hiport) { return $loport; } elsif ($loport == 0) { return ":$hiport"; } elsif ($hiport == 65535) { return "$loport:"; } else { return "$loport:$hiport"; } } # Put an ICMP specification in type/code format sub FormatIptICMP { my ($type, $code) = @_; return "$type/$code" if defined $code; return $type; } # Generate the source/dest address/port ("middle") portion of a rule sub GenerateMiddle { my ($backend, $source, $dest, $pdesc, %oopt) = @_; my ($proto, @popt) = @$pdesc; my $spart = ""; my $dpart = ""; $spart .= " -s " . FormatIptIP($source) if $source->masklen(); $dpart .= " -d " . FormatIptIP($dest) if $dest->masklen(); if ($proto eq 'tcp' || $proto eq 'udp') { $spart .= " --sport " . FormatIptPort($popt[0]) if $popt[0]; $dpart .= " --dport " . FormatIptPort($popt[1]) if $popt[1]; } elsif ($proto eq 'icmp') { $dpart .= " --icmp-type " . FormatIptICMP(@popt) if defined $popt[0]; } $spart .= " -i $oopt{iint}" if $oopt{iint}; $dpart .= " -o $oopt{oint}" if $oopt{oint}; return $spart . $dpart; } # Generate the reversed portion of a rule for stateless replies sub GenerateReverse { my ($backend, $source, $dest, $pdesc, %oopt) = @_; my ($proto, @popt) = @$pdesc; my $spart = ""; my $dpart = ""; $spart .= " -s " . FormatIptIP($dest) if $dest->masklen(); $dpart .= " -d " . FormatIptIP($source) if $source->masklen(); if ($proto eq 'icmp' && defined $popt[0]) { my ($type, $code) = @popt; $type = $ICMPPAIR{$type} if exists $ICMPPAIR{$type}; $dpart .= " --icmp-type " . FormatIptICMP($type, $code); } elsif ($proto eq 'tcp' || $proto eq 'udp') { $spart .= " --sport " . FormatIptPort($popt[1]) if $popt[1]; $dpart .= " --dport " . FormatIptPort($popt[0]) if $popt[0]; $dpart .= " ! --syn" if $proto eq 'tcp'; } $spart .= " -i $oopt{oint}" if $oopt{oint}; $dpart .= " -o $oopt{iint}" if $oopt{iint}; return $spart . $dpart; } # Parse the various sections of a rule # Returns: (command, pdesc, sip, dip, oopts) sub ParseRule { my ($command, $protname, @args) = @_; my ($sip, $dip, @popts, %oopt); while (@args) { my $token = shift @args; if ($token =~ /^type|code|sport|dport$/) { push @popts, $token, shift @args; } elsif ($token =~ /^iint|oint|chain$/) { $oopt{$token} = shift @args; } elsif ($token =~ /^oneway|log$/) { $oopt{$token} = 1; } else { if (!defined $sip) { $sip = BuildIPList($token); } elsif (!defined $dip) { $dip = BuildIPList($token); } else { die "Unrecognized keyword $token"; } } } my $pdesc = ParseProto($protname, @popts); return ($command, $pdesc, $sip, $dip, %oopt); } # Generate actual backend rules from metawall syntax sub GenerateRules { my ($chain, $backend, @args) = @_; my %actions = ( permit => 'ACCEPT', accept => 'ACCEPT', deny => 'DROP', drop => 'DROP', reject => 'REJECT' ); print "\n$COMMENT @args\n"; my ($command, $pdesc, $sip, $dip, %oopt) = ParseRule(@args); my $rulebeg = "$IPTABLES -A $chain"; $rulebeg .= " -p $pdesc->[0]" unless $pdesc->[0] eq 'ip'; my $action; if ($command eq 'jump') { die "No jump target" unless exists $oopt{chain}; $action = $oopt{chain}; } else { $action = $actions{$command}; } foreach my $src (@$sip) { foreach my $dst (@$dip) { my $rulemid = GenerateMiddle($backend, $src, $dst, $pdesc, %oopt); DoCommand("$rulebeg$rulemid -j LOG") if $oopt{log}; DoCommand("$rulebeg$rulemid -j $action"); if ($backend eq 'ipt' && $action eq 'ACCEPT' && !$oopt{oneway}) { my $rulerev; $rulerev = GenerateReverse($backend, $src, $dst, $pdesc, %oopt); if ($rulerev ne $rulemid) { DoCommand("$rulebeg$rulerev -j LOG") if $oopt{log}; DoCommand("$rulebeg$rulerev -j $action"); } } } } } # Display the usage message sub PrintUsage { print "Usage: $0 [-hnpvx] [-b backend] [-i chain] [rulefile ...]\n"; print " -b Select backend target (default: iptct)\n"; print " -h Print this help message\n"; print " -i Install metawall on the specified chain\n"; print " -n Print a list of builtin networks\n"; print " -p Print a list of builtin protocols\n"; print " -v Print the version number\n"; print " -x Execute rules instead of printing them\n"; print "\nRun 'perldoc $0' for more detailed documentation.\n"; exit; } # Display the currently defined protocols sub PrintProtocols { foreach my $key (sort keys %ProDef) { my $proto = $ProDef{$key}->[0]; print "$key\t$proto"; if ($proto eq 'icmp') { my ($type, $code) = @{$ProDef{$key}}[1,2]; print " type $type" if defined $type; print " code $code" if defined $code; } elsif ($proto eq 'tcp' || $proto eq 'udp') { my ($sport, $dport) = @{$ProDef{$key}}[1,2]; print " sport ", FormatIptPort($sport) if defined $sport; print " dport ", FormatIptPort($dport) if defined $dport; } print "\n"; } exit; } # Display the currently defined networks sub PrintNetworks { foreach my $key (sort keys %NetDef) { print "$key: @{$NetDef{$key}}\n"; } exit; } # Process a statement from the input configuration sub ProcessConfig { my ($cref, $backend, $command, @args) = @_; if ($command eq 'define') { die "Invalid define statement" unless @args == 2; my ($alias, $value) = @args; CheckSymbol($alias); $NetDef{$alias} = BuildIPList($value); } elsif ($command eq 'protocol') { die "Insufficient arguments for protocol statement" unless @args > 1; my $proto = shift @args; CheckSymbol($proto); warn "Redefining protocol $proto" if exists $ProDef{$proto}; $ProDef{$proto} = ParseProto(@args); } elsif ($command =~ /^permit|accept|deny|drop|reject|jump$/) { die "Insufficient arguments for $command" if @args < 3; GenerateRules($$cref, $backend, $command, @args); } elsif ($command eq 'chain') { die "Invalid chain statement" unless @args == 1; my $name = shift @args; CheckSymbol($name); print "\n$COMMENT $command $name\n"; CreateChain($name) unless exists $Chain{$name}; $$cref = $name; } else { die "Unrecognized keyword $command"; } } # # Main Code # # Process command line options my %opt; getopts('b:hi:npvx', \%opt); PrintUsage() if $opt{h}; PrintNetworks() if $opt{n}; PrintProtocols() if $opt{p}; if ($opt{v}) { print "$VERSION\n"; exit; } my $backend = lc($opt{b}) || 'iptct'; die "Unsupported backend $backend" unless $TARGETS{$backend}; $EXEC = $opt{x}; # Create the default chain my $chain = 'metawall'; CreateChain($chain, $backend) unless exists $Chain{$chain}; # Install any necessary initital rules into the main chain InitChain($chain, $backend); # Process configuration while (<>) { chop; next if /^\s*#/; next if /^\s*$/; ProcessConfig(\$chain, $backend, split); } # Install the metawall chain if necessary if ($opt{i}) { print "\n$COMMENT Installing metawall into $opt{i} chain\n"; DoCommand("$IPTABLES -P $opt{i} DROP"); DoCommand("$IPTABLES -F $opt{i}"); DoCommand("$IPTABLES -A $opt{i} -j metawall"); } __END__ =head1 NAME metawall.pl - Generate firewall rules using a metalanguage =head1 SYNOPSIS metawall.pl [-hnpvx] [-b I] [-i I] I =head1 DESCRIPTION Metawall is a tool that generates low-level packet filtering rules using a higher-level language that allows you to define your own protocols and netblock macros. A unique feature of metawall is its support for multiple backend targets, allowing you to generate stateful or stateless firewalls on different packet filtering platforms using the same source files. This release of metawall supports two backends. The 'iptct' backend is the default and generates stateful rules for iptables using the connection tracking module. The other option is 'ipt', which generates stateless iptables rules that do not require the connection tracking module. There will hopefully be more backend support in the future, with IP Filter being the next likely candidate. WARNING: This release of metawall is considered B quality, both because it has received only limited testing, and because the command language is still subject to change in future releases. Metawall is intended for use by someone who understands IP security; it is not a magic bullet that will secure your network. Metawall is provided as is without warranty of any kind, either expressed or implied. To paraphrase a well-known virus parody, "Metawall may recalibrate your refrigerator's coolness setting so all your ice cream goes melty, demagnetize the strips on all your credit cards, screw up the tracking on your television and use subspace field harmonics to scratch any CD's you try to play." =head1 PREREQUISITES Metawall was developed under Perl 5.6.1, though it may run on earlier versions. It requires the C module (available on CPAN). =head1 OPTIONS =over 4 =item -b I This option allows you to select the backend target for rule generation. Currently the valid selections are ipt or iptct. The default is iptct. See the BACKENDS section for more details. =item -h Display the help text. =item -i I Install the rule chain generated by metawall into the specified chain. This would usually be the FORWARD chain on a firewall, though you might wish to design rulesets for the INPUT and OUTPUT chains as well. Note that this option does not actually take effect unless -x is also being used. =item -n Display the definitions of the builtin network macros. Currently the only builtin netblock is RFC1918. =item -p Display the definitions of the builtin protocols. Some common application level protocols are predefined in the metawall script (e.g. http). These definitions can be overridden by the protocol statement, though doing so will generate a runtime warning. =item -v Display the metawall version number. =item -x Execute the iptables commands produced by metawall. The default is to just print the commands. =back =head1 BACKENDS Metawall supports different backend packet filtering systems. Backends are selected with the -b command line option. Details on each backend are below. =over 4 =item iptct The iptct backend generates iptables rules that require the connection tracking module. Connection tracking performs stateful monitoring of IP flows and can dynamically allow reply packets for existing connections. When using this backend, a rule will automatically be generated at the beginning of the main chain which permits all reply traffic to existing connections. =item ipt The ipt backend generates stateless iptables rules which do not require the connection tracking module. In stateless mode, metawall generates a pair of rules for each permit/accept command, one of which allows the reply traffic for the rule. This behavior can be suppressed by using the C option in a rule. =back =head1 COMMANDS The primary input to metawall is a rule file written in metawall's own language. Lines are processed individually with one rule per line. Blank lines and lines beginning with the hash mark (#) are ignored. Each line begins with a command which is followed by some number of parameters, depending on the specific command being used. The best way to learn to use these commands is probably by looking at the EXAMPLES section. Valid commands are listed below. =over 4 =item chain I A chain is simply a list of packet filtering rules that are executed sequentially. Metawall supports the iptables feature of multiple chains. The chain command selects the current chain and creates a new chain if necessary. All rules generated after the chain command become part of that chain. The jump command can be used to create a rule that transfers control to a different chain. The default chain is named metawall. =item define I I The define command is one of the most powerful and useful features of metawall. It allows you to define a symbolic name that will then represent an arbitrary list of network blocks when used in rules. In addition, if the list of network blocks can be merged into larger equivalent blocks, this will be done, courtesy of the NetAddr::IP compact function. The name must consist only of alphanumeric or underscore characters, beginning with an alpha. The netblock list is a comma separated list of hostnames or IP addresses with optional netmasks. Netmasks are separated from the network address by a slash and may be in either CIDR bit form (e.g. /24) or dotted-quad form (e.g. /255.255.255.0). The netblock list may contain more macros previously created by define commands. No spaces are allowed in the netblock list. There are some netblock macros built into the metawall script; these can be displayed with the -n option on the command line. =item protocol I I I The protocol command is similar to the define command, but instead of defining macros for network blocks it defines them for protocols. Metawall provides a number of builtin protocols whose definitions can be displayed by using the -p option on the command line. The name must follow the same rules as defined names. The base protocol may be a real IP protocol such as icmp, tcp, or udp or it may be another protocol already created by the protocol statement. If the IP protocol in question is icmp, tcp, or udp, you may specify additional options listed below. =over 4 =item code I This option is only valid with the ICMP protocol, and specifies the ICMP code field. Valid codes range from 0-255. You may not specify a code unless you also specify an ICMP type. =item dport I This option is only valid with the TCP or UDP protocol. It specifies the allowed port range for the destination port field. The port range may be a single integer or a colon-separated range. The number before or after the colon may be omitted when the lower or upper end of the range is 0 or 65535 respectively. This option is commonly used to distinguish between different application layer protocols, since they often use well known destination port numbers. =item sport I This option is only valid with the TCP or UDP protocol. It specifies the allowed port range for the source port field. See the dport option for info on the port range format. =item type I This option is only valid with the ICMP protocol, and specifies the ICMP type field. Valid types range from 0-255. =back =item deny/drop I The deny and drop commands are synonyms; they perform the same function. Any packet which matches the specified rule will be dropped, and processing of the remainder of the chain will terminate. See the RULES section for information on rule specification. =item reject I The reject command is similar to the deny/drop command, but in addition to dropping the packet it will generate an ICMP unreachable message back to the packet source. See the RULES section for information on rule specification. =item permit/accept I The permit and accept commands are synonyms; they perform the same function. Any packets which matches the specified rule will be permitted through, and processing of the remainder of the chain will terminate. The permit command is generally expected to allow a two-way traffic stream. Stateful backends will do this using the state table; on stateless backends a pair of rules will be generated, one for the forward traffic and one for replies. The C option can be used to override this behavior on stateless backends. See the RULES section for information on rule specification. =item jump I The jump command is used to transfer control to a different chain when the rule is matched. The rule associated with a jump command must include the chain option, which specifies which chain to jump to. See the RULES section for information on rule specification. =back =head1 RULES Most metawall commands produce packet filtering rules. Those commands include permit/accept, deny/drop, reject, and jump. Every rule has a similar format that looks like this: I I I [I] I [I] Actually, the parser allows everything after the protocol argument to be arbitrarily ordered, except that the source must come before the destination. It is suggested that you keep source and destination options grouped after the source and destination addresses for clarity, but this is not required. The protocol argument may be any protocol that is built into metawall (use the -p command line option for a list) or anything that has already been defined by using a protocol command. The source and destination arguments are formatted exactly the same as the right hand side of the C command. In other words, a comma separated list of networks or already defined macros. The keyword 'any' for the source or destination is equivalent to 0.0.0.0/0. Options may be specified anywhere in the line after the protocol argument. All options described in the protocol command in the COMMANDS section are supported here as well, and they will override options associated with the chosen protocol. These option definitions will not be repeated here. The following options are also supported: =over 4 =item chain I This option is only valid when used with the jump command. In that case, it specifies the name of the chain to jump to if the rule matches. =item iint I The C option specifies what interface the packet must be received on. The rule will not match if the packet was not input on the specified interface. =item log This option causes packets matching this rule to be logged via syslog. There is currently no way to modify the logging level, though this will probably be added in the next release. =item oint I The C option specifies what interface the packet will be transmitted on. The rule will not match if the packet will not be output on the specified interface. =item oneway This option suppresses the generation of the reply rule when using a stateless backend. This can be particularly useful when trying to permit certain ICMP traffic in one direction only, but it can be used with any protocol. =back =head1 EXAMPLES This section provides a number of examples of metawall command syntax. These examples are B recommendations of what rules you might want to use! The rules that are appropriate for any firewall must be determined according to the security policy at that site. These serve only to demonstrate the types of things that can be done with metawall. # Define some network macros define BOGONS RFC1918,0.0.0.0/8,127.0.0.0/8,169.254.0.0/16 define OURNET 192.0.2.0/24 define DNS_SERVERS 192.0.2.5,192.0.2.6 # We need special protocols protocol foobar tcp dport 2256 protocol quake3 udp dport 27960:27975 # Deny all traffic from bogus (spoofed) addresses deny ip BOGONS any # Prevent spoofing our addresses if eth1 is the outside deny ip OURNET iint eth1 any log # Allow some common outbound protocols permit ftp OURNET any permit ssh OURNET any permit http OURNET any permit https OURNET any # We also use the all-powerful foobar protocol permit foobar OURNET any # Allow external access for some services permit http any www.example.com permit dns any DNS_SERVERS permit quake3 any 192.0.2.50 # Allow zone transfers to our secondaries permit dnsxfr ns2.mydyndns.org,ns3.mydyndns.org DNS_SERVERS # Allow email to/from mailservers permit smtp 192.0.2.20 any permit smtp any 192.0.2.20 # See what outbound stuff we are dropping deny ip OURNET any log # Make sure everything else is dropped deny ip any any =head1 HISTORY =over 4 =item Version 0.10 Initial release with iptables support =back =head1 AUTHOR Steve Snodgrass =head1 COPYRIGHT Copyright (C)2002 Steven R. Snodgrass This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA =head1 AVAILABILITY The metawall home page can be found at L. =head1 ACKNOWLEDGEMENTS Thanks to Luis E. Munoz for his excellent NetAddr::IP module, and to my wonderful wife for allowing me to hide out in my den long enough to hack this thing together. :-) =head1 SCRIPT CATEGORIES Networking =head1 README Metawall is a perl script that allows you to write firewall rules in a simple metalanguage. These rules can then be used to generate packet filtering commands for a variety of backend targets.