/* * Copyright 2011 Thomas Bocek * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package net.tomp2p.connection2; import java.io.IOException; import java.net.InetAddress; import java.util.Collection; import java.util.HashMap; import java.util.Map; import net.tomp2p.natpmp.Gateway; import net.tomp2p.natpmp.MapRequestMessage; import net.tomp2p.natpmp.NatPmpDevice; import net.tomp2p.natpmp.NatPmpException; import net.tomp2p.natpmp.ResultCode; import net.tomp2p.upnp.InternetGatewayDevice; import net.tomp2p.upnp.UPNPResponseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is used to do automatic port forwarding. It maps with PMP und UPNP and also unmaps them. It creates a * shutdown hook in case the user exits the application without a proper shutdown. * * @author Thomas Bocek */ public class NATUtils { private static final Logger LOGGER = LoggerFactory.getLogger(NATUtils.class); // UPNP private final Map<InternetGatewayDevice, Integer> internetGatewayDevicesUDP = new HashMap<InternetGatewayDevice, Integer>(); private final Map<InternetGatewayDevice, Integer> internetGatewayDevicesTCP = new HashMap<InternetGatewayDevice, Integer>(); // NAT-PMP private NatPmpDevice pmpDevice; private final Object shutdownLock = new Object(); private boolean runOnce = false; /** * Constructor. */ public NATUtils() { setShutdownHookEnabled(); } /** * Maps with the PMP protocol (http://en.wikipedia.org/wiki/NAT_Port_Mapping_Protocol). One of the drawbacks of this * protocol is that it needs to know the IP of the router. To get the IP of the router in Java, we need to use * netstat and parse the output. * * @param internalPortUDP * The UDP internal port * @param internalPortTCP * The TCP internal port * @param externalPortUDP * The UDP external port * @param externalPortTCP * The TCP external port * @return True, if the mapping was successful (i.e. the router supports PMP) * @throws NatPmpException * the router does not supports PMP */ public boolean mapPMP(final int internalPortUDP, final int internalPortTCP, final int externalPortUDP, final int externalPortTCP) throws NatPmpException { InetAddress gateway = Gateway.getIP(); pmpDevice = new NatPmpDevice(gateway); MapRequestMessage mapTCP = new MapRequestMessage(true, internalPortTCP, externalPortTCP, Integer.MAX_VALUE, null); MapRequestMessage mapUDP = new MapRequestMessage(false, internalPortUDP, externalPortUDP, Integer.MAX_VALUE, null); pmpDevice.enqueueMessage(mapTCP); pmpDevice.enqueueMessage(mapUDP); pmpDevice.waitUntilQueueEmpty(); // UDP is nice to have, but we need TCP return mapTCP.getResultCode() == ResultCode.Success; } /** * Maps with UPNP protocol (http://en.wikipedia.org/wiki/Internet_Gateway_Device_Protocol). Since this uses * broadcasting to discover routers, no calling the external program netstat is necessary. * * @param internalHost * The internal host to map the ports to * @param internalPortUDP * The UDP internal port * @param internalPortTCP * The TCP internal port * @param externalPortUDP * The UDP external port * @param externalPortTCP * The TCP external port * @return True, if at least one mapping was successful (i.e. the router supports UPNP) * @throws IOException * Exception */ public boolean mapUPNP(final String internalHost, final int internalPortUDP, final int internalPortTCP, final int externalPortUDP, final int externalPortTCP) throws IOException { // -1 sets the default timeout to 1500 ms Collection<InternetGatewayDevice> internetGDs = InternetGatewayDevice.getDevices(-1); if (internetGDs == null) { LOGGER.info("no UPNP device found"); return false; } boolean once = false; for (InternetGatewayDevice igd : internetGDs) { LOGGER.info("Found device " + igd); try { if (externalPortUDP != -1) { boolean mappedUDP = igd.addPortMapping("TomP2P mapping UDP", "UDP", internalHost, externalPortUDP, internalPortUDP); if (mappedUDP) { internetGatewayDevicesUDP.put(igd, externalPortUDP); } } } catch (IOException e) { LOGGER.warn("error in mapping UPD UPNP " + e); continue; } catch (UPNPResponseException e) { LOGGER.warn("error in mapping UDP UPNP " + e); continue; } try { if (externalPortTCP != -1) { boolean mappedTCP = igd.addPortMapping("TomP2P mapping TCP", "TCP", internalHost, externalPortTCP, internalPortTCP); if (mappedTCP) { internetGatewayDevicesTCP.put(igd, externalPortTCP); } } } catch (IOException e) { LOGGER.warn("error in mapping TCP UPNP " + e); continue; } catch (UPNPResponseException e) { LOGGER.warn("error in mapping TCP UPNP " + e); continue; } once = true; } return once; } /** * Unmap the device that has been mapped previously. Used during shutdown. */ private void unmapUPNP() { for (Map.Entry<InternetGatewayDevice, Integer> entry : internetGatewayDevicesTCP.entrySet()) { try { entry.getKey().deletePortMapping(null, entry.getValue(), "TCP"); } catch (IOException e) { LOGGER.warn("not removed TCP mapping " + entry.toString() + e); } catch (UPNPResponseException e) { LOGGER.warn("not removed TCP mapping " + entry.toString() + e); } LOGGER.info("removed TCP mapping " + entry.toString()); } for (Map.Entry<InternetGatewayDevice, Integer> entry : internetGatewayDevicesUDP.entrySet()) { try { entry.getKey().deletePortMapping(null, entry.getValue(), "UDP"); } catch (IOException e) { LOGGER.warn("not removed UDP mapping " + entry.toString() + e); } catch (UPNPResponseException e) { LOGGER.warn("not removed UDP mapping " + entry.toString() + e); } LOGGER.info("removed UDP mapping " + entry.toString()); } } /** * Registers a shutdownhook to clean the NAT mapping. If this is not called, then the mapping may stay until the * router is rebooted. */ private void setShutdownHookEnabled() { synchronized (shutdownLock) { // Set to enabled. // The shutdown hook simply runs the shutdown method. Thread t = new Thread(new Runnable() { public void run() { shutdown(); } }, "TomP2P:NATUtils:ShutdownHook"); Runtime.getRuntime().addShutdownHook(t); } } /** * Since shutdown is also called from the shutdown hook, it might get called twice. Thus, this method deregister NAT * mappings only once. If it already has been called, this method does nothing. */ public void shutdown() { synchronized (shutdownLock) { if (runOnce) { return; } runOnce = true; unmapUPNP(); if (pmpDevice != null) { pmpDevice.shutdown(); } } } }