diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java index 1849a911f..403ae74cf 100644 --- a/router/java/src/net/i2p/router/transport/TransportManager.java +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -74,6 +74,7 @@ public class TransportManager implements TransportEventListener { private final Map _pluggableTransports; private final RouterContext _context; private final UPnPManager _upnpManager; + private final SimpleTimer2.TimedEvent _upnpRefresher; private final DHSessionKeyBuilder.PrecalcRunner _dhThread; private final X25519KeyFactory _xdhThread; private final boolean _enableUDP; @@ -104,6 +105,8 @@ public class TransportManager implements TransportEventListener { /** not forever, since they may update */ private static final long SIGTYPE_BANLIST_DURATION = 36*60*60*1000L; + private static final long UPNP_REFRESH_TIME = UPnP.LEASE_TIME_SECONDS * 1000L / 3; + public TransportManager(RouterContext context) { _context = context; _log = _context.logManager().getLog(TransportManager.class); @@ -120,6 +123,7 @@ public class TransportManager implements TransportEventListener { boolean isProxied = isProxied(); boolean enableUPnP = !isProxied && _context.getBooleanPropertyDefaultTrue(PROP_ENABLE_UPNP); _upnpManager = enableUPnP ? new UPnPManager(context, this) : null; + _upnpRefresher = enableUPnP ? new UPnPRefresher() : null; _enableUDP = _context.getBooleanPropertyDefaultTrue(PROP_ENABLE_UDP); _enableNTCP1 = isNTCPEnabled(context) && context.getProperty(PROP_NTCP1_ENABLE, DEFAULT_NTCP1_ENABLE); @@ -453,8 +457,10 @@ public class TransportManager implements TransportEventListener { // Always start on Android, as we may have a cellular IPv4 address but // are routing all traffic through WiFi. // Also, conditions may change rapidly. - if (_upnpManager != null && (SystemVersion.isAndroid() || Addresses.getAnyAddress() == null)) + if (_upnpManager != null && (SystemVersion.isAndroid() || Addresses.getAnyAddress() == null)) { _upnpManager.start(); + _upnpRefresher.schedule(UPNP_REFRESH_TIME); + } configTransports(); _log.debug("Starting up the transport manager"); // Let's do this in a predictable order to make testing easier @@ -491,8 +497,10 @@ public class TransportManager implements TransportEventListener { * Can be restarted. */ synchronized void stopListening() { - if (_upnpManager != null) + if (_upnpManager != null) { + _upnpRefresher.cancel(); _upnpManager.stop(); + } for (Transport t : _transports.values()) { t.stopListening(); } @@ -989,6 +997,23 @@ public class TransportManager implements TransportEventListener { } } + /** + * Periodic refresh of UPnP ports. + * This is required because UPnP leases expire. + * UPnPManager.Rescanner finds new devices but does not refresh the ports. + * Caller must schedule. + * + * @since 0.9.50 + */ + private class UPnPRefresher extends SimpleTimer2.TimedEvent { + public UPnPRefresher() { super(_context.simpleTimer2()); } + + public void timeReached() { + transportAddressChanged(); + reschedule(UPNP_REFRESH_TIME); + } + } + List getMostRecentErrorMessages() { List rv = new ArrayList(16); for (Transport t : _transports.values()) { diff --git a/router/java/src/net/i2p/router/transport/UPnP.java b/router/java/src/net/i2p/router/transport/UPnP.java index 6e149ef41..115cadf81 100644 --- a/router/java/src/net/i2p/router/transport/UPnP.java +++ b/router/java/src/net/i2p/router/transport/UPnP.java @@ -5,6 +5,7 @@ package net.i2p.router.transport; import java.io.Serializable; import java.net.InetAddress; +import java.net.Inet6Address; import java.net.UnknownHostException; import java.net.URI; import java.net.URISyntaxException; @@ -101,6 +102,8 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis private static final String WAN_IP_CONNECTION_2 = "urn:schemas-upnp-org:service:WANIPConnection:2"; private static final String WAN_IPV6_CONNECTION = "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"; + public static final int LEASE_TIME_SECONDS = 3*60*60; + private Device _router; private Service _service; private Service _service6; @@ -1201,7 +1204,14 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis synchronized(lock) { for(ForwardPort port : portsToForward) { sb.append("
"); - sb.append(port.isIP6 ? "IPv6: " : "IPv4: "); + if (port.isIP6) { + sb.append("IPv6 "); + sb.append(((IPv6ForwardPort) port).getIP()).append(' '); + } else { + sb.append("IPv4 "); + if (addr != null) + sb.append(DataHelper.escapeHTML(addr)).append(' '); + } if(portsForwarded.contains(port)) // {0} is TCP or UDP // {1,number,#####} prevents 12345 from being output as 12,345 in the English locale. @@ -1295,7 +1305,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis add.setArgumentValue("NewEnabled","1"); // 3 hours // MUST be longer than max RI republish which is 52 minutes - int leaseTime = _permanentLeasesOnly ? 0 : 3*60*60; + int leaseTime = _permanentLeasesOnly ? 0 : LEASE_TIME_SECONDS; add.setArgumentValue("NewLeaseDuration", leaseTime); boolean rv = add.postControlAction(); @@ -1370,8 +1380,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis add.setArgumentValue("InternalPort", port); add.setArgumentValue("Protocol", fp.protocol); // permanent leases aren't supported by miniupnpd anyway - int leaseTime = 3*60*60; - add.setArgumentValue("LeaseTime", leaseTime); + add.setArgumentValue("LeaseTime", LEASE_TIME_SECONDS); int uid = fp.getUID(); if (uid < 0) { uid = getNewUID(); @@ -1398,7 +1407,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis int newuid = Integer.parseInt(a.getValue()); if (newuid != uid) { if (_log.shouldWarn()) - _log.warn("Updated UID from " + uid + " to " + newuid); + _log.warn("Updating UID from " + uid + " to " + newuid + " for " + fp); fp.setUID(newuid); } } catch (NumberFormatException nfe) {} @@ -1410,7 +1419,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis int level = rv ? Log.INFO : Log.WARN; if (_log.shouldLog(level)) { StringBuilder buf = new StringBuilder(); - buf.append("AddPinhole result for ").append(ip).append(' ').append(fp.protocol).append(" port ").append(port); + buf.append("AddPinhole result for ").append(fp.toString()); UPnPStatus status = add.getStatus(); if (status != null) buf.append(" Status: ").append(status.getCode()).append(' ').append(status.getDescription()); @@ -1621,10 +1630,10 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis if (!noLog && _log.shouldLog(level)) { String ip = fp.getIP(); if (retval) { - _log.log(level, "UPnP: Removed IPv6 mapping for " + fp.name + ' ' + ip + ' ' + port + " / " + protocol); + _log.log(level, "UPnP: Removed IPv6 mapping for " + fp); } else { StringBuilder buf = new StringBuilder(); - buf.append("UPnP: Failed to remove IPv6 mapping for ").append(fp.getIP()).append(" port ").append(fp.portNumber).append(" / ").append(protocol); + buf.append("UPnP: Failed to remove IPv6 mapping for ").append(fp.toString()); UPnPStatus status = remove.getStatus(); if (status != null) buf.append(" Status: ").append(status.getCode()).append(' ').append(status.getDescription()); @@ -1662,6 +1671,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis portsToForward.clear(); portsToForwardNow = null; } else { + portsToForwardNow = new HashSet(); // Some ports to keep, some ports to dump // Ports in ports but not in portsToForwardNow we must forward // Ports in portsToForwardNow but not in ports we must dump @@ -1676,19 +1686,55 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis // Do we need to re-forward anyway? or poll the router? //} else { // Needs forwarding - if(portsToForwardNow == null) portsToForwardNow = new HashSet(); portsToForwardNow.add(port); //} } for(ForwardPort port : portsToForward) { if(ports.contains(port)) { // Should be forwarded, has been forwarded, cool. + if (port.isIP6) { + // copy over uid and expiration from existing + ports.remove(port); + ports.add(port); + portsToForwardNow.remove(port); + portsToForwardNow.add(port); + if (_log.shouldWarn()) + _log.warn("Retaining: " + port); + } } else { - // TODO don't dump old ipv6 immediately if temporary + boolean keep = false; + if (port.isIP6) { + // Don't dump old ipv6 immediately if deprecated + IPv6ForwardPort v6port = (IPv6ForwardPort) port; + long now = _context.clock().now(); + long exp = v6port.getExpiration(); + if (exp > 0) { + keep = exp < now; + } else { + try { + Inet6Address v6addr = (Inet6Address) InetAddress.getByName(v6port.getIP()); + // Addresses caches the result, so don't use isDeprecated(), it may not be current + if (Addresses.isTemporary(v6addr)) { + v6port.setExpiration(now + 24*60*60*1000L); + keep = true; + if (_log.shouldWarn()) + _log.warn("Address now deprecated, continue forwarding for 24h: " + v6port); + } + } catch (UnknownHostException uhe) {} + } + if (keep) { + // copy over uid and expiration from existing + ports.add(port); + portsToForwardNow.remove(port); + portsToForwardNow.add(port); + } + } - // Needs dropping - if(portsToDumpNow == null) portsToDumpNow = new HashSet(); - portsToDumpNow.add(port); + if (!keep) { + // Needs dropping + if (portsToDumpNow == null) portsToDumpNow = new HashSet(); + portsToDumpNow.add(port); + } } } portsToForward.clear(); @@ -1764,6 +1810,15 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis } else { fps = new ForwardPortStatus(ForwardPortStatus.PROBABLE_FAILURE, "UPnP port forwarding apparently failed", port.portNumber); } + if (port.isIP6) { + // Don't report result if deprecated + IPv6ForwardPort v6port = (IPv6ForwardPort) port; + if (v6port.getExpiration() > 0) { + if (_log.shouldWarn()) + _log.warn("Not reporting result for deprecated " + v6port + " - " + fps.reasonString); + continue; + } + } map.put(port, fps); } forwardCallback.portForwardStatus(map); @@ -1809,6 +1864,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis static class IPv6ForwardPort extends ForwardPort { private final String _ip; private int _uid = -1; + private long _expires; /** * @param ip the IPv6 address being forwarded @@ -1829,6 +1885,16 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis * @param uid 0-65535 */ public synchronized void setUID(int uid) { _uid = uid; } + + /** + * @return absolute time or 0 if unset + */ + public synchronized long getExpiration() { return _expires; } + + /** + * @param expires absolute time + */ + public synchronized void setExpiration(long expires) { _expires = expires; } @Override public int hashCode() { @@ -1845,6 +1911,11 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis IPv6ForwardPort f = (IPv6ForwardPort) o; return _ip.equals(f.getIP()) && super.equals(o); } + + @Override + public String toString() { + return "IPv6FP " + name + ' ' + protocol + ' ' + _ip + ' ' + portNumber + ' ' + _uid + ' ' + _expires; + } } /**