pyroman-0.5.0~beta1/ 0000755 0001750 0001750 00000000000 12202236377 013266 5 ustar erich erich pyroman-0.5.0~beta1/examples/ 0000755 0001750 0001750 00000000000 12201515645 015101 5 ustar erich erich pyroman-0.5.0~beta1/examples/xml/ 0000755 0001750 0001750 00000000000 12201515645 015701 5 ustar erich erich pyroman-0.5.0~beta1/examples/xml/04_conntrack.xml 0000644 0001750 0001750 00000002771 11044712623 020716 0 ustar erich erich
pyroman-0.5.0~beta1/examples/xml/25_networks.xml 0000644 0001750 0001750 00000000373 10572163037 020613 0 ustar erich erich
pyroman-0.5.0~beta1/examples/xml/01_loopback.xml 0000644 0001750 0001750 00000000435 10572163037 020522 0 ustar erich erich
pyroman-0.5.0~beta1/examples/xml/10_interfaces.xml 0000644 0001750 0001750 00000000603 10572163037 021050 0 ustar erich erich
pyroman-0.5.0~beta1/examples/xml/03_standard_chains.xml 0000644 0001750 0001750 00000002123 11044712623 022047 0 ustar erich erich
pyroman-0.5.0~beta1/examples/xml/30_localhost.xml 0000644 0001750 0001750 00000001721 10572163037 020721 0 ustar erich erich
pyroman-0.5.0~beta1/examples/xml/20_services.xml 0000644 0001750 0001750 00000004024 12201515403 020537 0 ustar erich erich
pyroman-0.5.0~beta1/examples/xml/00_iptables-defaults.xml 0000644 0001750 0001750 00000001742 10572163037 022341 0 ustar erich erich
pyroman-0.5.0~beta1/examples/personal-firewall/ 0000755 0001750 0001750 00000000000 12201515645 020527 5 ustar erich erich pyroman-0.5.0~beta1/examples/personal-firewall/05_skype.py 0000644 0001750 0001750 00000001753 11617614126 022552 0 ustar erich erich """
Restrict skype.
This prevents most outgoing connections, which may trigger some corporate
firewalls. This is supposed to prevent your computer from becoming a supernode.
In order to use this, you must create a group "skype" and then set skype to
"setgid skype" so it is run as group skype. (Alternatively, you can use a
separate user to run skype as, and change --gid-owner to --uid-owner below)
If you just blindly add this ruleset, it will likely fail with "Bad value".
If you want to further secure your Skype usage, you might want to also block
access to any local IP addresses. It has been reported that skype can be
tricked into arbitrary connections. So it might allow access to private
services! You should migrate to an open source solution instead.
"""
# Low level iptables rules.
iptables(Firewall.output, "-p tcp -m owner --gid-owner skype -m multiport ! --dports 80,443 -j %s" % Firewall.reject)
iptables(Firewall.output, "-p udp -m owner --gid-owner skype -j %s" % Firewall.reject)
pyroman-0.5.0~beta1/examples/personal-firewall/10_interfaces.py 0000644 0001750 0001750 00000000133 10571554347 023533 0 ustar erich erich """
Treat all interfaces the same for the single-host setup.
"""
add_interface("any", "*")
pyroman-0.5.0~beta1/examples/personal-firewall/25_networks.py 0000644 0001750 0001750 00000000306 11620042010 023243 0 ustar erich erich """
Define the networks available here, or more precisely "hostgroups".
Many of your policy rules will probably target whole subnets.
"""
add_host(
name="ANY",
ip="0.0.0.0/0 ::/0",
iface="any"
)
pyroman-0.5.0~beta1/examples/personal-firewall/30_localhost.py 0000644 0001750 0001750 00000001406 11620042010 023355 0 ustar erich erich """
Simple definition for localhost.
The magic thing is that hostname is Firewall.hostname, so the rules are used
in the INPUT and OUTPUT chains instead of FORWARD.
"""
add_host(
name="localhost",
hostname=Firewall.hostname,
ip="0.0.0.0/0 ::/0",
iface="any"
)
# if you want a restriction on outgoing connections, add that here.
allow(
client="localhost",
server="ANY"
)
# only allow certain incoming connections:
allow(
client="ANY",
server="localhost",
service="ssh mdns www ping"
)
# if you want to protect some 'unprivileged ports', add that here
# reject(
# client="ANY",
# server="localhost"
# service="8080/tcp"
# )
# open unprivileged ports by default. Remove if you don't want that.
allow(
client="ANY",
server="localhost",
service="unprivileged"
)
pyroman-0.5.0~beta1/examples/example1/ 0000755 0001750 0001750 00000000000 12201515645 016615 5 ustar erich erich pyroman-0.5.0~beta1/examples/example1/32_mail.py 0000644 0001750 0001750 00000000776 10377107574 020441 0 ustar erich erich """
This is an example configuration for a mail server.
"""
# a really simple host definition
add_host(
name="mail",
ip="10.100.1.1",
iface="dmz"
)
# and offering a whole set of services with just one statement.
allow(
client="ANY DMZ INT",
server="mail",
service="mail www ssh ping"
)
# But we need to setup a NAT for this host
# This is a bidirection nat, i.e. this host will use the .79
# IP for outgoing connections, too.
add_nat(
client="ANY INT",
server="mail",
ip="12.34.56.79",
dir="both"
)
pyroman-0.5.0~beta1/examples/example1/21_extra_services.py 0000644 0001750 0001750 00000001600 10377113252 022514 0 ustar erich erich """
We'll define some extra services here.
"""
# Voice over IP
# These are not reliable - some ports are assigned dynamically...
# We rely on our SIP gateway to pick source ports between 7070 and 7080...
# Also this will allow connections from any source to any destination port
# given below, you could probably nail this down to signaling and data
# separately. On the long run, a connection tracking helper would be more
# helpful; otherwise you might need to completely expose your VoIP server.
add_service("sipout", dports="5060/udp 6112/udp 7070:7080/udp 8000/udp")
add_service("sipin",
dports="3478/udp 8000:65000/udp",
sports="3479/udp 5060/udp 15060/udp 6112/udp 7070:7080/udp 8000/udp"
)
add_service("voip", include="sipout sipin")
# Typical filesharing ports - Bittorrent
add_service("bittor", dports="6880:6890/tcp 696/tcp 16880:16882/tcp")
add_service("fshare", include="bittor")
pyroman-0.5.0~beta1/examples/example1/80_workstations.py 0000644 0001750 0001750 00000002045 10377113252 022246 0 ustar erich erich """
The workstation network are several hosts which are not to be accessible
from the outside, but which may access just about anything - although they
are mainly used for email and surfing anyway.
The remaining hosts in the DMZ network are treated similarly.
We don't define a host here - we use the earlier defined network.
Other "special" hosts in the network will be treated separately by earlier
rules, this is a mere "default" rule.
"""
# reject windows broadcasts and filesharing
reject(
client = "INT DMZ",
server = "ANY",
service = "fshare win"
)
# allow surfing and all kinds of internet applications
allow(
client = "INT DMZ",
server = "ANY"
# note: no service restrictions here
)
# we also need to NAT the hosts in these network
# this is an outgoing NAT (which is treated somewhat special, by
# applying the IP to the client instead of using it as filter
# but this syntax seemed more intuitive...
# if your DMZ uses real IPs, you can remove it from the clients
add_nat(
client="INT DMZ",
server="ANY",
ip="12.34.56.78",
dir="out"
)
pyroman-0.5.0~beta1/examples/example1/33_web.py 0000644 0001750 0001750 00000000733 10377113252 020254 0 ustar erich erich """
A really simple webserver configuration.
These examples are just boring... ;-)
But without NAT they would be even more boring. ;-)
"""
# web server
add_host(
name="web",
ip="10.0.1.2",
iface="dmz"
)
# offering, well, web service.
allow(
client="ANY DMZ INT",
server="web",
service="www ssh ping"
)
# internal hosts may access FTP, too
allow(
client="INT",
server="web",
service="ftp"
)
# setup NAT
add_nat(
client="ANY INT",
server="web",
ip="12.34.56.80"
)
pyroman-0.5.0~beta1/examples/example1/10_interfaces.py 0000644 0001750 0001750 00000001171 10377113252 021612 0 ustar erich erich """
In this example setup, we use named interfaces, as you can achieve by either
udev or ifrename. This is recommended, so your interfaces do not change names
by a kernel upgrade causing the drivers to be loaded in a different order
(and thus assigning different names to your physical interfaces).
This will effect your system on many levels, that's why it should be handled
on a hardware initialization level, and not within Pyroman (we could obviously
lookup interface names by MAC address, but that won't fix your routing!)
"""
add_interface("int", "ethINT tapVPN")
add_interface("dmz", "ethDMZ")
add_interface("ext", "ethEXT")
pyroman-0.5.0~beta1/examples/example1/03_standard_chains.py 0000644 0001750 0001750 00000002654 11044712623 022624 0 ustar erich erich """
Pyroman uses some standard chains, set in it's config.
These chains are used by the "allow()", "reject()" and "drop()" commandos
for nicer rule writing, and probably should do exactly that.
If you want maximal performance, you'll want to change these to ACCEPT and DROP
directly by calling 'Firewall.accept = "ACCEPT"' and removing the lines below.
The (small) benefits of using this approach is that you can easily disable
the rules (by modifying 'drop' and 'reject') without reloading your firewall
and that you get complete traffic counters in these chains.
The variables "Firewall.accept", "Firewall.drop" and "Firewall.reject" are
used here, so you can change them in one place only.
"""
Firewall.accept="accept"
add_chain(Firewall.accept)
# Kernel and iptables can do new string matches?
if Firewall.iptables_version(min="1.3.4") and \
Firewall.kernel_version(min="2.6.12"):
# Drop bittorrent traffic
iptables(Firewall.accept, '-m string --string "BitTorrent protocol" ' + \
'--algo bm --from 0 --to 100 -j DROP')
# add accept default rule to the chain
iptables(Firewall.accept, "-j ACCEPT")
# this is a silent drop
Firewall.drop="drop"
add_chain(Firewall.drop)
iptables(Firewall.drop, "-j DROP")
# .. these are clean "reject" rules (i.e. send 'connection refused' back)
Firewall.reject="reject"
add_chain(Firewall.reject)
iptables(Firewall.reject, "-p tcp -j REJECT --reject-with tcp-reset")
iptables(Firewall.reject, "-j REJECT")
pyroman-0.5.0~beta1/examples/example1/06_ssh_scanner_block.py 0000644 0001750 0001750 00000003362 10377113252 023160 0 ustar erich erich """
NOTE: this rule contains an interface name (ethEXT) hardcoded!
Replace this with your external interface to apply this rule to incoming
connections only!
SSH scanners are rather annyoing and may pose a security risk if you are
unable to enforce a good password policy on all your machines.
The following rules (with optional logging) will drop incoming SSH
connections on a per-host basis if they come in too quickly.
The rate of 5/60s is arbitrary, but worked just fine to make SSH scanners
give up without interrupting regular users at all and without allowing
to many brute-force tries.
Note that if you e.g. have a script which will log in to many SSH servers
quickly, you should either whitelist your source host or disable this, since
such a script can easily trigger these rules.
"""
iptables("INPUT", "-i ethEXT -p tcp --dport 22 -m state --state NEW \
-m recent --set --name SSH")
iptables("FORWARD","-i ethEXT -p tcp --dport 22 -m state --state NEW \
-m recent --set --name SSH")
# uncomment to log
#iptables("INPUT", "-i ethEXT -p tcp --dport 22 -m state --state NEW \
# -m recent --update --seconds 60 --hitcount 5 --rttl \
# --name SSH -j LOG --log-prefix \"SSH_brute_force \"")
#iptables("FORWARD", "-i ethEXT -p tcp --dport 22 -m state --state NEW \
# -m recent --update --seconds 60 --hitcount 5 --rttl \
# --name SSH -j LOG --log-prefix \"SSH_brute_force \"")
# Drop connections when they hit the treshold
iptables("INPUT", "-i ethEXT -p tcp --dport 22 -m state --state NEW \
-m recent --update --seconds 60 --hitcount 5 --rttl --name SSH -j DROP")
iptables("FORWARD","-i ethEXT -p tcp --dport 22 -m state --state NEW \
-m recent --update --seconds 60 --hitcount 5 --rttl --name SSH -j DROP")
pyroman-0.5.0~beta1/examples/example1/README 0000644 0001750 0001750 00000000246 10377113401 017473 0 ustar erich erich This is an enhanced configuration example for Pyroman.
Note that you need the "base" example, too!
This example contains some example "real-world" host definitions.
pyroman-0.5.0~beta1/examples/example1/98_log.py 0000644 0001750 0001750 00000001253 10377113252 020271 0 ustar erich erich """
This is a couple of rules to setup logging of rejected packets (if you want to)
"""
remove_cruft = True
logging_enabled = False
# remove cruft
if remove_cruft:
# reject invalid connections, don't log them
iptables("INPUT", "-m state --state INVALID -j DROP")
iptables("FORWARD", "-m state --state INVALID -j DROP")
iptables("OUTPUT", "-m state --state INVALID -j DROP")
# log unknown packets with a limit
if logging_enabled:
iptables("INPUT", "-j LOG -m limit --limit 1/sec --log-prefix \"I-unknown:\"")
iptables("FORWARD", "-j LOG -m limit --limit 1/sec --log-prefix \"F-unknown:\"")
iptables("OUTPUT", "-j LOG -m limit --limit 1/sec --log-prefix \"O-unknown:\"")
pyroman-0.5.0~beta1/examples/example1/31_firewall.py 0000644 0001750 0001750 00000003427 10377113252 021305 0 ustar erich erich """
This is our firewall host. Since we're going to use this policy on this host,
make sure the "hostname" propery is set to the value of the "hostname" command
so pyroman will detect that this is the local host.
When pyroman detects a "localhost", meaning hostname==Firewall.hostname, it
will put these rules into the "INPUT" and "OUTPUT" instead of the "FORWARD"
chains, so this is essential!
If you run Pyroman on only one host, it's safe to use Firewall.hostname here
just like we did with the broadcasts. If you want to use these rules on
multiple hosts (e.g. a failover firewall), you can setup different
policies this way but have the identical configuration files on both hosts!
The firewall has three interfaces with different policies and several IPs.
There are different services running on the different interfaces.
Alltogether this is the most complex host in our configuration...
(Note that in my setup I have two copies of this host, which only differ
by the hostname and some of the IPs, and they do a failover)
"""
# on the internal interface
add_host(
name="firewallI",
hostname="firewall",
ip="12.34.56.78 10.100.0.254 10.100.1.254",
iface="int"
)
# on the DMZ interface
add_host(
name="firewallD",
hostname="firewall",
ip="12.34.56.78 10.100.0.254 10.100.1.254",
iface="dmz"
)
# in the external network
add_host(
name="firewallE",
hostname="firewall",
ip="12.34.56.78",
iface="ext"
)
# service definitions for the firewall
allow(
client="INT",
server="firewallI",
service="http dns ssh ping heartb dhcp"
)
allow(
client="DMZ",
server="firewallD",
service="http dns ssh ping heartb"
)
allow(
client="ANY",
server="firewallE",
service="http dns ssh ping heartb openvpn"
)
# allow all outgoing connections by the firewall
allow(
client="firewallI firewallD firewallE"
)
pyroman-0.5.0~beta1/examples/example1/26_broadcasts.py 0000644 0001750 0001750 00000003313 10420467414 021624 0 ustar erich erich """
Broadcasts are a very special thing. We have to treat them like they
were addressed to localhost on any host. For this we use a tiny trick with
PyroMan - the Firewall.hostname variable.
Also note that there are different kinds of broadcasts. A client searching
for a DHCP server will (have to) use the address "255.255.255.255", while
a client with a real IP will use the broadcast suiteable for his network
and netmask.
DHCP clients will also use the source IP 0.0.0.0, we should treat that
properly. Maybe it would have been easier to just handle this using raw
iptables commands than the PyroMan framework...
We define a different pseudo host "broadcast" for each network, since we
have different broadcast addresses on each, and don't want to allow
neither incorrect broadcasts, nor have the same broadcast services on
each of the interfaces. A simpler setup would use only one broadcast vhost,
and assign all broadcast adresses to it.
"""
# internal broadcasts
add_host(
name="broadcastI",
hostname=Firewall.hostname, # always on localhost!
ip="255.255.255.255 10.0.0.255",
iface="int"
)
# broadcasts in DMZ network
add_host(
name="broadcastD",
hostname=Firewall.hostname, # always on localhost!
ip="255.255.255.255 10.0.1.255",
iface="dmz"
)
# broadcasts in external network
add_host(
name="broadcastE",
hostname=Firewall.hostname, # always on localhost!
ip="255.255.255.255 12.34.56.255",
iface="ext"
)
# Allow DHCP requests on internal interface (no valid client IP availble)
allow(server="broadcastI", service="dhcp")
# allow heartbeat (high-availability/failover) on all interfaces
# this is crypted, so no need to do any client restirctions
allow(server="broadcastI broadcastD broadcastE", service="heartb")
pyroman-0.5.0~beta1/examples/base/ 0000755 0001750 0001750 00000000000 12201515645 016013 5 ustar erich erich pyroman-0.5.0~beta1/examples/base/01_loopback.py 0000644 0001750 0001750 00000000324 11620042010 020437 0 ustar erich erich """
We don't want to have any restrictions on the loopback interface,
and want to have the allow rules early in the firewall rules
"""
ipXtables("INPUT", "-i lo -j ACCEPT")
ipXtables("OUTPUT", "-o lo -j ACCEPT")
pyroman-0.5.0~beta1/examples/base/04_conntrack.py 0000644 0001750 0001750 00000003334 11620042010 020636 0 ustar erich erich """
Use connection tracking for accepting established connections.
Allowing already established connections as well as related connections
(e.g. data connections for FTP control channels) is usually safe. This will
match most of the traffic, so the other rules only apply to NEW connections.
To stay true to the deny-everything-except-if-explicitly-allowed, rules for
NEW connections go into separate chains: input, output, and forward.
"""
Firewall.input = "input"
add_chain(Firewall.input)
# IPv6 only: drop RH0 type.
ip6tables("INPUT", "-m rt --rt-type 0 -j %s" % Firewall.drop)
# Both IPv4 and IPv6: allow established connections
ipXtables("INPUT", "-m conntrack --ctstate ESTABLISHED,RELATED -j %s" % Firewall.accept)
ipXtables("INPUT", "-m conntrack --ctstate INVALID -j %s" % Firewall.drop)
ipXtables("INPUT", "-m conntrack --ctstate NEW -j %s" % Firewall.input)
Firewall.output = "output"
add_chain(Firewall.output)
# IPv6 only: drop RH0 type.
ip6tables("OUTPUT", "-m rt --rt-type 0 -j %s" % Firewall.drop)
# Both IPv4 and IPv6: allow established connections
ipXtables("OUTPUT", "-m conntrack --ctstate ESTABLISHED,RELATED -j %s" % Firewall.accept)
ipXtables("OUTPUT", "-m conntrack --ctstate INVALID -j %s" % Firewall.drop)
ipXtables("OUTPUT", "-m conntrack --ctstate NEW -j %s" % Firewall.output)
Firewall.forward = "forward"
add_chain(Firewall.forward)
# IPv6 only: drop RH0 type.
ip6tables("FORWARD", "-m rt --rt-type 0 -j %s" % Firewall.drop)
# Both IPv4 and IPv6: allow established connections
ipXtables("FORWARD", "-m conntrack --ctstate ESTABLISHED,RELATED -j %s" % Firewall.accept)
ipXtables("FORWARD", "-m conntrack --ctstate INVALID -j %s" % Firewall.drop)
ipXtables("FORWARD", "-m conntrack --ctstate NEW -j %s" % Firewall.forward)
pyroman-0.5.0~beta1/examples/base/10_interfaces.py 0000644 0001750 0001750 00000000342 11044712623 021006 0 ustar erich erich """
We create our interface groups here.
"""
# setup an interface/group alias here, for example:
# add_interface("int", "eth0 ppp+")
# 'any interface' is a good choice for "never ever accept" rules.
add_interface("any", "*")
pyroman-0.5.0~beta1/examples/base/03_standard_chains.py 0000644 0001750 0001750 00000002024 11620042010 021773 0 ustar erich erich """
Pyroman uses some standard chains, set in its config.
These chains are used by the "allow()", "reject()" and "drop()" commands.
The (small) benefits of using this approach is that you can easily disable
the rules (by modifying 'drop' and 'reject') without reloading your firewall
and that you get complete traffic counters in these chains.
If you don't want to use them, you can just remove this file altogether.
The variables "Firewall.accept", "Firewall.drop" and "Firewall.reject" are
used here, so you can change them in one place only.
"""
# note that we're using the lowercase chain name
Firewall.accept = "accept"
add_chain(Firewall.accept)
ipXtables(Firewall.accept, "-j ACCEPT")
# this is a silent drop
Firewall.drop = "drop"
add_chain(Firewall.drop)
ipXtables(Firewall.drop, "-j DROP")
# .. these are clean "reject" rules (i.e. send 'connection refused' back)
Firewall.reject = "reject"
add_chain(Firewall.reject)
ipXtables(Firewall.reject, "-p tcp -j REJECT --reject-with tcp-reset")
ipXtables(Firewall.reject, "-j REJECT")
pyroman-0.5.0~beta1/examples/base/00_iptables-defaults.py 0000644 0001750 0001750 00000001600 11044604306 022266 0 ustar erich erich """
These are the iptables builtin chains.
In order to use the builtins in Pyroman, we need to define objects for them.
Note how we set chain policies!
"""
add_chain("INPUT", default="DROP")
add_chain("OUTPUT", default="DROP")
add_chain("FORWARD", default="DROP")
add_chain("OUTPUT", id="natOUT", default="ACCEPT", table="nat")
add_chain("PREROUTING", id="natPRE", default="ACCEPT", table="nat")
add_chain("POSTROUTING", id="natPOST", default="ACCEPT", table="nat")
add_chain("INPUT", id="manIN", default="ACCEPT", table="mangle")
add_chain("OUTPUT", id="manOUT", default="ACCEPT", table="mangle")
add_chain("FORWARD", id="manFWD", default="ACCEPT", table="mangle")
add_chain("PREROUTING", id="manPRE", default="ACCEPT", table="mangle")
add_chain("POSTROUTING", id="manPOST", default="ACCEPT", table="mangle")
pyroman-0.5.0~beta1/examples/base/20_services.py 0000644 0001750 0001750 00000005055 12201515403 020506 0 ustar erich erich """
We'll define some standard services now.
Usually, you could just give the port spec in the allow rules, but sometimes
it's more convenient to have an alias name, and you can also do grouping here.
The aliases below will allow you to write e.g. "ssh" instead of "ssh/tcp",
but some aliases are more complex (e.g. the 'mail' alias which covers all
common email ports) or the "www" alias for http and https ports.
"ping" on the other hand is clearly more readable than "echo-request/icmp".
In general it's up to the user whether or not he wants to use these aliases.
Ports can be given with their names (as of /etc/services), with port ranges
(in iptables syntax, i.e. 12:34) trailed by their protocol (12:34/tcp)
"""
### these are shorthands for very common services
# Ping
add_service("ping", dports="echo-request/icmp echo-request/icmpv6")
# Secure Shell
add_service("ssh", dports="ssh/tcp")
# Domain Name Server
add_service("dns", dports="domain/udp")
# Network Time Protocol
add_service("ntp", dports="ntp/udp")
# Auth / Ident service. Mainy used for IRC nowadays
add_service("auth", dports="auth/tcp")
# HTTP and HTTPS on different ports
add_service("http", dports="www/tcp")
add_service("https", dports="https/tcp")
add_service("www", include="http https")
# FTP
add_service("ftp", dports="ftp/tcp")
# Email protocols
add_service("smtp", dports="smtp/tcp")
add_service("ssmtp", dports="ssmtp/tcp")
add_service("pop3", dports="pop3/tcp")
add_service("pop3s", dports="pop3s/tcp")
add_service("imap", dports="imap/tcp")
add_service("imaps", dports="imaps/tcp")
add_service("submission", dports="submission/tcp")
add_service("mail", include="smtp ssmtp pop3 pop3s imap imaps")
# LDAP
add_service("ldap", dports="ldap/tcp")
# Heartbeat pings
add_service("heartb", dports="694/udp")
# OpenVPN tunnel
add_service("openvpn", dports="1194/udp")
# DHCP
add_service("dhcp", sports="bootpc/udp", dports="bootps/udp")
# Multicast DNS / Bonjour / Rendevouz / Avahi
add_service("mdns", sports="5353/udp", dports="5353/udp")
# IPSec (the first two are protocols, not ports!)
add_service("ipsec", dports="esp ah isakmp/udp")
# Windows shares are really annoying
# this is a set of 4 ports on two protocols and two directions each...
add_service("winTCPin", sports="137:139/tcp 445/tcp")
add_service("winTCPout", dports="137:139/tcp 445/tcp")
add_service("winUDPin", sports="137:139/udp 445/udp")
add_service("winUDPout", dports="137:139/udp 445/udp")
add_service("win", include="winTCPin winTCPout winUDPin winUDPout")
# unprivileged ports
add_service("unprivileged", dports="1024:65535/tcp 1024:65535/udp")
pyroman-0.5.0~beta1/examples/base/README 0000644 0001750 0001750 00000001250 11021725626 016672 0 ustar erich erich Pyroman "base" configuration files
These are Pyroman example configuration files. Pyroman ships with multiple
sets of configuration examples. This is the "base" example, which contains
rules you are unlikely to do without - they setup the framework in which
Pyroman operates by defining some basic services and such.
Your distribution probably has the configuration examples installed in
/usr/share/doc/pyroman/examples for your reference.
When you run pyroman with the "base" configuration only, the resulting
firewall will reject all connections (including outgoing ones) - there are no
hosts defined, and no "allow" statements in this setup. So it's up to you to
add these!
pyroman-0.5.0~beta1/examples/base/25_networks.py 0000644 0001750 0001750 00000001032 11620042010 020524 0 ustar erich erich """
Define the networks available here, or more precisely "hostgroups".
Many of your policy rules will probably target whole subnets.
Common "hostgroups" include your network zones and an "ANY" network which
applies to all hosts.
"""
# First is the internal network, we're using a /24 network here only
# and it's connected to our "internal" interface
#add_host(
# name="INT",
# ip="10.0.0.0/24",
# iface="int"
#)
# you probably will want a 'network' specification like this.
add_host(
name="ANY",
ip="0.0.0.0/0 ::/0",
iface="any"
)
pyroman-0.5.0~beta1/INSTALL 0000644 0001750 0001750 00000001014 10375202205 014303 0 ustar erich erich Installation:
Copy the contents of the "lib" directory to a location of your choice,
Recommendation: /usr/share/pyroman/ (should be fairly FHS compliant)
and modify the main pyroman command to use this path, so it will find
it's libraries.
Then create your rules in a directory of your choice, for example
/etc/pyroman, and put that path into the pyroman launcher, too.
You can find some example rules in examples/, copy all the basic
files and then have a look at the detailed example and adopt that to
suit your needs.
pyroman-0.5.0~beta1/doc/ 0000755 0001750 0001750 00000000000 12201515645 014030 5 ustar erich erich pyroman-0.5.0~beta1/doc/pyroman.8 0000644 0001750 0001750 00000007471 10466507136 015626 0 ustar erich erich .TH PYROMAN 8
.SH NAME
pyroman \- a firewall configuration utility
.SH SYNOPSIS
.hy 0
.na
.TP
.B pyroman
[
.B \-hvnspP
] [
.BI \-r " RULESDIR"
] [
.BI \-t " SECONDS"
]
.br
[
.B \-\-help
] [
.B \-\-version
] [
.B \-\-safe
] [
.B \-\-no\-act
]
.br
[
.B \-\-print
] [
.B \-\-print-verbose
] [
.BI \-\-rules= "RULESDIR"
]
.br
[
.BI \-\-timeout= "SECONDS"
] [
.B safe
]
.SH DESCRIPTION
.B pyroman
is a firewall configuration utility.
.PP
It will compile a set of configuration files to iptables statements to
setup IP packet filtering for you.
.PP
While it is not necessary for operating and using Pyroman, you should
have understood how IP, TCP, UDP, ICMP and the other commonly used
Internet protocols work and interact. You should also have understood the
basics of iptables in order to make use of the full functionality.
.PP
.B pyroman
does not try to hide all the iptables complexity from you, but tries to
provide you with a convenient way of managing a complex networks firewall.
For this it offers a compact syntax to add new firewall rules, while still
exposing access to add arbitrary iptables rules.
.SH OPTIONS
.PD 0
.TP
.BI \-r " RULESDIR," \-\-rules= "RULES "
Load the rules from directory
.I RULESDIR
instead of the default directory (usually
.B /etc/pyroman
)
.TP
.BI \-t " SECONDS," \-\-timeout= "SECONDS "
Wait
.I SECONDS
seconds after applying the changes for the user to type
.B OK
to confirm he can still access the firewall. This implies
.I \-\-safe
but allows you to use a different timeout.
.TP
.BR \-h ", " \-\-help
Print a summary of the command line options and exit.
.TP
.BR \-V ", " \-\-version
Print the version number of
.B pyroman
and exit.
.TP
.BR \-s ", " \-\-safe ", " safe
When the firewall was committed, wait 30 seconds for the user to type
.B OK
to confirm, that he can still access the firewall (i.e. the network
connection wasn't blocked by the firewall).
Otherwise, the firewall changes will be undone, and the firewall will be
restored to the previous state.
Use the
.BI \-\-timeout= "SECONDS"
option to change the timeout.
.TP
.BR \-n ", " \-\-no\-act
Don't actually run iptables. This can be used to check if
.B pyroman
accepts the configuration files.
.TP
.BR \-p ", " \-\-print
Instead of running iptables, output the generated rules.
.TP
.BR \-P ", " \-\-print-verbose
Instead of running iptables, output the generated rules. Each statement
will have one comment line explaining how this rules was generated. This
will usually include the filename and line number, and is useful for
debugging.
.SH CONFIGURATION
Configuration of pyroman consists of a number of files in the directory
.IR /etc/pyroman .
These files are in python syntax, although you do not need to be a python
programmer to use these rules. There is only a small number of statements
you need to know:
.TP
.B add_host
Define a new host or network
.TP
.B add_interface
Define a new interface (group)
.TP
.B add_service
Add a new service alias (note that you can always use
e.g. www/tcp to reference the www tcp service as defined in /etc/services)
.TP
.B add_nat
Define a new NAT (Network Address Translation) rule
.TP
.B allow
Allow a service, client, server combination
.TP
.B reject
Reject access for this service, client, server combination
.TP
.B drop
Drop packets for this service, client, server combination
.TP
.B add_rule
Add a rule for this service, client, server and target combination
.TP
.B iptables
Add an arbitrary iptables statement to be executed at beginning
.TP
.B iptables_end
Add an arbitrary iptables statement to be executed at the end
.TP
Detailed parameters for these functions can be looked up by caling
.nf
cd /usr/share/pyroman
pydoc ./commands.py
.fi
.SH BUGS
None known as of pyroman-0.4 release
.SH AUTHOR
.B pyroman
was written by Erich Schubert
.SH SEE ALSO
.BR iptables (8),
.BR iptables-restore (8)
.BR iptables-load (8)
pyroman-0.5.0~beta1/AUTHORS 0000644 0001750 0001750 00000000100 10403056107 014315 0 ustar erich erich Initial version written by:
Erich Schubert
pyroman-0.5.0~beta1/pyroman/ 0000755 0001750 0001750 00000000000 12201516557 014753 5 ustar erich erich pyroman-0.5.0~beta1/pyroman/rule.py 0000644 0001750 0001750 00000011303 11623006266 016270 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from service import Service
from port import PortInvalidSpec
from chain import Chain
from exception import PyromanException
class Rule:
"""
A rule is an allow/drop/reject statement for a certain combination of hosts
and services. They are processed in sequence, and grouped into chains with
similar filters to increase efficiency.
"""
def __init__(self,target,server,client,service,loginfo):
"""
Create a new rule, with given action, source, destination and service
target -- the action if the rule matches, e.g. do_accept
server -- host the packets are addressed to
client -- host the packets originate from
service -- service (i.e. ports) the packages use
loginfo -- user information about origin of this rule for errors
"""
# store parameters for later use
self.target = target
self.server = server
self.client = client
self.service = service
self.loginfo = loginfo
if not server and not client:
raise PyromanException("Rules need at least a server or a client at %s" % loginfo)
# a complete verification will be done when all user files have been
# processed (and thus no new services can be added any more)
def generate(self):
"""
Generate iptables-rules for this firewall rule.
"""
inface = ""
outface = ""
# look up interfaces
if self.client:
inface = self.client.iface
if self.server:
outface = self.server.iface
if inface == outface \
and self.client and not self.client.islocalhost() \
and self.server and not self.server.islocalhost():
return
# skip forwarding chains when not forwarding
if not Firewall.forwarding:
if not ((self.client and self.client.islocalhost()) or \
(self.server and self.server.islocalhost())):
return
chain = Chain.get_chain(inface, outface, \
self.client, self.server, self.loginfo)
vrules4, vrules6 = [""], [""]
if self.service:
vrules4 = self.service.get_filter("d", 4)
vrules6 = self.service.get_filter("d", 6)
for vr in vrules4:
chain.append4("%s -j %s" % (vr, self.target), self.loginfo)
for vr in vrules6:
chain.append6("%s -j %s" % (vr, self.target), self.loginfo)
def prepare(self):
"""
Prepare object by replacing string references with object pointers
"""
# already checked in verify run.
if self.server != "":
self.server = Firewall.hosts[self.server]
else:
self.server = None
# already checked in verify run.
if self.client != "":
self.client = Firewall.hosts[self.client]
else:
self.client = None
# already checked in verify run.
if self.service != "":
self.service = Firewall.services[self.service]
else:
self.service = None
def verify(self):
"""
Run some basic verifications on the rule
This will e.g. verify that the hosts referred to do exist, services are
properly defined and so on.
"""
# verify server name given
if self.server != "":
if not Firewall.hosts.has_key(self.server):
raise PyromanException("Rule refers to unknown host as server: '%s' at %s" \
% (self.server, self.loginfo))
# verify client name given
if self.client != "":
if not Firewall.hosts.has_key(self.client):
raise PyromanException("Rule refers to unknown host as client: '%s' at %s" \
% (self.client, self.loginfo))
# for services not yet defined, try to autocreate them
if self.service != "" and self.service not in Firewall.services:
try:
s = Service(name=self.service,sports="",dports=self.service,
include=None, loginfo=self.loginfo)
Firewall.services[self.service] = s
except PortInvalidSpec:
raise PyromanException("Rule refers to unknown service: '%s' at %s" \
% (self.service, self.loginfo))
pyroman-0.5.0~beta1/pyroman/port.py 0000644 0001750 0001750 00000007557 12202236247 016323 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
import re, socket
from exception import PyromanException
class PortInvalidSpec(Exception):
"""
Exception class for invalid port specifications
"""
def __init__(self, err, spec):
"""
Create new InvalidPortSpecification
"""
self.err = err
self.spec = spec
def __str__(self):
"""
Return error message
"""
return self.err
class Port:
"""
This class represents a single tcp/udp/icmp port
(or the whole protocol!)
It's instantiated from a string, which is parsed and checked
and it contains a to_filter method to generate an iptables filter
"""
# Split and verify syntax of statement
preg = re.compile("^(?:([a-z0-9\-]+|[0-9]+(?:\:[0-9]+)?)(?:/))?(tcp|udp|esp|ah|icmp|icmpv6|ipv6-icmp|esp|ah)$")
# verify port range
prreg = re.compile("^([0-9]+:)?[0-9]+$")
def __init__(self, spec):
"""
Initialize port from a specification string of the type "123/tcp"
If the string is not parseable, PortInavlidSpec is raised
"""
self.proto = ""
self.port = None
# if a spec is given, process
if spec != "":
m = self.preg.match( spec )
if m is None:
raise PortInvalidSpec("Invalid port specification: %s" % spec, spec)
self.port = m.group(1)
self.proto = m.group(2)
# if it's a named port, verify it's resolveable...
if self.proto in ["udp", "tcp"] and not self.prreg.match(self.port):
try:
socket.getservbyname(self.port, self.proto)
except socket.error:
raise PortInvalidSpec("Port %s/%s not defined in /etc/services" % (self.port, self.proto), spec)
def get_filter_proto(self):
"""
Return iptables rule to filter for this protocol as string
"""
if self.proto:
return "-p %s" % self.proto
def get_filter_port(self, dir):
"""
Return iptables rule to filter for this specific port as string
An appropriate protocol filter needs to be added, too (use the
get_filter_proto method for that). Note that source and destination
filters only make sense if they use the same protocol!
dir -- direction ("d" for destination or "s" for source filter)
"""
if not self.port:
return ""
if self.proto in ["tcp", "udp"]:
return "--" + dir + "port " + self.port
elif self.proto == "icmp":
# ICMP doesn't have source ports
if dir == "d":
return "--icmp-type " + self.port
else:
return ""
elif self.proto in [ "icmpv6", "ipv6-icmp"]:
# ICMP doesn't have source ports
if dir == "d":
return "--icmpv6-type " + self.port
else:
return ""
elif self.proto in ("esp", "ah"):
# no port for ESP and AH
return ""
else:
raise PyromanException("Unknown protocol: %s" % self.proto)
def forIPv4(self):
"""
Return true when compatible with IPv4
"""
return self.proto not in ["icmpv6", "ipv6-icmp"]
def forIPv6(self):
"""
Return true when compatible with IPv6
"""
return self.proto not in ["icmp", "ah"]
pyroman-0.5.0~beta1/pyroman/host.py 0000644 0001750 0001750 00000010405 11620042010 016260 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from util import Util
from exception import PyromanException
class Host:
"""
Represents a single host or a subnet with the same permissions
"""
def __init__(self, name, ip, iface, hostname="", loginfo=""):
"""
Create a new host object, with the given name, IP specification and interface
name -- Nickname for the host
ip -- IP specification for the host or subnet (e.g. "127.0.0.1 10.0.0.0/24")
iface -- Interface nickname this is connected to (only one!)
"""
# verify and store name
if name == "" and not Util.verify_name(name):
raise PyromanException("Host '%s' lacking a valid name at %s" \
% (name, iface, loginfo))
if Firewall.hosts.has_key(name):
raise PyromanException("Duplicate host specification: '%s' at %s" % (name, loginfo))
self.name = name
# verify and store IPs
if ip == "":
raise PyromanException("Host '%s' definition lacking IP address at %s" % (name, loginfo))
self.ip = Util.splitter.split(ip)
for i in self.ip:
if not Util.verify_ipnet(i):
raise PyromanException("IP specification '%s' invalid for host '%s' at %s" \
% (i, name, loginfo))
# verify and store interface
self.iface = iface
if iface == "":
raise PyromanException("Host definition '%s' lacking kernel interfaces at %s" \
% (name, loginfo))
# store "real" hostname (which may be longer than nick)
# this is used for "localhost detection"
self.hostname = hostname
# store loginfo
self.loginfo = loginfo
# register with firewall
Firewall.hosts[name] = self
def get_filter4(self, dir):
"""
Generate filter rules for this host by generating a list of
filter rules for all source specifications
dir -- either "d" or "s" for destination filter or source filter
"""
# when necessary, turn around filter directions
result = []
for i in self.ip:
if Util.verify_ip4net(i):
# for the "any" IP we don't need to print a parameter
if i == "0.0.0.0/0":
result.append("")
elif i != "":
result.append( "-%s %s" % (dir, i) )
return result
def get_filter6(self, dir):
"""
Generate filter rules for this host by generating a list of
filter rules for all source specifications
dir -- either "d" or "s" for destination filter or source filter
"""
# when necessary, turn around filter directions
result = []
for i in self.ip:
if Util.verify_ip6net(i):
# for the "any" IP we don't need to print a parameter
if i == "::/0":
result.append("")
elif i != "":
result.append( "-%s %s" % (dir, i) )
return result
def islocalhost(self):
"""
Check if the host is localhost by comparing the hostname given to the
hostname of the current machine
"""
return self.hostname == Firewall.hostname
def prepare(self):
"""
Prepare host for compilation
"""
# lookup interface
# this was verified in the verify run already
self.iface = Firewall.interfaces[self.iface]
def verify(self):
"""
Verify that the host is properly specified.
Verifies that the interface given was properly defined.
"""
if not Firewall.interfaces.has_key(self.iface):
raise PyromanException("Host '%s' is assigned interface '%s' which is not defined at %s" \
% (self.name, self.iface, self.loginfo))
pyroman-0.5.0~beta1/pyroman/chain.py 0000644 0001750 0001750 00000012741 11620042010 016372 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from exception import PyromanException
class Chain:
"""
Helper functions for chain management
"""
def make_chain_name(inface, outface, client, server):
"""
Generate chain name from interface and host names
The maximum length for iptables is 32 chars, this will be
checked and an error reported if the name becomes too long.
"""
if client and client.islocalhost():
if not outface:
outface = inface
inface = None
if server and server.islocalhost():
if not inface:
inface = outface
outface = None
# get ascii names
ifname, ofname, cname, sname = ("","","","")
if inface:
ifname = inface.name
if outface:
ofname = outface.name
if client:
cname = client.name
if server:
sname = server.name
chain = "%s_%s_%s_%s" % (ifname, ofname, cname, sname)
if len(chain) >= 32:
raise PyromanException("Chain name length too long, use shorter nicknames: %s" % chain)
return chain
make_chain_name = staticmethod(make_chain_name)
def get_chain(inface, outface, client, server, loginfo):
"""
Make a chain for the given hosts and add it to the interface chain
"""
chain = Chain.make_chain_name(inface, outface, client, server)
if not Firewall.chains.has_key(chain):
c = Chain(chain, loginfo)
parent = Firewall.forward
# if we are talking about localhost, things are different...
if client and client.islocalhost():
parent = Firewall.output
if not outface:
outface = inface
inface = None
if server and server.islocalhost():
parent = Firewall.input
if not inface:
inface = outface
outface = None
if not Firewall.chains.has_key(parent):
raise PyromanException("Unknown chain specified: %s" % parent)
p = Firewall.chains[parent]
# this is localhost talking to localhost...
if server and server.islocalhost() and client and client.islocalhost():
raise PyromanException("Localhost talking to localhost?")
crules4 = [""]
crules6 = [""]
srules4 = [""]
srules6 = [""]
ifrules = [""]
ofrules = [""]
if client:
crules4 = client.get_filter4("s")
crules6 = client.get_filter6("s")
if server:
srules4 = server.get_filter4("d")
srules6 = server.get_filter6("d")
if inface:
ifrules = inface.get_filter("s")
if outface:
ofrules = outface.get_filter("d")
for cr in crules4:
for sr in srules4:
for infi in ifrules:
for outfi in ofrules:
filter = "%s %s %s %s -j %s" % (infi, outfi, cr, sr, chain)
p.append4(filter, loginfo)
for cr in crules6:
for sr in srules6:
for infi in ifrules:
for outfi in ofrules:
filter = "%s %s %s %s -j %s" % (infi, outfi, cr, sr, chain)
p.append6(filter, loginfo)
Firewall.chains[chain]=c
return c
else:
return Firewall.chains[chain]
get_chain = staticmethod(get_chain)
def __init__(self, name, loginfo, default="-", table="filter"):
"""
Create a new chain
name -- Name for this chain
loginfo -- Why the chain was created, for error reporting
default -- default target for this chain (for INPUT, OUTPUT, FORWARD)
table -- table this chain resides in
"""
self.name = name
self.loginfo = loginfo
self.table = table
self.default = default
self.rules4 = []
self.rules6 = []
self.rules4_end = []
self.rules6_end = []
def append4(self, statement, loginfo):
"""
Append a statement to a chain
"""
self.rules4.append([statement, loginfo])
def append4_end(self, statement, loginfo):
"""
Append a statement to a chain
"""
self.rules4_end.append([statement, loginfo])
def append6(self, statement, loginfo):
"""
Append a statement to a chain
"""
self.rules6.append([statement, loginfo])
def append6_end(self, statement, loginfo):
"""
Append a statement to a chain
"""
self.rules6_end.append([statement, loginfo])
def get_init(self):
"""
Get the chain initializer statement
"""
return ":%s %s" % (self.name, self.default)
def get_rules4(self):
"""
Get the rules for IPv4
"""
lines = []
for r in self.rules4:
lines.append(["-A %s %s" % (self.name, r[0]), r[1]])
for r in self.rules4_end:
lines.append(["-A %s %s" % (self.name, r[0]), r[1]])
return lines
def get_rules6(self):
"""
Get the rules for IPv6
"""
lines = []
for r in self.rules6:
lines.append(["-A %s %s" % (self.name, r[0]), r[1]])
for r in self.rules6_end:
lines.append(["-A %s %s" % (self.name, r[0]), r[1]])
return lines
pyroman-0.5.0~beta1/pyroman/xmlsyntax.py 0000644 0001750 0001750 00000020360 11617622227 017377 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from util import Util
from exception import PyromanException
from xml.dom import minidom
import host, service, interface, nat, rule, chain
def parseXML(filename):
"""
Parse an XML file into pyroman statements.
filename -- filename to be parsed.
"""
tree = minidom.parse(filename)
docroot = tree.documentElement
assert(docroot.nodeName == "pyroman")
for node in docroot.childNodes:
if node.nodeType == node.ELEMENT_NODE:
_processNode(node, filename)
def _processNode(node, filename=None):
"""
Process a single node from the document tree
"""
if node.nodeName == "param":
_processParamNode(node, filename)
elif node.nodeName == "iptables":
_processIptablesNode(node, filename)
elif node.nodeName == "host":
_processHostNode(node, filename)
elif node.nodeName == "interface":
_processInterfaceNode(node, filename)
elif node.nodeName == "service":
_processServiceNode(node, filename)
elif node.nodeName == "chain":
_processChainNode(node, filename)
elif node.nodeName == "nat":
_processNatNode(node, filename)
elif node.nodeName == "allow" \
or node.nodeName == "drop" \
or node.nodeName == "reject" \
or node.nodeName == "rule":
_processRuleNode(node, filename)
def _processParamNode(node, filename=None):
"""
Process a node to represent a parameter assignment
"""
name = None
value = None
if node.hasAttribute("name"):
name = node.getAttribute("name")
else:
raise PyromanException("No parameter name given in param tag in %s" % filename)
if node.hasAttribute("value"):
value = node.getAttribute("value")
else:
raise PyromanException("No value given for parameter '%s' in %s" % (name, filename))
if name.startswith("Firewall."):
shortname = name[len("Firewall."):]
if not shortname in dir(Firewall):
raise PyromanException("Unknown parameter '%s' in %s" % (name, filename))
try:
setattr(Firewall, shortname, value)
except:
raise PyromanException("Setting parameter '%s' failed in %s" % (name, filename))
def _processIptablesNode(node, filename=None):
"""
Process a node equivalent to an iptables() command
"""
chain = None
filter = None
if node.hasAttribute("chain"):
chain = node.getAttribute("chain")
if node.hasAttribute("filter"):
filter = node.getAttribute("filter")
filter = filter.replace("*accept*", Firewall.accept)
filter = filter.replace("*drop*", Firewall.drop)
filter = filter.replace("*reject*", Firewall.reject)
filter = filter.replace("*input*", Firewall.input)
filter = filter.replace("*output*", Firewall.output)
filter = filter.replace("*forward*", Firewall.forward)
if chain == "*accept*":
chain = Firewall.accept
elif chain == "*drop*":
chain = Firewall.drop
elif chain == "*reject*":
chain = Firewall.reject
elif chain == "*input*":
chain = Firewall.input
elif chain == "*output*":
chain = Firewall.output
elif chain == "*forward*":
chain = Firewall.forward
if not Firewall.chains.has_key(chain):
raise PyromanException("Firewall chain %s not defined at %s" % (chain, filename))
Firewall.chains[chain].append(filter, filename)
def _processChainNode(node, filename=None):
"""
Process a node equivalent to an add_chain() command
"""
name = None
default = "-"
table = "filter"
id = None
if node.hasAttribute("name"):
name = node.getAttribute("name")
if name == "*accept*":
name = Firewall.accept
elif name == "*drop*":
name = Firewall.drop
elif name == "*reject*":
name = Firewall.reject
elif name == "*input*":
name = Firewall.input
elif name == "*output*":
name = Firewall.output
elif name == "*forward*":
name = Firewall.forward
if node.hasAttribute("default"):
default = node.getAttribute("default")
if node.hasAttribute("table"):
table = node.getAttribute("table")
if node.hasAttribute("id"):
id = node.getAttribute("id")
if not id: id = name
if Firewall.chains.has_key(id):
raise PyromanException("Firewall chain %s defined multiple times at %s" % (name, filename))
Firewall.chains[id] = chain.Chain(name, filename, default=default, table=table)
def _processServiceNode(node, filename=None):
"""
Process a node equivalent to an add_service() command
"""
dict = {}
if node.hasAttribute("name"):
dict["name"] = node.getAttribute("name")
if node.hasAttribute("sports"):
dict["sports"] = node.getAttribute("sports")
if node.hasAttribute("dports"):
dict["dports"] = node.getAttribute("dports")
if node.hasAttribute("include"):
dict["include"] = node.getAttribute("include")
if filename:
dict["loginfo"] = filename
service.Service(**dict)
def _processInterfaceNode(node, filename=None):
"""
Process a node equivalent to an add_interface() command
"""
dict = {}
if node.hasAttribute("name"):
dict["name"] = node.getAttribute("name")
if node.hasAttribute("iface"):
dict["iface"] = node.getAttribute("iface")
if filename:
dict["loginfo"] = filename
interface.Interface(**dict)
def _processHostNode(node, filename=None):
"""
Process a node equivalent to an add_host() command
"""
dict = {}
if node.hasAttribute("name"):
dict["name"] = node.getAttribute("name")
if node.hasAttribute("ip"):
dict["ip"] = node.getAttribute("ip")
if node.hasAttribute("iface"):
dict["iface"] = node.getAttribute("iface")
if node.hasAttribute("hostname"):
dict["hostname"] = node.getAttribute("hostname")
if dict["hostname"] == "*localhost*":
dict["hostname"] = Firewall.hostname
if filename:
dict["loginfo"] = filename
host.Host(**dict)
def _processRuleNode(node, filename=None):
"""
Process a node corresponding to an allow/drop/reject or add_rule rule
"""
target=None
server=""
client=""
service=""
if node.hasAttribute("server"):
server = node.getAttribute("server")
if node.hasAttribute("client"):
client = node.getAttribute("client")
if node.hasAttribute("service"):
service = node.getAttribute("service")
if node.hasAttribute("target"):
target = node.getAttribute("target")
elif node.nodeName == "allow":
target = "allow"
elif node.nodeName == "drop":
target = "drop"
elif node.nodeName == "reject":
target = "reject"
if server=="" and client=="" and service=="":
raise PyromanException("rule/allow/drop/reject node without any of server, client or service specified.")
if not target:
raise PyromanException("rule node without target!")
for srv in Util.splitter.split(server):
for cli in Util.splitter.split(client):
for svc in Util.splitter.split(service):
Firewall.rules.append(rule.Rule(target,srv,cli,svc,filename))
def _processNatNode(node, filename=None):
"""
Process a node corresponding to an add_nat statement
"""
client=""
server=None
ip=None
port=None
dport=None
dir="in"
if node.hasAttribute("client"):
client = node.getAttribute("client")
if node.hasAttribute("server"):
server = node.getAttribute("server")
if node.hasAttribute("ip"):
ip = node.getAttribute("ip")
if node.hasAttribute("port"):
port = node.getAttribute("port")
if node.hasAttribute("dport"):
dport = node.getAttribute("dport")
if node.hasAttribute("dir"):
dir = node.getAttribute("dir")
if not server or not ip:
raise PyromanException("Server not specified for NAT at %s" % filename)
if (dir == "out"):
(client, server) = (server, client)
Firewall.nats.append(nat.Nat(client, server, ip, port, dport, dir, filename))
pyroman-0.5.0~beta1/pyroman/interface.py 0000644 0001750 0001750 00000005603 11617622227 017273 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from util import Util
from exception import PyromanException
class Interface:
"""
Interfaces (or -groups) are used to filter based on physical networks
"""
def __init__(self, name, iface, loginfo):
"""
Create a new interface
name -- name for this interface (-group)
iface -- kernel interface names, e.g. "eth0 eth1"
"""
if name == "" or not Util.verify_name(name):
raise PyromanException("Interface lacking a valid name (name: %s, iface: %s) at %s" \
% (name, iface, loginfo))
if Firewall.interfaces.has_key(name):
raise PyromanException("Duplicate interface specification: %s at %s" % (name, loginfo))
if iface == "":
raise PyromanException("Interface definition lacking kernel interfaces: %s at %s" \
% (name, loginfo))
self.name = name
self.iface = Util.splitter.split(iface)
self.loginfo = loginfo
# register with firewall
Firewall.interfaces[name] = self
def get_filter(self, dir):
"""
Generate filter rules for this interface by generating a list of
filter rules for all source specifications
dir -- either "d" or "s" for destination filter or source filter
"""
idir = None
if dir == "d":
idir = "o"
elif dir == "s":
idir = "i"
else:
raise PyromanException("Unknown direction specified: %s" % dir)
# when necessary, turn around filter directions
result = []
for i in self.iface:
if i == "": continue
if i == "*":
result.append( "" )
else:
result.append( "-%s %s" % (idir, i) )
return result
def prepare(self):
"""
Prepare interface definition for generation
Nothing to do here for now
"""
pass
def verify(self):
"""
Verify that the interface definition is complete, in addition to the
checks at creation time. (Currenlty, no additional checks are done.)
"""
# no additional checks yet
pass
pyroman-0.5.0~beta1/pyroman/util.py 0000644 0001750 0001750 00000011053 11617622227 016304 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
import re, inspect, socket
from exception import PyromanException
class Util:
"""
Some utility functions for the firewall system
"""
# regexp used for splitting strings
splitter = re.compile("(?:,\s*|\s+)")
# regexp to ignore lines in manual rules
ignoreline = re.compile("^\s*(#|$)")
# nicknames may only contain these chars
namefilter = re.compile("^[a-zA-Z0-9]+$")
namefilter_service = re.compile("^[a-zA-Z0-9/]+$")
def get_callee(depth=3):
"""
Return information about the calling function
"""
frame = inspect.stack(depth)
return "%s:%d" % (frame[depth-1][1], frame[depth-1][2])
get_callee = staticmethod(get_callee)
def verify_ip4(ip):
"""
Verify that the given string is an IPv4 address
"""
try:
socket.inet_pton(socket.AF_INET, ip)
return True
except socket.error:
return False
verify_ip4 = staticmethod(verify_ip4)
def verify_ip6(ip):
"""
Verify that the given string is an IPv6 address
"""
try:
socket.inet_pton(socket.AF_INET6, ip)
return True
except socket.error:
return False
verify_ip6 = staticmethod(verify_ip6)
def verify_ip4net(ip):
"""
Verify that the given string describes an IPv4 network
"""
l = ip.split("/", 1)
if len(l) == 0:
return False
if not Util.verify_ip4(l[0]):
return False
if len(l) == 1:
return True
n = int(l[1])
return (n >= 0) and (n <= 32)
verify_ip4net = staticmethod(verify_ip4net)
def verify_ip6net(ip):
"""
Verify that the given string describes an IPv6 network
"""
l = ip.split("/", 1)
if len(l) == 0:
return False
if not Util.verify_ip6(l[0]):
return False
if len(l) == 1:
return True
n = int(l[1])
return (n >= 0) and (n <= 128)
verify_ip6net = staticmethod(verify_ip6net)
def verify_ip(ip):
"""
Verify that the given string is an IPv4 or IPv6 address
"""
return Util.verify_ip4(ip) or Util.verify_ip6(ip)
verify_ip = staticmethod(verify_ip)
def verify_ipnet(ip):
"""
Verify that the given string is an IPv4 or IPv6 address
"""
return Util.verify_ip4net(ip) or Util.verify_ip6net(ip)
verify_ipnet = staticmethod(verify_ipnet)
def verify_name(name, servicename=False):
"""
Verify that a name only contains a certain set of characters
to avoid problems with rule names derived from it.
name -- name to be checked
servicename -- Set to true to allow the / char additionally
"""
if servicename:
m = Util.namefilter_service.match(name)
if m:
return True
else:
m = Util.namefilter.match(name)
if m:
return True
return False
verify_name = staticmethod(verify_name)
def compare_versions(ver1, ver2):
"""
Compare to version numbers
returns True if ver1 is less or equal to ver2
ver1 -- first version number
ver2 -- second version number
"""
def compsplit(s):
"""
Split a version number component into a pair of (int,str)
"""
if not s[0].isdigit():
return (None, s)
for i in range(1,len(s)):
if not s[i].isdigit():
return (int(s[:i]),s[i:])
return (int(s),"")
v1c = ver1.split(".")
v2c = ver2.split(".")
minl = min(len(v1c),len(v2c))
for i in range(minl):
(v1, v1s) = compsplit(v1c[i])
(v2, v2s) = compsplit(v2c[i])
# one has digits, one hasn't?
if not v1: return -1
if not v2: return +1
if v1 != v2:
assert(cmp(v1,v2) != 0)
return cmp(v1, v2)
# compare remaining string
c = cmp(v1s,v2s)
if c != 0: return c
if len(v1c) < len(v2c): return -1
if len(v1c) > len(v2c): return +1
return 0
compare_versions = staticmethod(compare_versions)
pyroman-0.5.0~beta1/pyroman/pyroman.py 0000644 0001750 0001750 00000024024 11623006266 017012 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
import sys, socket, signal, re
from util import Util
from iptables import Iptables
#import iptables_parse
from exception import PyromanException
import subprocess
class Firewall:
"""
Main firewall class.
Note that the current code is NOT designed to support multiple
firewall objects, but only uses class variables.
hostname -- automatically filled with the computers hostname
timeout -- timeout for confirmation of the firewall by the user. 0 will disable auto-rollback
vercmd -- command to do an external firewall verification
accept -- target chain name for the accept() user command
drop -- target chain name for the drop() user command
reject -- target chain name for the reject() user command
services -- hashmap of known services
hosts -- hashmap of known hosts
interfaces -- hashmap of known interfaces
chains -- hashmap of known chains
nats -- list of NAT rules
rules -- list of firewall rules
"""
hostname = socket.gethostname()
# Timeout when the firewall setup will be rolled back when
# no OK is received.
timeout = 0
# Don't do external verification by default
vercmd = None
# Target names for the "accept", "drop" and "reject" commands
accept = "ACCEPT"
drop = "DROP"
reject = "REJECT"
input = "INPUT"
output = "OUTPUT"
forward = "FORWARD"
services = {}
hosts = {}
interfaces = {}
chains = {}
nats = []
rules = []
# forwarding firewall. Default to yes
forwarding = True
# for testing kernel version
kernelversioncmd = ["/bin/uname", "-r"]
_kernelversion = None
def __init__(self):
"""
Dummy initialization function, will raise PyromanException(an exception!)
"""
raise PyromanException("Instanciation not supported!")
class Error(Exception):
"""
Basic Exception class
"""
pass
def verify():
"""
Verify the data inserted into the firewall
"""
for s in Firewall.services.values():
s.verify()
for h in Firewall.hosts.values():
h.verify()
for i in Firewall.interfaces.values():
i.verify()
for r in Firewall.rules:
r.verify()
verify = staticmethod(verify)
def prepare():
"""
Prepare for generation run
"""
for s in Firewall.services.values():
s.prepare()
for h in Firewall.hosts.values():
h.prepare()
for i in Firewall.interfaces.values():
i.prepare()
for r in Firewall.rules:
r.prepare()
prepare = staticmethod(prepare)
def iptables_version(min=None, max=None):
"""
Return iptables version or test for a minimum and/or maximum version
min -- minimal iptables version required
max -- maximum iptables version required
"""
return Iptables.version(min=min, max=max)
iptables_version = staticmethod(iptables_version)
def generate():
"""
Generate the rules from the specifications given
"""
Firewall.prepare()
for r in Firewall.rules:
r.generate()
for n in Firewall.nats:
n.generate()
generate = staticmethod(generate)
def calciptableslines():
"""
Calculate the lines to be passed to iptables
"""
# prepare firewall rules
l4, l6 = [], []
# collect tables
tables = []
for c in Firewall.chains.values():
if not c.table in tables:
tables.append(c.table)
# process tables
for t in tables:
# try to provide some useful help info, in case some error occurs
l4.append( ["*%s" % t, "table select statement for table %s" % t] )
if t != "nat":
l6.append( ["*%s" % t, "table select statement for table %s" % t] )
# first create all chains
for c in Firewall.chains.values():
if c.table == t:
l4.append( [c.get_init(), c.loginfo] )
if t != "nat":
l6.append( [c.get_init(), c.loginfo] )
# then write rules (which might -j to a table not yet created otherwise)
for c in Firewall.chains.values():
if c.table == t:
for l in c.get_rules4():
l4.append(l)
if t != "nat":
for l in c.get_rules6():
l6.append(l)
# commit after each table, try to make a useful error message possible
l4.append(["COMMIT", "commit statement for table %s" % t ])
if t != "nat":
l6.append(["COMMIT", "commit statement for table %s" % t ])
return l4, l6
calciptableslines = staticmethod(calciptableslines)
def rollback(savedlines):
"""
Rollback changes to the firewall, and report rollback success to the user
savedlines -- saved firewall setting to be restored.
"""
# restore old iptables rules
restored = Iptables.restore(savedlines)
if restored:
sys.stderr.write("*"*70+"\n")
sys.stderr.write(" FIREWALL ROLLBACK FAILED.\n")
sys.stderr.write("*"*70+"\n")
else:
sys.stderr.write("Firewall initialization failed. Rollback complete.\n")
rollback = staticmethod(rollback)
def print_rules(verbose):
"""
Print the calculated rules, as they would be passed to iptables.
"""
r4, r6 = Firewall.calciptableslines()
print "#### IPv4 rules"
for line in r4:
if verbose:
# print reasoning
print "# %s" % line[1]
print line[0]
print "#### IPv6 rules"
for line in r6:
if verbose:
# print reasoning
print "# %s" % line[1]
print line[0]
print_rules = staticmethod(print_rules)
def execute_rules(terse_mode=False):
"""
Execute the generated rules, rollback on error.
If Firewall.timeout is set, give the user some time to accept the
new configuration, otherwise roll back automatically.
"""
def user_confirm_timeout_handler(signum, frame):
"""
This handler is called when the user does not confirm
firewall changes withing the given time limit.
The firewall will then be rolled back.
"""
raise Firewall.Error("Success not confirmed by user")
r4, r6 = Firewall.calciptableslines()
# Save old firewall.
if terse_mode:
sys.stderr.write("backing up current... ")
else:
sys.stderr.write("Backing up current firewall...\n")
savedlines = Iptables.save()
# parse the firewall setup
#try:
# parsed = iptables_parse.parse(savedlines)
#except:
# pass
# now try to execute the new rules
successful = False
try:
if terse_mode:
sys.stderr.write("activating new... ")
successful = Iptables.commit( (r4, r6) )
if terse_mode:
sys.stderr.write("success")
else:
sys.stderr.write("New firewall commited successfully.\n")
if Firewall.vercmd:
vcmd = subprocess.Popen(Firewall.vercmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = vcmd.communicate()
if len(result[0]) > 0 or len(result[1]) > 0:
if len(result[0]) > 0:
sys.stderr.write("Verification command output:\n")
sys.stderr.write(result[0])
if len(result[1]) > 0:
sys.stderr.write("Verification command error output:\n")
sys.stderr.write(result[1])
if vcmd.returncode != 0:
raise Firewall.Error("External verification command failed.")
if Firewall.timeout > 0:
sys.stderr.write("To accept the new configuration, type 'OK' within %d seconds!\n" % Firewall.timeout)
# setup timeout
signal.signal(signal.SIGALRM, user_confirm_timeout_handler)
signal.alarm(Firewall.timeout)
# wait for user input
input = sys.stdin.readline()
# reset alarm handling
signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)
if not re.search("^(OK|YES)", input, re.I):
raise Firewall.Error("Success not confirmed by user")
except Iptables.Error, e:
if terse_mode:
sys.stderr.write("error... restoring backup.\n")
else:
sys.stderr.write("*"*70+"\n")
sys.stderr.write("An Iptables error occurred. Starting firewall restoration.\n")
Firewall.rollback(savedlines)
# show exception
sys.stderr.write("%s\n" % e);
except Firewall.Error, e:
if terse_mode:
sys.stderr.write("error. Restoring old firewall.\n")
else:
sys.stderr.write("*"*70+"\n")
sys.stderr.write("A Firewall error occurred. Starting firewall restoration.\n")
Firewall.rollback(savedlines)
# show exception
sys.stderr.write("%s\n" % e);
except:
if terse_mode:
sys.stderr.write("error. Restoring old firewall.\n")
else:
sys.stderr.write("*"*70+"\n")
sys.stderr.write("An unknown error occurred. Starting firewall restoration.\n")
Firewall.rollback(savedlines)
sys.stderr.write("\nHere is the exception triggered during execution:\n")
raise
execute_rules = staticmethod(execute_rules)
def kernel_version(min=None, max=None):
"""
Return kernel version or test for a minimum and/or maximum version
min -- minimal kernel version required
max -- maximum kernel version required
"""
if not Firewall._kernelversion:
# query iptables version
kvcmd = subprocess.Popen(Firewall.kernelversioncmd,
stdout=subprocess.PIPE)
result = kvcmd.communicate()[0]
Firewall._kernelversion = result.strip()
# still no version number? - raise PyromanException(an exception)
if not Firewall._kernelversion:
raise Error("Couldn't get kernel version!")
if not min and not max:
return Firewall._kernelversion
if min:
if Util.compare_versions(Firewall._kernelversion, min) < 0:
return False
if max:
if Util.compare_versions(Firewall._kernelversion, max) > 0:
return False
return True
kernel_version = staticmethod(kernel_version)
pyroman-0.5.0~beta1/pyroman/iptables.py 0000644 0001750 0001750 00000015057 11623006266 017136 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
import sys, re
import subprocess
from util import Util
class Iptables:
"""
Interface to controlling iptables
"""
# iptables commands to use
ip4tablessave = [ "/sbin/iptables-save", "-c" ]
ip4tablesrestore = [ "/sbin/iptables-restore", "-c" ]
ip4tablesset = [ "/sbin/iptables-restore" ]
ip6tablessave = [ "/sbin/ip6tables-save", "-c" ]
ip6tablesrestore = [ "/sbin/ip6tables-restore", "-c" ]
ip6tablesset = [ "/sbin/ip6tables-restore" ]
iptablesversion = [ "/sbin/iptables", "--version" ]
# match for debugging errors
match_errorline = re.compile(r"^Error occurred at line: ([0-9]+)$")
match_errormsg = re.compile(r"^ip6?tables-restore(?: v[0-9.]+)?: (?:ip6?tables-restore: )?(.+)$")
match_hidemsg = re.compile(r"^Try `ip6?tables-restore -h' or 'ip6?tables-restore --help' for more information.$")
match_version = re.compile(r"^ip6?tables v([0-9]+\.[0-9.]+)$", re.M)
# version number cache
_version = None
# Handled error class
class Error(Exception):
"""
Basic exception class
"""
pass
def version(min=None, max=None):
"""
Return iptables version or test for a minimum and/or maximum version
min -- minimal iptables version required
max -- maximum iptables version required
"""
if not Iptables._version:
# query iptables version
ivcmd = subprocess.Popen(Iptables.iptablesversion,
stdout=subprocess.PIPE)
ivstr = ivcmd.communicate()[0]
m = Iptables.match_version.match(ivstr)
if m and m.group(1):
Iptables._version = m.group(1)
# still no version number? - raise an exception
if not Iptables._version:
raise Error("Couldn't get iptables version!")
if not min and not max:
return Iptables._version
if min:
if Util.compare_versions(Iptables._version, min) < 0: return False
if max:
if Util.compare_versions(Iptables._version, max) > 0: return False
return True
version = staticmethod(version)
def save():
"""
Dump current iptables ruleset into an array of strings
"""
# save old iptables status
scm4 = subprocess.Popen(Iptables.ip4tablessave,
stdout=subprocess.PIPE, stderr=sys.stderr)
sav4 = scm4.communicate()[0].split("\n")
scm6 = subprocess.Popen(Iptables.ip6tablessave,
stdout=subprocess.PIPE, stderr=sys.stderr)
sav6 = scm6.communicate()[0].split("\n")
return (sav4, sav6)
save = staticmethod(save)
def restore(savedlines):
"""
Restore iptables rules from a list of strings
(generated by iptables_save)
"""
sav4, sav6 = savedlines
# restore old iptables rules
rcmd4 = subprocess.Popen(Iptables.ip4tablesrestore,
stdin=subprocess.PIPE, stdout=sys.stderr, stderr=sys.stderr)
for line in sav4:
rcmd4.stdin.write(line)
rcmd4.communicate()
# restore old ip6tables rules
rcmd6 = subprocess.Popen(Iptables.ip6tablesrestore,
stdin=subprocess.PIPE, stdout=sys.stderr, stderr=sys.stderr)
for line in sav6:
rcmd6.stdin.write(line)
rcmd6.communicate()
# TODO: is there any sensible way to combine these error codes?
if rcmd4.returncode != 0:
return rcmd4.returncode
return rcmd6.returncode
restore = staticmethod(restore)
def commit(lines):
"""
Commit iptables rules from a list of (annotated!) commands
"""
lines4, lines6 = lines
# TODO: verify that the lines don't contain linewraps
# and have logging info!
scmd = subprocess.Popen(Iptables.ip4tablesset,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in lines4:
scmd.stdin.write(line[0])
scmd.stdin.write("\n")
scmd.stdin.close()
# output any error
errormsg = [ None, None ]
for line in scmd.stdout.readlines():
# skip empty lines
if line=="\n":
continue
# try to grab the error message with line number
if not errormsg[1]:
m = Iptables.match_errorline.match(line)
if m:
errormsg[1] = lines4[int(m.group(1))-1][1]
continue
# try to grab a detailed error description
if not errormsg[0]:
m = Iptables.match_errormsg.match(line)
if m:
errormsg[0] = m.group(1)
continue
# ignore the default "info" message
m = Iptables.match_hidemsg.match(line)
if m:
continue
# print remaining lines
sys.stderr.write(line)
success = (scmd.wait() == 0)
if not success:
if errormsg:
raise Iptables.Error("Firewall commit failed: %s, caused by %s" % (errormsg[0], errormsg[1]))
else:
raise Iptables.Error("Firewall commit failed due to unknown error.")
scmd = subprocess.Popen(Iptables.ip6tablesset,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in lines6:
scmd.stdin.write(line[0])
scmd.stdin.write("\n")
scmd.stdin.close()
# output any error
errormsg = [ None, None ]
for line in scmd.stdout.readlines():
# skip empty lines
if line=="\n":
continue
# try to grab the error message with line number
if not errormsg[1]:
m = Iptables.match_errorline.match(line)
if m:
errormsg[1] = lines6[int(m.group(1))-1][1]
continue
# try to grab a detailed error description
if not errormsg[0]:
m = Iptables.match_errormsg.match(line)
if m:
errormsg[0] = m.group(1)
continue
# ignore the default "info" message
m = Iptables.match_hidemsg.match(line)
if m:
continue
# print remaining lines
sys.stderr.write(line)
success = (scmd.wait() == 0)
if not success:
if errormsg:
raise Iptables.Error("Firewall commit failed: %s, caused by %s" % (errormsg[0], errormsg[1]))
else:
raise Iptables.Error("Firewall commit failed due to unknown error.")
return success
commit = staticmethod(commit)
pyroman-0.5.0~beta1/pyroman/exception.py 0000644 0001750 0001750 00000002474 11617622227 017334 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
class PyromanException(Exception):
"""
Representation of a Pyroman exception
"""
def __init__(self, message):
"""
Just inherit everything.
"""
Exception(self, message)
self.message = message
def __str__(self):
return self.message
pyroman-0.5.0~beta1/pyroman/nat.py 0000644 0001750 0001750 00000013167 11623046574 016123 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from util import Util
from port import Port, PortInvalidSpec
from chain import Chain
from exception import PyromanException
class Nat:
"""
Represents a Network Address Translation rule.
"""
def __init__(self, client, server, ip, port, dport, dir, loginfo):
"""
Create a new NAT rule
client -- clients allowed to access this NAT rule
server -- host nick the NAT is applied to
ip -- IP that is used in NAT
port -- Ports that are used in NAT
dport -- Destination port for single port redirections
dir -- incoming, outgoing or bidirecitonal NAT
Note that the NAT is always applied to the "server" host, the UI
accessible function is responsible to eventually exchange client
and server for "outgoing" NATs (where the naming of client, server
makes more sense the other way, think of workstations accessing
web server via a NAT)
"""
if server == "":
raise PyromanException("Nat lacking a server host (client: %s, server: %s, ip: %s) at %s" % (client, server, ip, loginfo))
if ip == "":
raise PyromanException("Nat lacking IP address: (client: %s, server: %s) at %s" % (client, server, loginfo))
if dir not in ["in", "out", "both"]:
raise PyromanException("Nat with invalid direction: (client: %s, server: %s, ip: %s, dir: %s) at %s" % (client, server, ip, dir, loginfo))
if not Util.verify_ip4(ip):
raise PyromanException("Nat with invalid IP address: (client: %s, server: %s, ip: %s) at %s" % (client, server, ip, loginfo))
if port:
try:
self.port = Port(port)
except PortInvalidSpec:
raise PyromanException("Nat port specification invalid: (client: %s, server: %s, ip: %s, port: %s) at %s " % (client, server, ip, port, loginfo))
if not self.port.forIPv4():
raise PyromanException("Non-IPv4 port specified: "+port)
else:
self.port = None
if dport:
try:
self.dport = Port(dport)
except PortInvalidSpec:
raise PyromanException("Nat dport specification invalid: (client: %s, server: %s, ip: %s, port: %s, dport: %s) at %s " % (client, server, ip, port, dport, loginfo))
if not self.dport.forIPv4():
raise PyromanException("Non-IPv4 port specified: "+dport)
else:
self.dport = None
if self.dport and not (self.port.proto == self.dport.proto):
raise PyromanException("Nat ports have different protocols: (client: %s, server: %s, ip: %s, port: %s, dport: %s) at %s" % (client, server, ip, port, dport, loginfo))
if dport and not port:
raise PyromanException("Nat with destination port, but no source port: (client: %s, server: %s, ip: %s, dport: %s) at %s" % (client, server, ip, dport, loginfo))
self.client = Util.splitter.split(client)
self.server = Util.splitter.split(server)
self.ip = ip
# port, dport are set above
self.dir = dir
self.loginfo = loginfo
def gen_snat(self, client, server):
"""
Internal helper function, with client, server objects
"""
iff = client.iface.get_filter("d")
target = "SNAT --to-source %s" % self.ip
# do we have a port restriction?
pfilter = ""
if self.port and self.dport:
pfilter = self.dport.get_filter_proto() + " " + self.dport.get_filter_port("s")
target = target + ":%s" % self.port.port
elif self.port:
pfilter = self.port.get_filter_proto() + " " + self.port.get_filter_port("s")
c = Firewall.chains["natPOST"]
for sip in server.ip:
filter = iff[0] + " -s %s" % sip
c.append4("%s %s -j %s" % (filter, pfilter, target), self.loginfo)
def gen_dnat(self, client, server):
"""
Internal helper function, with client, server objects
"""
iff = client.iface.get_filter("s")
filter = iff[0] + " -d %s" % self.ip
# do we have a port restriction?
pfilter = ""
if self.port:
pfilter = self.port.get_filter_proto() + " " + self.port.get_filter_port("d")
c = Firewall.chains["natPRE"]
for sip in server.ip:
target = "DNAT --to-destination %s" % sip
if self.dport:
target = target + ":%s" % self.dport.port
c.append4("%s %s -j %s" % (filter, pfilter, target), self.loginfo)
def generate(self):
for c in self.client:
for s in self.server:
client = Firewall.hosts[c]
server = Firewall.hosts[s]
# sanity checks, that should be moved to "verify"
if not client or not server:
raise PyromanException("Client or server not found for NAT defined at %s" % self.loginfo)
if client.iface == server.iface:
raise PyromanException("client interface and server interface match (i.e. cannot NAT!) for NAT defined at %s" % self.loginfo)
if self.dir in ["in", "both"]:
self.gen_dnat(client, server)
if self.dir in ["out", "both"]:
self.gen_snat(client, server)
pyroman-0.5.0~beta1/pyroman/service.py 0000644 0001750 0001750 00000011033 12201515403 016750 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from util import Util
from port import Port, PortInvalidSpec
from exception import PyromanException
class Service:
"""
A service can consist of several source and destination ports.
by including other services, any restriction on source-destination
port combinations is possible, also with different protocols.
"""
def __init__(self, name, sports="", dports="", include="", loginfo=""):
"""
Create new service object with string parameters
sports -- source port specifications like "www/tcp dns/udp"
dports -- destination port specifications
include -- include rules for other services to be included/aliased
loginfo -- reference line number for user error messages
"""
if name == "" or not Util.verify_name(name, servicename=True):
raise PyromanException("service lacking a proper name: '%s' at %s" \
% (name, loginfo))
if Firewall.services.has_key(name):
raise PyromanException("Duplicate service specification: '%s' at %s" % (name, loginfo))
if sports == "" and dports == "" and include == "" and (name != "ANY"):
raise PyromanException("service specification invalid: '%s' at %s" % (name, loginfo))
self.name = name
self.loginfo = loginfo
try:
self.sports = map( lambda p: Port(p), Util.splitter.split(sports) )
self.dports = map( lambda p: Port(p), Util.splitter.split(dports) )
except PortInvalidSpec, p:
raise PyromanException("Service '%s' contains invalid port spec '%s' at %s: %s" \
% (name, p.spec, loginfo, p.err))
# store includes, cannot be verified yet
self.include = []
if include:
self.include = Util.splitter.split(include)
# register with firewall object
Firewall.services[name] = self
def get_filter(self, dir, v4v6):
"""
Generate filter rules for this service by generating a list of
filter rules for all source and destination port combinations
dir -- either "d" or "s" for destination filter or source filter
v4v6 -- either 4 or 6 for IPv4 or IPv6
"""
# set 1 and 2 to source/dest filter characters
if dir == "d":
dir1 = "s"
dir2 = "d"
elif dir == "s":
dir1 = "d"
dir2 = "s"
else:
raise PyromanException("Invalid direction specified: %s" % dir)
result = []
for sp in self.sports:
if v4v6 == 4 and not sp.forIPv4(): continue
if v4v6 == 6 and not sp.forIPv6(): continue
for dp in self.dports:
if v4v6 == 4 and not dp.forIPv4(): continue
if v4v6 == 6 and not dp.forIPv6(): continue
# only generate rules when source and destination protocol match
if not sp.proto or not dp.proto or sp.proto == dp.proto:
f1 = ""
if sp.proto:
f1 = sp.get_filter_proto() + " "
elif dp.proto:
f1 = dp.get_filter_proto() + " "
f2 = sp.get_filter_port(dir1)
f3 = dp.get_filter_port(dir2)
if f1.strip() or f2 != "" or f3 != "":
result.append( f1 + " " + f2 + " " + f3 )
for i in self.include:
result.extend( i.get_filter(dir, v4v6) )
return result
def prepare(self):
"""
Prepare for generation run by looking up includes
"""
# look up includes, was verified in verify run
self.include = map( lambda s: Firewall.services[s], self.include )
def verify(self):
"""
Verify that the service doesn't try to include a service which is not
defined. Future versions might want to add a loop detection.
"""
for i in self.include:
if not i == "" and not Firewall.services.has_key(i):
raise PyromanException("Service '%s' tries to include undefined '%s' at %s" \
% (self.name, i, self.loginfo))
pyroman-0.5.0~beta1/pyroman/__init__.py 0000644 0001750 0001750 00000003016 11620042010 017042 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
# Not much to init here.
from exception import PyromanException
from pyroman import Firewall
from xmlsyntax import parseXML
from commands import *
__all__ = [ 'PyromanException', 'Firewall', 'parseXML', 'add_chain',
'add_host', 'add_interface', 'add_nat', 'add_rule', 'add_service', 'allow',
'drop', 'host', 'interface', 'iptables', 'iptables_end',
'ip6tables', 'ip6tables_end', 'ipXtables', 'ipXtables_end',
'nat', 'port', 'reject', 'rule', 'service']
pyroman-0.5.0~beta1/pyroman/commands.py 0000644 0001750 0001750 00000020115 11620042010 017103 0 ustar erich erich """
Commands to be used in pyroman rules files.
allow, reject, drop are just convenience commands, that can be replaced by
add_rule(Firewall.allow, ...) etc. but that are easier to read.
"""
#Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
from pyroman import Firewall
from util import Util
from chain import Chain
from exception import PyromanException
import port, service, interface, host, nat, rule
def add_service(name, sports="", dports="", include=None):
"""
Add a new named service to the list of services
name -- name of the new service
sports -- source port specification like "www/tcp 53/udp"
dports -- destination port specification
include -- services to be included / aliased
Note that services can be autocreated when names such as "www/tcp" or
"53/udp" are used, so you mainly use this to group services or make easier
aliases (e.g. "www" = "http/tcp https/tcp")
"""
loginfo = Util.get_callee(3)
service.Service(name, sports, dports, include, loginfo)
def add_interface(name, iface):
"""
Create a new named interface
name -- name for this interface (-group)
iface -- kernel interfaces in this group, e.g. "eth0 eth1"
"""
loginfo = Util.get_callee(3)
interface.Interface(name, iface, loginfo)
def add_host(name, ip, iface, hostname=None):
"""
Create a new host object.
name -- Nickname for the host
ip -- IP specification for host or subnet (e.g. "127.0.0.1 10.0.0.0/24")
iface -- Interface nickname this is connected to (only one!)
hostname -- Real hostname, as returned by "hostname". Used for
"localhost" detection only (i.e. use INPUT and OUTPUT, not
FORWARD chains), so only needed for these hosts. Defaults to
the nickname, which will usually be fine. You can use
hostname = Firewall.hostname to make e.g. a broadcast "host"
always "local".
"""
loginfo = Util.get_callee(3)
if not hostname:
hostname = name
host.Host(name, ip, iface, hostname, loginfo)
def add_nat(client="", server=None, ip=None, port=None, dport=None, dir="in"):
"""
Create a new NAT rule
client -- clients that may use this NAT
server -- server to be accessed via this NAT
ip -- IP that the NAT redirects/uses
port -- Ports that are redirected by the NAT
dport -- Destination port for the NAT
dir -- set to "in", "out" or "both" for directions, default is "in"
beware that "out" inverts client, server, to make more sense
for hosts that aren't reachable from outside (i.e. NAT is
applied to the client, not to the server, whereas in "in" and
"both", it is always applied to the server)
"""
loginfo = Util.get_callee(3)
if not server or not ip:
raise PyromanException("Server not specified for NAT (server: %s, ip: %s) at %s" % (server, ip, loginfo))
# special case: "out" NAT type
if dir=="out":
(client, server) = (server, client)
Firewall.nats.append(nat.Nat(client, server, ip, port, dport, dir, loginfo))
def add_rule(target, server="", client="", service=""):
"""
Add an arbitrary rule to the list of rules.
Allow, reject, drop are special cases of this.
target -- target for the rule
server -- server host nickname
client -- client host nickname
service -- service this rule applies to
"""
loginfo = Util.get_callee(4)
if server == "" and client == "" and service == "":
raise PyromanException("allow() called without parameters at %s" % loginfo)
for srv in Util.splitter.split(server):
for cli in Util.splitter.split(client):
for svc in Util.splitter.split(service):
Firewall.rules.append(rule.Rule(target,srv,cli,svc,loginfo))
def add_chain(name, default="-", table="filter", id=None):
"""
Create a new firewall chain.
name -- name of the chain in iptables
id -- internal ID for the chain, defaults to name
default -- default target, use for built-in chains
table -- table this chain resides in, defaults to "filter"
"""
if not id:
id = name
if Firewall.chains.has_key(id):
raise PyromanException("Firewall chain %s defined multiple times at %s" % (id, Util.get_callee(3)))
loginfo = "Chain %s created by %s" % (name, Util.get_callee(3))
Firewall.chains[id] = Chain(name, loginfo, default=default, table=table)
def allow(server="", client="", service=""):
"""
Add an 'allow' rule to the list of rules.
This calls add_rule(Firewall.accept, ...)
server -- server host nickname
client -- client host nickname
service -- service this rule applies to
"""
add_rule(Firewall.accept, server, client, service)
def reject(server="", client="", service=""):
"""
Add a 'reject' rule to the list of rules
This calls add_rule(Firewall.reject, ...)
server -- server host nickname
client -- client host nickname
service -- service this rule applies to
"""
add_rule(Firewall.reject, server, client, service)
def drop(server="", client="", service=""):
"""
Add a 'drop' rule to the list of rules
This calls add_rule(Firewall.drop, ...)
server -- server host nickname
client -- client host nickname
service -- service this rule applies to
"""
add_rule(Firewall.drop, server, client, service)
def iptables(chain, filter):
"""
Add an arbitrary iptables command.
chain -- chain to add the rules to
filter -- iptables parameters
"""
loginfo = Util.get_callee(3)
if not Firewall.chains.has_key(chain):
raise PyromanException("Firewall chain %s not known (use add_chain!) at %s" % (chain, loginfo))
Firewall.chains[chain].append4(filter, loginfo)
def iptables_end(chain, filter):
"""
Add an arbitrary iptables command after any statement added
by the "allow", "drop", "reject", "add_rule" or "iptables" commands.
chain -- chain to add the rules to
filter -- iptables parameters
"""
loginfo = Util.get_callee(3)
if not Firewall.chains.has_key(chain):
raise PyromanException("Firewall chain %s not known (use add_chain!) at %s" % (chain, loginfo))
Firewall.chains[chain].append4_end(filter, loginfo)
def ip6tables(chain, filter):
"""
Add an arbitrary ip6tables command.
chain -- chain to add the rules to
filter -- iptables parameters
"""
loginfo = Util.get_callee(3)
if not Firewall.chains.has_key(chain):
raise PyromanException("Firewall chain %s not known (use add_chain!) at %s" % (chain, loginfo))
Firewall.chains[chain].append6(filter, loginfo)
def ip6tables_end(chain, filter):
"""
Add an arbitrary ip6tables command after any statement added
by the "allow", "drop", "reject", "add_rule" or "iptables" commands.
chain -- chain to add the rules to
filter -- iptables parameters
"""
loginfo = Util.get_callee(3)
if not Firewall.chains.has_key(chain):
raise PyromanException("Firewall chain %s not known (use add_chain!) at %s" % (chain, loginfo))
def ipXtables(chain, filter):
"""
Add an arbitrary iptables + ip6tables command.
chain -- chain to add the rules to
filter -- iptables parameters
"""
iptables(chain, filter)
ip6tables(chain, filter)
def ipXtables_end(chain, filter):
"""
Add an arbitrary iptables + ip6tables command after any statement added
by the "allow", "drop", "reject", "add_rule" or "iptables" commands.
chain -- chain to add the rules to
filter -- iptables parameters
"""
iptables_end(chain, filter)
ip6tables_end(chain, filter)
pyroman-0.5.0~beta1/pyroman/iptables_parse.py 0000644 0001750 0001750 00000015047 11617622227 020333 0 ustar erich erich #Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
import sys, re, math
from xml.dom.minidom import getDOMImplementation
from xml.dom import minidom
# data collection variables
tables = dict()
# status variables
curtable = None
# some regexpes
counter_re = re.compile(r"^\[(\d+):(\d+)\]$")
# we're not enforcing much right now
# note that we're assuming anything behind the -j statement belongs to the jump
fullline_re = re.compile(r"^(?:\[(\d+):(\d+)\] )?(?:-A ([^ ]+))(?: (.*))?(?: -j ([^ ]+)(?: (.+))?)$")
# for splitting options, this uses a zero-width-lookahead to check for a -
optsplit_re = re.compile(r" (?=-)")
# from 1.3.6. documentation
builtin_targets = set([
'ACCEPT', 'DROP', 'RETURN', \
'CLASSIFY', 'CLUSTERIP', 'CONNMARK', 'CONNSECMARK', \
'DNAT', 'DSCP', 'ECN', 'IPV4OPTSSTRIP', 'LOG', 'MARK', 'MASQUERADE', \
'MIRROR', 'NETMAP', 'NFQUEUE', 'NOTRACK', 'REDIRECT', 'REJECT', \
'ROUTE', 'SAME', 'SECMARK', 'SET', 'SNAT', 'TARPIT', 'TCPMSS', 'TOS', \
'TRACE', 'TTL', 'ULOG'])
class parsedtable:
def __init__(self, name):
self.name = name
self.chains = dict()
def addChain(self, chain):
assert(not self.chains.has_key(chain.name))
self.chains[chain.name] = chain
def getChain(self, chainname):
return self.chains[chainname]
def __str__(self):
return self.name
def link(self):
for c in self.chains.keys():
self.chains[c].link()
class parsedchain:
def __init__(self, name, table, default, packets=None, bytes=None):
self.name = name
self.table = table
self.default = default
self.dpackets = packets
self.dbytes = bytes
self.tpackets = None
self.tbytes = None
# filled during linking
self.references = set()
self.referenced_by = set()
if self.default == '-':
self.default = 'RETURN'
if name in builtin_targets:
print >>sys.stderr, "Warning: chainname %s matches potential built-in chain." % name
self.rules = []
# register in table
table.addChain(self)
def appendRule(self, rule):
self.rules.append(rule)
def __str__(self):
return self.name
def link(self):
"""
Do chain interlinking as objects
"""
try:
self.default = self.table.getChain(self.default)
self.default.addReferenceBy(self)
except KeyError:
if not self.default in builtin_targets:
print >>sys.stderr, "Target %s not found, and not in list of known builtins." % self.default
pass
self.references.add(self.default)
for rule in self.rules:
self.references.add(rule.link())
self.references.add(rule)
def addReferenceBy(self, other):
self.referenced_by.add(other)
def getObjects(self):
"""
Return objects referenced in this chain
"""
return self.references
def getPackets(self):
if self.dpackets == None: return None
if self.tpackets == None:
self.calcPacketsBytes()
return self.tpackets
def getBytes(self):
if self.dbytes == None: return None
if self.tbytes == None:
self.calcPacketsBytes()
return self.tbytes
def calcPacketsBytes(self):
(tpackets, tbytes) = (self.dpackets, self.dbytes)
for r in self.rules:
tpackets += r.packets
tbytes += r.bytes
self.tpackets = tpackets
self.tbytes = tbytes
class parsedrule:
def __init__(self, chain, filter, target, targetopts, packets=None, bytes=None):
self.chain = chain
self.filter = filter
self.target = target
self.targetopts = targetopts
self.packets = packets
self.bytes = bytes
# register in table
chain.appendRule(self)
def shortRule(self):
filterstr = ""
if self.filter:
for f in self.filter:
if f.startswith("-m "): continue
filterstr += " " + f
return filterstr[1:]
def __str__(self):
filterstr = ""
if self.filter:
filterstr = " ".join(self.filter)
targetostr = ""
if self.targetopts:
targetostr = " ".join(self.targetopts)
return "-A %s %s -j %s %s" % (self.target, filterstr, self.target, targetostr)
def link(self):
try:
self.target = self.chain.table.getChain(self.target)
self.target.addReferenceBy(self.chain)
except KeyError:
if not self.target in builtin_targets:
print >>sys.stderr, "Target %s not found, and not in list of known builtins." % self.default
pass
return self.target
def parse(lines):
for line in lines:
line = line.strip()
# skip comments
if line[0] == '#': continue
# beginning and end of a table
if line[0] == '*':
tablename = line[1:]
assert(not tables.has_key(tablename))
curtable = parsedtable(tablename)
tables[tablename] = curtable
continue
if line == 'COMMIT':
curtable = None
continue
# beginning of a new chain
if line[0] == ':':
assert(curtable)
s = line[1:].split()
assert( len(s) == 2 or len(s) == 3)
(chainname, default) = s[0:2]
(packets, bytes) = (None,None)
if len(s) == 3:
m = counter_re.match(s[2])
assert(m)
(packets, bytes) = (int(m.group(1)), int(m.group(2)))
parsedchain(chainname, curtable, default, packets, bytes)
continue
# iptables statements
m = fullline_re.match(line)
if m:
(packets, bytes) = (None, None)
if m.group(1) and m.group(2):
(packets, bytes) = (int(m.group(1)), int(m.group(2)))
(chainname, filters) = (m.group(3),m.group(4))
(target, targetopts) = (m.group(5),m.group(6))
# do a best-guess split for the options
if filters != None:
filters = optsplit_re.split(filters)
if targetopts != None:
targetops = optsplit_re.split(targetopts)
curchain = curtable.getChain(chainname)
parsedrule(curchain, filters, target, targetopts, packets, bytes)
continue
print >>sys.stderr, "Line didn't parse: ", line
# process chain interlinking
for t in tables.keys():
tables[t].link()
pyroman-0.5.0~beta1/COPYING 0000644 0001750 0001750 00000002063 10374755056 014332 0 ustar erich erich Copyright (c) 2006 Erich Schubert erich@debian.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
pyroman-0.5.0~beta1/bin/ 0000755 0001750 0001750 00000000000 12201517203 014023 5 ustar erich erich pyroman-0.5.0~beta1/bin/pyroman 0000755 0001750 0001750 00000011227 11623006266 015452 0 ustar erich erich #!/usr/bin/python
""" Pyroman, an iptables firewall configuration tool """
# where the pyroman libraries are found - e.g. /usr/share/pyroman
library_path = "./"
# where the rules are located - e.g. /etc/pyroman
default_rules_path = "./examples/base"
# timeout for the "safe" mode invocation
safe_timeout_default = 30
#Copyright (c) 2011 Erich Schubert erich@debian.org
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
import sys, glob, os, getopt
# path to the main pyroman code
sys.path.insert(0, library_path)
# usercommands, the Firewall class and the firewall object
# should be available to user rules
from pyroman import *
# Pyroman version
version = "0.5.0~alpha1"
def usage():
print """\
Usage: pyroman [-hiVnspP] [--help] [--init] [--version] [--no-act]
[-r rulesdir] [--rules=rulesdir] [--print] [--print-verbose]
[-t seconds] [--timeout=seconds] [--verification-cmd=cmd] [--safe] [safe]\
"""
def main():
rules_path = default_rules_path
no_act = False
print_only = False
verbose_print = False
terse_mode = False
# parse options
try:
opts, args = getopt.getopt(sys.argv[1:], "hiVr:t:snpP", \
["help", "init", "version", "rules=", "timeout=", "safe", "no-act",
"print", "print-verbose", "verification-cmd=", "verification-command="])
except getopt.GetoptError:
usage()
sys.exit(2)
for o, a in opts:
# help screen
if o in ("-h", "--help"):
usage()
sys.exit()
# print version number
if o in ("-V", "--version"):
print "Pyroman version %s" % version
sys.exit()
# "init" mode (reduced output)
if o in ("-i", "--init"):
terse_mode = True
# Use different directory for rules
if o in ("-r", "--rules"):
rules_path = a
# Setup timeout, implies -s
if o in ("-t", "--timeout"):
Firewall.timeout = int(a)
# When given the "safe" parameter, setup a timeout.
if o in ("-s", "--safe"):
if not Firewall.timeout:
Firewall.timeout = safe_timeout_default
# Use external verification command
if o in ("--verification-cmd", "--verification-command"):
Firewall.vercmd = a
# Don't execute firewall
if o in ("-n", "--no-act"):
no_act = True
# Print resulting firewall only
if o in ("-p", "--print"):
print_only = True
verbose_print = False
# Print resulting firewall only
if o in ("-P", "--print-verbose"):
print_only = True
verbose_print = True
# When given the "safe" parameter, setup a timeout.
if len(args) > 0:
if len(args) == 1 and args[0] == "safe":
if not Firewall.timeout:
Firewall.timeout = safe_timeout_default
else:
print "Unknown parameter passed."
usage()
sys.exit(2)
run(rules_path, no_act, print_only, verbose_print, terse_mode)
def run(rules_path, no_act, print_only, verbose_print, terse_mode):
# load user rules alphabetically
rfiles = glob.glob(os.path.join(rules_path,"*.py"))
rfiles.extend( glob.glob(os.path.join(rules_path,"*.xml")) )
if len(rfiles) < 1:
print "No rule files found in directory '%s'!" % rules_path
sys.exit(1)
rfiles.sort()
for nam in rfiles:
try:
if nam.endswith(".py"):
execfile(nam)
elif nam.endswith(".xml"):
parseXML(nam)
except Exception, e:
print "An exception occurred during parsing '%s':" % nam
print e
sys.exit(3)
# do some consistency checks
try:
Firewall.verify()
except Exception, e:
print "An exception occurred during verification:"
print e
sys.exit(3)
# generate...
try:
Firewall.generate()
except Exception, e:
print "An exception occurred during generation:"
print e
sys.exit(3)
# execute firewall
if no_act:
print "Syntax checks passed, would commit to iptables now."
elif print_only:
Firewall.print_rules(verbose_print)
else:
Firewall.execute_rules(terse_mode)
if __name__ == "__main__":
main()