/* Copyright 2007 Freenet Project Inc.
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package freenet.io;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Iterator;
import freenet.io.comm.Peer;
import freenet.l10n.NodeL10n;
import freenet.node.FSParseException;
import freenet.node.ProgramDirectory;
import freenet.support.Logger;
import freenet.support.SimpleFieldSet;
import freenet.support.io.FileUtil;
/**
* Track packet traffic to/from specific peers and IP addresses, in order to
* determine whether we are open to the internet.
*
* Normally there would be one tracker per port i.e. per UdpSocketHandler.
* @author toad
*/
public class AddressTracker {
/** PeerAddressTrackerItem's by Peer */
private final HashMap<Peer, PeerAddressTrackerItem> peerTrackers;
/** InetAddressAddressTrackerItem's by InetAddress */
private final HashMap<InetAddress, InetAddressAddressTrackerItem> ipTrackers;
/** Maximum number of Item's of either type */
private int MAX_ITEMS = DEFAULT_MAX_ITEMS;
static final int DEFAULT_MAX_ITEMS = 1000;
static final int SEED_MAX_ITEMS = 10000;
private long timeDefinitelyNoPacketsReceivedIP;
private long timeDefinitelyNoPacketsSentIP;
private long timeDefinitelyNoPacketsReceivedPeer;
private long timeDefinitelyNoPacketsSentPeer;
private long brokenTime;
public static AddressTracker create(long lastBootID, ProgramDirectory runDir, int port) {
File data = runDir.file("packets-"+port+".dat");
File dataBak = runDir.file("packets-"+port+".bak");
dataBak.delete();
FileInputStream fis = null;
try {
fis = new FileInputStream(data);
BufferedInputStream bis = new BufferedInputStream(fis);
InputStreamReader ir = new InputStreamReader(bis, "UTF-8");
BufferedReader br = new BufferedReader(ir);
SimpleFieldSet fs = new SimpleFieldSet(br, false, true);
return new AddressTracker(fs, lastBootID);
} catch (IOException e) {
// Fall through
} catch (FSParseException e) {
Logger.warning(AddressTracker.class, "Failed to load from disk for port "+port+": "+e, e);
// Fall through
} finally {
if(fis != null)
try {
fis.close();
} catch (IOException e) {
// Ignore
}
}
return new AddressTracker();
}
private AddressTracker() {
timeDefinitelyNoPacketsReceivedIP = System.currentTimeMillis();
timeDefinitelyNoPacketsSentIP = System.currentTimeMillis();
timeDefinitelyNoPacketsReceivedPeer = System.currentTimeMillis();
timeDefinitelyNoPacketsSentPeer = System.currentTimeMillis();
peerTrackers = new HashMap<Peer, PeerAddressTrackerItem>();
ipTrackers = new HashMap<InetAddress, InetAddressAddressTrackerItem>();
}
private AddressTracker(SimpleFieldSet fs, long lastBootID) throws FSParseException {
int version = fs.getInt("Version");
if(version != 2)
throw new FSParseException("Unknown Version "+version);
long savedBootID = fs.getLong("BootID");
if(savedBootID != lastBootID) throw new FSParseException("Unable to load address tracker table, assuming an unclean shutdown: Last ID was " +
lastBootID+" but stored "+savedBootID);
// Sadly we don't know whether there were packets arriving during the gap,
// and some insecure firewalls will use incoming packets to keep tunnels open
//timeDefinitelyNoPacketsReceived = fs.getLong("TimeDefinitelyNoPacketsReceived");
timeDefinitelyNoPacketsReceivedPeer = System.currentTimeMillis();
timeDefinitelyNoPacketsReceivedIP = System.currentTimeMillis();
timeDefinitelyNoPacketsSentPeer = fs.getLong("TimeDefinitelyNoPacketsSentPeer");
timeDefinitelyNoPacketsSentIP = fs.getLong("TimeDefinitelyNoPacketsSentIP");
peerTrackers = new HashMap<Peer, PeerAddressTrackerItem>();
SimpleFieldSet peers = fs.subset("Peers");
if(peers != null) {
Iterator<String> i = peers.directSubsetNameIterator();
if(i != null) {
while(i.hasNext()) {
SimpleFieldSet peer = peers.subset(i.next());
PeerAddressTrackerItem item = new PeerAddressTrackerItem(peer);
peerTrackers.put(item.peer, item);
}
}
}
ipTrackers = new HashMap<InetAddress, InetAddressAddressTrackerItem>();
SimpleFieldSet ips = fs.subset("IPs");
if(ips != null) {
Iterator<String> i = ips.directSubsetNameIterator();
if(i != null) {
while(i.hasNext()) {
SimpleFieldSet peer = ips.subset(i.next());
InetAddressAddressTrackerItem item = new InetAddressAddressTrackerItem(peer);
ipTrackers.put(item.addr, item);
}
}
}
}
public void sentPacketTo(Peer peer) {
packetTo(peer, true);
}
public void receivedPacketFrom(Peer peer) {
packetTo(peer, false);
}
private void packetTo(Peer peer, boolean sent) {
Peer peer2 = peer.dropHostName();
if(peer2 == null) {
Logger.error(this, "Impossible: No host name in AddressTracker.packetTo for "+peer);
return;
}
peer = peer2;
InetAddress ip = peer.getAddress();
long now = System.currentTimeMillis();
synchronized(this) {
PeerAddressTrackerItem peerItem = peerTrackers.get(peer);
if(peerItem == null) {
peerItem = new PeerAddressTrackerItem(timeDefinitelyNoPacketsReceivedPeer, timeDefinitelyNoPacketsSentPeer, peer);
if(peerTrackers.size() > MAX_ITEMS) {
Logger.error(this, "Clearing peer trackers on "+this);
peerTrackers.clear();
ipTrackers.clear();
timeDefinitelyNoPacketsReceivedPeer = now;
timeDefinitelyNoPacketsSentPeer = now;
}
peerTrackers.put(peer, peerItem);
}
if(sent)
peerItem.sentPacket(now);
else
peerItem.receivedPacket(now);
InetAddressAddressTrackerItem ipItem = ipTrackers.get(ip);
if(ipItem == null) {
ipItem = new InetAddressAddressTrackerItem(timeDefinitelyNoPacketsReceivedIP, timeDefinitelyNoPacketsSentIP, ip);
if(ipTrackers.size() > MAX_ITEMS) {
Logger.error(this, "Clearing IP trackers on "+this);
peerTrackers.clear();
ipTrackers.clear();
timeDefinitelyNoPacketsReceivedIP = now;
timeDefinitelyNoPacketsSentIP = now;
}
ipTrackers.put(ip, ipItem);
}
if(sent)
ipItem.sentPacket(now);
else
ipItem.receivedPacket(now);
}
}
public synchronized void startReceive(long now) {
timeDefinitelyNoPacketsReceivedIP = now;
timeDefinitelyNoPacketsReceivedPeer = now;
}
public synchronized void startSend(long now) {
timeDefinitelyNoPacketsSentIP = now;
timeDefinitelyNoPacketsSentPeer = now;
}
public synchronized PeerAddressTrackerItem[] getPeerAddressTrackerItems() {
PeerAddressTrackerItem[] items = new PeerAddressTrackerItem[peerTrackers.size()];
return peerTrackers.values().toArray(items);
}
public synchronized InetAddressAddressTrackerItem[] getInetAddressTrackerItems() {
InetAddressAddressTrackerItem[] items = new InetAddressAddressTrackerItem[ipTrackers.size()];
return ipTrackers.values().toArray(items);
}
public enum Status {
// Note: Order is important! We compare by ordinals in various places.
// FIXME switch to using member methods.
DEFINITELY_NATED,
MAYBE_NATED,
DONT_KNOW,
MAYBE_PORT_FORWARDED,
DEFINITELY_PORT_FORWARDED
}
/** If the minimum gap is at least this, we might be port forwarded.
* RFC 4787 requires at least 2 minutes, but many NATs have shorter timeouts. */
public final static long MAYBE_TUNNEL_LENGTH = MINUTES.toMillis(5) + SECONDS.toMillis(1);
/** If the minimum gap is at least this, we are almost certainly port forwarded.
* Some stateful firewalls do at least 30 minutes. Hopefully the below is
* sufficiently over the top! */
public final static long DEFINITELY_TUNNEL_LENGTH = HOURS.toMillis(12) + MINUTES.toMillis(1);
/** Time after which we ignore evidence that we are port forwarded */
public static final long HORIZON = HOURS.toMillis(24);
public long getLongestSendReceiveGap() {
return getLongestSendReceiveGap(HORIZON);
}
/**
* Find the longest send/known-no-packets-sent ... receive gap.
* It is highly unlikely that we are behind a NAT or symmetric
* firewall with a timeout less than the returned length.
*/
public long getLongestSendReceiveGap(long horizon) {
long longestGap = -1;
long now = System.currentTimeMillis();
PeerAddressTrackerItem[] items = getPeerAddressTrackerItems();
for(PeerAddressTrackerItem item: items) {
if(item.packetsReceived() <= 0) continue;
if(!item.peer.isRealInternetAddress(false, false, false)) continue;
longestGap = Math.max(longestGap, item.longestGap(horizon, now));
}
return longestGap;
}
public Status getPortForwardStatus() {
long minGap = getLongestSendReceiveGap(HORIZON);
if(minGap > DEFINITELY_TUNNEL_LENGTH)
return Status.DEFINITELY_PORT_FORWARDED;
if(minGap > MAYBE_TUNNEL_LENGTH)
return Status.MAYBE_PORT_FORWARDED;
// Only take isBroken into account if we're not sure.
// Somebody could be playing with us by sending bogus FNPSentPackets...
synchronized(this) {
if(isBroken()) return Status.DEFINITELY_NATED;
if(minGap == 0 && timePresumeGuilty > 0 && System.currentTimeMillis() > timePresumeGuilty)
return Status.MAYBE_NATED;
}
return Status.DONT_KNOW;
}
private boolean isBroken() {
return System.currentTimeMillis() - brokenTime < HORIZON;
}
public static String statusString(Status status) {
return NodeL10n.getBase().getString("ConnectivityToadlet.status."+status);
}
/** Persist the table to disk */
public void storeData(long bootID, ProgramDirectory runDir, int port) {
// Don't write to disk if we know we're NATed anyway!
if(isBroken()) return;
File data = runDir.file("packets-"+port+".dat");
File dataBak = runDir.file("packets-"+port+".bak");
dataBak.delete();
FileOutputStream fos = null;
try {
fos = new FileOutputStream(dataBak);
BufferedOutputStream bos = new BufferedOutputStream(fos);
OutputStreamWriter osw = new OutputStreamWriter(bos, "UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
SimpleFieldSet fs = getFieldset(bootID);
fs.writeTo(bw);
bw.flush();
bw.close();
fos = null;
FileUtil.renameTo(dataBak, data);
} catch (IOException e) {
Logger.error(this, "Cannot store packet tracker to disk");
return;
} finally {
if(fos != null)
try {
fos.close();
} catch (IOException e) {
// Ignore
}
}
}
private synchronized SimpleFieldSet getFieldset(long bootID) {
SimpleFieldSet sfs = new SimpleFieldSet(true);
sfs.put("Version", 2);
sfs.put("BootID", bootID);
sfs.put("TimeDefinitelyNoPacketsReceivedPeer", timeDefinitelyNoPacketsReceivedPeer);
sfs.put("TimeDefinitelyNoPacketsReceivedIP", timeDefinitelyNoPacketsReceivedIP);
sfs.put("TimeDefinitelyNoPacketsSentPeer", timeDefinitelyNoPacketsSentPeer);
sfs.put("TimeDefinitelyNoPacketsSentIP", timeDefinitelyNoPacketsSentIP);
PeerAddressTrackerItem[] peerItems = getPeerAddressTrackerItems();
SimpleFieldSet items = new SimpleFieldSet(true);
if(peerItems.length > 0) {
for(int i = 0; i < peerItems.length; i++)
items.put(Integer.toString(i), peerItems[i].toFieldSet());
sfs.put("Peers", items);
}
InetAddressAddressTrackerItem[] inetItems = getInetAddressTrackerItems();
items = new SimpleFieldSet(true);
if(inetItems.length > 0) {
for(int i = 0; i < inetItems.length; i++)
items.put(Integer.toString(i), inetItems[i].toFieldSet());
sfs.put("IPs", items);
}
return sfs;
}
/** Called when something changes at a higher level suggesting that the status may be wrong */
public void rescan() {
// Do nothing for now, as we don't maintain any final state yet.
}
public synchronized void setBroken() {
brokenTime = System.currentTimeMillis();
}
private long timePresumeGuilty = -1;
public synchronized void setPresumedGuiltyAt(long l) {
if(timePresumeGuilty <= 0)
timePresumeGuilty = l;
}
public synchronized void setPresumedInnocent() {
timePresumeGuilty = -1;
}
public synchronized void setHugeTracker() {
MAX_ITEMS = SEED_MAX_ITEMS;
}
}