UPnP: IPv6 part 3

WIP - more to follow
Add support for WANIPv6FirewallControl service
Add class extending PortForward to request forwarding to a specific IP, and to store UID
Bind POST socket to local IP for POST to WANIPv6FirewallControl service
Reduce max forward attempts
Don't sleep after last forward attempt fails
Log tweaks
This commit is contained in:
zzz
2021-02-28 08:36:29 -05:00
parent eb535762c4
commit 851752a57a

View File

@@ -103,6 +103,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
private Device _router;
private Service _service;
private Service _service6;
// UDN -> device
private final Map<String, Device> _otherUDNs;
private final Map<String, String> _eventVars;
@@ -169,6 +170,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
synchronized(lock) {
_router = null;
_service = null;
_service6 = null;
_serviceLacksAPM = false;
_permanentLeasesOnly = false;
}
@@ -286,14 +288,16 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
}
// Find valid service
List<Service> services = null;
Service service = null;
String extIP = null;
boolean subscriptionFailed = false;
if (!ignore) {
service = discoverService(dev);
if (service == null) {
services = discoverService(dev);
if (services == null) {
ignore = true;
} else {
service = services.get(0);
// does it have an external IP?
extIP = getNATAddress(service);
if (extIP == null) {
@@ -394,6 +398,8 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
_eventVars.clear();
_router = dev;
_service = service;
if (services.size() > 1)
_service6 = services.get(1);
_permanentLeasesOnly = false;
}
if (fpc != null)
@@ -427,10 +433,12 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
/**
* Traverses the structure of the router device looking for the port mapping service.
* The first Service will be the IPv4 Service.
* The second Service, if present, will be the IPv6 Service.
*
* @return the service or null
* @return the list of services, non-empty, one or two entries, or null
*/
private Service discoverService(Device router) {
private List<Service> discoverService(Device router) {
for (Device current : router.getDeviceList()) {
String type = current.getDeviceType();
if (!(WAN_DEVICE.equals(type) || WAN_DEVICE_2.equals(type)))
@@ -454,13 +462,16 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
}
}
}
if (_log.shouldInfo()) {
if (service != null) {
Service svc2 = current2.getService(WAN_IPV6_CONNECTION);
if (svc2 != null)
_log.info(_router.getFriendlyName() + " supports WANIPv6Connection, but we don't");
if (svc2 != null) {
List<Service> rv = new ArrayList<Service>(2);
rv.add(service);
rv.add(svc2);
return rv;
}
return Collections.singletonList(service);
}
if (service != null)
return service;
}
}
@@ -468,15 +479,18 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
}
private boolean tryAddMapping(String protocol, int port, String description, ForwardPort fp) {
if (_log.shouldLog(Log.WARN))
_log.warn("Registering a port mapping for " + port + "/" + protocol);
if (_log.shouldWarn())
_log.warn("Registering a port mapping for " + port + "/" + protocol + " IPv" + (fp.isIP6 ? '6' : '4'));
int nbOfTries = 0;
final int maxTries = fp.isIP6 ? 1 : 3;
boolean isPortForwarded = false;
while ((!_serviceLacksAPM) && nbOfTries++ < 5) {
while ((!_serviceLacksAPM) && nbOfTries++ < maxTries) {
//isPortForwarded = addMapping(protocol, port, "I2P " + description, fp);
isPortForwarded = addMapping(protocol, port, description, fp);
if(isPortForwarded || _serviceLacksAPM)
break;
if (++nbOfTries >= maxTries)
break;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {}
@@ -523,6 +537,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
runSearch = true;
_router = null;
_service = null;
_service6 = null;
_eventVars.clear();
_serviceLacksAPM = false;
_permanentLeasesOnly = false;
@@ -617,11 +632,13 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
boolean isIGD = (ROUTER_DEVICE.equals(type) || ROUTER_DEVICE_2.equals(type)) && dev.isRootDevice();
if (!isIGD)
continue;
Service service = discoverService(dev);
if (service == null)
List<Service> services = discoverService(dev);
if (services == null)
continue;
if (sid.equals(service.getSID()))
return dev;
for (Service service : services) {
if (sid.equals(service.getSID()))
return dev;
}
}
}
return null;
@@ -1117,6 +1134,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
Device router;
Service service;
Service service6;
synchronized(lock) {
if (!_otherUDNs.isEmpty()) {
sb.append("<b>");
@@ -1159,6 +1177,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
}
router = _router;
service = _service;
service6 = _service6;
}
listSubDev(null, router, sb);
String addr = getNATAddress(service);
@@ -1176,6 +1195,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
synchronized(lock) {
for(ForwardPort port : portsToForward) {
sb.append("<br>");
sb.append(port.isIP6 ? "IPv6: " : "IPv4: ");
if(portsForwarded.contains(port))
// {0} is TCP or UDP
// {1,number,#####} prevents 12345 from being output as 12,345 in the English locale.
@@ -1211,6 +1231,7 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
/**
* This always requests that the external port == the internal port, for now.
* Blocking!
* @return success
*/
private boolean addMapping(String protocol, int port, String description, ForwardPort fp) {
Service service;
@@ -1219,13 +1240,27 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
_log.error("Can't addMapping: " + isNATPresent() + " " + _router);
return false;
}
service = _service;
service = fp.isIP6 ? _service6 : _service;
}
// Just in case...
// this confuses my linksys? - zzz
//removeMapping(protocol, port, fp, true);
if (service == null) {
if (_log.shouldWarn())
_log.warn("No service for IPv" + (fp.isIP6 ? '6' : '4'));
return false;
}
if (fp.isIP6)
return addMappingV6(service, port, (IPv6ForwardPort) fp);
else
return addMappingV4(service, protocol, port, description, fp);
}
/**
* This always requests that the external port == the internal port, for now.
* Blocking!
*
* @return success
* @since 0.9.50 split out from above
*/
private boolean addMappingV4(Service service, String protocol, int port, String description, ForwardPort fp) {
Action add = service.getAction("AddPortMapping");
if(add == null) {
if (_serviceLacksAPM) {
@@ -1306,6 +1341,92 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
return rv;
}
/**
* This always requests that the external port == the internal port, for now.
* Blocking!
*
* @param service WANIPv6FirewallControl
* @return success
* @since 0.9.50
*/
private boolean addMappingV6(Service service, int port, IPv6ForwardPort fp) {
Action add = service.getAction("AddPinhole");
if (add == null) {
if (_log.shouldWarn())
_log.warn("UPnP device does not support pinholing");
return false;
}
String ip = fp.getIP();
add.setArgumentValue("RemoteHost", ip);
add.setArgumentValue("RemotePort", port);
add.setArgumentValue("InternalClient", ip);
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);
int uid = fp.getUID();
if (uid < 0) {
uid = getNewUID();
fp.setUID(uid);
}
add.setArgumentValue("UniqueID", uid);
// I2P - bind the POST socket to the given IP if we're sending to IPv6
boolean rv;
String hisIP = service.getRootDevice().getLocation(true);
if (hisIP != null && hisIP.contains(":")) {
rv = add.postControlAction(ip);
} else {
// this probably won't work if security is enabled
rv = add.postControlAction();
}
if (rv) {
synchronized(lock) {
portsForwarded.add(fp);
}
}
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);
UPnPStatus status = add.getStatus();
if (status != null)
buf.append(" Status: ").append(status.getCode()).append(' ').append(status.getDescription());
status = add.getControlStatus();
if (status != null)
buf.append(" ControlStatus: ").append(status.getCode()).append(' ').append(status.getDescription());
_log.log(level, buf.toString());
}
// 606 Action not authorized
return rv;
}
/**
* 65536 isn't a lot, so check for dups
*
* @return 0 - 65535
* @since 0.9.50
*/
private int getNewUID() {
synchronized(lock) {
while(true) {
int rv = _context.random().nextInt(65536);
boolean dup = false;
for (ForwardPort fp : portsToForward) {
if (fp.isIP6 && ((IPv6ForwardPort) fp).getUID() == rv) {
dup = true;
break;
}
}
if (!dup)
return rv;
}
}
}
/**
* @param dev non-null
@@ -1389,7 +1510,10 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
return rv;
}
/** blocking */
/**
* Blocking
* @return success
*/
private boolean removeMapping(String protocol, int port, ForwardPort fp, boolean noLog) {
Service service;
synchronized(lock) {
@@ -1397,9 +1521,25 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
_log.error("Can't removeMapping: " + isNATPresent() + " " + _router);
return false;
}
service = _service;
service = fp.isIP6 ? _service6 : _service;
}
if (service == null) {
if (_log.shouldWarn())
_log.warn("No service for IPv" + (fp.isIP6 ? '6' : '4'));
return false;
}
if (fp.isIP6)
return removeMappingV6(service, protocol, port, (IPv6ForwardPort) fp, noLog);
else
return removeMappingV4(service, protocol, port, fp, noLog);
}
/**
*
* @since 0.9.50 split out from above
* @return success
*/
private boolean removeMappingV4(Service service, String protocol, int port, ForwardPort fp, boolean noLog) {
Action remove = service.getAction("DeletePortMapping");
if(remove == null) {
if (_log.shouldLog(Log.WARN))
@@ -1416,8 +1556,50 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
portsForwarded.remove(fp);
}
if(_log.shouldLog(Log.WARN) && !noLog)
_log.warn("UPnP: Removed mapping for "+fp.name+" "+port+" / "+protocol);
if (!noLog && _log.shouldWarn()) {
if (retval)
_log.warn("UPnP: Removed IPv4 mapping for "+fp.name+" "+port+" / "+protocol);
else
_log.warn("UPnP: Failed to remove IPv4 mapping for "+fp.name+" "+port+" / "+protocol);
}
return retval;
}
/**
*
* @since 0.9.50
* @return success
*/
private boolean removeMappingV6(Service service, String protocol, int port, IPv6ForwardPort fp, boolean noLog) {
int uid = fp.getUID();
if (uid < 0)
return false;
Action remove = service.getAction("DeletePinhole");
if (remove == null) {
if (_log.shouldWarn())
_log.warn("Couldn't find DeletePinhole action!");
return false;
}
remove.setArgumentValue("UniqueID", uid);
// I2P - bind the POST socket to the given IP if we're sending to IPv6
boolean retval;
String hisIP = service.getRootDevice().getLocation(true);
if (hisIP != null && hisIP.contains(":")) {
String ip = fp.getIP();
retval = remove.postControlAction(ip);
} else {
retval = remove.postControlAction();
}
synchronized(lock) {
portsForwarded.remove(fp);
}
if (!noLog && _log.shouldWarn()) {
String ip = fp.getIP();
if (retval)
_log.warn("UPnP: Removed IPv6 mapping for " + fp.name + ' ' + ip + ' ' + port + " / " + protocol);
else
_log.warn("UPnP: Failed to remove IPv6 mapping for " + fp.name + ' ' + ip + ' ' + port + " / " + protocol);
}
return retval;
}
@@ -1468,6 +1650,8 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
if(ports.contains(port)) {
// Should be forwarded, has been forwarded, cool.
} else {
// TODO don't dump old ipv6 immediately if temporary
// Needs dropping
if(portsToDumpNow == null) portsToDumpNow = new HashSet<ForwardPort>();
portsToDumpNow.add(port);
@@ -1584,6 +1768,51 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
}
}
/**
* Extended to store the requested IP to be forwarded.
* @since 0.9.50
*/
static class IPv6ForwardPort extends ForwardPort {
private final String _ip;
private int _uid = -1;
/**
* @param ip the IPv6 address being forwarded
*/
public IPv6ForwardPort(String name, int protocol, int port, String ip) {
super(name, true, protocol, port);
_ip = ip;
}
public String getIP() { return _ip; }
/**
* @return 0-65535 or -1 if unset
*/
public synchronized int getUID() { return _uid; }
/**
* @param uid 0-65535
*/
public synchronized void setUID(int uid) { _uid = uid; }
@Override
public int hashCode() {
return _ip.hashCode() ^ super.hashCode();
}
/**
* Ignores UID
*/
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof IPv6ForwardPort)) return false;
IPv6ForwardPort f = (IPv6ForwardPort) o;
return _ip.equals(f.getIP()) && super.equals(o);
}
}
/**
* Dumps out device info in semi-HTML format
*/