/* Firewall Builder Copyright (C) 2009 NetCitadel, LLC Author: Vadim Kurland vadim@vk.crocodile.org $Id$ This program is free software which we release under the GNU General Public License. You may redistribute and/or modify this program under the terms of that license as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. To get a copy of the GNU General Public License, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "../../config.h" #include #include #include #include #include #include #include #include #include #include "CompilerDriver_pix.h" #include "PolicyCompiler_pix.h" #include "NATCompiler_pix.h" #include "RoutingCompiler_pix.h" #include "OSConfigurator_pix_os.h" #include "Helper.h" #include "fwbuilder/Resources.h" #include "fwbuilder/FWObjectDatabase.h" #include "fwbuilder/XMLTools.h" #include "fwbuilder/FWException.h" #include "fwbuilder/Firewall.h" #include "fwbuilder/Interface.h" #include "fwbuilder/Policy.h" #include "fwbuilder/NAT.h" #include "fwbuilder/Routing.h" #include "fwbuilder/IPv4.h" #include "fwbuilder/IPv6.h" #include "fwcompiler/Preprocessor.h" #include "fwbuilder/Cluster.h" #include "fwbuilder/ClusterGroup.h" #include "fwbuilder/Firewall.h" #include "fwbuilder/Interface.h" #include "fwbuilder/Policy.h" #include "fwbuilder/StateSyncClusterGroup.h" #include "fwbuilder/FailoverClusterGroup.h" #include #include #include #include #include using namespace std; using namespace libfwbuilder; using namespace fwcompiler; class sort_by_net_zone { string any_address_id; public: explicit sort_by_net_zone() { any_address_id = FWObjectDatabase::getStringId( FWObjectDatabase::ANY_ADDRESS_ID); } bool operator()(const FWObject *a, const FWObject *b) { if (Interface::constcast(a) && Interface::constcast(b)) { string netzone_a=a->getStr("network_zone"); string netzone_b=b->getStr("network_zone"); if ( netzone_a==any_address_id) return false; if ( netzone_b==any_address_id) return true; } return false; } }; QString CompilerDriver_pix::assembleManifest(Cluster *cluster, Firewall* fw, bool cluster_member) { QString script_buffer; QTextStream script(&script_buffer, QIODevice::WriteOnly); QString ofname = determineOutputFileName(cluster, fw, cluster_member, ".fw"); script << "!" << MANIFEST_MARKER << "* " << ofname << endl; return script_buffer; } QString CompilerDriver_pix::printActivationCommands(Firewall*) { return ""; } QString CompilerDriver_pix::assembleFwScript(Cluster *cluster, Firewall* fw, bool cluster_member, OSConfigurator *oscnf) { Configlet script_skeleton(fw, "cisco", "script_skeleton"); Configlet top_comment(fw, "cisco", "top_comment"); FWOptions* options = fw->getOptionsObject(); options->setStr("prolog_script", options->getStr("pix_prolog_script")); options->setStr("epilog_script", options->getStr("pix_epilog_script")); string vers = fw->getStr("version"); string platform = fw->getStr("platform"); bool outbound_acl_supported = Resources::platform_res[platform]->getResourceBool( string("/FWBuilderResources/Target/options/")+ "version_"+vers+ "/pix_outbound_acl_supported"); bool afpa = options->getBool("pix_assume_fw_part_of_any"); bool emulate_outb_acls = options->getBool("pix_emulate_out_acl"); bool generate_outb_acls = options->getBool("pix_generate_out_acl"); top_comment.setVariable("outbound_acl_supported", QString((outbound_acl_supported)?"supported":"not supported")); top_comment.setVariable("emulate_outb_acls", QString((emulate_outb_acls)?"yes":"no")); top_comment.setVariable("generate_outb_acls", QString((generate_outb_acls)?"yes":"no")); top_comment.setVariable("afpa", QString((afpa)?"yes":"no")); script_skeleton.setVariable("short_script", options->getBool("short_script")); script_skeleton.setVariable("not_short_script", ! options->getBool("short_script")); script_skeleton.setVariable("system_configuration_script", system_configuration_script.c_str()); script_skeleton.setVariable("policy_script", policy_script.c_str()); script_skeleton.setVariable("nat_script", nat_script.c_str()); script_skeleton.setVariable("routing_script", routing_script.c_str()); assembleFwScriptInternal(cluster, fw, cluster_member, oscnf, &script_skeleton, &top_comment, "!"); return script_skeleton.expand(); } string CompilerDriver_pix::run(const std::string &cluster_id, const std::string &firewall_id, const std::string &single_rule_id) { Cluster *cluster = NULL; if (!cluster_id.empty()) cluster = Cluster::cast(objdb->findInIndex(objdb->getIntId(cluster_id))); Firewall *fw = Firewall::cast(objdb->findInIndex(objdb->getIntId(firewall_id))); assert(fw); // Copy rules from the cluster object populateClusterElements(cluster, fw); if (cluster) { // PIX failover is dfferent from VRRP and other failover protocols // in that it does not create new virtual address. Instead, each // unit is configured with two ip addresses, one for the active // unit and another for standby one. When active unit fails, the // other one assumes its address. // // This matters because when we use cluster object or one of its // interfaces in rules, compiler should expand it to the set of // addresses that includes addresses of the corresponding // interface of both member firewalls. Method // CompilerDriver::copyFailoverInterface adds a copy of firewall // interface to the cluster object. This works for all firewalls, // but for PIX we need to add copies of interfaces from both // members. // FWObjectTypedChildIterator cl_iface = cluster->findByType(Interface::TYPENAME); for (; cl_iface != cl_iface.end(); ++cl_iface) { FailoverClusterGroup *failover_group = FailoverClusterGroup::cast( (*cl_iface)->getFirstByType(FailoverClusterGroup::TYPENAME)); if (failover_group) { FWObject *this_member_interface = NULL; list other_member_interfaces; for (FWObjectTypedChildIterator it = failover_group->findByType(FWObjectReference::TYPENAME); it != it.end(); ++it) { FWObject *intf = FWObjectReference::getObject(*it); assert(intf); if (intf->isChildOf(fw)) this_member_interface = intf; else other_member_interfaces.push_back(intf); } if (!other_member_interfaces.empty()) { for (list::iterator it=other_member_interfaces.begin(); it!=other_member_interfaces.end(); ++it) { cluster->addCopyOf(*it, true); } } } } } #if 0 FWObjectTypedChildIterator iface = fw->findByType(Interface::TYPENAME); for (; iface != iface.end(); ++iface) { (*iface)->dump(true, true); } #endif QString ofname = determineOutputFileName(cluster, fw, !cluster_id.empty(), ".fw"); FWOptions* options = fw->getOptionsObject(); QString script_buffer; try { commonChecks2(cluster, fw); pixClusterConfigurationChecks(cluster, fw); // Note that fwobjectname may be different from the name of the // firewall fw This happens when we compile a member of a cluster current_firewall_name = fw->getName().c_str(); bool pix_acl_basic = options->getBool("pix_acl_basic"); bool pix_acl_no_clear = options->getBool("pix_acl_no_clear"); bool pix_acl_substitution = options->getBool("pix_acl_substitution"); bool pix_add_clear_statements = options->getBool("pix_add_clear_statements"); if (!pix_acl_basic && !pix_acl_no_clear && !pix_acl_substitution) { if ( pix_add_clear_statements ) options->setBool("pix_acl_basic",true); else options->setBool("pix_acl_no_clear",true); } Helper helper(NULL); multimap netzone_objects; std::list all_interfaces = fw->getByTypeDeep(Interface::TYPENAME); for (std::list::iterator i=all_interfaces.begin(); i!=all_interfaces.end(); ++i) { Interface *iface = dynamic_cast(*i); assert(iface); if (iface->getOptionsObject()->getBool("cluster_interface")) continue; if ((iface->getOptionsObject()->getStr("type") == "" || iface->getOptionsObject()->getStr("type") == "ethernet") && iface->getByType(Interface::TYPENAME).size() > 0) { // Parent vlan interface (i.e. trunk) if (!iface->isUnprotected()) { QString err( "Interface %1 has vlan subinterfaces, it can not " "be used for ACL. Marking this interface \"unprotected\" " "to exclude it." ); warning(fw, NULL, NULL, err.arg(iface->getName().c_str()) .toStdString()); iface->setUnprotected(true); } } // Tests for label, security level and network zone make sense // only for interfaces that can be used in ACLs or to bind // ACLs to. Unnumbered interfaces can't, so we do not need to // run these checks. One example of unnumbered interface is // parent interface for vlan subinterfaces. if (iface->isUnnumbered()) continue; if (iface->isUnprotected()) continue; /* * there shouldn't be two interfaces with the same security level and same label * */ for (std::list::iterator j=all_interfaces.begin(); j!=all_interfaces.end(); ++j) { Interface *iface2 = dynamic_cast(*j); assert(iface2); if (iface2->isUnnumbered()) continue; if (iface2->isUnprotected()) continue; if (iface->getId()==iface2->getId()) continue; if (iface->getOptionsObject()->getBool("cluster_interface") || iface2->getOptionsObject()->getBool("cluster_interface")) continue; if (iface->getSecurityLevel()==iface2->getSecurityLevel()) { QString err( "Security level of each interface should be unique, " "however interfaces %1 (%2) and %3 (%4)" " have the same security level." ); abort(fw, NULL, NULL, err.arg(iface->getName().c_str()) .arg(iface->getLabel().c_str()) .arg(iface2->getName().c_str()) .arg(iface2->getLabel().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } if (iface->getLabel()==iface2->getLabel()) { QString err( "Label of each interface should be unique, " "however interfaces %1 (%2) and %3 (%4)" " have the same." ); abort(fw, NULL, NULL, err.arg(iface->getName().c_str()) .arg(iface->getLabel().c_str()) .arg(iface2->getName().c_str()) .arg(iface2->getLabel().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } } // We only do limited checks for dedicated failover // interfaces because they are not used in ACLs or // anywhere else in configuration, except in "failover" // commands. if (iface->isDedicatedFailover()) continue; /* * in PIX, we need network zones to be defined for all interfaces */ string netzone_id = iface->getStr("network_zone"); if (netzone_id=="") { QString err("Network zone definition is missing for interface %1 (%2)"); abort(fw, NULL, NULL, err.arg(iface->getName().c_str()) .arg(iface->getLabel().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } FWObject *netzone = objdb->findInIndex( FWObjectDatabase::getIntId(netzone_id)); if (netzone==NULL) { QString err("Network zone points at nonexisting object for interface %1 (%2)"); abort(fw, NULL, NULL, err.arg(iface->getName().c_str()) .arg(iface->getLabel().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } /* * netzone may be a group, in which case we need to expand it * (recursively). * * 1. We create new temporary object (type Group). * * 2. put it in the database somewhere * * 3. add all objects that belong to the network zone to this * group. We add objects directly, not as a reference. * * 4. finally replace reference to the old network zone object in the * interface with reference to this new group. * * 5. we store ID of the original network zone object * using iface->setStr("orig_netzone_id") * * This ensures netzones do not contain other groups and do not * require any recursive expanding anymore. Since objects were added * to netzones directly, we do not need to bother with dereferencing, * too. */ list ol; helper.expand_group_recursive(netzone,ol); FWObject *nz = objdb->createObjectGroup(); assert(nz!=NULL); nz->setName("netzone_"+iface->getLabel()); objdb->add(nz); for (list::iterator j=ol.begin(); j!=ol.end(); ++j) { netzone_objects.insert( pair(iface->getLabel(),*j)); nz->add(*j); } iface->setStr("orig_netzone_id", netzone_id ); iface->setStr("network_zone", FWObjectDatabase::getStringId(nz->getId()) ); } /* * the same object (network or host) can not belong to network zones * of two different interfaces. Map netzone_objects holds pairs * interface_id/object. We just make sure the same object does not * appear in two pairs with different interfaces. */ multimap::iterator k; for (k=netzone_objects.begin(); k!=netzone_objects.end(); ++k) { multimap::iterator l; l=k; ++l; for ( ; l!=netzone_objects.end(); ++l) { if ( l->second->getId() == k->second->getId() ) { if (k->first==l->first) { QString err("Object %1 is used more than once in network zone of interface %2"); abort(fw, NULL, NULL, err.arg(l->second->getName().c_str()) .arg(k->first.c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } else { QString err("Object %1 is used in network zones of " "interfaces %2 and %3"); abort(fw, NULL, NULL, err.arg(l->second->getName().c_str()) .arg(k->first.c_str()) .arg(l->first.c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } } } } /* Now that all checks are done, we can drop copies of cluster * interfaces that were added to the firewall by * CompilerDriver::populateClusterElements() */ list copies_of_cluster_interfaces; for (std::list::iterator i=all_interfaces.begin(); i!=all_interfaces.end(); ++i) { Interface *iface = Interface::cast(*i); assert(iface); if (iface->getOptionsObject()->getBool("cluster_interface")) copies_of_cluster_interfaces.push_back(iface); } while (copies_of_cluster_interfaces.size()) { fw->remove(copies_of_cluster_interfaces.front()); copies_of_cluster_interfaces.pop_front(); } all_interfaces = fw->getByTypeDeep(Interface::TYPENAME); for (std::list::iterator i=all_interfaces.begin(); i!=all_interfaces.end(); ++i) { Interface *iface = dynamic_cast(*i); assert(iface); /* * missing labels on interfaces * */ if (iface->getLabel()=="") { string lbl; if (iface->isDedicatedFailover()) { // dedicated failover interface misses label. This // interface can be used in failover cluster group // or state sync group. Assign label depending on // the function. FWObjectTypedChildIterator it = cluster->findByType(StateSyncClusterGroup::TYPENAME); StateSyncClusterGroup *state_sync_group = StateSyncClusterGroup::cast(*it); if (state_sync_group && state_sync_group->hasMember(iface)) lbl = "state"; if (!iface->getOptionsObject()->getStr("failover_group_id").empty()) lbl = "failover"; } if (lbl.empty()) { if (iface->getSecurityLevel()==0) lbl="outside"; else { if (iface->getSecurityLevel()==100) lbl="inside"; else { QString l("dmz%1"); lbl = l.arg(iface->getSecurityLevel()).toStdString(); } } } iface->setLabel(lbl); } } /* * now sort interfaces by their network zone "width" (that * is, more narrow network zone should go first, interface * with network zone "any" should be the last) * std::sort(fw->begin(), fw->end(), sort_by_net_zone() ); */ std::auto_ptr prep(new Preprocessor(objdb , fw, false)); prep->compile(); std::auto_ptr oscnf(new OSConfigurator_pix_os(objdb , fw, false)); if (inTestMode()) oscnf->setTestMode(); if (inEmbeddedMode()) oscnf->setEmbeddedMode(); oscnf->prolog(); oscnf->processFirewallOptions(); /* create compilers and run the whole thing */ std::auto_ptr n(new NATCompiler_pix(objdb, fw, false, oscnf.get())); RuleSet *nat = RuleSet::cast(fw->getFirstByType(NAT::TYPENAME)); if (nat) { n->setSourceRuleSet(nat); n->setRuleSetName(nat->getName()); if (inTestMode()) n->setTestMode(); if (inEmbeddedMode()) n->setEmbeddedMode(); n->setSingleRuleCompileMode(single_rule_id); n->setDebugLevel( dl ); if (rule_debug_on) n->setDebugRule( drn ); n->setVerbose( verbose ); if ( n->prolog() > 0 ) { n->compile(); n->epilog(); } else info(" Nothing to compile in NAT"); } std::auto_ptr c( new PolicyCompiler_pix(objdb, fw, false, oscnf.get() , n.get())); RuleSet *policy = RuleSet::cast(fw->getFirstByType(Policy::TYPENAME)); if (policy) { c->setSourceRuleSet(policy); c->setRuleSetName(policy->getName()); if (inTestMode()) c->setTestMode(); if (inEmbeddedMode()) c->setEmbeddedMode(); c->setSingleRuleCompileMode(single_rule_id); c->setDebugLevel( dl ); if (rule_debug_on) c->setDebugRule( drp ); c->setVerbose( verbose ); if ( c->prolog() > 0 ) { c->compile(); c->epilog(); } else info(" Nothing to compile in Policy"); } std::auto_ptr r(new RoutingCompiler_pix(objdb, fw, false, oscnf.get())); RuleSet *routing = RuleSet::cast(fw->getFirstByType(Routing::TYPENAME)); if (routing) { r->setSourceRuleSet(routing); r->setRuleSetName(routing->getName()); if (inTestMode()) r->setTestMode(); if (inEmbeddedMode()) r->setEmbeddedMode(); r->setSingleRuleCompileMode(single_rule_id); r->setDebugLevel( dl ); if (rule_debug_on) r->setDebugRule( drp ); r->setVerbose( verbose ); if ( r->prolog() > 0 ) { r->compile(); r->epilog(); } else info(" Nothing to compile in Routing"); } if (haveErrorsAndWarnings()) { all_errors.push_front(getErrors("").c_str()); } system_configuration_script = oscnf->getCompiledScript(); policy_script = c->getCompiledScript(); nat_script = n->getCompiledScript(); routing_script = r->getCompiledScript(); if (c->haveErrorsAndWarnings()) all_errors.push_back(c->getErrors("C ").c_str()); if (n->haveErrorsAndWarnings()) all_errors.push_back(n->getErrors("N ").c_str()); if (r->haveErrorsAndWarnings()) all_errors.push_back(r->getErrors("R ").c_str()); script_buffer = assembleFwScript( cluster, fw, !cluster_id.empty(), oscnf.get()); if (single_rule_compile_on) { return //all_errors.join("\n").toStdString() + policy_script + nat_script + routing_script; } QFileInfo finfo(ofname); if (finfo.isRelative()) { // if ofname is relative, it is relative to the // directory the program started in, which can be // different from wdir and different from the current dir // at this point because we do chdir to the directory // defined by the -d command line option QFileInfo new_finfo(start_current_dir, ofname); ofname = new_finfo.absoluteFilePath(); } info("Output file name: " + ofname.toStdString()); QFile fw_file(ofname); if (fw_file.open(QIODevice::WriteOnly)) { QTextStream fw_str(&fw_file); fw_str << script_buffer; fw_file.close(); fw_file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::ReadOther | QFile::ExeOwner | QFile::ExeGroup | QFile::ExeOther ); info(" Compiled successfully"); } else { QString err(" Failed to open file %1 for writing: %2; Current dir: %3"); abort(err.arg(fw_file.fileName()).arg(fw_file.error()).arg(QDir::current().path()).toStdString()); } } catch (FatalErrorInSingleRuleCompileMode &ex) { return getErrors(""); } return ""; } /* * Sanity checks for the cluster configuration. Per ticket #606: * * - state sync group must have master * * - one interface must be marked as "dedicated failover" for failover * group. * * - this interface must have failover group with members, one of * which must be master * * - failover interfaces in member firewalls must have ip addresses * which should be different but in the same subnet. * * - possibly another interface can be defined as "dedicated failover" * and used in state sync group. * * - if second interface is used for state sync, it must have ip * address in member firewalls (different) * * - addresses of the dedicated failover interfaces must belong to the * same subnet in each pair of failover inetrfaces (failover and state sync) * * - failover interfaces of both members used in the failover cluster * group of the cluster object must have the same name. * * - The same check should be performed in the state sync group. * * */ void CompilerDriver_pix::pixClusterConfigurationChecks(Cluster *cluster, Firewall*) { if (cluster==NULL) return; FWObjectTypedChildIterator it = cluster->findByType(StateSyncClusterGroup::TYPENAME); StateSyncClusterGroup *state_sync_group = StateSyncClusterGroup::cast(*it); if (state_sync_group->getStr("master_iface").empty()) { QString err("One of the interfaces in the state synchronization group " "must be marked as 'Master'"); abort(cluster, NULL, NULL, err.toStdString()); throw FatalErrorInSingleRuleCompileMode(); } pixClusterGroupChecks(state_sync_group); bool failover_group_inspected = false; list l2 = cluster->getByTypeDeep(Interface::TYPENAME); for (list::iterator i=l2.begin(); i!=l2.end(); ++i) { Interface *iface = dynamic_cast(*i); assert(iface); FailoverClusterGroup *failover_group = FailoverClusterGroup::cast( iface->getFirstByType(FailoverClusterGroup::TYPENAME)); if (failover_group) { for (FWObjectTypedChildIterator it = failover_group->findByType(FWObjectReference::TYPENAME); it != it.end(); ++it) { Interface *member_iface = Interface::cast(FWObjectReference::getObject(*it)); assert(member_iface); pixClusterGroupChecks(failover_group); if (member_iface->isDedicatedFailover()) { failover_group_inspected = true; } } } } } void CompilerDriver_pix::pixClusterGroupChecks(ClusterGroup *cluster_group) { FWObject *cluster = cluster_group; while (cluster && !Cluster::isA(cluster)) cluster = cluster->getParent(); FWObject *cluster_interface = NULL; FWObject *p = cluster_group->getParent(); if (Interface::isA(p)) cluster_interface = p; map addresses_and_masks; for (FWObjectTypedChildIterator it = cluster_group->findByType(FWObjectReference::TYPENAME); it != it.end(); ++it) { Interface *member_iface = Interface::cast(FWObjectReference::getObject(*it)); assert(member_iface); FWObject *member = member_iface->getParentHost(); if (cluster_interface) { // check consistency of the names. // In case of PIX the name of the cluster interface should match // names of member interfaces if (cluster_interface->getName() != member_iface->getName()) { QString err("Names of interfaces used in state synchronization " "or failover group must match the name of the " "cluster inetrface. Interface %1:%2 has the name " "that is different from the cluster interface name %3"); abort(cluster, NULL, NULL, err.arg(member->getName().c_str()) .arg(member_iface->getName().c_str()) .arg(cluster_interface->getName().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } } if (StateSyncClusterGroup::isA(cluster_group) && !member_iface->isDedicatedFailover()) { QString err("Interface %1 is used in a state synchronization " "but is not marked as 'Dedicated Failover' " "interface. All interfaces used for the state " "synchronization or failover must be marked " "'Dedicated Failover'. "); abort(member, NULL, NULL, err.arg(member_iface->getName().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } if (!member_iface->isRegular() || member_iface->countInetAddresses(true)==0) { QString err("Interface %1 which is used in state synchronization " "or failover group does not have an IP address. " "All interfaces used for the state " "synchronization or failover must have ip addresses."); abort(member, NULL, NULL, err.arg(member_iface->getName().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } QString key("%1:%2"); FWObjectTypedChildIterator it_addr = member_iface->findByType(IPv4::TYPENAME); IPv4* addr = IPv4::cast(*it_addr); addresses_and_masks[key.arg(member->getName().c_str()).arg(member_iface->getName().c_str())] = addr->getInetAddrMaskObjectPtr(); } if (addresses_and_masks.size() >= 2) { QString first_key; const InetAddr *first_network_addr = NULL; map::iterator it; for (it=addresses_and_masks.begin(); it!=addresses_and_masks.end(); ++it) { QString key = it->first; const InetAddrMask *am = it->second; if (first_network_addr == NULL) { first_key = key; first_network_addr = am->getNetworkAddressPtr(); } else { const InetAddr *network_addr = am->getNetworkAddressPtr(); if (*first_network_addr != *(network_addr)) { QString err("Interfaces used in state synchronization " "or failover group must have IP addresses on " "the same subnet. Interfaces %1 and %2 have " "addresses on different subnets: %3 , %4"); abort(cluster, NULL, NULL, err.arg(first_key).arg(key) .arg(first_network_addr->toString().c_str()) .arg(network_addr->toString().c_str()).toStdString()); throw FatalErrorInSingleRuleCompileMode(); } } } } }