pyroman-0.5.0~beta1/0000755000175000017500000000000012202236377013266 5ustar ericherichpyroman-0.5.0~beta1/examples/0000755000175000017500000000000012201515645015101 5ustar ericherichpyroman-0.5.0~beta1/examples/xml/0000755000175000017500000000000012201515645015701 5ustar ericherichpyroman-0.5.0~beta1/examples/xml/04_conntrack.xml0000644000175000017500000000277111044712623020716 0ustar ericherich pyroman-0.5.0~beta1/examples/xml/25_networks.xml0000644000175000017500000000037310572163037020613 0ustar ericherich pyroman-0.5.0~beta1/examples/xml/01_loopback.xml0000644000175000017500000000043510572163037020522 0ustar ericherich pyroman-0.5.0~beta1/examples/xml/10_interfaces.xml0000644000175000017500000000060310572163037021050 0ustar ericherich pyroman-0.5.0~beta1/examples/xml/03_standard_chains.xml0000644000175000017500000000212311044712623022047 0ustar ericherich pyroman-0.5.0~beta1/examples/xml/30_localhost.xml0000644000175000017500000000172110572163037020721 0ustar ericherich pyroman-0.5.0~beta1/examples/xml/20_services.xml0000644000175000017500000000402412201515403020537 0ustar ericherich pyroman-0.5.0~beta1/examples/xml/00_iptables-defaults.xml0000644000175000017500000000174210572163037022341 0ustar ericherich pyroman-0.5.0~beta1/examples/personal-firewall/0000755000175000017500000000000012201515645020527 5ustar ericherichpyroman-0.5.0~beta1/examples/personal-firewall/05_skype.py0000644000175000017500000000175311617614126022552 0ustar ericherich""" 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.py0000644000175000017500000000013310571554347023533 0ustar ericherich""" Treat all interfaces the same for the single-host setup. """ add_interface("any", "*") pyroman-0.5.0~beta1/examples/personal-firewall/25_networks.py0000644000175000017500000000030611620042010023243 0ustar ericherich""" 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.py0000644000175000017500000000140611620042010023355 0ustar ericherich""" 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/0000755000175000017500000000000012201515645016615 5ustar ericherichpyroman-0.5.0~beta1/examples/example1/32_mail.py0000644000175000017500000000077610377107574020441 0ustar ericherich""" 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.py0000644000175000017500000000160010377113252022514 0ustar ericherich""" 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.py0000644000175000017500000000204510377113252022246 0ustar ericherich""" 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.py0000644000175000017500000000073310377113252020254 0ustar ericherich""" 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.py0000644000175000017500000000117110377113252021612 0ustar ericherich""" 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.py0000644000175000017500000000265411044712623022624 0ustar ericherich""" 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.py0000644000175000017500000000336210377113252023160 0ustar ericherich""" 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/README0000644000175000017500000000024610377113401017473 0ustar ericherichThis 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.py0000644000175000017500000000125310377113252020271 0ustar ericherich""" 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.py0000644000175000017500000000342710377113252021305 0ustar ericherich""" 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.py0000644000175000017500000000331310420467414021624 0ustar ericherich""" 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/0000755000175000017500000000000012201515645016013 5ustar ericherichpyroman-0.5.0~beta1/examples/base/01_loopback.py0000644000175000017500000000032411620042010020437 0ustar ericherich""" 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.py0000644000175000017500000000333411620042010020636 0ustar ericherich""" 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.py0000644000175000017500000000034211044712623021006 0ustar ericherich""" 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.py0000644000175000017500000000202411620042010021773 0ustar ericherich""" 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.py0000644000175000017500000000160011044604306022266 0ustar ericherich""" 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.py0000644000175000017500000000505512201515403020506 0ustar ericherich""" 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/README0000644000175000017500000000125011021725626016672 0ustar ericherichPyroman "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.py0000644000175000017500000000103211620042010020524 0ustar ericherich""" 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/INSTALL0000644000175000017500000000101410375202205014303 0ustar ericherichInstallation: 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/0000755000175000017500000000000012201515645014030 5ustar ericherichpyroman-0.5.0~beta1/doc/pyroman.80000644000175000017500000000747110466507136015626 0ustar ericherich.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/AUTHORS0000644000175000017500000000010010403056107014315 0ustar ericherichInitial version written by: Erich Schubert pyroman-0.5.0~beta1/pyroman/0000755000175000017500000000000012201516557014753 5ustar ericherichpyroman-0.5.0~beta1/pyroman/rule.py0000644000175000017500000001130311623006266016270 0ustar ericherich#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.py0000644000175000017500000000755712202236247016323 0ustar ericherich#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.py0000644000175000017500000001040511620042010016260 0ustar ericherich#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.py0000644000175000017500000001274111620042010016372 0ustar ericherich#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.py0000644000175000017500000002036011617622227017377 0ustar ericherich#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.py0000644000175000017500000000560311617622227017273 0ustar ericherich#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.py0000644000175000017500000001105311617622227016304 0ustar ericherich#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.py0000644000175000017500000002402411623006266017012 0ustar ericherich#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.py0000644000175000017500000001505711623006266017136 0ustar ericherich#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.py0000644000175000017500000000247411617622227017334 0ustar ericherich#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.py0000644000175000017500000001316711623046574016123 0ustar ericherich#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.py0000644000175000017500000001103312201515403016750 0ustar ericherich#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__.py0000644000175000017500000000301611620042010017042 0ustar ericherich#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.py0000644000175000017500000002011511620042010017103 0ustar ericherich""" 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.py0000644000175000017500000001504711617622227020333 0ustar ericherich#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/COPYING0000644000175000017500000000206310374755056014332 0ustar ericherichCopyright (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/0000755000175000017500000000000012201517203014023 5ustar ericherichpyroman-0.5.0~beta1/bin/pyroman0000755000175000017500000001122711623006266015452 0ustar ericherich#!/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()