package freenet.node; import static java.util.concurrent.TimeUnit.SECONDS; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import freenet.config.InvalidConfigValueException; import freenet.config.SubConfig; import freenet.io.comm.FreenetInetAddress; import freenet.io.comm.Peer; import freenet.io.comm.UdpSocketHandler; import freenet.l10n.NodeL10n; import freenet.node.useralerts.IPUndetectedUserAlert; import freenet.node.useralerts.InvalidAddressOverrideUserAlert; import freenet.node.useralerts.SimpleUserAlert; import freenet.node.useralerts.UserAlert; import freenet.pluginmanager.DetectedIP; import freenet.pluginmanager.FredPluginBandwidthIndicator; import freenet.pluginmanager.FredPluginIPDetector; import freenet.pluginmanager.FredPluginPortForward; import freenet.support.HTMLNode; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.api.StringCallback; import freenet.support.io.NativeThread; import freenet.support.transport.ip.HostnameSyntaxException; import freenet.support.transport.ip.IPAddressDetector; import freenet.support.transport.ip.IPUtil; /** * Detect the IP address of the node. Doesn't return port numbers, doesn't have access to per-port * information (NodeCrypto - UdpSocketHandler etc). */ public class NodeIPDetector { private static volatile boolean logMINOR; private static volatile boolean logDEBUG; static { Logger.registerLogThresholdCallback(new LogThresholdCallback(){ @Override public void shouldUpdate(){ logMINOR = Logger.shouldLog(LogLevel.MINOR, this); logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this); } }); } /** Parent node */ final Node node; /** Ticker */ /** Explicit forced IP address */ FreenetInetAddress overrideIPAddress; /** Explicit forced IP address in string form because we want to keep it even if it's invalid and therefore unused */ String overrideIPAddressString; /** IP address from last time */ FreenetInetAddress oldIPAddress; /** Detected IP's and their NAT status from plugins */ DetectedIP[] pluginDetectedIPs; /** Last detected IP address */ FreenetInetAddress[] lastIPAddress; private class MinimumMTU { /** The minimum reported MTU on all detected interfaces */ private int minimumMTU = Integer.MAX_VALUE; /** Report a new MTU from an interface or detector. * If the minimum MTU has changed, returns true. */ boolean report(int mtu) { if(mtu <= 0) return false; if(mtu < minimumMTU) { Logger.normal(this, "Reducing the MTU to "+minimumMTU); minimumMTU = mtu; return true; } return false; } public int get() { return minimumMTU > 0 ? minimumMTU : 1500; } } private final MinimumMTU minimumMTUIPv4 = new MinimumMTU(); private final MinimumMTU minimumMTUIPv6 = new MinimumMTU(); /** IP address detector */ private final IPAddressDetector ipDetector; /** Plugin manager for plugin IP address detectors e.g. STUN */ final IPDetectorPluginManager ipDetectorManager; /** UserAlert shown when ipAddressOverride has a hostname/IP address syntax error */ private InvalidAddressOverrideUserAlert invalidAddressOverrideAlert; private boolean hasValidAddressOverride; /** UserAlert shown when we can't detect an IP address */ private IPUndetectedUserAlert primaryIPUndetectedAlert; // FIXME redundant? see lastIPAddress FreenetInetAddress[] lastIP; /** Set when we have grounds to believe that we may be behind a symmetric NAT. */ boolean maybeSymmetric; /** Have the detector plugins been queried (or found to be non-existent)? */ private boolean hasDetectedPM; /** Have we checked peers and local interfaces for our IP address? */ private boolean hasDetectedIAD; /** Subsidiary detectors: NodeIPPortDetector's which rely on this object */ private NodeIPPortDetector[] portDetectors; private boolean hasValidIP; private boolean firstDetection = true; SimpleUserAlert maybeSymmetricAlert; public NodeIPDetector(Node node) { this.node = node; ipDetectorManager = new IPDetectorPluginManager(node, this); ipDetector = new IPAddressDetector(SECONDS.toMillis(10), this); invalidAddressOverrideAlert = new InvalidAddressOverrideUserAlert(node); primaryIPUndetectedAlert = new IPUndetectedUserAlert(node); portDetectors = new NodeIPPortDetector[0]; } public synchronized void addPortDetector(NodeIPPortDetector detector) { portDetectors = Arrays.copyOf(portDetectors, portDetectors.length+1); portDetectors[portDetectors.length - 1] = detector; } /** * What is my IP address? Use all globally available information (everything which isn't * specific to a given port i.e. opennet or darknet) to determine our current IP addresses. * Will include more than one IP in many cases when we are not strictly multi-homed. For * example, if we have a DNS name set, we will usually return an IP as well. * * Will warn the user with a UserAlert if we don't have sufficient information. */ FreenetInetAddress[] detectPrimaryIPAddress(boolean dumpLocalAddresses) { boolean addedValidIP = false; Logger.minor(this, "Redetecting IPs..."); ArrayList<FreenetInetAddress> addresses = new ArrayList<FreenetInetAddress>(); if(overrideIPAddress != null) { // If the IP is overridden and the override is valid, the override has to be the first element. // overrideIPAddress will be null if the override is invalid addresses.add(overrideIPAddress); if(overrideIPAddress.isRealInternetAddress(false, true, false)) addedValidIP = true; } if(!node.dontDetect()) { addedValidIP |= innerDetect(addresses); } if(node.clientCore != null) { boolean hadValidIP; synchronized(this) { hadValidIP = hasValidIP; hasValidIP = addedValidIP; if(firstDetection) { hadValidIP = !addedValidIP; firstDetection = false; } } if(hadValidIP != addedValidIP) { if (addedValidIP) { if(logMINOR) Logger.minor(this, "Got valid IP"); onAddedValidIP(); } else { if(logMINOR) Logger.minor(this, "No valid IP"); onNotAddedValidIP(); } } } else if(logMINOR) Logger.minor(this, "Client core not loaded"); synchronized(this) { hasValidIP = addedValidIP; } lastIPAddress = addresses.toArray(new FreenetInetAddress[addresses.size()]); if(dumpLocalAddresses) { ArrayList<FreenetInetAddress> filtered = new ArrayList<FreenetInetAddress>(lastIPAddress.length); for(FreenetInetAddress addr: lastIPAddress) { if(addr == null) continue; if(addr == overrideIPAddress && addr.hasHostnameNoIP()) filtered.add(addr); else if(addr.hasHostnameNoIP()) continue; else if(IPUtil.isValidAddress(addr.getAddress(), false)) filtered.add(addr); } return filtered.toArray(new FreenetInetAddress[filtered.size()]); } return lastIPAddress; } boolean hasValidIP() { synchronized(this) { return hasValidIP; } } private void onAddedValidIP() { node.clientCore.alerts.unregister(primaryIPUndetectedAlert); node.onAddedValidIP(); } private void onNotAddedValidIP() { node.clientCore.alerts.register(primaryIPUndetectedAlert); } /** * Core of the IP detection algorithm. * @param addresses * @param addedValidIP * @return */ private boolean innerDetect(List<FreenetInetAddress> addresses) { boolean addedValidIP = false; InetAddress[] detectedAddrs = ipDetector.getAddressNoCallback(); assert(detectedAddrs != null); synchronized(this) { hasDetectedIAD = true; } for(InetAddress detectedAddr: detectedAddrs) { FreenetInetAddress addr = new FreenetInetAddress(detectedAddr); if(!addresses.contains(addr)) { Logger.normal(this, "Detected IP address: "+addr); addresses.add(addr); if(addr.isRealInternetAddress(false, false, false)) addedValidIP = true; } } if((pluginDetectedIPs != null) && (pluginDetectedIPs.length > 0)) { for(DetectedIP pluginDetectedIP: pluginDetectedIPs) { InetAddress addr = pluginDetectedIP.publicAddress; if(addr == null) continue; FreenetInetAddress a = new FreenetInetAddress(addr); if(!addresses.contains(a)) { Logger.normal(this, "Plugin detected IP address: "+a); addresses.add(a); if(a.isRealInternetAddress(false, false, false)) addedValidIP = true; } } } boolean hadAddedValidIP = addedValidIP; int confidence = 0; // Try to pick it up from our connections if(node.peers != null) { PeerNode[] peerList = node.peers.myPeers(); HashMap<FreenetInetAddress,Integer> countsByPeer = new HashMap<FreenetInetAddress,Integer>(); // FIXME use a standard mutable int object, we have one somewhere for(PeerNode pn: peerList) { if(!pn.isConnected()) { if(logDEBUG) Logger.minor(this, "Not connected"); continue; } if(!pn.isRealConnection()) { // Only let seed server connections through. // We have to trust them anyway. if(!(pn instanceof SeedServerPeerNode)) continue; if(logMINOR) Logger.minor(this, "Not a real connection and not a seed node: "+pn); } if(logMINOR) Logger.minor(this, "Maybe a usable connection for IP: "+pn); Peer p = pn.getRemoteDetectedPeer(); if(logMINOR) Logger.minor(this, "Remote detected peer: "+p); if(p == null || p.isNull()) continue; FreenetInetAddress addr = p.getFreenetAddress(); if(logMINOR) Logger.minor(this, "Address: "+addr); if(addr == null) continue; if(!IPUtil.isValidAddress(addr.getAddress(false), false)) { if(logMINOR) Logger.minor(this, "Address not valid"); continue; } if(logMINOR) Logger.minor(this, "Peer "+pn.getPeer()+" thinks we are "+addr); if(countsByPeer.containsKey(addr)) { countsByPeer.put(addr, countsByPeer.get(addr) + 1); } else { countsByPeer.put(addr, 1); } } if(countsByPeer.size() == 1) { Entry<FreenetInetAddress, Integer> countByPeer = countsByPeer.entrySet().iterator().next(); FreenetInetAddress addr = countByPeer.getKey(); confidence = countByPeer.getValue(); Logger.minor(this, "Everyone agrees we are "+addr); if(!addresses.contains(addr)) { if(addr.isRealInternetAddress(false, false, false)) addedValidIP = true; addresses.add(addr); } } else if(countsByPeer.size() > 1) { // Take two most popular addresses. FreenetInetAddress best = null; FreenetInetAddress secondBest = null; int bestPopularity = 0; int secondBestPopularity = 0; for(Map.Entry<FreenetInetAddress,Integer> entry : countsByPeer.entrySet()) { FreenetInetAddress cur = entry.getKey(); int curPop = entry.getValue(); Logger.minor(this, "Detected peer: "+cur+" popularity "+curPop); if(curPop >= bestPopularity) { secondBestPopularity = bestPopularity; bestPopularity = curPop; secondBest = best; best = cur; } } if(best != null) { boolean hasRealDetectedAddress = false; for(InetAddress detectedAddr: detectedAddrs) { if(IPUtil.isValidAddress(detectedAddr, false)) hasRealDetectedAddress = true; } if((bestPopularity > 1) || !hasRealDetectedAddress) { if(!addresses.contains(best)) { Logger.minor(this, "Adding best peer "+best+" ("+bestPopularity+ ')'); addresses.add(best); if(best.isRealInternetAddress(false, false, false)) addedValidIP = true; } confidence = bestPopularity; if((secondBest != null) && (secondBestPopularity > 1)) { if(!addresses.contains(secondBest)) { Logger.minor(this, "Adding second best peer "+secondBest+" ("+secondBest+ ')'); addresses.add(secondBest); if(secondBest.isRealInternetAddress(false, false, false)) addedValidIP = true; } } } } } } // Add the old address only if we have no choice, or if we only have the word of two peers to go on. if((!(hadAddedValidIP || confidence > 2)) && (oldIPAddress != null) && !oldIPAddress.equals(overrideIPAddress)) { addresses.add(oldIPAddress); // Don't set addedValidIP. // There is an excellent chance that this is out of date. // So we still want to nag the user, until we have some confirmation. } return addedValidIP; } private String l10n(String key) { return NodeL10n.getBase().getString("NodeIPDetector."+key); } private String l10n(String key, String pattern, String value) { return NodeL10n.getBase().getString("NodeIPDetector."+key, pattern, value); } FreenetInetAddress[] getPrimaryIPAddress(boolean dumpLocal) { if(lastIPAddress == null) return detectPrimaryIPAddress(dumpLocal); return lastIPAddress; } public boolean hasDirectlyDetectedIP() { InetAddress[] addrs = ipDetector.getAddress(node.executor); if(addrs == null || addrs.length == 0) return false; for(InetAddress addr: addrs) { if(IPUtil.isValidAddress(addr, false)) { if(logMINOR) Logger.minor(this, "Has a directly detected IP: "+addr); return true; } } return false; } /** * Process a list of DetectedIP's from the IP detector plugin manager. * DetectedIP's can tell us what kind of NAT we are behind as well as our public * IP address. */ public void processDetectedIPs(DetectedIP[] list) { pluginDetectedIPs = list; boolean mtuChanged = false; for(DetectedIP pluginDetectedIP: pluginDetectedIPs) reportMTU(pluginDetectedIP.mtu, pluginDetectedIP.publicAddress instanceof Inet6Address); redetectAddress(); } /** * Is called by IPAddressDetector to inform NodeIPDetector about the MTU * associated to this interface */ public void reportMTU(int mtu, boolean forIPv6) { boolean mtuChanged = false; if(forIPv6) mtuChanged |= minimumMTUIPv6.report(mtu); else mtuChanged |= minimumMTUIPv4.report(mtu); if (mtuChanged) node.updateMTU(); } public void redetectAddress() { FreenetInetAddress[] newIP = detectPrimaryIPAddress(false); NodeIPPortDetector[] detectors; synchronized(this) { if(Arrays.equals(newIP, lastIP)) return; lastIP = newIP; detectors = portDetectors; } for(NodeIPPortDetector detector: detectors) detector.update(); node.writeNodeFile(); } public void setOldIPAddress(FreenetInetAddress freenetAddress) { this.oldIPAddress = freenetAddress; } public int registerConfigs(SubConfig nodeConfig, int sortOrder) { // IP address override nodeConfig.register("ipAddressOverride", "", sortOrder++, true, false, "NodeIPDectector.ipOverride", "NodeIPDectector.ipOverrideLong", new StringCallback() { @Override public String get() { if(overrideIPAddressString == null) return ""; else return overrideIPAddressString; } @Override public void set(String val) throws InvalidConfigValueException { boolean hadValidAddressOverride = hasValidAddressOverride(); // FIXME do we need to tell anyone? if(val.length() == 0) { // Set to null overrideIPAddressString = val; overrideIPAddress = null; lastIPAddress = null; redetectAddress(); return; } FreenetInetAddress addr; try { addr = new FreenetInetAddress(val, false, true); } catch (HostnameSyntaxException e) { throw new InvalidConfigValueException(l10n("unknownHostErrorInIPOverride", "error", "hostname or IP address syntax error")); } catch (UnknownHostException e) { throw new InvalidConfigValueException(l10n("unknownHostErrorInIPOverride", "error", e.getMessage())); } // Compare as IPs. if(addr.equals(overrideIPAddress)) return; overrideIPAddressString = val; overrideIPAddress = addr; lastIPAddress = null; synchronized(this) { hasValidAddressOverride = true; } if(!hadValidAddressOverride) { onGetValidAddressOverride(); } redetectAddress(); } }); hasValidAddressOverride = true; overrideIPAddressString = nodeConfig.getString("ipAddressOverride"); if(overrideIPAddressString.length() == 0) overrideIPAddress = null; else { try { overrideIPAddress = new FreenetInetAddress(overrideIPAddressString, false, true); } catch (HostnameSyntaxException e) { synchronized(this) { hasValidAddressOverride = false; } String msg = "Invalid IP override syntax: "+overrideIPAddressString+" in config: "+e.getMessage(); Logger.error(this, msg); System.err.println(msg+" but starting up anyway, ignoring the configured IP override"); overrideIPAddress = null; } catch (UnknownHostException e) { // **FIXME** This never happens for this reason with current FreenetInetAddress(String, boolean, boolean) code; perhaps it needs review? String msg = "Unknown host: "+overrideIPAddressString+" in config: "+e.getMessage(); Logger.error(this, msg); System.err.println(msg+" but starting up anyway with no IP override"); overrideIPAddress = null; } } // Temporary IP address hint nodeConfig.register("tempIPAddressHint", "", sortOrder++, true, false, "NodeIPDectector.tempAddressHint", "NodeIPDectector.tempAddressHintLong", new StringCallback() { @Override public String get() { return ""; } @Override public void set(String val) throws InvalidConfigValueException { if(val.length() == 0) { return; } if(overrideIPAddress != null) return; try { oldIPAddress = new FreenetInetAddress(val, false); } catch (UnknownHostException e) { throw new InvalidConfigValueException("Unknown host: "+e.getMessage()); } redetectAddress(); } }); String ipHintString = nodeConfig.getString("tempIPAddressHint"); if(ipHintString.length() > 0) { try { oldIPAddress = new FreenetInetAddress(ipHintString, false); } catch (UnknownHostException e) { String msg = "Unknown host: "+ipHintString+" in config: "+e.getMessage(); Logger.error(this, msg); System.err.println(msg); oldIPAddress = null; } } return sortOrder; } /** Start all IP detection related processes */ public void start() { boolean haveValidAddressOverride = hasValidAddressOverride(); if(!haveValidAddressOverride) { onNotGetValidAddressOverride(); } node.executor.execute(ipDetector, "IP address re-detector"); redetectAddress(); // 60 second delay for inserting ARK to avoid reinserting more than necessary if we don't detect IP on startup. // Not a FastRunnable as it can take a while to start the insert node.getTicker().queueTimedJob(new Runnable() { @Override public void run() { NodeIPPortDetector[] detectors; synchronized(this) { detectors = portDetectors; } for(NodeIPPortDetector detector: detectors) detector.startARK(); } }, SECONDS.toMillis(60)); } public void onConnectedPeer() { // Run off thread, but at high priority. // Initial messages don't need an up to date IP for the node itself, but // announcements do. However announcements are not sent instantly. node.executor.execute(new PrioRunnable() { @Override public void run() { ipDetectorManager.maybeRun(); } @Override public int getPriority() { return NativeThread.HIGH_PRIORITY; } }); } public void registerIPDetectorPlugin(FredPluginIPDetector detector) { ipDetectorManager.registerDetectorPlugin(detector); } public void unregisterIPDetectorPlugin(FredPluginIPDetector detector) { ipDetectorManager.unregisterDetectorPlugin(detector); } public synchronized boolean isDetecting() { return !(hasDetectedPM && hasDetectedIAD); } void hasDetectedPM() { if(logMINOR) Logger.minor(this, "hasDetectedPM() called", new Exception("debug")); synchronized(this) { hasDetectedPM = true; } } public int getMinimumDetectedMTU(boolean ipv6) { return ipv6 ? minimumMTUIPv6.get() : minimumMTUIPv4.get(); } public int getMinimumDetectedMTU() { return Math.min(minimumMTUIPv4.get(), minimumMTUIPv6.get()); } public void setMaybeSymmetric() { if(ipDetectorManager != null && ipDetectorManager.isEmpty()) { if(maybeSymmetricAlert == null) { maybeSymmetricAlert = new SimpleUserAlert(true, l10n("maybeSymmetricTitle"), l10n("maybeSymmetric"), l10n("maybeSymmetricShort"), UserAlert.ERROR); } if(node.clientCore != null && node.clientCore.alerts != null) node.clientCore.alerts.register(maybeSymmetricAlert); } else { if(maybeSymmetricAlert != null) node.clientCore.alerts.unregister(maybeSymmetricAlert); } } public void registerPortForwardPlugin(FredPluginPortForward forward) { ipDetectorManager.registerPortForwardPlugin(forward); } public void unregisterPortForwardPlugin(FredPluginPortForward forward) { ipDetectorManager.unregisterPortForwardPlugin(forward); } //TODO: ugly: deal with multiple instances properly public synchronized void registerBandwidthIndicatorPlugin(FredPluginBandwidthIndicator indicator) { bandwidthIndicator = indicator; } public synchronized void unregisterBandwidthIndicatorPlugin(FredPluginBandwidthIndicator indicator) { bandwidthIndicator = null; } public synchronized FredPluginBandwidthIndicator getBandwidthIndicator() { return bandwidthIndicator; } private FredPluginBandwidthIndicator bandwidthIndicator; boolean hasValidAddressOverride() { synchronized(this) { return hasValidAddressOverride; } } private void onGetValidAddressOverride() { node.clientCore.alerts.unregister(invalidAddressOverrideAlert); } private void onNotGetValidAddressOverride() { node.clientCore.alerts.register(invalidAddressOverrideAlert); } public void addConnectionTypeBox(HTMLNode contentNode) { ipDetectorManager.addConnectionTypeBox(contentNode); } public boolean noDetectPlugins() { return !ipDetectorManager.hasDetectors(); } public boolean hasJSTUN() { return ipDetectorManager.hasJSTUN(); } }