Commit 2690be45 authored by Ryan Jackson's avatar Ryan Jackson

Initial client code and rules for Linux firewalls

Made the following changes to the clientside code to support Linux
firewalls:

- Made os_fwconfig_line() actually do something.
- getfwconfig() adds an 'IPS' hash to the fwinfo hash.  This contains
  the IP address for each host, much like how the 'MACS' hash contains
  the MAC address for each host.  This is needed because ebtables (which
  is needed for ARP proxying) doesn't resolve hostnames.

Rules are stored in firewall/iptables-fw-rules.  Syntax is similar to
fw-rules, but without the rule number (since iptables doesn't use rule
numbers).  These should be equivalent to our ipfw-based rules, but I
haven't tested every case yet to confirm this.  I'm sure some changes
will be necessary.
parent 4a65de87
......@@ -2304,6 +2304,7 @@ sub getfwconfig($$;$)
my @fwrules = ();
my @fwhosts = ();
my %fwhostmacs = ();
my %fwhostips = ();
my %fwsrvmacs = ();
$$infoptr = undef;
......@@ -2374,8 +2375,9 @@ sub getfwconfig($$;$)
push(@fwhosts,
"NAME=$host IP=$ip ALIASES=''");
# and save off the MACs
# and save off the MACs and IPs
$fwhostmacs{$host} = $mac;
$fwhostips{$host} = $ip;
} elsif ($line =~ /$spat/) {
my $srv = $1;
my $ip = $2;
......@@ -2461,6 +2463,10 @@ sub getfwconfig($$;$)
if (%fwhostmacs) {
$fwinfo->{"MACS"} = \%fwhostmacs;
}
if (%fwhostips) {
$fwinfo->{"IPS"} = \%fwhostips;
}
# make a pass over the rules, expanding variables
my $bad = 0;
......
......@@ -1220,18 +1220,257 @@ sub os_getnfsmounts($)
return 0;
}
sub os_fwconfig_line($@)
{
my ($fwinfo, @fwrules) = @_;
my ($upline, $downline);
my $errstr = "*** WARNING: Linux firewall not implemented\n";
sub os_fwconfig_line($@) {
my ($fwinfo, @fwrules) = @_;
my ($upline, $downline);
my $pdev;
my $vlandev;
my $myip;
my $mymask;
$myip = `cat $BOOTDIR/myip`;
chomp($myip);
$mymask = `cat $BOOTDIR/mynetmask`;
chomp($mymask);
warn $errstr;
$upline = "echo $errstr; exit 1";
$downline = "echo $errstr; exit 1";
if ($fwinfo->{TYPE} ne "iptables" && $fwinfo->{TYPE} ne "iptables-vlan") {
warn "*** WARNING: unsupported firewall type '", $fwinfo->{TYPE}, "'\n";
return ("false", "false");
}
return ($upline, $downline);
# XXX debugging
my $logaccept = defined($fwinfo->{LOGACCEPT}) ? $fwinfo->{LOGACCEPT} : 0;
my $logreject = defined($fwinfo->{LOGREJECT}) ? $fwinfo->{LOGREJECT} : 0;
my $dotcpdump = defined($fwinfo->{LOGTCPDUMP}) ? $fwinfo->{LOGTCPDUMP} : 0;
#
# Convert MAC info to a useable form and filter out the firewall itself
#
my $href = $fwinfo->{MACS};
while (my ($node,$mac) = each(%$href)) {
if ($mac eq $fwinfo->{OUT_IF}) {
delete($$href{$node});
} elsif ($mac =~ /^(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})$/) {
$$href{$node} = "$1:$2:$3:$4:$5:$6";
} else {
warn "*** WARNING: Bad MAC returned for $node in fwinfo: $mac\n";
return ("false", "false");
}
}
$href = $fwinfo->{SRVMACS};
while (my ($node,$mac) = each(%$href)) {
if ($mac eq $fwinfo->{OUT_IF}) {
delete($$href{$node});
} elsif ($mac =~ /^(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})$/) {
$$href{$node} = "$1:$2:$3:$4:$5:$6";
} else {
warn "*** WARNING: Bad MAC returned for $node in fwinfo: $mac\n";
return ("false", "false");
}
}
#
# VLAN enforced layer2 firewall with Linux iptables
#
if ($fwinfo->{TYPE} eq "iptables-vlan") {
if (!defined($fwinfo->{IN_VLAN})) {
warn "*** WARNING: no VLAN for iptables-vlan firewall, NOT SETUP!\n";
return ("false", "false");
}
$pdev = `$BINDIR/findif $fwinfo->{IN_IF}`;
chomp($pdev);
my $vlanno = $fwinfo->{IN_VLAN};
$vlandev = $pdev . '.' . $vlanno;
$upline = "modprobe 8021q\n";
$upline .= "vconfig add $pdev $vlanno > /dev/null\n";
$upline .= "ifconfig $vlandev up\n";
$upline .= "brctl addbr br0\n";
$upline .= "ifconfig br0 up\n";
$upline .= "brctl addif br0 $pdev\n";
$upline .= "brctl addif br0 $vlandev\n";
$upline .= "ifconfig br0 $myip netmask $mymask\n";
$upline .= "ip route flush dev br0\n";
$upline .= "ip route show | while read line; do\n";
$upline .= " echo \$line | grep 'dev $pdev' > /dev/null || continue\n";
$upline .= " new_route=`echo \$line | sed s/$pdev/br0/`\n";
$upline .= " ip route del \$line\n";
$upline .= " ip route add \$new_route\n";
$upline .= "done\n";
$upline .= "ifconfig $pdev 0.0.0.0\n";
$downline .= " ifconfig $pdev $myip netmask $mymask\n";
$downline .= " ip route flush dev $pdev\n";
$downline .= " ip route show | while read line; do\n";
$downline .= " echo \$line | grep 'dev br0' > /dev/null || continue\n";
$downline .= " new_route=`echo \$line | sed s/br0/$pdev/`\n";
$downline .= " ip route del \$line\n";
$downline .= " ip route add \$new_route\n";
$downline .= " done\n";
$downline .= " ip route flush dev br0\n";
$downline .= " ifconfig br0 down\n";
$downline .= " ifconfig $vlandev down\n";
$downline .= " brctl delif br0 $vlandev\n";
$downline .= " brctl delif br0 $pdev\n";
$downline .= " brctl delbr br0\n";
$downline .= " vconfig rem $vlandev > /dev/null\n";
#
# Setup proxy ARP entries.
#
if (defined($fwinfo->{MACS})) {
$upline .= "ebtables -t nat -F PREROUTING\n";
# publish servers (including GW) on inside and for us on outside
if (defined($fwinfo->{SRVMACS})) {
my $href = $fwinfo->{SRVMACS};
while (my ($ip,$mac) = each %$href) {
$upline .= "ebtables -t nat -A PREROUTING -i $vlandev " .
"-p ARP --arp-opcode Request " .
"--arp-ip-dst $ip -j arpreply " .
"--arpreply-mac $mac\n";
}
}
# provide node MACs to outside
my $href = $fwinfo->{MACS};
while (my ($node,$mac) = each %$href) {
my $ip = $fwinfo->{IPS}{$node};
$upline .= "ebtables -t nat -A PREROUTING -i $pdev " .
"-p ARP --arp-opcode Request " .
"--arp-ip-dst $ip -j arpreply " .
"--arpreply-mac $mac\n";
}
$upline .= "ebtables -t nat -A PREROUTING -p ARP " .
"--arp-ip-dst $myip -j ACCEPT\n";
$upline .= "ebtables -t nat -A PREROUTING -p ARP -j DROP\n";
if ($dotcpdump) {
$upline .= " tcpdump -i $vlandev ".
"-w $LOGDIR/in.tcpdump >/dev/null 2>&1 &\n";
$upline .= " tcpdump -i $pdev ".
"-w $LOGDIR/out.tcpdump not vlan >/dev/null 2>&1 &\n";
$downline .= " killall tcpdump >/dev/null 2>&1\n";
}
}
# XXX HACK ALERT
# The rules may contain hostnames which iptables will try to resolve.
# Normally this isn't a problem, but if we've set up a bridge device
# it may take a bit before the bridge starts letting packets through
# again.
$upline .= "sleep 30\n";
} else {
$upline .= "sysctl -w net.ipv4.ip_forward=1\n";
$downline .= "sysctl -w net.ipv4.ip_forward=0\n";
}
# XXX This is ugly. Older version of iptables can't handle source or
# destination hosts or nets in the format a,b,c,d. Newer versions of
# iptables automatically expand this to separate rules for each host/net,
# so we need to do the same thing here. Since a rule could contain
# multiple sources and multiple dests, we expand each separately.
my @new_rules;
foreach my $rule (@fwrules) {
$rulestr = $rule->{RULE};
if ($rulestr =~ /^(.+)\s+(-s|--source)\s+([\S]+)(.+)/) {
push @new_rules, "$1 $2 $_ $4" for split(/,/, $3);
} else {
push @new_rules, $rulestr;
}
}
@fwrules = ();
foreach my $rulestr (@new_rules) {
if ($rulestr =~ /^(.+)\s+(-d|--destination)\s+([\S]+)(.+)/) {
push @fwrules, "$1 $2 $_ $4" for split(/,/, $3);
} else {
push @fwrules, $rulestr;
}
}
@new_rules = ();
foreach my $rulestr (@fwrules) {
$rulestr =~ s/pdev/$pdev/g;
$rulestr =~ s/vlandev/$vlandev/g;
$rulestr =~ s/\s+me\s+/ $myip /g;
# Ugh. iptables wants port ranges in the form a:b, but
# our firewall variable expansion can contain a-b. Try
# to fix it.
$rulestr =~ s/\s+--dport\s+(\S+)-(\S+)/ --dport $1:$2/;
$rulestr =~ s/\s+--sport\s+(\S+)-(\S+)/ --sport $1:$2/;
if ($logaccept && $rulestr =~ /-j ACCEPT$/) {
if ($rulestr =~ /^iptables\s+/) {
push @new_rules, $rulestr;
$rulestr =~ s/ACCEPT$/LOG/;
} elsif ($rulestr =~ /^ebtables\s+/) {
$rulestr =~ s/ACCEPT$/--log -j ACCEPT/;
}
push @new_rules, $rulestr;
} elsif ($logreject && $rulestr =~ /-j (DENY|DROP)$/) {
my $action = $1;
if ($rulestr =~ /^iptables\s+/) {
push @new_rules, $rulestr;
$rulestr =~ s/$action$/LOG/;
} elsif ($rulestr =~ /^ebtables\s+/) {
$rulestr =~ s/$action$/--log -j $action/;
}
push @new_rules, $rulestr;
} else {
push @new_rules, $rulestr;
}
}
@fwrules = @new_rules;
foreach my $rulestr (@fwrules) {
if ($rulestr =~ /^iptables\s+/) {
$upline .= " $rulestr || {\n";
$upline .= " echo 'WARNING: could not load iptables rule:'\n";
$upline .= " echo ' $rulestr'\n";
$upline .= " iptables -F\n";
$upline .= " exit 1\n";
$upline .= " }\n";
} elsif ($rulestr =~ /^ebtables\s+/) {
$upline .= " $rulestr || {\n";
$upline .= " echo 'WARNING: could not load ebtables rule:'\n";
$upline .= " echo ' $rulestr'\n";
$upline .= " ebtables -F\n";
$upline .= " exit 1\n";
$upline .= " }\n";
}
}
# This is a brute-force way to flush all ebtables and iptables
# rules, delete all custom chains, and restore all built-in
# chains to their default policies. This will produce errors
# since not all tables exist for both tools, and not every
# chain exists for all tables, so all output is sent to /dev/null.
for my $table (qw/filter nat mangle raw broute/) {
$downline .=
" iptables -t $table -F > /dev/null 2>&1 || true\n";
$downline .=
" iptables -t $table -X > /dev/null 2>&1 || true\n";
$downline .=
" ebtables -t $table -F > /dev/null 2>&1 || true\n";
$downline .=
" ebtables -t $table -X > /dev/null 2>&1 || true\n";
for my $chain (qw/INPUT OUTPUT FORWARD PREROUTING POSTROUTING BROUTING/) {
$downline .=
" iptables -t $table -P $chain ACCEPT > /dev/null 2>&1 || true\n";
$downline .=
" ebtables -t $table -P $chain ACCEPT > /dev/null 2>&1 || true\n";
}
}
return ($upline, $downline);
}
sub os_fwrouteconfig_line($$$)
......
......@@ -12,7 +12,8 @@ TBDB = @TBDBNAME@
MDOPTS = --compact --skip-extended-insert --no-create-info --skip-set-charset
FW_SCRIPTS = initfwvars.pl
FW_FILES = open.sql closed.sql basic.sql elabinelab.sql
FW_FILES = open.sql closed.sql basic.sql elabinelab.sql iptables-open.sql iptables-closed.sql \
iptables-basic.sql iptables-elabinelab.sql
include $(OBJDIR)/Makeconf
......@@ -24,6 +25,9 @@ all: $(FW_SCRIPTS) $(FW_FILES)
include $(TESTBED_SRCDIR)/GNUmakerules
iptables-%.sql: genconfig-iptables.pl
$(SRCDIR)/genconfig-iptables.pl -f $(SRCDIR)/iptables-fw-rules -M $* > $@
%.sql: genconfig.pl
$(SRCDIR)/genconfig.pl -f $(SRCDIR)/fw-rules -M $* > $@
......
#!/usr/bin/perl -w
#
# EMULAB-COPYRIGHT
# Copyright (c) 2005-2011 University of Utah and the Flux Group.
# All rights reserved.
#
use Getopt::Std;
use English;
my $datafile = "fw-rules";
my $optlist = "eMIf:";
my $domysql = 0;
my $doiptables = 1;
my $expand = 0;
my $qualifiers = 0;
my @lines;
sub usage()
{
print "Usage: genconfig-iptables [-MI] config ...\n".
" -e expand EMULAB_ variables\n".
" -f file specify the input rules file\n".
" -q include qualifiers\n".
" -M generate mysql commands\n".
" -I generate iptables commands\n".
"\n".
" Valid configs are: open, closed, basic, elabinelab\n";
exit(1);
}
my %fwvars;
sub getfwvars()
{
# XXX for Utah Emulab as of 11/11
$fwvars{EMULAB_GWIP} = "155.98.36.1";
$fwvars{EMULAB_GWMAC} = "00:d0:bc:f4:14:f8";
$fwvars{EMULAB_NS} = "155.98.32.70";
$fwvars{EMULAB_CNET} = "155.98.36.0/22";
$fwvars{EMULAB_BOSSES} = "boss,subboss";
$fwvars{EMULAB_SERVERS} = "boss,subboss,ops";
$fwvars{EMULAB_MCADDR} = "234.0.0.0/8";
$fwvars{EMULAB_MCPORT} = "1025-65535";
}
sub expandfwvars($)
{
my ($rule) = @_;
getfwvars() if (!%fwvars);
if ($rule =~ /EMULAB_\w+/) {
foreach my $key (keys %fwvars) {
$rule =~ s/$key/$fwvars{$key}/g
if (defined($fwvars{$key}));
}
if ($rule =~ /EMULAB_\w+/) {
warn("*** WARNING: Unexpanded firewall variable in: \n".
" $rule\n");
}
}
return $rule;
}
sub doconfig($)
{
my ($config) = @_;
my $ruleno = 1;
my ($type, $style, $enabled);
if ($doiptables) {
print "# $config\n";
print "iptables -F\n";
print "iptables -X\n";
}
if ($domysql) {
$type = "iptables-vlan";
$style = lc($config);
# XXX
$style = "emulab" if ($style eq "elabinelab");
$enabled = 1;
print "DELETE FROM `default_firewall_rules` WHERE ".
"type='$type' AND style='$style';\n";
}
foreach my $line (@lines) {
next if ($line !~ /#.*$config/);
next if ($line =~ /^#/);
if ($line =~ /#\s*(\d+):.*/) {
$ruleno = $1;
} else {
$ruleno++;
}
my $qual;
if ($line =~ /#.*\+(\w+)/) {
$qual = $1;
}
($rule = $line) =~ s/\s*#.*//;
chomp($rule);
$rule = expandfwvars($rule) if ($expand);
if ($doiptables) {
print "$rule # config=$config";
print ", $qual only)"
if ($qualifiers && $qual);
print "\n";
}
if ($domysql) {
if ($qualifiers) {
print "INSERT INTO `default_firewall_rules` VALUES (".
"'$type','$style',$enabled,$qual,$ruleno,'$rule');\n";
} else {
print "INSERT INTO `default_firewall_rules` VALUES (".
"'$type','$style',$enabled,$ruleno,'$rule');\n";
}
}
}
print "\n";
}
%options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"M"})) {
$domysql = 1;
$doiptables = 0;
}
if (defined($options{"I"})) {
$doiptables = 1;
$domysql = 0;
}
if (defined($options{"e"})) {
$expand = 1;
}
if (defined($options{"f"})) {
$datafile = $options{"f"};
}
if (defined($options{"q"})) {
$qualifiers = 1;
}
if (@ARGV == 0) {
usage();
}
@lines = `cat $datafile`;
foreach my $config (@ARGV) {
$config = uc($config);
doconfig($config);
}
exit(0);
#
# EMULAB-COPYRIGHT
# Copyright (c) 2005-2011 University of Utah and the Flux Group.
# All rights reserved.
#
#
# Firewall rule template.
#
# Each line consists of an iptables or ebtables rule, a '#' denoted "comment"
# at the end of the line indicates a rule number to use, a comma separated
# list of styles to which the rule applies, and an optional qualifier that
# indicates the types of firewalled nodes to which the rule should apply.
#
# Styles:
#
# OPEN allows everything
# CLOSED allows only Emulab infrastructure services
# BASIC CLOSED + ssh from anywhere
# ELABINELAB Elab-in-elab, eliminates many Emulab services
#
# Qualifiers:
#
# WINDOWS For nodes running some variant of Windows
# SAMENET For nodes that are on the same subnet as any
# "control" host (boss, subbosses, ops, fs).
#
# Note that currently, we do not support the qualifier. Rules with a
# qualifier are applied unconditionally to the style which they are a part of.
#
# Variables expanded by rc.firewall script that can be used here:
#
# EMULAB_GWIP IP address of gateway
# EMULAB_NS IP address of name server
# EMULAB_CNET Node control network in CIDR notation
# EMULAB_MCADDR Multicast address range used by frisbee
# EMULAB_MCPORT Port range used by frisbee
# EMULAB_BOSSES Comma separated list of subbosses (including "boss"),
# used for services that subbosses provide
# (dhcp/tftp/frisbee).
# EMULAB_SERVERS Comma separated list of all servers
# (EMULAB_BOSSES + "ops" + "fs")
#
# Currently these are sufficient for rules we use. Note that you can
# safely use symbolic hostnames "boss", "ops", "fs", "users", "ntp1"
# and "ntp2" as they are all guaranteed to resolve, either via the local
# hosts file or via DNS (assuming the firewall is not yet up or allows
# DNS traffic, which it should at that point in time).
#
# For an Emulab in Emulab setup, the names "myboss", "myops" and "myfs"
# are also valid for naming the respective inner servers.
#
# Additionally, the tokens 'pdev', 'vlandev', and 'me' will be replaced
# with the physical control net device, the VLAN device, and the firewall's
# control net IP address respectively.
#
#
# Set up default policies for the standard chains
# For all but the wide-open case, the default should
# be to DROP.
#
iptables -P INPUT DROP # BASIC,CLOSED,ELABINELAB
iptables -P OUTPUT DROP # BASIC,CLOSED,ELABINELAB
iptables -P FORWARD DROP # BASIC,CLOSED,ELABINELAB
#
# Match existing dynamic rules very early
#
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # BASIC,CLOSED,ELABINELAB
iptables -A OUTPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # BASIC,CLOSED,ELABINELAB
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # BASIC,CLOSED,ELABINELAB
#
# Create a chain for forwarded/bridged packets coming
# from nodes on the vlan. If it already exists, flush
# it. Likewise for packets coming from nodes outside
# the vlan. Note that these don't affect packets sent
# to the firewall itself.
#
iptables -N INSIDE # BASIC,CLOSED,ELABINELAB
iptables -F INSIDE # BASIC,CLOSED,ELABINELAB
iptables -N OUTSIDE # BASIC,CLOSED,ELABINELAB
iptables -F OUTSIDE # BASIC,CLOSED,ELABINELAB
iptables -A FORWARD -m physdev --physdev-in vlandev -j INSIDE # BASIC,CLOSED,ELABINELAB
iptables -A FORWARD -m physdev --physdev-in pdev -j OUTSIDE # BASIC,CLOSED,ELABINELAB
# Can talk to myself. Does this do anything?
# This appears to be used by elvind?
#iptables -A INPUT -s me -d me -j ACCEPT # BASIC,CLOSED,ELABINELAB
#
# Nobody on the inside can talk to the firewall.
# Prevents anyone spoofing "me", "boss", "ops", etc.
#
iptables -A INSIDE -d me -j DROP # BASIC,CLOSED,ELABINELAB
#
# No one on the inside can talk to other experiments' nodes and visa-versa.
#
# XXX currently we only do this for the heavier weight firewalls because
# the user cannot override this.
#
# Note that this does not apply to nodes within this experiment because
# those packets never come to the firewall.
#
# Note also that EMULAB_CNET is only the "node control net" and does not
# include the public/private nets for boss, ops, etc.
#
# XXX yuk! The gateway *is* part of EMULAB_CNET, and assorted packets do
# come from it:
# * IGMP and PIM traffic
# * DHCP replies from boss appear to have come from the gateway
# (due to the helper function).
# so for now we allow any IP traffic from the gateway.
#
#
# XXX yuk 2! In a non-segmented control network or in a configuration with
# subbosses, some or all of the server machines will be a part of "the node
# control net" so we cannot unconditionally block all traffic to/from outside
# control net addresses. Here we allow through all traffic involving the known
# servers and let later rules further limit it.
#
iptables -A OUTSIDE -s EMULAB_SERVERS -j ACCEPT # CLOSED,ELABINELAB+SAMENET
iptables -A INSIDE -d EMULAB_SERVERS -j ACCEPT # CLOSED,ELABINELAB+SAMENET
iptables -A OUTSIDE -s EMULAB_GWIP -j ACCEPT # CLOSED,ELABINELAB
#
# Otherwise, nodes inside/outside of the firewall cannot talk to each other.
#
iptables -A INSIDE -d EMULAB_CNET -j DROP # CLOSED,ELABINELAB
iptables -A OUTSIDE -d EMULAB_CNET -j DROP # CLOSED,ELABINELAB
#
# Inside nodes cannot spoof other IP addresses.
#
# Beyond this rule we no longer have to check to make sure that source
# hosts like "boss" and "ops" come in the correct interface.
#
iptables -A INSIDE -s 0.0.0.0 -j ACCEPT # BASIC,CLOSED,ELABINELAB
iptables -A INSIDE -s 255.255.255.255 -j ACCEPT # BASIC,CLOSED,ELABINELAB
iptables -A INSIDE -s EMULAB_CNET -j ACCEPT # BASIC,CLOSED,ELABINELAB
iptables -A INSIDE -j DROP # BASIC,CLOSED,ELABINELAB