diff --git a/router/java/src/net/i2p/router/transport/GeoIP.java b/router/java/src/net/i2p/router/transport/GeoIP.java index 2188074b3a731760d3bcf8f3f053ce319478347d..e996824826f95fc6da4610e53857ab2fc58cd00b 100644 --- a/router/java/src/net/i2p/router/transport/GeoIP.java +++ b/router/java/src/net/i2p/router/transport/GeoIP.java @@ -5,12 +5,19 @@ package net.i2p.router.transport; */ import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOException; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.Map; +import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -28,6 +35,7 @@ import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.router.RouterAddress; import net.i2p.data.router.RouterInfo; +import net.i2p.router.Blocklist; import net.i2p.router.Router; import net.i2p.router.RouterContext; import net.i2p.router.transport.udp.UDPTransport; @@ -36,6 +44,7 @@ import net.i2p.update.UpdateType; import net.i2p.util.Addresses; import net.i2p.util.ConcurrentHashSet; import net.i2p.util.Log; +import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SystemVersion; /** @@ -68,6 +77,7 @@ public class GeoIP { private final Set<Long> _notFound; private final AtomicBoolean _lock; private int _lookupRunCount; + private static final Map<String, List<String>> _associatedCountries; static final String PROP_GEOIP_ENABLED = "routerconsole.geoip.enable"; public static final String PROP_GEOIP_DIR = "geoip.dir"; @@ -82,11 +92,23 @@ public class GeoIP { private static final String DEBIAN_GEOIPV6_FILE = "/usr/share/GeoIP/GeoIPv6.dat"; private static final boolean DISABLE_DEBIAN = false; private static final boolean ENABLE_DEBIAN = !DISABLE_DEBIAN && !(SystemVersion.isWindows() || SystemVersion.isAndroid()); + private static final String PROP_BLOCK_MY_COUNTRY = "i2np.blockMyCountry"; /** maxmind API */ private static final String UNKNOWN_COUNTRY_CODE = "--"; /** db-ip.com https://db-ip.com/faq.php */ private static final String UNKNOWN_COUNTRY_CODE2 = "ZZ"; + static { + // To block additional countries b,c,d when blocking country a, + // put the list a,b,c,d for country a. + _associatedCountries = new HashMap<String, List<String>>(2); + List<String> c = new ArrayList<String>(2); + c.add("cn"); + c.add("hk"); + _associatedCountries.put("cn", c); + _associatedCountries.put("hk", c); + } + /** * @param context RouterContext in production, I2PAppContext for testing only */ @@ -148,8 +170,9 @@ public class GeoIP { Thread.currentThread().setPriority(pri - 1); try { LookupJob j = new LookupJob(); - j.run(); - updateOurCountry(); + long ts = j.runit(); + if (ts > 0) + updateOurCountry(ts); } finally { if (pri > Thread.MIN_PRIORITY) Thread.currentThread().setPriority(pri); @@ -160,12 +183,20 @@ public class GeoIP { private static final int CLEAR = 8; public void run() { + runit(); + } + + /** + * @return timestamp of the geoip ipv4 file used, or 0 on failure + */ + public long runit() { if (_lock.getAndSet(true)) - return; + return 0; int toSearch = 0; int found = 0; File geoip2 = getGeoIP2(); DatabaseReader dbr = null; + long rv = 0; long start = _context.clock().now(); try { // clear the negative cache every few runs, to prevent it from getting too big @@ -199,8 +230,11 @@ public class GeoIP { try { ls = new LookupService(f, LookupService.GEOIP_STANDARD); Date date = ls.getDatabaseInfo().getDate(); - if (date != null) - notifyVersion("GeoIPv4", date.getTime()); + if (date != null) { + long time = date.getTime(); + notifyVersion("GeoIPv4", time); + rv = time; + } for (int i = 0; i < search.length; i++) { Long ipl = search[i]; long ip = ipl.longValue(); @@ -228,6 +262,11 @@ public class GeoIP { // Maxmind v2 database try { dbr = openGeoIP2(geoip2); + long time = dbr.getMetadata().getBuildDate().getTime(); + if (time > 0) { + notifyVersion("GeoIP2", time); + rv = time; + } for (int i = 0; i < search.length; i++) { Long ipl = search[i]; String ipv4 = toV4(ipl); @@ -250,6 +289,14 @@ public class GeoIP { } else { // Tor-style database String[] countries = readGeoIPFile(search); + if (countries.length > 0) { + String geoDir = _context.getProperty(PROP_GEOIP_DIR, GEOIP_DIR_DEFAULT); + File geoFile = new File(geoDir); + if (!geoFile.isAbsolute()) + geoFile = new File(_context.getBaseDir(), geoDir); + geoFile = new File(geoFile, GEOIP_FILE_DEFAULT); + rv = geoFile.lastModified(); + } for (int i = 0; i < countries.length; i++) { if (countries[i] != null) { _IPToCountry.put(search[i], countries[i]); @@ -344,9 +391,78 @@ public class GeoIP { if (_log.shouldInfo()) _log.info("GeoIP processing finished, looked up: " + toSearch + " found: " + found + " time: " + (_context.clock().now() - start)); + return rv; } } + /** + * Write all IP ranges for country to blocklist-country.txt. + * Inline, blocking. + * + * @param two-letter lower-case country code + * @since 0.9.48 + */ + private void countryToIP(String country) { + while (_lock.getAndSet(true)) { + try { Thread.sleep(1000); } catch (InterruptedException ie) { return; } + } + File geoip2 = getGeoIP2(); + DatabaseReader dbr = null; + if (_log.shouldInfo()) + _log.info("Generating blocklist for our country " + country); + long start = _context.clock().now(); + File fout = new File(_context.getConfigDir(), Blocklist.BLOCKLIST_COUNTRY_FILE); + BufferedWriter out = null; + List<String> countries = _associatedCountries.get(country); + if (countries == null) + countries = Collections.singletonList(country); + try { + File f = new File(_context.getProperty(PROP_DEBIAN_GEOIP, DEBIAN_GEOIP_FILE)); + // if we have both, prefer the most recent. + // The Debian data can be pretty old. + // For now, we use the file date, we don't open it up to get the metadata. + if (ENABLE_DEBIAN && f.exists() && + (geoip2 == null || f.lastModified() > geoip2.lastModified())) { + // Maxmind v1 database + LookupService ls = null; + try { + out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(fout), "UTF-8")); + ls = new LookupService(f, LookupService.GEOIP_STANDARD); + for (String c : countries) { + ls.countryToIP(c, out); + // TODO close out, tell blocklist + } + } catch (IOException ioe) { + _log.error("GeoIP failure", ioe); + } catch (InvalidDatabaseException ide) { + _log.error("GeoIP failure", ide); + } finally { + if (ls != null) ls.close(); + } + } else if (geoip2 != null) { + // Maxmind v2 database + try { + out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(fout), "UTF-8")); + dbr = openGeoIP2(geoip2); + for (String c : countries) { + dbr.countryToIP(c, out); + // TODO close out, tell blocklist + } + } catch (IOException ioe) { + _log.error("GeoIP2 failure", ioe); + } + } else { + // Tor-style database, unsupported + } + } finally { + if (out != null) try { out.close(); } catch (IOException e) {} + if (dbr != null) try { dbr.close(); } catch (IOException ioe) {} + _lock.set(false); + } + if (_log.shouldInfo()) + _log.info("Finished generating blocklist for our country, time: " + (_context.clock().now() - start)); + } + /** * Get the GeoIP2 database file * @@ -377,8 +493,6 @@ public class GeoIP { DatabaseReader rv = b.build(); if (_log.shouldDebug()) _log.debug("Opened GeoIP2 Database, Metadata: " + rv.getMetadata()); - long time = rv.getMetadata().getBuildDate().getTime(); - notifyVersion("GeoIP2", time); return rv; } @@ -543,8 +657,9 @@ public class GeoIP { * and it will be there next time at startup. * * Does nothing in I2PAppContext + * @param ts the timestamp of the geoip file that was read, greater than zero */ - private void updateOurCountry() { + private void updateOurCountry(long ts) { if (! (_context instanceof RouterContext)) return; RouterContext ctx = (RouterContext) _context; @@ -591,6 +706,17 @@ public class GeoIP { boolean isStrict = ctx.commSystem().isInStrictCountry(); if (_log.shouldInfo()) _log.info("Old country was strict? " + wasStrict + "; new country is strict? " + isStrict); + if (isStrict || ctx.getBooleanProperty(Router.PROP_HIDDEN_HIDDEN) || + ctx.getBooleanProperty(PROP_BLOCK_MY_COUNTRY)) { + // generate country blocklist + countryToIP(country); + // go thru the netdb + banCountry(ctx, country); + } else { + // remove country blocklist, won't take effect until restart + File bc = new File(_context.getConfigDir(), Blocklist.BLOCKLIST_COUNTRY_FILE); + bc.delete(); + } if (wasStrict != isStrict && ctx.getProperty(Router.PROP_HIDDEN_HIDDEN) == null) { if (isStrict) { String name = fullName(country); @@ -601,6 +727,36 @@ public class GeoIP { } ctx.router().rebuildRouterInfo(); } + } else if (country != null) { + // No change, but we may need to update blocklist-country.txt + boolean isStrict = ctx.commSystem().isInStrictCountry(); + if (isStrict || ctx.getBooleanProperty(Router.PROP_HIDDEN_HIDDEN) || + ctx.getBooleanProperty(PROP_BLOCK_MY_COUNTRY)) { + // check country blocklist timestamp + File bc = new File(_context.getConfigDir(), Blocklist.BLOCKLIST_COUNTRY_FILE); + long lm = bc.lastModified(); + if (lm < ts) { + // regenerate blocklist + countryToIP(country); + } + if (_lookupRunCount == 1) { + // go thru the netdb + banCountry(ctx, country); + } + } + } + } + + /** + * @param two-letter lower-case country code + * @since 0.9.48 + */ + private static void banCountry(RouterContext ctx, String country) { + for (Hash h : ctx.netDb().getAllRouters()) { + String hisCountry = ctx.commSystem().getCountry(h); + if (country.equals(hisCountry)) { + ctx.banlist().banlistRouterForever(h, "In our country"); + } } } @@ -741,6 +897,7 @@ public class GeoIP { g.blockingLookup(); System.out.println("Lookup took " + (System.currentTimeMillis() - start)); /*** + g.countryToIP("af"); for (int i = 0; i < tests.length; i++) System.out.println(tests[i] + " : " + g.get(tests[i])); ***/