{"id":19070737,"url":"https://github.com/powerdns/weakforced","last_synced_at":"2025-04-28T14:44:20.740Z","repository":{"id":29722312,"uuid":"33265470","full_name":"PowerDNS/weakforced","owner":"PowerDNS","description":"Anti-Abuse for servers at authentication time","archived":false,"fork":false,"pushed_at":"2025-04-17T08:03:23.000Z","size":7562,"stargazers_count":127,"open_issues_count":10,"forks_count":34,"subscribers_count":21,"default_branch":"master","last_synced_at":"2025-04-17T22:49:34.036Z","etag":null,"topics":["anti-bot","attack-prevention","c-plus-plus","gplv3","hacktoberfest","intrusion-detection","intrusion-prevention","linux","macos","security"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"gb112211/AndroidTestScripts","license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PowerDNS.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2015-04-01T18:37:52.000Z","updated_at":"2025-04-17T08:03:30.000Z","dependencies_parsed_at":"2023-11-12T10:22:33.060Z","dependency_job_id":"37b44dcf-f85e-4e16-b5ea-46e70e551980","html_url":"https://github.com/PowerDNS/weakforced","commit_stats":null,"previous_names":[],"tags_count":46,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PowerDNS%2Fweakforced","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PowerDNS%2Fweakforced/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PowerDNS%2Fweakforced/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PowerDNS%2Fweakforced/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PowerDNS","download_url":"https://codeload.github.com/PowerDNS/weakforced/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251332311,"owners_count":21572593,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anti-bot","attack-prevention","c-plus-plus","gplv3","hacktoberfest","intrusion-detection","intrusion-prevention","linux","macos","security"],"created_at":"2024-11-09T01:20:11.627Z","updated_at":"2025-04-28T14:44:20.716Z","avatar_url":"https://github.com/PowerDNS.png","language":"C++","readme":"Weakforced\n----------\n\nThe goal of 'wforce' is to detect brute forcing of passwords across many\nservers, services and instances.  In order to support the real world, brute\nforce detection policy can be tailored to deal with \"bulk, but legitimate\"\nusers of your service, as well as botnet-wide slowscans of passwords.\n\nThe aim is to support the largest of installations, providing services to\nhundreds of millions of users.  The current version of weakforced is not\nquite there yet, although it certainly scales to support up to ten\nmillion users, if not more. The limiting factor is number of logins\nper second at peak.\n\nWforce is a project by Dovecot, PowerDNS and Open-Xchange. For historical\nreasons, it lives in the PowerDNS github organization. If you have any questions, email\nneil.cook@open-xchange.com.\n\nFor detailed technical documentation, please go to [https://powerdns.github.io/weakforced/](https://powerdns.github.io/weakforced/).\n\nHere is how it works:\n * Report successful logins via JSON http-api\n * Report unsuccessful logins via JSON http-api\n * Query if a login should be allowed to proceed, should be delayed, or ignored via http-api\n * API for querying the status of logins, IP addresses etc.\n * Runtime console for server introspection\n\nwforce is aimed to receive message from services like:\n\n * IMAP\n * POP3\n * Webmail logins\n * FTP logins\n * Authenticated SMTP\n * Self-service logins\n * Password recovery services\n\nBy gathering failed and successful login attempts from as many services as\npossible, brute forcing attacks as well as other suspicious behaviour\ncan be detected and prevented more effectively.\n\nInspiration:\nhttp://www.techspot.com/news/58199-developer-reported-icloud-brute-force-password-hack-to-apple-nearly-six-month-ago.html\n\nInstalling\n----------\n\nDocker:\n\nThere is a docker image hosted on docker hub, see [https://powerdns.github.io/weakforced/](https://powerdns.github.io/weakforced/) for more details.\n\nFrom GitHub:\n\nThe easy way:\n\n```\n$ git clone https://github.com/PowerDNS/weakforced.git\n$ cd weakforced\n$ git submodule init\n$ git submodule update\n$ builder/build.sh debian-bullseye | debian-bookworm | el-7 | el-8  | el-9 | amazon-2\n```\nThis will build packages (`wforce`,`wforce-trackalert` and `wforce-debuginfo`) for the appropriate OS. You will need docker for the builder to work.\n\nNote that since the 2.12 release, the built packages include an openresty luajit fork (`wforce-lua-dist`); this is because that fork\nfixes issues in the main luajit library that wforce runs into under conditions of high load. The `wforce-lua-dist` package\nalso contains some lua modules that have proved useful to wforce deployments over the years as well as luarocks to install\nnew modules.\n\nThe hard way:\n\n```\n$ git clone https://github.com/PowerDNS/weakforced.git\n$ cd weakforced\n$ autoreconf -i\n$ ./configure\n$ make\n```\n\nThis requires recent versions of libtool, automake and autoconf to be\ninstalled.\n\nIt also requires:\n * A compiler supporting C++ 17\n * Lua 5.1+ development libraries (or LuaJIT if you configure --with-luajit)\n * Boost 1.61+\n * Protobuf compiler and protobuf development libraries\n * Getdns development libraries (if you want to use the DNS lookup functionality)\n * libsodium\n * python + virtualenv for regression testing\n * libgeoip-dev for GeoIP support\n * libsystemd-dev for systemd support\n * pandoc for building the manpages\n * libcurl-dev (OpenSSL version)\n * libhiredis-dev\n * libssl-dev\n * libprometheus-cpp (https://github.com/jupp0r/prometheus-cpp)\n * libmaxminddb-dev\n * libyaml-cpp-dev\n * libdrogon (https://github.com/drogonframework/drogon) - Used for the HTTP server\n * libjsoncpp-dev\n * libuuid-dev\n * libz-dev\n * docker for regression testing\n * python3 rather than python2\n * python-bottle for regression testing of webhooks\n\nTo build on OS X, `brew install readline` and use\n`./configure PKG_CONFIG_PATH=\u003cpath to your openssl installation\u003e LDFLAGS=-L/usr/local/opt/readline/lib CPPFLAGS=-I/usr/local/opt/readline/include`\n\nAdd --with-luajit to the end of the configure line if you want to use LuaJIT.\n\nPolicies\n--------\n\nThere is a sensible, if very simple, default policy in wforce.conf (running without\nthis means *no* policy), and extensive support for crafting your own policies using\nthe insanely great Lua scripting language.\n\nNote that although there is\na single Lua configuration file, the canonicalize, reset, report and allow functions run in\ndifferent lua states from the rest of the configuration. This mostly\n\"just works\", but may lead to unexpected behaviour such as running Lua\ncommands at the server Lua prompt, and getting multiple answers\n(because Lua commands are passed to all Lua states).\n\nSample:\n\n```lua\n-- set up the things we want to track\nfield_map = {}\n-- use hyperloglog to track cardinality of (failed) password attempts\nfield_map[\"diffFailedPasswords\"] = \"hll\"\n-- track those things over 6x10 minute windows\nnewStringStatsDB(\"OneHourDB\", 600, 6, field_map)\n\n-- this function counts interesting things when \"report\" is invoked\nfunction twreport(lt)\n\tsdb = getStringStatsDB(\"OneHourDB\")\n\tif (not lt.success)\n\tthen\n\t   sdb:twAdd(lt.remote, \"diffFailedPasswords\", lt.pwhash)\n\t   addrlogin = lt.remote:tostring() .. lt.login\n\t   sdb:twAdd(addrlogin, \"diffFailedPasswords\", lt.pwhash)\n\tend\nend\n\nfunction allow(lt)\n\tsdb = getStringStatsDB(\"OneHourDB\")\n\tif(sdb:twGet(lt.remote, \"diffFailedPasswords\") \u003e 50)\n\tthen\n\t\treturn -1, \"\", \"\", {} -- BLOCK!\n\tend\n\t// concatenate the IP address and login string\n\taddrlogin = lt.remote:tostring() .. lt.login\t\n\tif(sdb:twGet(addrlogin, \"diffFailedPasswords\") \u003e 3)\n\tthen\n\t\treturn 3, \"tarpitted\", \"diffFailedPasswords\", {} -- must wait for 3 seconds\n\tend\n\n\treturn 0, \"\", \"\", {} -- OK!\nend\n```\n\nMany more metrics are available to base decisions on. Some example\ncode is in [wforce.conf](wforce/wforce.conf), and more extensive examples are\nin [wforce.conf.example](wforce/wforce.conf.example). For full\n[documentation](docs/manpages), use \"man wforce.conf\".\n\nTo report (if you configured with 'webserver(\"127.0.0.1:8084\", \"secret\")'):\n\n```bash\n$ for a in {1..101}\n  do\n    curl -X POST -H \"Content-Type: application/json\" --data '{\"login\":\"ahu\", \"remote\": \"127.0.0.1\", \"pwhash\":\"1234'$a'\", \"success\":\"false\"}' \\\n    http://127.0.0.1:8084/?command=report -u wforce:secret\n  done\n```\n\nThis reports 101 failed logins for one user, but with different password hashes.\n\nNow to look up if we're still allowed in:\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"login\":\"ahu\", \"remote\": \"127.0.0.1\", \"pwhash\":\"1234\"}' \\\n  http://127.0.0.1:8084/?command=allow -u wforce:super\n{\"status\": -1, \"msg\": \"diffFailedPasswords\"}\n```\n\nIt appears we are not!\n\nYou can also provide additional information for use by weakforce using\nthe optional \"attrs\" object. An example:\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"login\":\"ahu\", \"remote\": \"127.0.0.1\",\n\"pwhash\":\"1234\", \"attrs\":{\"attr1\":\"val1\", \"attr2\":\"val2\"}}' \\\n  http://127.0.0.1:8084/?command=allow -u wforce:super\n{\"status\": 0, \"msg\": \"\"}\n```\n\nAn example using the optional attrs object using multi-valued\nattributes:\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"login\":\"ahu\", \"remote\": \"127.0.0.1\",\n\"pwhash\":\"1234\", \"attrs\":{\"attr1\":\"val1\", \"attr2\":[\"val2\",\"val3\"]}}' \\\n  http://127.0.0.1:8084/?command=allow -u wforce:super\n{\"status\": 0, \"msg\": \"\"}\n```\n\nThere is also a command to reset the stats for a given login and/or IP\nAddress, using the 'reset' command, the logic for which is also\nimplemented in Lua. The default configuration for reset is as follows:\n\n```lua\nfunction reset(type, login, ip)\n\t sdb = getStringStatsDB(\"OneHourDB\")\n\t if (string.find(type, \"ip\"))\n\t then\n\t\tsdb:twReset(ip)\n\t end\n\t if (string.find(type, \"login\"))\n\t then\n\t\tsdb:twReset(login)\n\t end\n\t if (string.find(type, \"ip\") and string.find(type, \"login\"))\n\t then\n\t\tiplogin = ip:tostring() .. login\n\t\tsdb:twReset(iplogin)\n\t end\n\t return true\nend\n```\n\nTo test it out, try the following to reset the login 'ahu':\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"login\":\"ahu\"}'\\\n  http://127.0.0.1:8084/?command=reset -u wforce:super\n{\"status\": \"ok\"}\n```\n\nYou can reset IP addresses also:\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"ip\":\"128.243.21.16\"}'\\\n  http://127.0.0.1:8084/?command=reset -u wforce:super\n{\"status\": \"ok\"}\n```\n\nOr both in the same command (this helps if you are tracking stats using compound keys\ncombining both IP address and login):\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"login\":\"ahu\", \"ip\":\"FE80::0202:B3FF:FE1E:8329\"}'\\\n  http://127.0.0.1:8084/?command=reset -u wforce:super\n{\"status\": \"ok\"}\n```\n\nFinally there is a \"ping\" command, to check the server is up and\nanswering requests:\n\n```bash\n$ curl -X GET http://127.0.0.1:8084/?command=ping -u wforce:super\n{\"status\": \"ok\"}\n```\n\nConsole\n-------\nAvailable over TCP/IP, like this:\n```lua\nsetKey(\"Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE=\")\ncontrolSocket(\"0.0.0.0:4004\")\n```\n\nLaunch wforce as a daemon (`wforce --daemon`), to connect, run `wforce -c`.\nComes with autocomplete and command history. If you put an actual IP address\nin place of 0.0.0.0, you can use the same config to listen and connect\nremotely.\n\nTo get some stats, try:\n```lua\n\u003e stats()\n40 reports, 8 allow-queries, 40 entries in database\n```\n\nThe [wforce manpage](docs/manpages/wforce.1.md) describes the\ncommand-line options and all the possible console commands in more detail.\n\nSpec\n----\nWforce accepts reports with 4 mandatory fields plus multiple optional\nfields.\n\nMandatory:\n * login (string): the user name or number or whatever\n * remote (ip address): the address the user arrived on\n * pwhash (string): a highly truncated hash of the password used\n * success (boolean): was the login a success or not?\n\nOptional:\n * policy_reject (boolean) -  If the login was not successful only because of a policy-based reject from wforce (i.e. the username and password were correct).\n * attrs (json object): additional information about the login. For\n example, attributes from a user database.\n * device_id (string) - A string that represents the device that the user\n   logged in from. For HTTP this would typically be the User-Agent\n   string, and for IMAP it would be the IMAP client ID command string.\n * protocol (string) - A string representing the protocol used to login,\n e.g. \"http\", \"imap\", \"pop3\".\n * tls (boolean) - Whether or not the login was secured with TLS.\n\nThe entire HTTP API is [documented](docs/swagger/wforce_api.7.yml)\nusing the excellent OpenAPI (swagger) specification. \n\nThe pwhash field deserves some clarification. In order to\ndistinguish actual brute forcing of a password, and repeated incorrect but\nidentical login attempts, we need some marker that tells us if passwords are\ndifferent.\n\nNaively, we could hash the password, but this would spread knowledge of\nsecret credentials beyond where it should reasonably be. Even if we salt and\niterate the hash, or use a specific 'slow' hash, we're still spreading\nknowledge.\n\nHowever, if we take any kind of hash and truncate it severely, for example\nto 12 bits, the hash tells us very little about the password itself - since\none in 4096 random strings will match it anyhow. But for detecting multiple\nidentical logins, it is good enough.\n\nFor additional security, hash the login name together with the password - this\nprevents detecting different logins that might have the same password.\n\nNOTE: wforce does not require any specific kind of hashing scheme, but it\nis important that all services reporting successful/failed logins use the\nsame scheme!\n\nWhen in doubt, try:\n\n```SQL\nTRUNCATE(SHA256(SECRET + LOGIN + '\\x00' + PASSWORD), 12)\n```\n\nWhich denotes to take the first 12 bits of the hash of the concatenation of\na secret, the login, a 0 byte and the password.  Prepend 4 0 bits to get\nsomething that can be expressed as two bytes.\n\nAPI Calls\n---------\nWe can call 'report', and 'allow' commands. The optional 'attrs' field\nenables the client to send additional data to weakforced.\n\nTo report, POST to /?command=report a JSON object with fields from the\nLoginTuple as described above.\n\nTo request if a login should be allowed, POST to /?command=allow, again with\nthe LoginTuple. The result is a JSON object with a \"status\" field. If this is -1, do\nnot perform login validation (i.e. provide no clue to the client if the password\nwas correct or not, or even if the account exists).\n\nIf 0, allow login validation to proceed. If a positive number, sleep this\nmany seconds until allowing login validation to proceed.\n\nCustom API Endpoints\n--------------------\n\nYou can create custom API commands (REST Endpoints) using the\nfollowing configuration:\n\n```lua\nsetCustomEndpoint(\"custom\", customfunc)\n```\n\nwhich will create a new API command \"custom\", which calls the Lua\nfunction \"customfunc\" whenever that command is invoked. Parameters to\ncustom commands are always in the same form, which is key-value pairs\nwrapped in an 'attrs' object. For example, the following parameters\nsents as json in the message body would be valid:\n\n```json\n{ \"attrs\" : { \"key\" : \"value\" }}\n```\n\nCustom functions return values are also key-value pairs, this time\nwrapped in an 'r_attrs' object, along with a boolean success field,\nfor example:\n\n```json\n{ \"r_attrs\" : { \"key\" : \"value\" }, \"success\" : true}\n```\n\nAn example configuration for a custom API endpoint would look like:\n\n```lua\nfunction custom(args)\n\tfor k,v in pairs(args.attrs) do\n\t\tinfoLog(\"custom func argument attrs\", { key=k, value=v });\n\tend\n\t-- return consists of a boolean, followed by { key-value pairs }\n\treturn true, { key=value }\nend\nsetCustomEndpoint(\"custom\", custom)\n```\n\nAn example curl command would be:\n\n```lua\n% curl -v -X POST -H \"Content-Type: application/json\" --data\n  '{\"attrs\":{\"login1\":\"ahu\", \"remote\": \"127.0.0.1\",  \"pwhash\":\"1234\"}}'\n  http://127.0.0.1:8084/?command=custom -u wforce:super\n{\"r_attrs\": {}, \"success\": true}\n```\n\nWebHooks\n---------\nIt is possible to configure webhooks, which get called whenever\nspecific events occur. To do this, use the \"addWebHook\" configuration\ncommand. For example:\n\n```lua\nconfig_keys={}\nconfig_keys[\"url\"] = \"http://webhooks.example.com:8080/webhook/\"\nconfig_keys[\"secret\"] = \"verysecretcode\"\nevents = { \"report\", \"allow\" }\naddWebHook(events, config_keys)\n```\n\nThe above will call the webhook at the specified url, for every report\nand allow command received, with the body of the POST containing the\noriginal json data sent to wforce. For more information use [\"man\nwforce.conf\"](docs/manpages/wforce.conf.5.md) and\n[\"man wforce_webhook\"](docs/manpages/wforce_webhook.5.md).\n\nCustom WebHooks\n----------------\n\nCustom webhooks can also be defined, which are not invoked based on\nspecific events, but instead from Lua. Configuration is similar to\nnormal webhooks:\n\n```lua\nconfig_keys={}\nconfig_keys[\"url\"] = \"http://webhooks.example.com:8080/webhook/\"\nconfig_keys[\"secret\"] = \"verysecretcode\"\nconfig_keys[\"content-type\"] = \"application/json\"\naddCustomWebHook(\"mycustomhook\", config_keys)\n```\n\nHowever, the webhook will only be invoked via the Lua\n\"runCustomWebHook\" command, for example:\n\n```lua\nrunCustomWebHook(mycustomhook\", \"{ \\\"foo\\\":\\\"bar\\\" }\")\n```\n\nThe above command will invoke the custom webhook \"mycustomhook\" with\nthe data contained in the second argument, which is simply a Lua\nstring. No parsing of the data is performed, however the Content-Type\nof the webhook, which defaults to application/json can be customized\nas shown above.\n\nBlacklists\n----------\n\nBlacklisting capability is provided via either REST endpoints or Lua\ncommands, to add/delete IP addresses, logins or IP:login tuples from\nthe Blacklist. Blacklist information can be replicated (see below),\nand also optionally persisted in a Redis DB. Use \"man wforce.conf\" to\nlearn more about the blacklist commands.\n\nLoad balancing: siblings\n------------------------\nFor high-availability or performance reasons it may be desireable to run\nmultiple instances of wforce. To present a unified view of status however,\nthese instances then need to share data. To do so, wforce\nimplements a simple knowledge-sharing system.\n\nThe original version of wforce simply broadcast all received report\ntuples (best effort, UDP) to all siblings. However the latest version\nonly broadcasts incremental changes to the underlying state databases,\nnamely the stats dbs and the blacklist.\n\nThe sibling list is parsed such that we don't broadcast messages to ourselves\naccidentally, and can thus be identical across all servers.\n\nEven if you configure siblings, stats db data is not replicated by default. To do\nthis, use the \"twEnableReplication()\" command on each\nstats db for which you wish to enable replication. Blacklist\ninformation is automatically replicated if you have configured siblings.\n\nTo define siblings, use:\n\n```lua\nsetKey(\"Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE=\")\naddSibling(\"192.168.1.79\")\naddSibling(\"192.168.1.30\")\naddSibling(\"192.168.1.54\")\nsiblingListener(\"0.0.0.0\")\n```\n\nThe first line sets the authentication and encryption key for our sibling\ncommunications. To make your own key (recommended), run `makeKey()` on the\nconsole and paste the output in all your configuration files.\n\nThis last line configures that we also listen to our other siblings (which\nis nice).  The default port is 4001, the protocol is UDP.\n\nTo view sibling stats:\n\n```lua\n\u003e siblings()\nAddress                             Send Successes  Send Failures  Rcv Successes   Rcv Failures     Note\n192.168.1.79:4001                   18              7              0               0\n192.168.1.30:4001                   25              0              0               0\n192.168.1.54:4001                   0               0              0               0                Self\n```\n\nWith this setup, several wforces are all kept in sync, and can be load\nbalanced behind (for example) haproxy, which incidentally can also offer SSL.\n\nGeoIP2 Support\n-------------\n\nGeoIP support is provided using the GeoIP2 Maxmind APIs DBs\n(i.e. DBs ending in .mmdb). This is the preferred integration to use,\nas support for GeoIP Legacy DBs will be discontinued by Maxmind\nin 2019.\n\nGeoIP2 DBs are represented by a Lua object that is created with the\nfollowing call:\n\n```lua\nnewGeoIP2DB(\"Name\", \"/path/to/file.mmdb\")\n```\n\nThe Lua object is retrieved with the following call:\n\n```lua\nlocal mygeodb = getGeoIP2DB(\"Name\")\n```\n\nYou can then lookup information using the following calls:\n\n* lookupCountry() - Returns the 2 letter country code associated with\n  the IP address\n* lookupISP() - Returns the name of the ISP associated with the IP\n  address (requires the Maxmind ISP DB, which is only available on\n  subscription)\n* lookupCity - Rather than only returning a city name, this call\nreturns a Lua table which includes the following information:\n    * country_code\n    * country_name\n    * region\n    * city\n    * postal_code\n    * continent_code\n    * latitude\n    * longitude\n\nFor example:\n\n```lua\nlocal geoip_data = mygeodp:lookupCity(newCA(\"128.243.21.16\"))\nprint(geoip_data.city)\nprint(geoip_data.longitude)\nprint(geoip_data.latitude)\n```\n\nLegacy GeoIP Support\n-------------\n\nSupport for legacy GeoIP databases (i.e. ending in .dat) is\ndeprecated, since Maxmind will be discontinuing support for them\nin 2019.\n\nThree types of GeoIP lookup are supported:\n\n* Country lookups - Initialized with initGeoIPDB() and looked up\n    with lookupCountry()\n* ISP Lookups - Initialized with initGeoIPISPDB() and looked up with\n  lookupISP()\n* City Lookup - Initialized with initGeoIPCityDB() and looked up\n  with lookupCity()\n\nThe Country and ISP lookups return a string, while lookupCity()\nreturns a Lua map consisting of the following keys:\n* country_code\n* country_name\n* region\n* city\n* postal_code\n* continent_code\n* latitude\n* longitude\n\nFor example:\n```lua\nlocal geoip_data = lookupCity(newCA(\"128.243.21.16\"))\nprint(geoip_data.city)\n```\n\nWhen a DB is initialized, wforce attempts to open both v4 and v6\nversions of the database. If either is not found an error is thrown,\nso make sure both ipv4 and v6 versions of each DB are\ninstalled.\n\nAdditionally, when using the free/lite versions of the\ndatabases, you may see errors such as \"initGeoIPCityDB(): Error\ninitialising GeoIP (No geoip v6 city db available)\". This is usually\nbecause the filenames for the \"lite\" DBs are not the same as the\nexpected filenames for the full DBs, specifically all files must\nstart with GeoIP rather than GeoLite. Creating symbolic links to the\nexpected filenames will fix this problem, for example:\n\n```\nln -s GeoLiteCityv6.dat GeoIPCityv6.dat\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpowerdns%2Fweakforced","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpowerdns%2Fweakforced","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpowerdns%2Fweakforced/lists"}