package net.i2p.i2ptunnel; import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.util.Clock; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; import net.i2p.util.SystemVersion; /** * Count how often something happens with a particular peer and all peers. * This offers basic DOS protection but is not a complete solution. * * This is a little different from the one in streaming, in that the * ban time is different from the check time, and we keep a separate * map of throttled peers with individual time stamps. * The streaming version is lightweight but "sloppy" since it * uses a single time bucket for all. * * @since 0.9.9 */ class ConnThrottler { private int _max; private int _totalMax; private long _checkPeriod; private long _throttlePeriod; private long _totalThrottlePeriod; private int _currentTotal; private final Map<Hash, Record> _peers; private long _totalThrottleUntil; private final String _action; private final Log _log; private final DateFormat _fmt; /* * @param max per-peer, 0 for unlimited * @param totalMax for all peers, 0 for unlimited * @param period check window (ms) * @param throttlePeriod how long to ban a peer (ms) * @param totalThrottlePeriod how long to ban all peers (ms) * @param action just a name to note in the log */ public ConnThrottler(int max, int totalMax, long period, long throttlePeriod, long totalThrottlePeriod, String action, Log log) { updateLimits(max, totalMax, period, throttlePeriod, totalThrottlePeriod); _peers = new HashMap<Hash, Record>(4); _action = action; _log = log; // for logging _fmt = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM); _fmt.setTimeZone(SystemVersion.getSystemTimeZone()); new Cleaner(); } /* * All periods in ms * @param max per-peer, 0 for unlimited * @param totalMax for all peers, 0 for unlimited * @since 0.9.3 */ public synchronized void updateLimits(int max, int totalMax, long checkPeriod, long throttlePeriod, long totalThrottlePeriod) { _max = max; _totalMax = totalMax; _checkPeriod = Math.max(checkPeriod, 10*1000); _throttlePeriod = Math.max(throttlePeriod, 10*1000); _totalThrottlePeriod = Math.max(totalThrottlePeriod, 10*1000); } /** * Checks both individual and total. Increments before checking. */ public synchronized boolean shouldThrottle(Hash h) { // all throttled already? if (_totalMax > 0) { if (_totalThrottleUntil > 0) { if (_totalThrottleUntil > Clock.getInstance().now()) return true; _totalThrottleUntil = 0; } } // do this first, so we don't increment total if individual throttled if (_max > 0) { Record rec = _peers.get(h); if (rec != null) { // peer throttled already? if (rec.getUntil() > 0) return true; rec.increment(); long now = Clock.getInstance().now(); if (rec.countSince(now - _checkPeriod) > _max) { long until = now + _throttlePeriod; String date = _fmt.format(new Date(until)); _log.logAlways(Log.WARN, "Throttling " + _action + " until " + date + " after exceeding max of " + _max + " in " + DataHelper.formatDuration(_checkPeriod) + ": " + h.toBase64()); rec.ban(until); return true; } } else { _peers.put(h, new Record()); } } if (_totalMax > 0 && ++_currentTotal > _totalMax) { if (_totalThrottleUntil == 0) { _totalThrottleUntil = Clock.getInstance().now() + _totalThrottlePeriod; String date = _fmt.format(new Date(_totalThrottleUntil)); _log.logAlways(Log.WARN, "*** Throttling " + _action + " from ALL peers until " + date + " after exceeding max of " + _max + " in " + DataHelper.formatDuration(_checkPeriod)); } return true; } return false; } /** * start over */ public synchronized void clear() { _currentTotal = 0; _totalThrottleUntil = 0; _peers.clear(); } /** * Keep a list of seen times, and a ban-until time. * Caller must sync all methods. */ private static class Record { private final List<Long> times; private long until; public Record() { times = new ArrayList<Long>(8); increment(); } /** Caller must synch */ public int countSince(long time) { for (Iterator<Long> iter = times.iterator(); iter.hasNext(); ) { if (iter.next().longValue() < time) iter.remove(); else break; } return times.size(); } /** Caller must synch */ public void increment() { times.add(Long.valueOf(Clock.getInstance().now())); } /** Caller must synch */ public void ban(long untilTime) { until = untilTime; // don't need to save times if banned times.clear(); } /** Caller must synch */ public long getUntil() { if (until < Clock.getInstance().now()) until = 0; return until; } } private class Cleaner extends SimpleTimer2.TimedEvent { /** schedules itself */ public Cleaner() { super(SimpleTimer2.getInstance(), _checkPeriod); } public void timeReached() { synchronized(ConnThrottler.this) { if (_totalMax > 0) _currentTotal = 0; if (_max > 0 && !_peers.isEmpty()) { long then = Clock.getInstance().now() - _checkPeriod; for (Iterator<Record> iter = _peers.values().iterator(); iter.hasNext(); ) { Record rec = iter.next(); if (rec.getUntil() <= 0 && rec.countSince(then) <= 0) iter.remove(); } } } schedule(_checkPeriod); } } }