/** * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. * * This software is licensed under the GNU General Public License v3 or later. * * It is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package com.cloud.network.rules; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.ejb.Local; import javax.naming.ConfigurationException; import org.apache.log4j.Logger; import com.cloud.api.commands.ListPortForwardingRulesCmd; import com.cloud.event.EventVO; import com.cloud.event.dao.EventDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.NetworkRuleConflictException; import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.IPAddressVO; import com.cloud.network.IpAddress; import com.cloud.network.Network; import com.cloud.network.Network.GuestIpType; import com.cloud.network.NetworkManager; import com.cloud.network.dao.FirewallRulesDao; import com.cloud.network.dao.IPAddressDao; import com.cloud.network.rules.FirewallRule.Purpose; import com.cloud.network.rules.FirewallRule.State; import com.cloud.network.rules.dao.PortForwardingRulesDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.UserContext; import com.cloud.uservm.UserVm; import com.cloud.utils.Pair; import com.cloud.utils.component.Inject; import com.cloud.utils.component.Manager; import com.cloud.utils.db.DB; import com.cloud.utils.db.Transaction; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.Ip; import com.cloud.utils.net.NetUtils; import com.cloud.vm.Nic; import com.cloud.vm.UserVmVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.UserVmDao; @Local(value={RulesManager.class, RulesService.class}) public class RulesManagerImpl implements RulesManager, RulesService, Manager { private static final Logger s_logger = Logger.getLogger(RulesManagerImpl.class); String _name; @Inject PortForwardingRulesDao _forwardingDao; @Inject FirewallRulesDao _firewallDao; @Inject IPAddressDao _ipAddressDao; @Inject UserVmDao _vmDao; @Inject AccountManager _accountMgr; @Inject NetworkManager _networkMgr; @Inject EventDao _eventDao; @Override public void detectRulesConflict(FirewallRule newRule, IpAddress ipAddress) throws NetworkRuleConflictException { assert newRule.getSourceIpAddress().equals(ipAddress.getAddress()) : "You passed in an ip address that doesn't match the address in the new rule"; List rules = _firewallDao.listByIpAndNotRevoked(newRule.getSourceIpAddress()); assert (rules.size() >= 1) : "For network rules, we now always first persist the rule and then check for network conflicts so we should at least have one rule at this point."; if (ipAddress.isOneToOneNat() && rules.size() > 1) { throw new NetworkRuleConflictException("There are already rules in existence for the " + newRule.getSourceIpAddress()); } for (FirewallRuleVO rule : rules) { if (rule.getId() == newRule.getId()) { continue; // Skips my own rule. } if (rule.getNetworkId() != newRule.getNetworkId() && rule.getState() != State.Revoke) { throw new NetworkRuleConflictException("New rule is for a different network than what's specified in rule " + rule.getXid()); } if (rule.getProtocol().equals(NetUtils.NAT_PROTO)) { throw new NetworkRuleConflictException("There is already a one to one NAT specified for " + newRule.getSourceIpAddress()); } if ((rule.getSourcePortStart() <= newRule.getSourcePortStart() && rule.getSourcePortEnd() >= newRule.getSourcePortStart()) || (rule.getSourcePortStart() <= newRule.getSourcePortEnd() && rule.getSourcePortEnd() >= newRule.getSourcePortEnd()) || (newRule.getSourcePortStart() <= rule.getSourcePortStart() && newRule.getSourcePortEnd() >= rule.getSourcePortStart()) || (newRule.getSourcePortStart() <= rule.getSourcePortEnd() && newRule.getSourcePortEnd() >= rule.getSourcePortEnd())) { //we allow port forwarding rules with the same parameters but different protocols if (!(rule.getPurpose() == Purpose.PortForwarding && newRule.getPurpose() == Purpose.PortForwarding && newRule.getProtocol() != rule.getProtocol())) { throw new NetworkRuleConflictException("The range specified, " + newRule.getSourcePortStart() + "-" + newRule.getSourcePortEnd() + ", conflicts with rule " + rule.getId() + " which has " + rule.getSourcePortStart() + "-" + rule.getSourcePortEnd()); } } } if (s_logger.isDebugEnabled()) { s_logger.debug("No network rule conflicts detected for " + newRule + " against " + (rules.size() - 1) + " existing rules"); } } @Override public void checkIpAndUserVm(IpAddress ipAddress, UserVm userVm, Account caller) throws InvalidParameterValueException, PermissionDeniedException { if (ipAddress == null || ipAddress.getAllocatedTime() == null || ipAddress.getAllocatedToAccountId() == null) { throw new InvalidParameterValueException("Unable to create ip forwarding rule on address " + ipAddress + ", invalid IP address specified."); } if (userVm == null) { return; } if (userVm.getState() == VirtualMachine.State.Destroyed || userVm.getState() == VirtualMachine.State.Expunging) { throw new InvalidParameterValueException("Invalid user vm: " + userVm.getId()); } _accountMgr.checkAccess(caller, userVm); // validate that IP address and userVM belong to the same account if (ipAddress.getAllocatedToAccountId().longValue() != userVm.getAccountId()) { throw new InvalidParameterValueException("Unable to create ip forwarding rule, IP address " + ipAddress + " owner is not the same as owner of virtual machine " + userVm.toString()); } // validate that userVM is in the same availability zone as the IP address if (ipAddress.getDataCenterId() != userVm.getDataCenterId()) { throw new InvalidParameterValueException("Unable to create ip forwarding rule, IP address " + ipAddress + " is not in the same availability zone as virtual machine " + userVm.toString()); } } @Override @DB public PortForwardingRule createPortForwardingRule(PortForwardingRule rule, Long vmId) throws NetworkRuleConflictException { UserContext ctx = UserContext.current(); Account caller = ctx.getCaller(); Ip ipAddr = rule.getSourceIpAddress(); IPAddressVO ipAddress = _ipAddressDao.findById(ipAddr); Ip dstIp = rule.getDestinationIpAddress(); long networkId; UserVmVO vm = null; Network network = null; if (vmId != null) { // validate user VM exists vm = _vmDao.findById(vmId); if (vm == null) { throw new InvalidParameterValueException("Unable to create ip forwarding rule on address " + ipAddress + ", invalid virtual machine id specified (" + vmId + ")."); } dstIp = null; List nics = _networkMgr.getNics(vm); for (Nic nic : nics) { Network ntwk = _networkMgr.getNetwork(nic.getNetworkId()); if (ntwk.getGuestType() == GuestIpType.Virtual && nic.getIp4Address() != null) { network = ntwk; dstIp = new Ip(nic.getIp4Address()); break; } } if (network == null) { throw new CloudRuntimeException("Unable to find ip address to map to in vm id=" + vmId); } } else { network = _networkMgr.getNetwork(rule.getNetworkId()); if (network == null) { throw new InvalidParameterValueException("Unable to get the network " + rule.getNetworkId()); } } _accountMgr.checkAccess(caller, network); networkId = network.getId(); long accountId = network.getAccountId(); long domainId = network.getDomainId(); checkIpAndUserVm(ipAddress, vm, caller); boolean isNat = NetUtils.NAT_PROTO.equals(rule.getProtocol()); if (isNat && (ipAddress.isSourceNat() || ipAddress.isOneToOneNat())) { throw new NetworkRuleConflictException("Can't do one to one NAT on ip address: " + ipAddress.getAddress()); } Transaction txn = Transaction.currentTxn(); txn.start(); PortForwardingRuleVO newRule = new PortForwardingRuleVO(rule.getXid(), rule.getSourceIpAddress(), rule.getSourcePortStart(), rule.getSourcePortEnd(), dstIp, rule.getDestinationPortStart(), rule.getDestinationPortEnd(), rule.getProtocol(), networkId, accountId, domainId, vmId); newRule = _forwardingDao.persist(newRule); if (isNat) { ipAddress.setOneToOneNat(true); _ipAddressDao.update(ipAddress.getAddress(), ipAddress); } txn.commit(); boolean success = false; try { detectRulesConflict(newRule, ipAddress); if (!_firewallDao.setStateToAdd(newRule)) { throw new CloudRuntimeException("Unable to update the state to add for " + newRule); } success = true; return newRule; } catch (Exception e) { txn.start(); _forwardingDao.remove(newRule.getId()); if (isNat) { ipAddress.setOneToOneNat(false); _ipAddressDao.update(ipAddress.getAddress(), ipAddress); } txn.commit(); if (e instanceof NetworkRuleConflictException) { throw (NetworkRuleConflictException)e; } throw new CloudRuntimeException("Unable to add rule for " + newRule.getSourceIpAddress(), e); } finally { // Save and create the event String description; String ruleName = "ip forwarding"; String level = EventVO.LEVEL_INFO; if (success == true) { description = "created new " + ruleName + " rule [" + newRule.getSourceIpAddress() + ":" + newRule.getSourcePortStart() + "]->[" + newRule.getDestinationIpAddress() + ":" + newRule.getDestinationPortStart() + "]" + " " + newRule.getProtocol(); } else { level = EventVO.LEVEL_ERROR; description = "failed to create new " + ruleName + " rule [" + newRule.getSourceIpAddress() + ":" + newRule.getSourcePortStart() + "]->[" + newRule.getDestinationIpAddress() + ":" + newRule.getDestinationPortStart() + "]" + " " + newRule.getProtocol(); } } } protected Pair getUserVmGuestIpAddress(UserVm vm) { Ip dstIp = null; List nics = _networkMgr.getNics(vm); for (Nic nic : nics) { Network ntwk = _networkMgr.getNetwork(nic.getNetworkId()); if (ntwk.getGuestType() == GuestIpType.Virtual) { dstIp = new Ip(nic.getIp4Address()); return new Pair(ntwk, dstIp); } } throw new CloudRuntimeException("Unable to find ip address to map to in " + vm.getId()); } @DB protected void revokeRule(FirewallRuleVO rule, Account caller, long userId) { if (caller != null) { _accountMgr.checkAccess(caller, rule); } Transaction txn = Transaction.currentTxn(); txn.start(); if (rule.getState() == State.Staged) { if (s_logger.isDebugEnabled()) { s_logger.debug("Found a rule that is still in stage state so just removing it: " + rule); } _firewallDao.remove(rule.getId()); } else if (rule.getState() == State.Add || rule.getState() == State.Active) { rule.setState(State.Revoke); _firewallDao.update(rule.getId(), rule); } if (NetUtils.NAT_PROTO.equals(rule.protocol) && rule.getSourcePortStart() == -1) { if (s_logger.isDebugEnabled()) { s_logger.debug("Removing one to one nat so setting the ip back to one to one nat is false: " + rule.getSourceIpAddress()); } IPAddressVO ipAddress = _ipAddressDao.findById(rule.getSourceIpAddress()); ipAddress.setOneToOneNat(false); _ipAddressDao.update(ipAddress.getAddress(), ipAddress); } // Save and create the event String ruleName = rule.getPurpose() == Purpose.Firewall ? "Firewall" : (rule.getProtocol().equals(NetUtils.NAT_PROTO) ? "ip forwarding" : "port forwarding"); StringBuilder description = new StringBuilder("deleted ").append(ruleName).append(" rule [").append(rule.getSourceIpAddress()).append(":").append(rule.getSourcePortStart()).append("-").append(rule.getSourcePortEnd()).append("]"); if (rule.getPurpose() == Purpose.PortForwarding) { PortForwardingRuleVO pfRule = (PortForwardingRuleVO)rule; description.append("->[").append(pfRule.getDestinationIpAddress()).append(":").append(pfRule.getDestinationPortStart()).append("-").append(pfRule.getDestinationPortEnd()).append("]"); } description.append(" ").append(rule.getProtocol()); txn.commit(); } @Override public boolean revokePortForwardingRule(long ruleId, boolean apply) { UserContext ctx = UserContext.current(); Account caller = ctx.getCaller(); PortForwardingRuleVO rule = _forwardingDao.findById(ruleId); if (rule == null) { throw new InvalidParameterValueException("Unable to find " + ruleId); } _accountMgr.checkAccess(caller, rule); revokeRule(rule, caller, ctx.getCallerUserId()); if (apply) { return applyPortForwardingRules(rule.getSourceIpAddress(), true); } else { return true; } } @Override public boolean revokePortForwardingRule(long vmId) { UserVmVO vm = _vmDao.findById(vmId); if (vm == null) { return false; } List rules = _forwardingDao.listByVm(vmId); for (PortForwardingRuleVO rule : rules) { revokePortForwardingRule(rule.getId(), true); } return true; } public List listFirewallRules(Ip ip) { return _firewallDao.listByIpAndNotRevoked(ip); } @Override public List listPortForwardingRulesForApplication(Ip ip) { return _forwardingDao.listForApplication(ip); } @Override public List listPortForwardingRules(ListPortForwardingRulesCmd cmd) { Account caller = UserContext.current().getCaller(); List rules = null; if(cmd.getIpAddress() != null){ Ip ipAddress = new Ip(cmd.getIpAddress()); IPAddressVO ipAddressVO = _ipAddressDao.findById(ipAddress); if (ipAddressVO == null || !ipAddressVO.readyToUse()) { throw new InvalidParameterValueException("Ip address not ready for port forwarding rules yet: " + ipAddress); } rules = _forwardingDao.listByIp(ipAddress); _accountMgr.checkAccess(caller, rules.toArray(new PortForwardingRuleVO[rules.size()])); } else { rules = _forwardingDao.listByAccount(caller.getAccountId()); } return rules; } @Override public boolean applyPortForwardingRules(Ip ip, boolean continueOnError) { try { return applyPortForwardingRules(ip, continueOnError, null); } catch (ResourceUnavailableException e) { s_logger.warn("Unable to reapply port forwarding rules for " + ip); return false; } } protected boolean applyPortForwardingRules(Ip ip, boolean continueOnError, Account caller) throws ResourceUnavailableException { List rules = _forwardingDao.listForApplication(ip); if (rules.size() == 0) { s_logger.debug("There are no rules to apply for " + ip); return true; } if (caller != null) { _accountMgr.checkAccess(caller, rules.toArray(new PortForwardingRuleVO[rules.size()])); } if (!_networkMgr.applyRules(rules, continueOnError)) { s_logger.debug("Rules are not completely applied"); return false; } for (PortForwardingRuleVO rule : rules) { if (rule.getState() == FirewallRule.State.Revoke) { _forwardingDao.remove(rule.getId()); } else if (rule.getState() == FirewallRule.State.Add) { rule.setState(FirewallRule.State.Active); _forwardingDao.update(rule.getId(), rule); } } return true; } @Override public List searchForIpForwardingRules(Ip ip, Long id, Long vmId, Long start, Long size) { return _forwardingDao.searchNatRules(ip, id, vmId, start, size); } @Override public boolean applyPortForwardingRules(Ip ip, Account caller) throws ResourceUnavailableException { return applyPortForwardingRules(ip, false, caller); } @Override public boolean revokeAllRules(Ip ip, long userId) throws ResourceUnavailableException { List rules = _forwardingDao.listByIpAndNotRevoked(ip); if (s_logger.isDebugEnabled()) { s_logger.debug("Releasing " + rules.size() + " rules for " + ip); } for (PortForwardingRuleVO rule : rules) { revokeRule(rule, null, userId); } applyPortForwardingRules(ip, true, null); // Now we check again in case more rules have been inserted. rules = _forwardingDao.listByIpAndNotRevoked(ip); if (s_logger.isDebugEnabled()) { s_logger.debug("Successfully released rules for " + ip + " and # of rules now = " + rules.size()); } return rules.size() == 0; } @Override public boolean configure(String name, Map params) throws ConfigurationException { _name = name; return true; } @Override public boolean start() { return true; } @Override public boolean stop() { return true; } @Override public String getName() { return _name; } @Override public List listFirewallRulesByIp(Ip ip) { return null; } @Override public boolean releasePorts(Ip ip, String protocol, FirewallRule.Purpose purpose, int... ports) { return _firewallDao.releasePorts(ip, protocol, purpose, ports); } @Override @DB public FirewallRuleVO[] reservePorts(IpAddress ip, String protocol, FirewallRule.Purpose purpose, int... ports) throws NetworkRuleConflictException { FirewallRuleVO[] rules = new FirewallRuleVO[ports.length]; Transaction txn = Transaction.currentTxn(); txn.start(); for (int i = 0; i < ports.length; i++) { rules[i] = new FirewallRuleVO(null, ip.getAddress(), ports[i], protocol, ip.getAssociatedWithNetworkId(), ip.getAllocatedToAccountId(), ip.getAllocatedInDomainId(), purpose); rules[i] = _firewallDao.persist(rules[i]); } txn.commit(); boolean success = false; try { for (FirewallRuleVO newRule : rules) { detectRulesConflict(newRule, ip); } success = true; return rules; } finally { if (!success) { txn.start(); for (FirewallRuleVO newRule : rules) { _forwardingDao.remove(newRule.getId()); } txn.commit(); } } } @Override public List gatherPortForwardingRulesForApplication(List addrs) { List allRules = new ArrayList(); for (IpAddress addr : addrs) { if (!addr.readyToUse()) { if (s_logger.isDebugEnabled()) { s_logger.debug("Skipping " + addr + " because it is not ready for propation yet."); } continue; } allRules.addAll(_forwardingDao.listForApplication(addr.getAddress())); } if (s_logger.isDebugEnabled()) { s_logger.debug("Found " + allRules.size() + " rules to apply for the addresses."); } return allRules; } @Override public List listByNetworkId(long networkId) { return _forwardingDao.listByNetworkId(networkId); } }