package net.i2p.router.transport; /* * public domain */ import java.net.InetAddress; import java.util.HashSet; import java.util.Map; import java.util.Set; import net.i2p.router.RouterContext; import static net.i2p.router.transport.Transport.AddressSource.SOURCE_UPNP; import net.i2p.util.Addresses; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; import net.i2p.util.Translate; import org.cybergarage.util.Debug; import org.freenetproject.DetectedIP; import org.freenetproject.ForwardPort; import org.freenetproject.ForwardPortCallback; import org.freenetproject.ForwardPortStatus; /** * Bridge from the I2P RouterAddress data structure to * the freenet data structures * * @since 0.7.4 * @author zzz */ class UPnPManager { private final Log _log; private final RouterContext _context; private final UPnP _upnp; private final UPnPCallback _upnpCallback; private volatile boolean _isRunning; private volatile boolean _shouldBeRunning; private volatile long _lastRescan; private boolean _errorLogged; private boolean _disconLogged; private InetAddress _detectedAddress; private final TransportManager _manager; private final SimpleTimer2.TimedEvent _rescanner; /** * This is the TCP HTTP Event listener * We move these so we don't conflict with other users of the same upnp library * UPnP also binds to port 1900 UDP for multicast reception - this cannot be changed. */ private static final String PROP_HTTP_PORT = "i2np.upnp.HTTPPort"; private static final int DEFAULT_HTTP_PORT = 7652; /** this is the UDP SSDP Search reply listener */ private static final String PROP_SSDP_PORT = "i2np.upnp.SSDPPort"; private static final int DEFAULT_SSDP_PORT = 7653; private static final long RESCAN_MIN_DELAY = 60*1000; private static final long RESCAN_SHORT_DELAY = 2*60*1000; // minimum UPnP announce interval is 30 minutes. Let's be faster // 30 minutes is also the default "lease time" in cybergarage. // It expires after 31 minutes. private static final long RESCAN_LONG_DELAY = 14*60*1000; public UPnPManager(RouterContext context, TransportManager manager) { _context = context; _manager = manager; _log = _context.logManager().getLog(UPnPManager.class); // UPnP wants to bind to IPv6 link local interfaces by default, but what UPnP router // is going to want to talk IPv6 anyway? Just make it easy and force IPv4 only org.cybergarage.upnp.UPnP.setEnable(org.cybergarage.upnp.UPnP.USE_ONLY_IPV4_ADDR); // set up logging in the UPnP package Debug.initialize(context); _upnp = new UPnP(context); _upnp.setHTTPPort(_context.getProperty(PROP_HTTP_PORT, DEFAULT_HTTP_PORT)); _upnp.setSSDPPort(_context.getProperty(PROP_SSDP_PORT, DEFAULT_SSDP_PORT)); _upnpCallback = new UPnPCallback(); _rescanner = new Rescanner(); } /** * Blocking, may take a while */ public synchronized void start() { if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Start"); _shouldBeRunning = true; if (!_isRunning) { long b = _context.clock().now(); try { _isRunning = _upnp.runPlugin(); if (_log.shouldLog(Log.INFO)) _log.info("UPnP runPlugin took " + (_context.clock().now() - b)); } catch (RuntimeException e) { // NPE in UPnP (ticket #728), can't let it bring us down if (!_errorLogged) { _log.error("UPnP error, please report", e); _errorLogged = true; } } } if (_isRunning) { _rescanner.schedule(RESCAN_LONG_DELAY); if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Start Done"); } else { _rescanner.schedule(RESCAN_SHORT_DELAY); // Do we have a non-loopback, non-broadcast address? // If not, that's why it failed (HTTPServer won't start) if (!Addresses.isConnected()) { if (!_disconLogged) { _log.logAlways(Log.WARN, "UPnP start failed - no network connection?"); _disconLogged = true; } } else { _log.error("UPnP start failed - port conflict?"); } } } /** * Blocking, may take a while, up to 20 seconds */ public synchronized void stop() { if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Stop"); _shouldBeRunning = false; _rescanner.cancel(); if (_isRunning) _upnp.terminate(); _isRunning = false; _detectedAddress = null; if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Stop Done"); } /** * Call when IP or network connectivity might have changed. * Starts UPnP if previous start failed, else starts a search. * Must have called start() first, and not called stop(). * * Should be fast. This only starts the search, the responses * will come in over the MX time (3 seconds). * * @since 0.9.18 */ public synchronized void rescan() { if (!_shouldBeRunning) return; if (_context.router().gracefulShutdownInProgress()) return; long now = System.currentTimeMillis(); if (_lastRescan + RESCAN_MIN_DELAY > now) return; _lastRescan = now; if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Rescan Start"); if (_isRunning) { // TODO default search MX (jitter) is 3 seconds... reduce? // See also: // Adaptive Jitter Control for UPnP M-Search // Kevin Mills and Christopher Dabrowski _upnp.search(); } else { start(); } } /** * Initiate a UPnP search * * @since 0.9.18 */ private class Rescanner extends SimpleTimer2.TimedEvent { /** caller must schedule() */ public Rescanner() { super(_context.simpleTimer2()); } public void timeReached() { if (_shouldBeRunning) { rescan(); reschedule(_isRunning ? RESCAN_LONG_DELAY : RESCAN_SHORT_DELAY); } } } /** * Call when the ports might have changed * The transports can call this pretty quickly at startup, * which can have multiple UPnP threads running at once, but * that should be ok. */ public void update(Set<TransportManager.Port> ports) { if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Update with " + ports.size() + " ports"); //synchronized(this) { // TODO // called too often and may block for too long // may not have started if net was disconnected previously //if (!_isRunning && !ports.isEmpty()) // start(); if (!_isRunning) return; //} Set<ForwardPort> forwards = new HashSet<ForwardPort>(ports.size()); for (TransportManager.Port entry : ports) { String style = entry.style; int port = entry.port; int protocol = -1; if ("SSU".equals(style)) protocol = ForwardPort.PROTOCOL_UDP_IPV4; else if ("NTCP".equals(style)) protocol = ForwardPort.PROTOCOL_TCP_IPV4; else continue; if (_log.shouldLog(Log.DEBUG)) _log.debug("Adding: " + style + " " + port); ForwardPort fp = new ForwardPort(style, false, protocol, port); forwards.add(fp); } // non-blocking _upnp.onChangePublicPorts(forwards, _upnpCallback); } /** * This is the callback from UPnP. * It calls the TransportManager callbacks. */ private class UPnPCallback implements ForwardPortCallback { /** Called to indicate status on one or more forwarded ports. */ public void portForwardStatus(Map<ForwardPort,ForwardPortStatus> statuses) { if (_log.shouldLog(Log.DEBUG)) _log.debug("UPnP Callback:"); // Let's not have two of these running at once. // Deadlock reported in ticket #1699 // and the locking isn't foolproof in UDPTransport. // UPnP runs the callbacks in a thread, so we can block. // There is only one UPnPCallback, so lock on this synchronized(this) { locked_PFS(statuses); } } private void locked_PFS(Map<ForwardPort,ForwardPortStatus> statuses) { byte[] ipaddr = null; DetectedIP[] ips = _upnp.getAddress(); if (ips != null) { for (DetectedIP ip : ips) { // store the first public one and tell the transport manager if it changed if (TransportUtil.isPubliclyRoutable(ip.publicAddress.getAddress(), false)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("External address: " + ip.publicAddress + " type: " + ip.natType); if (!ip.publicAddress.equals(_detectedAddress)) { _detectedAddress = ip.publicAddress; // deadlock path 1 _manager.externalAddressReceived(SOURCE_UPNP, _detectedAddress.getAddress(), 0); } ipaddr = ip.publicAddress.getAddress(); break; } } } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("No external address returned"); } for (Map.Entry<ForwardPort, ForwardPortStatus> entry : statuses.entrySet()) { ForwardPort fp = entry.getKey(); ForwardPortStatus fps = entry.getValue(); if (_log.shouldLog(Log.DEBUG)) _log.debug(fp.name + " " + fp.protocol + " " + fp.portNumber + " status: " + fps.status + " reason: " + fps.reasonString + " ext port: " + fps.externalPort); String style; if (fp.protocol == ForwardPort.PROTOCOL_UDP_IPV4) style = "SSU"; else if (fp.protocol == ForwardPort.PROTOCOL_TCP_IPV4) style = "NTCP"; else continue; boolean success = fps.status >= ForwardPortStatus.MAYBE_SUCCESS; // deadlock path 2 _manager.forwardPortStatus(style, ipaddr, fp.portNumber, fps.externalPort, success, fps.reasonString); } } } /** * Warning - blocking, very slow, queries the active router, * will take many seconds if it has vanished. */ public String renderStatusHTML() { if (!_isRunning) return "<h3><a name=\"upnp\"></a>" + _t("UPnP is not enabled") + "</h3>\n"; return _upnp.renderStatusHTML(); } private static final String BUNDLE_NAME = "net.i2p.router.web.messages"; /** * Translate */ private final String _t(String s) { return Translate.getString(s, _context, BUNDLE_NAME); } }