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();
}
}