#
# Copyright (C) 2007, 2008 Red Hat, Inc.
# Authors:
# Thomas Woerner
#
# 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, see .
#
import os, os.path
import tempfile
import shutil
import types
import fw_services
import fw_icmp
from fw_config import _
from fw_functions import *
##############################################################################
class _Setting:
def __init__ (self, key, name, description=None, iptables=False,
ip6tables=False):
self.key = key
self.name = name
self.description = description
self.iptables = iptables
self.ip6tables = ip6tables
setting_list = [
_Setting("MODULES_UNLOAD", _("Unload modules on restart and stop"),
_("To ensure a sane state, the kernel firewall modules must be "
"unloaded when the firewall is restarted or stopped."),
True, True),
_Setting("SAVE_ON_STOP", _("Save on stop"),
_("Save the active firewall configuration with all changes since "
"the last start before stopping the firewall. Only do this if "
"you need to preserve the active state for the next start.")),
_Setting("SAVE_ON_RESTART", _("Save on restart"),
_("Save the active firewall configuration with all changes since "
"the last start before restarting the firewall. Only do this if "
"you need to preserve the active state for the next start.")),
_Setting("SAVE_COUNTER", _("Save and restore counter"),
_("Save on stop and Save on restart additionally "
"save rule and chain counter.")),
_Setting("STATUS_NUMERIC", _("Numeric status output"),
_("Print addresses and ports in numeric format for the status "
"output."),
True, True),
_Setting("STATUS_VERBOSE", _("Verbose status"),
_("Print information about the number of packets and bytes plus "
"the input- and outputdevice in the status "
"output.")),
_Setting("STATUS_LINENUMBERS", _("Status line numbers"),
_("Print a counter/number for every rule in the status output."),
True, True),
]
def getByKey(key):
for x in setting_list:
if x.key == key:
return x
return None
def getByName(name):
for x in setting_list:
if x.name == name:
return x
return None
##############################################################################
class ip4tablesConfig:
prefix = "IPTABLES_"
def __init__(self, filename):
self.filename = filename
self.clear()
def clear(self):
self.p_config = { }
self.set("%sMODULES" % self.prefix, [ ])
self.set("%sMODULES_UNLOAD" % self.prefix, "yes")
self.set("%sSAVE_ON_STOP" % self.prefix, "no")
self.set("%sSAVE_ON_RESTART" % self.prefix, "no")
self.set("%sSAVE_COUNTER" % self.prefix, "no")
self.set("%sSTATUS_NUMERIC" % self.prefix, "yes")
self.set("%sSTATUS_VERBOSE" % self.prefix, "no")
self.set("%sSTATUS_LINENUMBERS" % self.prefix, "yes")
def get(self, key):
if key in self.p_config.keys():
return self.p_config[key]
return None
def set(self, key, value):
if key[-8:] == "_MODULES":
self.p_config[key.strip()] = value
else:
self.p_config[key.strip()] = value.strip()
def __str__(self):
s = ""
for (key,value) in self.p_config.items():
if s:
s += '\n'
s += '%s = %s' % (key, value)
return s
# load self.filename
def read(self):
self.clear()
file = open(self.filename, "r")
for line in file.xreadlines():
if not line:
break
line = line.strip()
if len(line) < 1 or line[0] == '#':
continue
# get key/value pairs
p = line.split("=")
if len(p) != 2:
continue
key = p[0].strip()
value = p[1].strip()
# remove leading and trailing double quotes
if len(value) > 0 and value[0] == '"' and value[-1] == '"':
value = value[1:-1]
if key[-8:] == "_MODULES":
value = value.split()
self.p_config[key] = value
file.close()
# save to self.filename if there are key/value changes
def write(self):
if len(self.p_config) < 1:
# no changes: nothing to do
return
if os.path.exists(self.filename):
shutil.copy2(self.filename, "%s.old" % self.filename)
temp_dir = tempfile.mkdtemp()
temp_file = "%s/%s" % (temp_dir, "config")
fd = open(temp_file, "w")
modified = False
try:
file = open(self.filename, "r")
except:
file = None
else:
for line in file.xreadlines():
if not line: break
# remove newline at and on line
if line[-1:] == "\n":
line = line[:-1]
if len(line) < 1:
fd.write("\n")
continue
if line[0] != "#" and len(line) > 1:
p = line.split("=")
if len(p) != 2:
fd.write(line+"\n")
continue
key = p[0].strip()
value = p[1].strip()
# remove leading and trailing double quotes
if len(value) > 0 and value[0] == '"' and value[-1] == '"':
value = value[1:-1]
if key[-8:] == "_MODULES":
value = value.split()
if (key in self.p_config.keys() and \
self.p_config[key] != value) or \
key not in self.p_config.keys():
self._write(fd, key, self.p_config[key])
modified = True
del self.p_config[key]
else:
fd.write(line+"\n")
del self.p_config[key]
else:
fd.write(line+"\n")
# write remaining key/value pairs
if len(self.p_config) > 0:
fd.write("\n")
for (key,value) in self.p_config.items():
self._write(fd, key, value)
modified = True
if file:
file.close()
fd.close()
try:
file = open(self.filename, "w")
except:
shutil.rmtree(temp_dir)
raise IOError, "Permission denied: '%s'" % self.filename
os.chmod(self.filename, 0600)
# copy content
for line in open(temp_file, "r"):
file.write(line)
file.close()
shutil.rmtree(temp_dir)
def _write(self, fd, key, value):
if isinstance(value, types.ListType) or \
isinstance(value, types.TupleType):
val = " ".join(value)
else:
val = value
fd.write('%s="%s"\n' % (key, val))
##############################################################################
class ip6tablesConfig(ip4tablesConfig):
prefix = "IP6TABLES_"
##############################################################################
class iptablesClass:
prog = "iptables"
type = "ipv4"
def __init__(self, filename):
self.filename = filename
def write(self, conf):
if self.type == "ipv4":
reject_type = "icmp-host-prohibited"
else:
reject_type = "icmp6-adm-prohibited"
custom_mangle = [ ]
custom_nat = [ ]
custom_filter = [ ]
if conf.custom_rules and len(conf.custom_rules) > 0:
for (_type, table, filename) in conf.custom_rules:
if _type != self.type:
continue
# ignore missing files
if not os.path.exists(filename) or \
not os.path.isfile(filename):
continue
if table == "mangle":
custom_mangle.append(filename)
elif table == "nat":
custom_nat.append(filename)
elif table == "filter":
custom_filter.append(filename)
if os.path.exists(self.filename):
shutil.copy2(self.filename, "%s.old" % self.filename)
# do we have local or remote forwarding?
local_forward = False
remote_forward = False
if conf.forward_port:
for fwd in conf.forward_port:
if fwd.has_key("toaddr"):
remote_forward = True
else:
local_forward = True
mark_idx = 100
fd = open(self.filename, "w")
os.chmod(self.filename, 0600)
fd.write("# Firewall configuration written by system-config-firewall\n")
fd.write("# Manual customization of this file is not recommended.\n")
### MANGLE ###
if len(custom_mangle) > 0 or (self.type == "ipv4" and local_forward):
fd.write("*mangle\n")
fd.write(":PREROUTING ACCEPT [0:0]\n")
fd.write(":INPUT ACCEPT [0:0]\n")
fd.write(":FORWARD ACCEPT [0:0]\n")
fd.write(":OUTPUT ACCEPT [0:0]\n")
fd.write(":POSTROUTING ACCEPT [0:0]\n")
# custom rules
for filename in custom_mangle:
catFile(fd, filename)
if self.type == "ipv4" and \
(conf.forward_port and len(conf.forward_port) > 0):
for fwd in conf.forward_port:
if fwd.has_key("toaddr"):
continue
port = self._portStr(fwd["port"])
fwd["mark"] = mark_idx
mark_idx += 1
fd.write("-A PREROUTING -i %s -p %s --dport %s "
"-j MARK --set-mark 0x%x\n" % (fwd["if"],
fwd["proto"],
port,
fwd["mark"]))
fd.write("COMMIT\n")
### NAT ###
# no support for nat for netfilterv6 for now
if self.type == "ipv4" and \
((conf.masq and len(conf.masq) > 0) or len(custom_nat) > 0 or \
(conf.forward_port and len(conf.forward_port) > 0)):
fd.write("*nat\n")
fd.write(":PREROUTING ACCEPT [0:0]\n")
fd.write(":OUTPUT ACCEPT [0:0]\n")
fd.write(":POSTROUTING ACCEPT [0:0]\n")
# masquerading
if conf.masq:
for dev in conf.masq:
fd.write("-A POSTROUTING -o %s -j MASQUERADE\n" % dev)
# port forward
if conf.forward_port:
for fwd in conf.forward_port:
port = self._portStr(fwd["port"])
to = ""
mark = ""
if fwd.has_key("toaddr"):
to += fwd["toaddr"]
else:
mark = "-m mark --mark 0x%x " % fwd["mark"]
if fwd.has_key("toport"):
# the port range delimiter for DNAT is '-'
to += ":%s" % self._portStr(fwd["toport"], "-")
fd.write("-A PREROUTING -i %s -p %s --dport %s %s"
"-j DNAT --to-destination %s\n" % \
(fwd["if"], fwd["proto"], port, mark, to))
# custom rules
for filename in custom_nat:
catFile(fd, filename)
fd.write("COMMIT\n")
### FILTER ###
fd.write("*filter\n")
fd.write(":INPUT ACCEPT [0:0]\n")
fd.write(":FORWARD ACCEPT [0:0]\n")
fd.write(":OUTPUT ACCEPT [0:0]\n")
# INPUT
# accept established and related connections as early as possible
# RELATED is extremely important as it matches ICMP error messages
fd.write("-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT\n")
# icmp
self._icmp(conf, fd, "INPUT", reject_type)
# trust lo
fd.write("-A INPUT -i lo -j ACCEPT\n")
# Always allow ipv6-dhcp
if self.type == "ipv6":
fd.write("-A INPUT -m state --state NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT\n")
# trusted interfaces
if conf.trust:
for dev in conf.trust:
fd.write("-A INPUT -i %s -j ACCEPT\n" % dev)
# forward local
if self.type == "ipv4" and conf.forward_port:
for fwd in conf.forward_port:
if fwd.has_key("toaddr"):
continue
line = "-A INPUT -i %s -m state --state NEW -m %s -p %s" % \
(fwd["if"], fwd["proto"], fwd["proto"])
if fwd.has_key("toport"):
line += " --dport %s" % self._portStr(fwd["toport"])
line += " -m mark --mark 0x%x" % fwd["mark"]
line += " -j ACCEPT\n"
fd.write(line)
# open services
if conf.services and len(conf.services) > 0:
for service in conf.services:
svc = fw_services.getByKey(service)
for (port,proto) in svc.ports:
_state = ""
_dest = ""
_port = ""
if proto in [ "tcp", "udp" ]:
_state = "-m state --state NEW "
_proto = "-m %s -p %s " % (proto, proto)
else:
if self.type == "ipv4":
_proto = "-p %s " % proto
else:
_proto = "-m ipv6header --header %s " % proto
if port:
_port = "--dport %s " % port
if svc.destination.has_key(self.type):
_dest = "-d %s " % svc.destination[self.type]
fd.write("-A INPUT " + _state + _proto + _port + _dest +
"-j ACCEPT\n")
# open ports
if conf.ports and len(conf.ports) > 0:
for (ports, proto) in conf.ports:
fd.write("-A INPUT -m state --state NEW -m %s -p %s --dport %s "
"-j ACCEPT\n" % (proto, proto, self._portStr(ports)))
# FORWARD
if (conf.trust and len(conf.trust) > 0) or \
(self.type == "ipv4" and conf.masq and len(conf.masq) > 0) or \
(self.type == "ipv4" and remote_forward):
# accept established and related connections
fd.write("-A FORWARD -m state --state ESTABLISHED,RELATED "
"-j ACCEPT\n")
# icmp
self._icmp(conf, fd, "FORWARD", reject_type)
# trust lo
fd.write("-A FORWARD -i lo -j ACCEPT\n")
# trusted interfaces
if conf.trust:
for dev in conf.trust:
fd.write("-A FORWARD -i %s -j ACCEPT\n" % dev)
# allow to output to masqueraded interfaces (IPv4 only)
if self.type == "ipv4" and conf.masq:
for dev in conf.masq:
fd.write("-A FORWARD -o %s -j ACCEPT\n" % dev)
# forward remote
if self.type == "ipv4" and conf.forward_port and remote_forward:
for fwd in conf.forward_port:
if not fwd.has_key("toaddr"):
continue
if fwd.has_key("toport"):
port = self._portStr(fwd["toport"])
else:
port = self._portStr(fwd["port"])
fd.write("-A FORWARD -i %s -m state --state NEW "
"-m %s -p %s -d %s --dport %s "
"-j ACCEPT\n" % (fwd["if"], fwd["proto"],
fwd["proto"], fwd["toaddr"],
port))
# add custom filter rules
if len(custom_filter) > 0:
for _filename in custom_filter:
catFile(fd, _filename)
# reject remaining INPUT and OUTPUT
fd.write("-A INPUT -j REJECT --reject-with %s\n" % reject_type)
fd.write("-A FORWARD -j REJECT --reject-with %s\n" % reject_type)
# OUTPUT
# no output rules, yet
fd.write("COMMIT\n")
fd.close()
def _icmp(self, conf, fd, chain, reject_type):
if self.type == "ipv4":
proto = "-p icmp"
match = "-m icmp --icmp-type"
else:
proto = "-p ipv6-icmp"
match = "-m icmp6 --icmpv6-type"
for key in conf.block_icmp:
icmp = fw_icmp.getByKey(key)
if icmp.type and self.type not in icmp.type:
continue
fd.write("-A %s %s %s %s -j REJECT --reject-with %s\n" % \
(chain, proto, match, key, reject_type))
fd.write("-A %s %s -j ACCEPT\n" % (chain, proto))
def _portStr(self, port, delimiter=":"):
if len(port) == 1:
return "%s" % port
else:
return "%s%s%s" % (port[0], delimiter, port[1])
def _run(self, prog, arg, verbose=False):
cmd = "%s %s %s" % (prog, self.prog, arg)
if not verbose:
cmd += " >/dev/null 2>&1"
return os.system(cmd) >> 8
def start(self, verbose=False):
return self._run("/sbin/service", "start", verbose)
def restart(self, verbose=False):
return self._run("/sbin/service", "restart", verbose)
def condrestart(self, verbose=False):
return self._run("/sbin/service", "condrestart", verbose)
def status(self, verbose=False):
return self._run("/sbin/service", "status", verbose)
def stop(self, verbose=False):
return self._run("/sbin/service", "stop", verbose)
def chkconfig_on(self, verbose=False):
return self._run("/sbin/chkconfig", "on", verbose)
def chkconfig_off(self, verbose=False):
return self._run("/sbin/chkconfig", "off", verbose)
def unlink(self):
if os.path.exists(self.filename) and os.path.isfile(self.filename):
os.unlink(self.filename)
##############################################################################
class ip6tablesClass(iptablesClass):
prog = "ip6tables"
type = "ipv6"