From 63668e0ddaff02b9d9310c062e52106629559c47 Mon Sep 17 00:00:00 2001 From: ac-stef Date: Thu, 25 Feb 2016 22:01:13 +0100 Subject: [PATCH] add support for a geolocation table including two converter scripts to use Geolite2 and Webnet77 tables --- config/geoip.cfg | 19 +++++ config/servercmdline.txt | 1 + source/src/protos.h | 7 +- source/src/server.cpp | 7 ++ source/src/server.h | 2 + source/src/serverfiles.h | 88 ++++++++++++++++++++ source/src/tools.cpp | 7 +- source/src/tools.h | 1 + source/tools/conv_maxmind-geolite2_to_ac.awk | 61 ++++++++++++++ source/tools/conv_software77-geoip_to_ac.awk | 30 +++++++ 10 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 config/geoip.cfg create mode 100644 source/tools/conv_maxmind-geolite2_to_ac.awk create mode 100644 source/tools/conv_software77-geoip_to_ac.awk diff --git a/config/geoip.cfg b/config/geoip.cfg new file mode 100644 index 00000000..50d2b27f --- /dev/null +++ b/config/geoip.cfg @@ -0,0 +1,19 @@ +// geoip table +// =========== +// WARNING: You can NOT use CubeScript in here!! + +// The following schemes are accepted... + +// 1.2.3.4 CC +// 1.2.3.4/24 CC +// 1.2.3.4 - 2.3.4.5 CC +// 38273024 CC +// 38273024/24 CC +// 38273024-38797311 CC +// (with CC being ISO 3166-1 alpha-2 country codes) +// +// check source/tools/ for scripts to convert geoip lists from various free sources +// + + +192.168.0.0/16 XH // home diff --git a/config/servercmdline.txt b/config/servercmdline.txt index 26f5bbab..160fd56f 100644 --- a/config/servercmdline.txt +++ b/config/servercmdline.txt @@ -40,6 +40,7 @@ // -r // server maprot file, default config/maprot.cfg // -X // server passwords file, default config/serverpwd.cfg // -B // server IP blacklist file, default config/serverblacklist.cfg +// -G // server geoip file, default config/geoip.cfg // -K // server nickname blacklist file, default config/nicknameblacklist.cfg // -g // server forbidden words file, default config/forbidden.cfg // -P // voting permission string (read docs/commandline.html) diff --git a/source/src/protos.h b/source/src/protos.h index bae58dfc..83c0535c 100644 --- a/source/src/protos.h +++ b/source/src/protos.h @@ -1130,7 +1130,7 @@ struct serverconfigfile struct servercommandline { int uprate, serverport, syslogfacility, filethres, syslogthres, maxdemos, maxclients, kickthreshold, banthreshold, verbose, incoming_limit, afk_limit, ban_time, demotimelocal; - const char *ip, *master, *logident, *serverpassword, *adminpasswd, *demopath, *maprot, *pwdfile, *blfile, *nbfile, *infopath, *motdpath, *forbidden, *demofilenameformat, *demotimestampformat, *service; + const char *ip, *master, *logident, *serverpassword, *adminpasswd, *demopath, *maprot, *pwdfile, *blfile, *geoipfile, *nbfile, *infopath, *motdpath, *forbidden, *demofilenameformat, *demotimestampformat, *service; uchar *ssk; bool logtimestamp, demo_interm, loggamestatus; string motd, servdesc_full, servdesc_pre, servdesc_suf, voteperm, mapperm; @@ -1140,7 +1140,7 @@ struct servercommandline servercommandline() : uprate(0), serverport(CUBE_DEFAULT_SERVER_PORT), syslogfacility(6), filethres(-1), syslogthres(-1), maxdemos(5), maxclients(DEFAULTCLIENTS), kickthreshold(-5), banthreshold(-6), verbose(0), incoming_limit(10), afk_limit(45000), ban_time(20*60*1000), demotimelocal(0), ip(""), master(NULL), logident(""), serverpassword(""), adminpasswd(""), demopath(""), - maprot("config/maprot.cfg"), pwdfile("config/serverpwd.cfg"), blfile("config/serverblacklist.cfg"), nbfile("config/nicknameblacklist.cfg"), + maprot("config/maprot.cfg"), pwdfile("config/serverpwd.cfg"), blfile("config/serverblacklist.cfg"), geoipfile("config/geoip.cfg"), nbfile("config/nicknameblacklist.cfg"), infopath("config/serverinfo"), motdpath("config/motd"), forbidden("config/forbidden.cfg"), service(NULL), ssk(NULL), logtimestamp(false), demo_interm(false), loggamestatus(true), @@ -1159,7 +1159,7 @@ struct servercommandline int ai = atoi(a); // client: dtwhzbsave switch(arg[1]) - { // todo: gjlqEGHJQU + { // todo: gjlqEHJQU case 'Y': // private server key { uchar temp[64]; @@ -1240,6 +1240,7 @@ struct servercommandline case 'r': maprot = a; break; case 'X': pwdfile = a; break; case 'B': blfile = a; break; + case 'G': geoipfile = a; break; case 'K': nbfile = a; break; case 'I': infopath = a; break; case 'o': filterrichtext(motd, a); break; diff --git a/source/src/server.cpp b/source/src/server.cpp index 0872599a..35e63c73 100644 --- a/source/src/server.cpp +++ b/source/src/server.cpp @@ -79,6 +79,7 @@ servercontroller *svcctrl = NULL; servercommandline scl; servermaprot maprot; serveripblacklist ipblacklist; +serveripcclist geoiplist; servernickblacklist nickblacklist; serverforbiddenlist forbiddenlist; serverpasswords passwords; @@ -3845,6 +3846,7 @@ void rereadcfgs(void) { maprot.read(); ipblacklist.read(); + geoiplist.read(); nickblacklist.read(); forbiddenlist.read(); passwords.read(); @@ -4100,6 +4102,10 @@ void serverslice(uint timeout) // main server update, called from cube main lo c.salt = rnd(0x1000000)*((servmillis%1000)+1); char hn[1024]; copystring(c.hostname, (enet_address_get_host_ip(&c.peer->address, hn, sizeof(hn))==0) ? hn : "unknown"); + c.ip = ENET_NET_TO_HOST_32(c.peer->address.host); + const char *gi = geoiplist.check(c.ip); + copystring(c.country, gi ? gi : "--", 3); + entropy_add_byte(c.ip); logline(ACLOG_INFO,"[%s] client connected", c.hostname); sendservinfo(c); totalclients++; @@ -4335,6 +4341,7 @@ void initserver(bool dedicated) // maprot.next(false, true); // ensure minimum maprot length of '1' passwords.init(scl.pwdfile, scl.adminpasswd); ipblacklist.init(scl.blfile); + geoiplist.init(scl.geoipfile); nickblacklist.init(scl.nbfile); forbiddenlist.init(scl.forbidden); infofiles.init(scl.infopath, scl.motdpath); diff --git a/source/src/server.h b/source/src/server.h index 25024b75..45dd1498 100644 --- a/source/src/server.h +++ b/source/src/server.h @@ -229,6 +229,8 @@ struct client // server side version of "dynent" type int clientnum; ENetPeer *peer; string hostname; + enet_uint32 ip; + char country[4]; string name; int team; char lang[3]; diff --git a/source/src/serverfiles.h b/source/src/serverfiles.h index db30fd69..2f342bf0 100644 --- a/source/src/serverfiles.h +++ b/source/src/serverfiles.h @@ -714,6 +714,94 @@ struct servermaprot : serverconfigfile configset *get(int ccs) { return configsets.inrange(ccs) ? &configsets[ccs] : NULL; } }; +// generic commented IP list + +struct serveripcclist : serverconfigfile +{ + vector ipranges; + + bool parsecomment(iprangecc &ipr, const char *l) + { + memset(ipr.cc, 0, 4); + l += strspn(l, " "); + loopi(3) if(isprint(l[i])) ipr.cc[i] = l[i]; else break; + return ipr.cc[0] && ipr.cc[1]; + } + + const char *printcomment(iprangecc &ipr) + { + return ipr.cc[3] == '\0' ? ipr.cc : "ERR"; + } + + void read() + { + if(getfilesize(filename) == filelen) return; + ipranges.shrink(0); + if(!load()) return; + + iprangecc ir; + int line = 0, errors = 0; + char *l, *r, *p = buf; + logline(ACLOG_VERBOSE,"reading ip list '%s'", filename); + while(p < buf + filelen) + { + l = p; p += strlen(p) + 1; line++; + if((r = (char *) atoipr(l, &ir)) && parsecomment(ir, r)) + ipranges.add(ir); + else + logline(ACLOG_INFO," error in line %d, file %s: failed to parse '%s'", line, filename, r ? r : l), errors++; + } + DELETEA(buf); + ipranges.sort(cmpiprange); + int orglength = ipranges.length(); + loopv(ipranges) + { + if(!i) continue; + if(ipranges[i].ur <= ipranges[i - 1].ur) + { + if(ipranges[i].lr == ipranges[i - 1].lr && ipranges[i].ur == ipranges[i - 1].ur) + logline(ACLOG_VERBOSE," IP list entry %s got dropped (double entry)", iprtoa(ipranges[i])); + else + logline(ACLOG_VERBOSE," IP list entry %s got dropped (already covered by %s)", iprtoa(ipranges[i]), iprtoa(ipranges[i - 1])); + ipranges.remove(i--); continue; + } + if(ipranges[i].lr <= ipranges[i - 1].ur) + { + if(ipranges[i].cu == ipranges[i - 1].cu) // same comment + { + logline(ACLOG_VERBOSE," IP list entries %s and %s are joined due to overlap (both %s)", iprtoa(ipranges[i - 1]), iprtoa(ipranges[i]), printcomment(ipranges[i])); + ipranges[i - 1].ur = ipranges[i].ur; + ipranges.remove(i--); continue; + } + else + { + logline(ACLOG_VERBOSE," error: IP list entries %s|%s and %s|%s are overlapping - dropping %s|%s", iprtoa(ipranges[i - 1]), printcomment(ipranges[i - 1]), + iprtoa(ipranges[i]), printcomment(ipranges[i]), iprtoa(ipranges[i]), printcomment(ipranges[i])); + errors++; + ipranges.remove(i--); continue; + } + } + if(ipranges[i].lr - 1 == ipranges[i - 1].ur && ipranges[i].cu == ipranges[i - 1].cu) + { + logline(ACLOG_VERBOSE," concatenating IP list entries %s and %s (both %s)", iprtoa(ipranges[i - 1]), iprtoa(ipranges[i]), printcomment(ipranges[i])); + ipranges[i - 1].ur = ipranges[i].ur; + ipranges.remove(i--); continue; + } + } + loopv(ipranges) logline(ACLOG_VERBOSE," %s %s", iprtoa(ipranges[i]), printcomment(ipranges[i])); + logline(ACLOG_INFO,"read %d (%d) IP list entries from '%s', %d errors", ipranges.length(), orglength, filename, errors); + } + + const char *check(enet_uint32 ip) // ip: host byte order + { + iprangecc t, *res; + t.lr = ip; + t.ur = 0; + res = ipranges.search(&t, cmpipmatch); + return res ? printcomment(*res) : NULL; + } +}; + // serverblacklist.cfg struct serveripblacklist : serverconfigfile diff --git a/source/src/tools.cpp b/source/src/tools.cpp index ef7943a9..747f3305 100644 --- a/source/src/tools.cpp +++ b/source/src/tools.cpp @@ -466,7 +466,12 @@ const char *atoip(const char *s, enet_uint32 *ip) { unsigned int d[4]; int n; - if(!s || sscanf(s, "%u.%u.%u.%u%n", d, d + 1, d + 2, d + 3, &n) != 4) return NULL; + if(!s) return NULL; + if(sscanf(s, "%u.%u.%u.%u%n", d, d + 1, d + 2, d + 3, &n) != 4) + { + *ip = strtoul(s, (char **)&s, 0); // try single-integer IPs + return *ip > 0xffffff ? s : NULL; // require first octet to be non-zero to avoid misinterpreting faulty dotted IPs + } *ip = 0; loopi(4) { diff --git a/source/src/tools.h b/source/src/tools.h index cd900560..446c66f6 100644 --- a/source/src/tools.h +++ b/source/src/tools.h @@ -985,6 +985,7 @@ extern void entropy_add_block(const uchar *s, int len); extern void entropy_get(uchar *buf, int len); struct iprange { enet_uint32 lr, ur; }; +struct iprangecc : iprange { union { int ci; char cc[4]; enet_uint32 cu; }; }; extern const char *atoip(const char *s, enet_uint32 *ip); extern const char *atoipr(const char *s, iprange *ir); extern const char *iptoa(const enet_uint32 ip); diff --git a/source/tools/conv_maxmind-geolite2_to_ac.awk b/source/tools/conv_maxmind-geolite2_to_ac.awk new file mode 100644 index 00000000..035327d9 --- /dev/null +++ b/source/tools/conv_maxmind-geolite2_to_ac.awk @@ -0,0 +1,61 @@ +# convert Geolite2 lists for Assaultcube servers +# +# expects to process GeoLite2-Country-Locations-en.csv and GeoLite2-Country-Blocks-IPv4.csv (in that order) +# +# gawk -f conv_maxmind-geolite2_to_ac.awk GeoLite2-Country-Locations-en.csv GeoLite2-Country-Blocks-IPv4.csv > geoip.cfg +# +# note: the ISO 3166-1 alpha-2 list reserves XA..XZ for user-assigned code elements - we use xs, xp and xz here, see below +# +# download lists from http://dev.maxmind.com/geoip/geoip2/geolite2/ (please don't download too often: once a month should be enough) +# +# thanks to MaxMind: +# This product includes GeoLite2 data created by MaxMind, available from http://www.maxmind.com +# + +BEGIN { +FS="," # expect csv +getcc=0 +} + +# geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name +$1=="geoname_id" && $5=="country_iso_code" { + getcc=1 # use first line to switch mode + next +} + +# network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider +$1=="network" && $2=="geoname_id" { + getcc=0 # use first line to switch mode + next +} + +getcc==1 { + if ( length($5)==2 ) + cctab[$1]=$5 # has country_iso_code + else if ( length($3)==2 ) + cctab[$1]=tolower($3) # has continent_code (convert to lowercase to distinguish from country codes) + else + print "ERROR: no usable country code in line " $0 # should not occur + next +} + +getcc==0 { + if ( $2=="" ) { # pick geoname_id over represented_country_geoname_id over registered_country_geoname_id + if ( $4 == "" ) + cc=$3 + else + cc=$4 + } else { + cc=$2 + } + if ( cc=="" && $6!=0 ) + print $1 " xs" # use XS to mark satellite providers with no other country affiliation + else if ( cc=="" && $5!=0 ) + print $1 " xp" # use XP to mark anonymous proxies with no other country affiliation + else if ( cc in cctab ) + print $1 " " cctab[cc] + else + print $1 " xz // " $0 " not found" # use XZ to mark table conversion errors + next +} + diff --git a/source/tools/conv_software77-geoip_to_ac.awk b/source/tools/conv_software77-geoip_to_ac.awk new file mode 100644 index 00000000..5f641047 --- /dev/null +++ b/source/tools/conv_software77-geoip_to_ac.awk @@ -0,0 +1,30 @@ +# convert geo-ip lists from software77 for Assaultcube servers +# +# expects to process IPV4-CSV lists from Webnet77 (please consider donations, if you use their lists) +# +# gawk -f conv_software77-geoip_to_ac.awk IpToCountry.csv > geoip.cfg +# +# download lists from http://software77.net/geo-ip/ (please don't download too often: once a month should be enough) +# +# thanks to Webnet77: http://webnet77.com +# + +BEGIN { +FS="," # expect csv +getcc=0 +} + +function unquote(q) { + gsub("\"", "", q) + return q +} + +substr($1,1,1)=="#" { + next # comment +} + +# expect lines like "38273024","38797311","ripencc","1275523200","KZ","KAZ","Kazakhstan" +{ + print unquote($1) "-" unquote($2) " " unquote($5) " // " unquote($7) + #print unquote($1) "-" unquote($2) " " unquote($5) # use this to drop the comments +}