WFORCE.CONF(5)
π WFORCE.CONF(5) π
okay bestie, welcome to the wforce.conf manual page β yes it's a section 5 file format page, and yes it's literally the configuration file for the wforce daemon. drink water, stretch, and let's get into it because this one is long and we're doing all of it πΈ
β¨ NAME β¨
wforce.conf β configuration file for wforce daemon π
π DESCRIPTION π
bestie. listen. this file is read by wforce and it is a Lua script. not a config file with sad little key = value lines β a whole Lua script. it contains Lua commands that do two (2) big jobs:
- (a) configuration of the wforce daemon, and
- (b) functions that control the operation of the wforce daemon in response to HTTP commands β specifically "report", "allow" and "reset" π
you can point at a different version of this file with:
wforce -C private_wforce.conf ...
slay. custom config file. main character energy fr fr π¦
π CONFIGURATION-ONLY FUNCTIONS π
okay so the following functions are for configuration of wforce only β like, they cannot be called inside the allow/report/reset functions. no cap. keep them in the config zone π₯
setACL(<list of netmasks>)β set the access control list for the HTTP Server. for example, to allow access from any IP address, you specify:
setACL({"0.0.0.0/0"})
addACL(<netmask>)β add a netmask to the access control list for the HTTP server. for example, to allow access from127.0.0.0/8, you specify:
addACL("127.0.0.0/8")
addWebHook(<list of events>, <config key map>)β add a webhook for the specified events, with the specified configuration keys. seewforce_webhook(5)for the list of events, supported configuration keys, and details of the HTTP(S) POST sent to webhook URLs. for example:
config_keys={}
config_keys["url"] = "http://webhooks.example.com:8080/webhook/"
config_keys["secret"] = "verysecretcode"
events = { "report", "allow" }
addWebHook(events, config_keys)
addCustomWebHook(<custom webhook name>, <config key map>)β add a custom webhook, i.e. one which can be called from Lua (using"runCustomWebHook()"β see below) with arbitrary data, using the specified configuration keys. seewforce_webhook(5)for the supported config keys, and details of the HTTP(S) POST sent to webhook URLs. for example:
config_keys={}
config_keys["url"] = "http://webhooks.example.com:8080/webhook/"
config_keys["secret"] = "verysecretcode"
addCustomWebHook("mycustomhook", config_keys)
addCustomStat(<stat name>)β add a custom counter which can be used to track statistics. the stats for custom counters are logged every 5 minutes. the counter is incremented with the"incCustomStat"command. for example:
addCustomStat("custom_stat1")
setSiblings(<list of IP/Host[:port[:protocol]]>)β set the list of siblings to which stats db and blacklist/whitelist data should be replicated. if port is not specified it defaults to4001. if protocol is not specified it defaults toudp. this function is safe to call while wforce is running. for example:
setSiblings({"127.0.1.2", "sibling1.example.com:4004", "[::1]:4004:tcp"})
setSiblingsWithKey(<list of {IP/Host[:port[:protocol]], Encryption Key>)β identical tosetSiblings()except that it allows an encryption key to be specified for each sibling. for example:
setSiblingsWithKey({{"127.0.1.2", "Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE="}, {"127.0.1.3:4004:tcp", "KaiQkCHloe2ysXv2HbxBAFqHI4N8+ahmwYwsbYlDdF0="}})
addSibling(<IP/Hostname[:port[:protocol]]>)β add a sibling to the list to which all stats db and blacklist/whitelist data should be replicated. use[]to enclose IPv6 addresses. if port is not specified it defaults to4001. if protocol is not specified it defaults toudp. this function is safe to call while wforce is running. for example:
addSibling("192.168.1.23")
addSibling("192.168.1.23:4001:udp")
addSibling("192.168.1.23:4003:tcp")
addSibling("[::1]:4003:tcp")
addSibling("sibling1.example.com")
addSiblingWithKey(<IP[:port[:protocol]]>, <Encryption Key>)β identical toaddSibling(), except that an encryption key is specified to enable per-sibling encryption. for example:
addSiblingWithKey("192.168.1.23", "Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE=")
removeSibling(<IP/Host[:port[:protocol]]>)β remove a sibling to the list to which all stats db and blacklist/whitelist data should be replicated. use[]to enclose IPv6 addresses. if port is not specified it defaults to4001. if protocol is not specified it defaults toudp. this function is safe to call while wforce is running. for example:
removeSibling("192.168.1.23")
removeSibling("sibling1.example.com:4001:udp")
removeSibling("[::1]:4003:tcp")
siblingListener(<IP[:port]>)β listen for reports from siblings on the specified IP address and port. if port is not specified it defaults to4001. Wforce will always listen on both UDP and TCP ports. for example:
siblingListener("0.0.0.0:4001")
siblingListener("[::1]:4001")
setSiblingConnectTimeout(<timeout ms>)β sets a timeout in milliseconds for new connections to siblings. defaults to5000ms (5 seconds). for example:
setSiblingConnectTimeout(1000)
setMaxSiblingQueueSize(<size>)β sets the maximum size of the send and receive queues for replication events waiting to be processed. defaults to5000. this is only to handle short-term spikes in load/latency β if error messages relating to the recv queue max size being reached are seen, then you should consider using sharded string stats dbs (newShardedStringStatsDB), and/or tuning the stats db expiry sleep time (twSetExpireSleep). β¨
setMaxSiblingQueueSize(10000)
setJA3AttrName(<attribute name>)β sets the attribute name that will be used for determining the JA3 hash. it defaults toja3. note that a (single-valued) attribute with this name must be present in theLoginTupleattrs object. πsetNamedReportSinks(<name>, <list of IP[:port]>)β set a named list of report sinks to which all received reports should be forwarded over UDP. reports will be sent to the configured report sinks for a given name in a round-robin fashion if more than one is specified. reports are sent separately to each named report sink. if port is not specified it defaults to4501. replaces the deprecated"setReportSinks()". for example:
setNamedReportSinks("logstash", {"127.0.1.2", "127.0.1.3:4501"})
addNamedReportSink(<name>, <IP[:port]>)β add a report sink to the named list to which all received reports should be forwarded over UDP. reports will be sent to the configured report sinks for a given name in a round-robin fashion if more than one is specified. reports are sent separately to each named report sink. if port is not specified it defaults to4501. replaces the deprecated"addReportSink()". for example:
addNamedReportSink("logstash", "192.168.1.23")
webserver(<IP:port>, <password>)β (deprecated β seeaddListener()instead) listen for HTTP commands on the specified IP address and port. the password is used to authenticate client connections using basic authentication. for example:
webserver("0.0.0.0:8084", "super")
addListener(<IP:port>, <useSSL>, <cert file>, <key file>, <options>)β (replacement forwebserver()) listen for HTTP commands on the specified IP address and port. ifuseSSLis true, then HTTPS must be used, andcert_fileand key file are used, otherwise they are empty. options contains a list of key value pairs to configure the TLS connection; these follow the command line option names inhttps://www.openssl.org/docs/manmaster/man3/SSL_CONF_cmd.html. for example,"min_protocol"to set the minimum TLS protocol version. you can add as many listeners as you choose. for example:
addListener("0.0.0.0:8084", false, "", "", {})
addListener("1.2.3.4:1234", true, "/etc/wforce/cert.pem", "/etc/wforce/key.pem", {minimum_protocol="TLSv1.2"})
addListener("[::1]:9000", true, "/etc/wforce/cert.pem", "/etc/wforce/key.pem", {minimum_protocol="TLSv1.3"})
setWebserverPassword(<Password>)β (replacement for webserver password parameter) sets the basic authentication password for access to the webserver. this has been decoupled from theaddListener()command because multiple listeners can now be created, which was not previously possible. for example:
setWebserverPassword("foobar")
controlSocket(<IP[:port]>)β listen for control connections on the specified IP address and port. if port is not specified it defaults to5199. for example:
controlSocket("0.0.0.0:4004")
setKey(<key>)β use the specified key for authenticating connections from siblings. the key can be generated withmakeKey()from the console. seewforce(1)for instructions on running a console client. returnsfalseif the key could not be set (e.g. invalid base64). for example:
if not setKey("Ay9KXgU3g4ygK+qWT0Ut4gH8PPz02gbtPeXWPdjD0HE=")
then
...
setNumLuaStates(<num states>)β set the number of Lua Contexts that will be created to run allow/report/reset commands. defaults to10if not specified. see alsosetNumWorkerThreads()andsetNumSiblingThreads(). should be at least equal toNumWorkerThreads + NumSiblingThreads. for example:
setNumLuaStates(10)
setNumWorkerThreads(<num threads>)β set the number of threads in the pool used to run allow/report/reset commands. each thread uses a separate Lua Context, (seesetNumLuaStates()). defaults to4if not specified. for example:
setNumWorkerThreads(4)
setNumSiblingThreads(<num threads>)β set the number of threads in the pool used to process reports received from siblings. each thread uses a separate Lua Context, the number of which is set withsetNumLuaStates(). defaults to2if not specified. for example:
setNumSiblingThreads(2)
setNumWebHookThreads(<num threads>)β set the number of threads in the pool used to send webhook events. defaults to5if not specified. for example:
setNumWebHookThreads(2)
setMaxWebserverConns(<max conns>)β set the maximum number of active connections to the webserver. this can be used to limit the effect of too many queries to wforce. it defaults to10,000. for example:
setMaxWebserverConns(5000)
setNumWebHookConnsPerThread(<num conns>)β sets the max number of connections that each WebHook thread gets to play with. If you don't say anything, it defaults to 10. Heads up: this setting replaces the deprecatednum_connsper-hook configuration setting, so likeβ¦ stop using that one bestie π
setNumWebHookConnsPerThread(50)
setWebHookQueueSize(<queue size>)β sets how big the queue for webhook events is. If that queue gets too chonky, webhooks get discarded and an error gets logged π. The default queue size is 50000, which honestly should be perfect for most use-cases, no cap.
setWebHookQueueSize(100000)
setWebHookTimeoutSecs(<timeout secs>)β the max time a request is allowed to take for webhooks, periodt. For example:
setWebHookTimeoutSecs(2)
newGeoIP2DB(<db name>, <filename>)β opens and initializes a GeoIP2 database β¨. You gotta pick a name, and you also gotta hand over the filename of the database you want opened. Then, to actually get an object you can run lookups with, you use thegetGeoIP2DB()function. For example:
newGeoIP2DB("CityDB", "/usr/share/GeoIP/GeoLite2-City.mmdb")
initGeoIPDB()β Deprecated β usenewGeoIP2DB()π. Initializes the country-level IPv4 and IPv6 GeoIP Legacy databases. If either of these databases isn't installed, this command fails andwforcewill not start. For example:
initGeoIPDB()
initGeoIPCityDB()β Deprecated β usenewGeoIP2DB()π«Ά. Initializes the city-level IPv4 and IPv6 GeoIP Legacy databases. Same deal: if either one isn't installed, the command fails andwforcewill not start. And listen β make sure these databases have the right names if you're rocking the free/lite DBs, because you might need to create symbolic links, e.g.GeoIPCityv6.datβGeoLiteCityv6.dat. For example:
initGeoIPCityDB()
initGeoIPISPDB()β Deprecated β usenewGeoIP2DB()πΈ. Initializes the ISP-level IPv4 and IPv6 GeoIP Legacy databases. And yeah, if either of these isn't installed, this command fails andwforcewill not start. For example:
initGeoIPISPDB()
newDNSResolver(<resolver name>)β creates a shiny new DNS resolver object with the name you give it. Quick note tho: this does not return the resolver object β you get that from thegetDNSResolver()function. For example:
newDNSResolver("MyResolver")
setHLLBits(<num bits>)β sets the accuracy of the HyperLogLog algorithm. The value can be anywhere from 4β30, and a higher number means more accuracy β but at the cost of using (potentially a lot) more memory π. The default value is 6. If you need more accuracy, consider bumping it up slightly, but check your memory usage carefully, fr fr.setCountMinBits(<eps>, <gamma>)β sets the accuracy of the CountMin algorithm.epscan be between 0.01 and 1, and a lower number gives more accuracy at the expense of memory. The default is 0.05.gammais between 0 and 1, and a higher number gives higher accuracy; the default forgammais 0.2. If you want more accuracy, consider nudging these values slightly, but check your memory usage carefully π«Ά.newStringStatsDB(<stats db name>, <window size>, <num windows>, <field map>)β creates a new StatsDB object with the name you specify. Just like the others: this does not return the object β you get that with thegetStringStatsDB()function. For example:
field_map = {}
field_map["countLogins"] = "int"
field_map["diffPasswords"] = "hll"
field_map["countCountries"] = "countmin"
newStringStatsDB("OneHourDB", 900, 4, field_map)
newShardedStringStatsDB(<stats db name>, <window size>, <num windows>, <field map>, <num shards>)β literally identical tonewStringStatsDBexcept it creates a sharded DB, which is more scalable at higher query loads π₯. A good starting value for the number of shards might be 10.StringStatsDB:twSetv4Prefix(<prefix>)β sets the prefix to use for any IPv4 ComboAddress keys stored in the db. Like, specify24to group statistics for class C networks. For example:
statsdb:twSetv4Prefix(24)
StringStatsDB:twSetv6Prefix(<prefix>)β sets the prefix to use for any IPv6 ComboAddress keys stored in the db. For example:
statsdb:twSetv4Prefix(64)
StringStatsDB:twSetMaxSize(<size>)β sets the max number of keys allowed to be stored in the db. Once the db hits that size, keys get expired on an LRU basis π. The default size is 500K keys. For example:
statsdb:twSetMaxSize(1000000)
StringStatsDB:twEnableReplication()β turns on replication for this db; it only makes a difference if siblings have been configured. For example:
statsdb:twEnableReplication()
disableBuiltinBlacklists()β disables the built-in blacklisting checks, which lets them be checked from Lua instead. For example:
disableBuiltinBlacklists()
blacklistPersistDB(<ip>, <port>)β makes the blacklist persistent by storing entries in the specified redis DB πΎ. The IP address is a string, andportshould be 6379 unless you're running redis on a non-standard port. If you specify this option,wforcewill read all the blacklist entries from the redis DB on startup. For example:
blacklistPersistDB("127.0.0.1", 6379)
blacklistPersistReplicated()β stores blacklist entries that have been replicated in the redis DB. By default, replicated blacklist entries will not be persisted. So: if you use a local redis DB for eachwforceserver, then use this config option π«Ά. But if you use a single redis instance for all yourwforceservers, then you should not specify this option, because it'll cause unnecessary writes to the redis DB. For example:
blacklistPersistReplicated()
blacklistRedisUsername(<username>)β sets the Redis username for authentication to the redis DB. For example:
blacklistRedisUsername("foobar")
blacklistRedisPassword(<password>)β sets the Redis password for authentication to the redis DB π. For example:
blacklistRedisPassword("secret")
blacklistPersistConnectTimeout(<timeout secs>)β sets the connect timeout for connecting to the persistent redis DB. If the timeout is exceeded during connection at startup,wforcewill exit; otherwise, during normal operation, if the timeout is exceeded an error gets logged. For example:
blacklistPersistConnectTimeout(2)
blacklistPersistRWTimeout(<timeout secs>, <timeout usecs>)β sets the timeout for reading from and writing to the Redis DB. For example:
blacklistPersistRWTimeout(0, 50000)
disableBuiltinWhitelists()β disables the built-in whitelisting checks, so they can be checked from Lua instead. For example:
disableBuiltinWhitelists()
whitelistPersistDB(<ip>, <port>)β makes the whitelist persistent by storing entries in the specified redis DB β¨. The IP address is a string, andportshould be 6379 unless redis is running on a non-standard port. If you specify this option,wforcereads all the whitelist entries from the redis DB on startup. For example:
whitelistPersistDB("127.0.0.1", 6379)
whitelistRedisUsername(<username>)β sets the Redis username for authentication to the redis DB. For example:
whitelistRedisUsername("foobar")
whitelistRedisPassword(<password>)β sets the Redis password for authentication to the redis DB π. For example:
whitelistRedisPassword("secret")
whitelistPersistReplicated()β stores whitelist entries that have been replicated in the redis DB. By default, replicated whitelist entries are not persisted. If you use a local redis DB for eachwforceserver, use this config option. But if you use a single redis instance for allwforceservers, you should not specify it, since it'll cause unnecessary writes to the redis DB. For example:
whitelistPersistReplicated()
whitelistPersistConnectTimeout(<timeout secs>)β sets the connect timeout for connecting to the persistent redis DB. If the timeout is exceeded during connection at startup,wforcewill exit; otherwise, during normal operation, if the timeout is exceeded an error gets logged. For example:
whitelistPersistConnectTimeout(2)
whitelistPersistRWTimeout(<timeout secs>, <timeout usecs>)β sets the timeout for reading from and writing to the Redis DB. For example:
whitelistPersistRWTimeout(0, 50000)
setBlacklistIPRetMsg(<msg>)β sets the message returned to clients whose IP address is blacklisted π€. The strings"{ip}"and"{login}"get substituted for the actual IP address and login name. For example:
setBlacklistIPRetMsg("Go away your IP {ip} is blacklisted")
setBlacklistLoginRetMsg(<msg>)β sets the message returned to clients whose login is blacklisted π€. The strings"{ip}"and"{login}"get substituted for the actual IP address and login name. For example:
setBlacklistLoginRetMsg("Go away your login {login} is blacklisted")
setBlacklistIPLoginRetMsg(<msg>)β sets the message returned to clients whose IP address/login is blacklisted π€. The strings"{ip}"and"{login}"get substituted for the actual IP address and login name. For example:
setBlacklistIPLoginRetMsg("Go away your IP {ip}/Login {login} is blacklisted")
setBlacklistJA3RetMsg(<msg>)β sets the message returned to clients whose JA3 hash is blacklisted π€. The string"{ja3}"gets substituted for the actual JA3 hash. For example:
setBlacklistJA3RetMsg("Go away your client {ja3} is blacklisted")
setBlacklistIPJA3RetMsg(<msg>)β sets the message returned to clients whose IP address/JA3 hash is blacklisted π€. The strings"{ip}"and"{ja3}"get substituted for the actual IP address and JA3 hash. For example:
setBlacklistIPLoginRetMsg("Go away your IP {ip}/Client {ja3} is blacklisted")
setAllow(<allow func>)β tellswforceto use the specified Lua function for handling all"allow"commands. For example:
setAllow(allow)
setReport(<report func>)β tellswforceto use the specified Lua function for handling all"report"commands. For example:
setReport(report)
setReset(<reset func>)β tellswforceto use the specified Lua function for handling all"reset"commands. For example:
and that reset function gets wired up like this:
setReset(reset)
setCanonicalize(<canonicalize func>)β bestie, this is how you tell wforce which Lua function handles all the "canonicalisation" duties. What does that even mean? It means mapping every alias a user has onto like, one login string. Soncook,neil.cookandneil.cook@foobar.comall collapse down toneil.cook@foobar.com. πΈ Want to go look people up in a real user database? Grab Lua modules like LuaSQL-MySQL (luarocks install luasql-mysql) or LuaLDAP (luarocks install lualdap). For example:
setCanonicalize(canonicalize)
setCustomEndpoint(<name of endpoint>, <boolean>, <custom lua function>)β okay so this one is main character energy: it creates a brand new custom REST endpoint with the name you give it, and when somebody hits that endpoint, your custom Lua function runs. Admins can arbitrarily extend the wforce REST API with new endpoints β and make as many as they want, no cap. π Heads up though: custom endpoints can only be reached via aPOSTmethod, and every argument arrives as key-value pairs of a top-levelattrsjson object (those get split into two tables β one for single-valued attrs, the other for multi-valued attrs β see CustomFuncArgs below π). And if that boolean argument istrue, then all the arguments to the custom function get sent to all configured named report sinks. The return info comes back with a booleansuccessand anr_attrsjson object holding the return key-value pairs. Peepwforce.conf.examplefor a full example. For example:
function custom(args)
for k,v in pairs(args.attrs) do
infoLog("custom func argument attrs", { key=k, value=v });
end
-- return consists of a boolean, followed by { key-value pairs }
return true, { key=value }
end
setCustomEndpoint("custom", false, custom)
-- Set boolean to true to enable arguments to be sent to all
-- report sinks
-- setCustomEndpoint("custom", true, custom)
setCustomGetEndpoint(<name of endpoint>, <custom lua function>)β same vibe as above, except this custom REST endpoint is reachable with a GET command (setCustomEndpoint()only does POST, remember). The function's return value is a string, and that string gets handed to the HTTP client as atext/plainresponse body. For example:
function textIPBlacklist()
local ipbl = getIPBlacklist()
local ret_table = {}
for i,j in pairs(ipbl)
do
for k,v in pairs(j)
do
if k == "ip"
then
table.insert(ret_table, v)
end
end
end
local s = table.concat(ret_table, "\n") .. "\n"
return s
end
setCustomGetEndpoint("textIPBlacklist", textIPBlacklist)
setVerboseAllowLog()β so when wforce is logging allow requests, it skips logging the ones that return0, purely for performance reasons (default behaviour, no hate). But if you do want those0-returning allow requests in your logs, call this function. For example:
setVerboseAllowLog()
addSyncHost(<sync host address>, <sync host password>, <replication address>, <callback address>)β use this config command if you want wforce to synchronize the contents of its StatsDBs with the rest of a cluster. π¦ Once any sync hosts are added, wforce tries to contact them in turn and hunts for a host that's been up longer than the number of seconds you set insetMinSyncHostUptime(). The sync host address should include a port (it defaults to8084). If it works out, that sync host sends the contents of its StatsDBs to this wforce instance over the replication address you specified β and that address should have the same port as configured insiblingListener()and defaults to4001. When it's all done it notifies this wforce instance using the callback address β which should have the same port as configured inwebserverand defaults to8084. N.B. β οΈ important: the encryption key of the local instance gets sent to the sync host, so if you care about keeping that key confidential, you may want to consider running an HTTPS reverse proxy in front of the sync host. For example:
-- Add 10.2.3.1:8084 as a sync host,
-- and use the password "super"
-- Send the DB dump to 10.2.1.1:4001
-- and let me know on 10.2.1.1:8084 when the dump is finished
addSyncHost("10.2.3.1:8084", "super", "10.2.1.1:4001", "10.2.1.1:8084")
-- Add https://10.2.3.1:8084 as a sync host,
-- and use the password "super"
-- Send the DB dump to https://10.2.1.1:4001
addSyncHost("https://host1.example.com:8084", "super", "10.2.1.1:4001", "https://host2.example.com:8084")
setMinSyncHostUptime(<seconds>)β the minimum amount of time any sync host has to have been up before it's allowed to send you the contents of its DBs. Defaults to3600. For example:
setMinSyncHostUptime(1800)
disableCurlPeerVerification()β turns off peer certificate checking on all outbound HTTPS connections. π¨ Not recommended, bestie, except for debugging or development purposes. For example:
disableCurlPeerVerification()
disableCurlHostVerification()β turns off hostname checking in the peer certificate for all outbound HTTPS connections. Again, not recommended unless you're debugging or developing. π«
disableCurlHostVerification()
setCurlCABundleFile(<Path to CA File>)β points at a file holding the certificate authorities to use when checking HTTPS server certificates. Use this when the standard installed root certs don't contain the certs you need. It should be a file containing1:Ncerts in PEM format. β¨
setCurlCABundleFile("/etc/ca/local_cas.pem")
setCurlClientCertAndKey(<Path to Cert File>, <Path to Key File>)β points at the certificate and key files to use for mutual TLS authentication (PEM format, obviously π ).
setCurlClientCertAndKey("/etc/certs/clientcert.pem", "/etc/certs/clientkey.pem")
setMetricsNoPassword()β disables password protection for the/metricsendpoint. That's it, that's the whole function. π
π GENERAL FUNCTIONS π
These ones are available anywhere β either as part of the configuration, or inside the allow/report/reset functions. So versatile. Iconic. β¨
getGeoIP2DB(<db name>)β returns an object you can use to do GeoIP lookups. Just so you know: the database has to be initialised first withnewGeoIP2DB(). For example:
local citydb = getGeoIP2DB("CityDB")
GeoIP2DB:lookupCountry(<ComboAddress>)β returns the two-character country code of the country that IP address lives in. You can make a ComboAddress object with thenewCA()function. For example:
my_country = countrydb:lookupCountry(newCA("8.8.8.8"))
my_country = countrydb:lookupCountry(lt.remote)
GeoIP2DB:lookupISP(<ComboAddress>)β returns the name of the ISP hosting that IP address. Make a ComboAddress withnewCA(). For example:
local my_isp = ispdb:lookupISP(newCA("128.243.16.21"))
GeoIP2DB:lookupCity(<ComboAddress>)β returns a map stuffed with info about the IP address, like the city name plus latitude and longitude. The full list of fields lives in GeoIPRecord below π. For example:
local gip_record = citydb:lookupCity(lt.remote)
local my_city = gip_record.city
local my_latitude = gip_record.latitude
GeoIP2DB:lookupStringValue(<ComboAddress>, <array of string values>)β returns a string matching the value of the field you specify with that array of string values. For example:
local city_name = citydb:lookupStringValue(newCA(ip_address), {"city", "names", "en"})
GeoIP2DB:lookupUIntValue(<ComboAddress>, <array of string values>)β returns an integer matching the value of the field you specify with that array of string values. For example:
local accuracy = citydb:lookupUIntValue(newCA(ip_address), {"location", "accuracy_radius"})
GeoIP2DB:lookupBoolValue(<ComboAddress>, <array of string values>)β returns a boolean matching the value of the field you specify with that array of string values. For example:
local eu = citydb:lookupBoolValue(newCA(ip_address), {"country", "is_in_european_union"})
GeoIP2DB:lookupDoubleValue(<ComboAddress>, <array of string values>)β returns the value corresponding to the field you specify with that array of string values (and that value can be eitherdoubleorfloatin the MMDB specification, fyi). For example:
local city_lat = citydb:lookupDoubleValue(newCA(ip_address), {"location", "latitude"})
local city_long = citydb:lookupDoubleValue(newCA(ip_address), {"location", "longitude"})
lookupCountry(<ComboAddress>)β β οΈ Deprecated β use the new GeoIP2 function instead, bestie. Returns the two-character country code of the country that the IP address is located in. A ComboAddress object can be created with thenewCA()function. For example:
my_country = lookupCountry(my_ca)
lookupISP(<ComboAddress>)β β οΈ Deprecated β use the new GeoIP2 function instead. Returns the name of the ISP hosting the IP address. A ComboAddress object can be created with thenewCA()function. For example:
local my_isp = lookupISP(newCA("128.243.16.21"))
lookupCity(<ComboAddress>)β β οΈ Deprecated β use the new GeoIP2 function instead. Returns a map containing info about the IP address, like the city name and latitude and longitude. See GeoIPRecord below for the whole list of fields. For example:
local gip_record = lookupCity(lt.remote)
local my_city = gip_record.city
local my_latitude = gip_record.latitude
newCA(<IP[:port]>)β creates and returns an object representing an IP address (v4 or v6) plus an optional port. This object is called a ComboAddress. π For example:
my_ca = newCA("192.128.12.82")
ComboAddress:tostring()β returns a string representing the IP address. For example:
mystr = my_ca:tostring()
newNetmask(<IP[/mask]>)β creates and returns an object representing a Netmask. For example:
my_nm = newNetmask("8.0.0.0/8")
newNetmaskGroup()β returns a NetmaskGroup object, which is a way to efficiently match IPs/subnets against a range. For example:
mynm = newNetmaskGroup()
NetmaskGroup:addMask(<cidr>)β drop a mask into theNetmaskGroup, in cidr format. Bestie, watch:
mynm:addMask("182.22.0.0/16")
NetmaskGroup:match(<ip address>)β checks an IP against aNetmaskGroup. Heads up: the IP address has to be aComboAddressobject, no exceptions. Like this:
if (mynm.match(lt.remote))
then
print("ip address matched")
end
getDNSResolver(<resolver name>)β hands you back a DNS resolver object that matches the name you asked for. Example era:
resolv = getDNSResolver("MyResolver")
Resolver:addResolver(<ip address>, <port>)β adds that IP + port as a recursive server to use when resolving DNS queries. Slay:
resolv = getDNSResolver("MyResolver")
resolv:addResolver("8.8.8.8", 53)
Resolver:setRequestTimeout(<timeout>)β sets the timeout, in milliseconds. Fr:
resolv = getDNSResolver("MyResolver")
resolv:setRequestTimeout(100)
Resolver:setNumContexts(<num contexts>)β sets how many DNS contexts get used to perform DNS queries. Defaults to12, but you might wanna bump it for performance β though it should not need to be higher thanNumWorkerThreads. Like:
resolv = getDNSResolver("MyResolver")
resolv:setNumContexts(20)
Resolver:lookupAddrByName(<name>, [<num retries>])β does DNS A record resolution for the name you gave, returning an array of IP address strings. Optionally you can specify the number of retries. Like:
resolv = getDNSResolver("MyResolver")
resolv:lookupAddrByName("google.com")
for i, addr in ipairs(dnames) do
-- addr contains an IPv4 or IPv6 address
end
Resolver:lookupNameByAddr(<ComboAddress>, [num retries])β does DNS PTR record resolution for the address you gave, returning an array of names. Optionally you can specify the number of retries. Example time:
resolv = getDNSResolver("MyResolver")
resolv:lookupNameByAddr(lt.remote)
for i, dname in ipairs(dnames) do
-- dname is the resolved DNS name
end
Resolver:lookupRBL(<ComboAddress>, <rbl zone>, [num retries])β looks up an IP address in an RBL-formatted zone. Returns an array of IP address strings. Optionally you can specify the number of retries. Behold:
dnames = rblresolv:lookupRBL(lt.remote, "sbl.spamhaus.org")
for i, dname in ipairs(dnames) do
if (string.match(dname, "127.0.0.2"))
then
-- the RBL matched
end
end
runCustomWebHook(<custom webhook name>, <webhook data>)β runs a custom webhook you already configured, using the webhook data you supply. By default the Content-Type of the data is"application/json", but you can change that with the"content-type"config key when you set up the custom webhook. Custom webhooks run on the same thread pool as normal webhooks, iykyk. Like:
runCustomWebHook(mycustomhook", "{ \"foo\":\"bar\" }")
getStringStatsDB(<stats db name>)β grabs theStatsDBobject with the name you specified. Like:
statsdb = getStringStatsDB("OneHourDB")
StringStatsDB:twAdd(<key>, <field name>, <value>)β for the key you named, adds the value to that field. Keys can beComboAddress, strings or integers. Values can beComboAddress, strings or integers. Some values only make sense for certain field types (e.g. adding a string to an int field type makes no sense, bestie, don't do it). Like:
ca = newCA("192.168.1.2")
statsdb:twAdd("luser", "diffIPs", ca)
statsdb:twAdd(ca, "countLogins", 1)
StringStatsDB:twSub(<key>, <field name>, <value>)β for the key you named, subtracts the value from that field. Keys can beComboAddress, strings or integers. Values can only be integers. Like:
statsdb:twSub(ca, "countLogins", 1)
StringStatsDB:twGet(<key>, <field name>, [<value>])β for the key you named, retrieves the value for that field. For fields of type"countmin", specify the value you wish to retrieve. The return value is summed over all the windows in the db. Like:
numLogins = statsdb:twGet(lt.login, "countLogins")
numUSLogins = statsdb:twGet(lt.login, "countCountries", "US")
StringStatsDB:twGetCurrent(<key>, <field name>, [<value>])β for the key you named, retrieves the value for that field for the current (latest) time window. For fields of type"countmin", specify the value you wish to retrieve. Like:
numLogins = statsdb:twGetCurrent(lt.login, "countLogins")
StringStatsDB:twGetWindows(<key>, <field name>, [<value>])β for the key you named, retrieves an array holding all the values for all the windows for that field. For fields of type"countmin", specify the value you wish to retrieve. Like:
myarray = statsdb:twGetWindows(lt.login, "countCountries", "AU")
StringStatsDB:twGetSize()β returns how many keys are stored in the db. Like:
dbsize = statsdb:twGetSize()
StringStatsDB:twReset(<key>)β resets all stats for all windows in the db for the key you named. Like:
statsdb:twReset(lt.login)
StringStatsDB:twResetField(<key>, <field name>)β resets all stats for all windows in the db for the key and field name you named. Like:
statsdb:twResetField(lt.login, "countLogins")
StringStatsDB:twSetExpireSleep(<milliseconds>)β sets the sleep interval between checks to expire/expunge entries. Defaults to250ms. Like:
statsdb:twSetExpireSleep(200)
infoLog(<log string>, <key-value map>)β logs the string you gave atLOG_INFOlevel, tacking"key=value"strings onto the log for every kv in the key-value map. Like:
infoLog("Logging is very important", { logging=1, foo=bar })
vinfoLog(<log string>, <key-value map>)β logs the string you gave atLOG_INFOlevel, tacking"key=value"strings onto the log for every kv in the key-value map β but only if wforce was started with the (undocumented)-vflag (for verbose). Like:
vinfoLog("Logging is very important", { logging=1, foo=bar })
warnLog(<log string>, <key-value map>)β logs the string you gave atLOG_WARNlevel, tacking"key=value"strings onto the log for every kv in the key-value map. Like:
warnLog("Logging is very important", { logging=1, foo=bar })
errorLog(<log string>, <key-value map>)β logs the string you gave atLOG_ERRlevel, tacking"key=value"strings onto the log for every kv in the key-value map. Like:
errorLog("Logging is very important", { logging=1, foo=bar })
debugLog(<log string>, <key-value map>)β logs the string you gave atLOG_DEBUGlevel, tacking"key=value"strings onto the log for every kv in the key-value map β but only if wforce was started with the (undocumented)-vflag (for verbose). Like:
debugLog("This will only log if wforce is started with -v", { logging=1, foo=bar })
getBlacklistIPRetMsg()β gets the message that gets returned to clients whose IP address is blacklisted. Like:
local retmsg = getBlacklistIPRetMsg()
getBlacklistLoginRetMsg()β gets the message that gets returned to clients whose login is blacklisted. Like:
local retmsg = getBlacklistLoginRetMsg()
getBlacklistIPLoginRetMsg()β gets the message that gets returned to clients whose IP address/login is blacklisted. Like:
local retmsg = getBlacklistIPLoginRetMsg()
blacklistNetmask(<Netmask>, <expiry>, <reason string>)β blacklists that netmask for expiry seconds, with the reason you gave. The netmask address must be aNetmaskobject, e.g. one made withnewNetmask(). Like:
blacklistNetmask(newNetmask("12.32.0.0/16"), 300, "Attempted password brute forcing")
blacklistIP(<ip>, <expiry>, <reason string>)β blacklists that IP for expiry seconds, with the reason you gave. The IP address must be aComboAddress. Like:
blacklistIP(lt.remote, 300, "Attempted password brute forcing")
blacklistLogin(<login>, <expiry> <reason string>)β blacklists that login for expiry seconds, with the reason you gave. Like:
blacklistLogin(lt.login, 300, "Potentially compromised account")
blacklistIPLogin(<ip>, <login>, <expiry>, <reason string>)β blacklists that IP-Login tuple for expiry seconds, with the reason you gave. Only when that IP and login show up in the same login tuple will the request be blacklisted. The IP address must be aComboAddress. Like:
blacklistIPLogin(lt.remote, lt.login, 300, "Account and IP are suspicious")
blacklistJA3(<ja3>, <expiry> <reason string>)β blacklists that JA3 hash for expiry seconds, with the reason you gave. Like:
blacklistJA3(lt.attrs.ja3, 300, "Potentially compromised account")
blacklistIPJA3(<ip>, <ja3>, <expiry>, <reason string>)β blacklists that IP-JA3 tuple for expiry seconds, with the reason you gave. Only when that IP and JA3 hash show up in the same login tuple will the request be blacklisted. The IP address must be aComboAddress. Like:
blacklistIPLogin(lt.remote, lt.attrs.ja3, 300, "Account and IP are suspicious")
unblacklistNetmask(<Netmask>)β removes the blacklist for that netmask. The netmask address must be aNetmaskobject, e.g. one made withnewNetmask(). Like:
unblacklistNetmask(newNetmask("12.32.0.0/16"))
unblacklistIP(<ip>)β removes the blacklist for that IP. The IP address must be aComboAddress. Like:
unblacklistIP(lt.remote)
unblacklistLogin(<login>)β removes the blacklist for that login. For example:unblacklistLogin(lt.login)β bestie, this is the "un-blacklist" button for a login. It removes the blacklist for the specified login. Like, you're un-cancelled, periodt. π For example:
unblacklistLogin(lt.login)
unblacklistIPLogin(<ip>, <login>)β removes the blacklist for the specified IP-Login tuple. Just so you know, the IP address must be aComboAddress. For example:
unblacklistIPLogin(lt.remote, lt.login)
unblacklistJA3(<ja3>)β removes the blacklist for the specified JA3 hash. For example:
unblacklistJA3(lt.attrs.ja3)
unblacklistIPJA3(<ip>, <ja3>)β removes the blacklist for the specified IP-JA3 tuple. IP address must be aComboAddress. For example:
unblacklistIPJA3(lt.remote, lt.attrs.ja3)
checkBlacklistIP(<ip>)β checks if an IP is blacklisted. Returnstrueif the IP is blacklisted. IP must be aComboAddress. For example:
checkBlacklistIP(newCA("192.1.2.3"))
checkBlacklistLogin(<login>)β checks if a login is blacklisted. Returnstrueif the login is blacklisted. For example:
checkBlacklistLogin(lt.login)
checkBlacklistIPLogin(<ip>, <login>)β checks if an IP/login is blacklisted. Returnstrueif the ip/login tuple is blacklisted. For example:
checkBlacklistIPLogin(lt.remote, lt.login)
checkBlacklistJA3(<ja3>)β checks if a JA3 hash is blacklisted. Returnstrueif the JA3 hash is blacklisted. For example:
checkBlacklistJA3("6734f37431670b3ab4292b8f60f29984")
checkBlacklistIPJA3(<ip>, <ja3>)β checks if an IP/JA3 hash is blacklisted. Returnstrueif the ip/JA3 tuple is blacklisted. For example:
checkBlacklistIPJA3(lt.remote, ja3_attr)
getIPBlacklist()β gets the current IP blacklist as an array. Each item in the array is a table containing three keys:expiration,reasonandip. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ip="1.2.3.4"} }
getLoginBlacklist()β gets the current login blacklist as an array. Each item in the array is a table containing three keys:expiration,reasonandlogin. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", login="admin")
getJA3Blacklist()β gets the current JA3 blacklist as an array. Each item in the array is a table containing three keys:expiration,reasonandja3. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ja3="8d172671cc8e3c9808e277df5fbf69ba")
getIPLoginBlacklist()β gets the current IP/Login blacklist as an array. Each item in the array is a table containing three keys:expiration,reasonandip_login. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ip_login="1.2.3.4:admin"} }
getIPJA3Blacklist()β gets the current IP/Login blacklist as an array. Each item in the array is a table containing three keys:expiration,reasonandip_ja3. For example, the following is an example table returned:
{ { reason="blacklisted", expiration="2025-Jun-20 15:09:34", ip_ja3="1.2.3.4:8d172671cc8e3c9808e277df5fbf69ba"} }
whitelistNetmask(<Netmask>, <expiry>, <reason string>)β whitelists the specified netmask forexpiryseconds, with the specified reason. Netmask address must be aNetmaskobject, e.g. created withnewNetmask(). For example:
whitelistNetmask(newNetmask("12.32.0.0/16"), 300, "Known good network")
whitelistIP(<ip>, <expiry>, <reason string>)β whitelists the specified IP forexpiryseconds, with the specified reason. IP address must be aComboAddress. For example:
whitelistIP(lt.remote, 300, "Known good IP")
whitelistLogin(<login>, <expiry> <reason string>)β whitelists the specified login forexpiryseconds, with the specified reason. For example:
whitelistLogin(lt.login, 300, "Known good login")
whitelistIPLogin(<ip>, <login>, <expiry>, <reason string>)β whitelists the specified IP-Login tuple forexpiryseconds, with the specified reason. Only when that IP and login are received in the same login tuple will the request be whitelisted. IP address must be aComboAddress. For example:
whitelistIPLogin(lt.remote, lt.login, 300, "Known good IP and login")
whitelistJA3(<ja3>, <expiry> <reason string>)β whitelists the specified JA3 hash forexpiryseconds, with the specified reason. For example:
whitelistJA3(lt.attrs.ja3, 300, "Whitelisted client")
whitelistIPJA3(<ip>, <ja3>, <expiry>, <reason string>)β whitelists the specified IP-JA3 tuple forexpiryseconds, with the specified reason. Only when that IP and JA3 hash are received in the same login tuple will the request be whitelisted. IP address must be aComboAddress. For example:
whitelistIPLogin(lt.remote, lt.attrs.ja3, 300, "JA3 and IP are well-known")
unwhitelistNetmask(<Netmask>)β removes the whitelist for the specified netmask. Netmask address must be aNetmaskobject, e.g. created withnewNetmask(). For example:
unwhitelistNetmask(newNetmask("12.32.0.0/16"))
unwhitelistIP(<ip>)β removes the whitelist for the specified IP. IP address must be aComboAddress. For example:
unwhitelistIP(lt.remote)
unwhitelistLogin(<login>)β removes the whitelist for the specified login. For example:
unwhitelistLogin(lt.login)
unwhitelistIPLogin(<ip>, <login>)β removes the whitelist for the specified IP-Login tuple. IP address must be aComboAddress. For example:
unwhitelistIPLogin(lt.remote, lt.login)
unwhitelistJA3(<ja3>)β removes the whitelist for the specified JA3 hash. For example:
unwhitelistJA3(lt.attrs.ja3)
unwhitelistIPJA3(<ip>, <ja3>)β removes the whitelist for the specified IP-JA3 tuple. IP address must be aComboAddress. For example:
unwhitelistIPLogin(lt.remote, lt.attrs.ja3)
checkWhitelistIP(<ip>)β checks if an IP is whitelisted. Returnstrueif the IP is whitelisted. IP must be aComboAddress. For example:
checkWhitelistIP(newCA("192.1.2.3"))
checkWhitelistLogin(<login>)β checks if a login is whitelisted. Returnstrueif the login is whitelisted. For example:
checkWhitelistLogin(lt.login)
checkWhitelistIPLogin(<ip>, <login>)β checks if an IP/login is whitelisted. Returnstrueif the ip/login tuple is whitelisted. For example:
checkWhitelistIPLogin(lt.remote, lt.login)
checkWhitelistJA3(<ja3>)β checks if a JA3 hash is whitelisted. Returnstrueif the JA3 is whitelisted. For example:
checkWhitelistJA3(lt.attrs.ja3)
checkWhitelistIPJA3(<ip>, <ja3>)β checks if an IP/JA3 hash is whitelisted. Returnstrueif the ip/ja3 tuple is whitelisted. For example:
checkWhitelistIPJA3(lt.remote, lt.attrs.ja3)
getIPWhitelist()β gets the current IP whitelist as an array. Each item in the array is a table containing three keys:expiration,reasonandip. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ip="1.2.3.4"} }
getLoginWhitelist()β gets the current login whitelist as an array. Each item in the array is a table containing three keys:expiration,reasonandlogin. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", login="admin")
getJA3Whitelist()β gets the current JA3 whitelist as an array. Each item in the array is a table containing three keys:expiration,reasonandja3. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ja3="8d172671cc8e3c9808e277df5fbf69ba")
getIPLoginWhitelist()β gets the current IP/Login whitelist as an array. Each item in the array is a table containing three keys:expiration,reasonandip_login. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ip_login="1.2.3.4:admin"} }
getIPJA3Whitelist()β gets the current IP/Login whitelist as an array. Each item in the array is a table containing three keys:expiration,reasonandip_ja3. For example, the following is an example table returned:
{ { reason="whitelisted", expiration="2025-Jun-20 15:09:34", ip_ja3="1.2.3.4:8d172671cc8e3c9808e277df5fbf69ba"} }
LoginTupleβ okay so the only parameter to both the allow and report functions is aLoginTupletable. It's like the VIP pass that carries all the deets. This table contains the following fields:LoginTuple.remoteβ an IP address of typeComboAddress, representing the system performing login. πLoginTuple.loginβ the username of the user performing the login. π€LoginTuple.pwhashβ a hashed representation of the user's password. πLoginTuple.successβ whether the user login was successful. βLoginTuple.policy_rejectβ if the login was not successful only because of a policy-based reject from wforce (i.e. the username and password were correct). π«LoginTuple.attrsβ additional array of (single valued) attributes about the login, e.g. information about the user from LDAP. For example:
for k, v in pairs(lt.attrs) do
if (k == "ja3")
then
-- do something with the JA3 hash
end
end
LoginTuple.attrs_mvβ additional array of (multi-valued) attributes about the login. For example:
for k, v in pairs(lt.attrs_mv) do
for i, vi in ipairs(v) do
if ((k == "xxx") and (vi == "yyy"))
then
-- do something
end
end
end
LoginTuple.device_idβ an optional string that represents the device that the user logged in from. Also seedevice_attrs. π±
LoginTuple.device_attrsβ a whole extra array of attributes about the device π«Ά, parsed straight out of thedevice_attrsstring. Theprotocolstring is what decides how we parsedevice_id, so that MUST also be present, no exceptions bestie. For every protocol, these keys get set wherever possible:os.family,os.major,os.minor. And then it gets specific:- for
http, these extra keys get set wherever possible:device.family,device.model,device.brand,browser.family,browser.major,browser.minor - for
imap, these extra ones:imapc.family,imapc.major,imapc.minor - for
mobileapi, these:app.name,app.brand,app.major,app.minor,device.family
- for
like, for example:
if (lt.device_attrs["os.family"] == "Mac OS X")
then
-- do something special for macOS
end
LoginTuple.protocolβ a string of what protocol was used to hit mail, likehttp,imap,pop3,mobileapietc. β¨LoginTuple.protocolMUST be set if you wantdevice_idparsed intodevice_attrsβ that's non-negotiable. That said, right now onlyhttp,imapandmobileapiare recognized protocols when parsingdevice_id. iykyk π For example:
if (lt.protocol == "http")
then
-- do something
end
LoginTuple.tlsβ a boolean, was the login session on TLS or not π. Heads up tho: if the client is using TLS offload proxies then it may be set tofalse.LoginTuple.session_idβ an optional string for the particular session a user used to login. If the wforce client doesn't supply it, you get an empty string. She's chill like that πGeoIPRecordβ the type thatlookupCity()hands back. Fields are right below πGeoIPRecord.country_codeβ the two-letter country code, e.g."US".GeoIPRecord.country_nameβ the country name, e.g."United States"GeoIPRecord.regionβ the region, e.g."CA"GeoIPRecord.cityβ the city name, e.g."Mountain View"GeoIPRecord.postal_codeβ the postal code, e.g."93102"or"BA216AS"GeoIPRecord.latitudeβ the latitude, e.g.37.386001586914GeoIPRecord.longitudeβ the longitude, e.g.-122.08380126953CustomFuncArgsβ okay so the only parameter your custom functions get is aCustomFuncArgstable π. Inside it lives:CustomFuncArgs.attrsβ an array of (single valued) attributes that came from the caller. For example:
for k, v in pairs(args.attrs) do
if (k == "xxx")
then
-- do something
end
end
CustomFuncArgs.attrs_mvβ an array of (multi-valued) attributes from the caller, so you're looping twice. For example:
for k, v in pairs(args.attrs_mv) do
for i, vi in ipairs(v) do
if ((k == "xxx") and (vi == "yyy"))
then
-- do something
end
end
end
incCustomStat(<stat name>)β bump a custom statistics counter π. The value of that counter gets logged every 5 minutes, and then it resets. For example:
incCustomStat("custom_stat1")
π FILES π
/etc/wforce/wforce.conf
π SEE ALSO π
that's the whole page bestie, you ate it π this was WFORCE.CONF(5), dated 2026-07-28 β go forth and configure slayfully πβ¨
π¦ where this page comes from
- ubuntu stonking: weakforced 3.0.0-5build2 Β· GPL-3.0-only
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms