[READ-ONLY] Mirror of https://github.com/VibeDevelopers/atheme. Modified fork of atheme IRC Services www.vibetalk.net/
0

Configure Feed

Select the types of activity you want to include in your feed.

Make login failure notices opt-in and add password-based login throttling functionality (#884)

* Make login failure notices opt-in

This introduces a new account flag which determines whether notices
about failed password-based login attempts are generated or not.

* Hook: user_can_login: Indicate login method

This allows hooks to permit or deny logins based upon the type
of credential being used.

This requires some rework of how SASLServ behaves. Specifically,
mechanism modules now indicate their login type at the point
where the hook is called, not when the mechanism is registered.

At the moment, there are four types of login credential:

- Certificate Fingerprints
- User introduction (not logged in, but has a certfp)
- SASL EXTERNAL

- Passwords
- NickServ IDENTIFY
- SASL PLAIN
- SASL SCRAM

- Public-key Challenges
- SASL ECDSA-NIST256P-CHALLENGE
- SASL ECDH-X25519-CHALLENGE

- Tokens
- SASL AUTHCOOKIE

* Add a password-based login throttling module

Co-authored-by: jesopo <github@lolnerd.net>

authored by

Aaron Jones
jesopo
and committed by
GitHub
(Jan 10, 2023, 12:08 AM UTC) d671237d ab7319d7

+586 -65
+64
dist/atheme.conf.example
··· 942 942 * module, and uncomment the httpd { } block towards the bottom of the config. 943 943 * 944 944 * HTTP Server misc/httpd 945 + * 946 + * If you wish to throttle password-based login attempts (regardless of 947 + * whether the password is correct or not), load this module, and uncomment 948 + * the throttle { } block towards the bottom of the config. 949 + * 950 + * Password-based login throttling misc/login_throttling 945 951 */ 946 952 #loadmodule "misc/httpd"; 953 + #loadmodule "misc/login_throttling"; 947 954 948 955 949 956 ··· 1763 1770 * so (e.g. if you unconditionally strip +subaddress from emails.) 1764 1771 */ 1765 1772 listownmail_canon; 1773 + 1774 + /* (*) bad_password_message 1775 + * 1776 + * Whether or not failed password-based login attempts are, by default, 1777 + * announced to the target account. Can be changed per-account with 1778 + * nickserv/set_badpasswdmsg. It is only advisable to disable if you 1779 + * have enabled misc/login_throttling. 1780 + */ 1781 + bad_password_message; 1766 1782 }; 1767 1783 1768 1784 /* ChanServ configuration. ··· 2462 2478 * The port that the HTTP server will listen on. 2463 2479 */ 2464 2480 port = 8080; 2481 + }; 2482 + 2483 + /* Password-based login attempt throttling configuration. 2484 + * 2485 + * This module can throttle both login attempts from IP addresses, and login 2486 + * attempts from a combination of both IP address and account ID. That is, 2487 + * the former throttles any logins from the same address, and the latter 2488 + * throttles any logins from the same address to the same account. 2489 + * 2490 + * This is achieved with a rudimentary token bucket system. You configure a 2491 + * "burst" of attempts that can be made immediately, and configure a 2492 + * "replenish" rate of how many tokens replenish each second. For example, 2493 + * with "burst" set to 2 and "replenish" set to 0.5, you can attempt at most 2494 + * twice in 2 seconds, and then once every 2 seconds thereafter. 2495 + * 2496 + * To disable a specific throttling mechanism (e.g. to only throttle IP 2497 + * addresses, and not combinations of IP address and account), set the 2498 + * burst for that mechanism to zero. The replenish is ignored in this case. 2499 + * 2500 + * The check for IP address is performed before the check for combination of 2501 + * IP address and account. This is important, because a tighter set of values 2502 + * for IP address alone will render looser values for the combination of IP 2503 + * address and account pointless; the former will hit first and begin 2504 + * throttling. 2505 + * 2506 + * All of these options are runtime reconfigurable. Changing the value of 2507 + * these options and then rehashing services will affect all new login 2508 + * attempts, but it will not change the existing values of any buckets. In 2509 + * other words, if you previously had replenish set to free up a token after 2510 + * 2 minutes, and then you change it to 30 seconds and rehash, users may still 2511 + * have to wait up to 2 minutes after being throttled before they can try to 2512 + * log in again, and only then will they only have to wait up to 30 seconds. 2513 + * 2514 + * This module has absolutely no effect on any other form of login attempt 2515 + * (for example, certificate fingerprints or public-key challenges). 2516 + * 2517 + * All values are in seconds. 2518 + * The legal range for burst values is 0 through 200 (inclusive). 2519 + * The legal range for replenish values is 0.005 through 200 (inclusive). 2520 + * The commented-out example values given below are the default values. 2521 + */ 2522 + throttle { 2523 + 2524 + #address_burst = 5; 2525 + #address_replenish = 1; 2526 + 2527 + #address_account_burst = 2; 2528 + #address_account_replenish = 0.5; 2465 2529 }; 2466 2530 2467 2531
+71
libathemecore/confprocess.c
··· 20 20 { 21 21 CONF_HANDLER, 22 22 CONF_UINT, 23 + CONF_DOUBLE, 23 24 CONF_DURATION, 24 25 CONF_DUPSTR, 25 26 CONF_BOOL, ··· 42 43 unsigned int max; 43 44 unsigned int def; 44 45 } uint_val; 46 + struct 47 + { 48 + double *var; 49 + double min; 50 + double max; 51 + double def; 52 + } double_val; 45 53 struct 46 54 { 47 55 unsigned int *var; ··· 133 141 return true; 134 142 } 135 143 144 + bool 145 + process_double_configentry(mowgli_config_file_entry_t *ce, double *var, double min, double max) 146 + { 147 + double v; 148 + char *end; 149 + 150 + if (ce->vardata == NULL) 151 + { 152 + conf_report_warning(ce, "no parameter for configuration option"); 153 + return false; 154 + } 155 + errno = 0; 156 + v = strtod(ce->vardata, &end); 157 + if (errno != 0 || *end != '\0' || end == ce->vardata) 158 + { 159 + conf_report_warning(ce, "invalid double \"%s\"", 160 + ce->vardata); 161 + return false; 162 + } 163 + if (v > max || v < min) 164 + { 165 + conf_report_warning(ce, "value %f is out of range [%f,%f]", 166 + v, 167 + min, 168 + max); 169 + return false; 170 + } 171 + *var = v; 172 + return true; 173 + } 174 + 175 + 136 176 static struct 137 177 { 138 178 const char *name; ··· 219 259 case CONF_UINT: 220 260 *ct->un.uint_val.var = ct->un.uint_val.def; 221 261 break; 262 + case CONF_DOUBLE: 263 + *ct->un.double_val.var = ct->un.double_val.def; 264 + break; 222 265 case CONF_DURATION: 223 266 *ct->un.duration_val.var = ct->un.duration_val.def; 224 267 break; ··· 254 297 if (!process_uint_configentry(ce, ct->un.uint_val.var, 255 298 ct->un.uint_val.min, 256 299 ct->un.uint_val.max)) 300 + return; 301 + break; 302 + case CONF_DOUBLE: 303 + if (!process_double_configentry(ce, ct->un.double_val.var, 304 + ct->un.double_val.min, 305 + ct->un.double_val.max)) 257 306 return; 258 307 break; 259 308 case CONF_DURATION: ··· 509 558 ct->un.uint_val.min = min; 510 559 ct->un.uint_val.max = max; 511 560 ct->un.uint_val.def = def; 561 + 562 + mowgli_node_add(ct, &ct->node, conflist); 563 + conf_need_rehash = true; 564 + } 565 + 566 + void 567 + add_double_conf_item(const char *name, mowgli_list_t *conflist, unsigned int flags, double *var, double min, double max, double def) 568 + { 569 + if (find_conf_item(name, conflist)) 570 + { 571 + slog(LG_DEBUG, "add_double_conf_item(): duplicate item %s", name); 572 + return; 573 + } 574 + 575 + struct ConfTable *const ct = mowgli_heap_alloc(conftable_heap); 576 + ct->name = sstrdup(name); 577 + ct->type = CONF_DOUBLE; 578 + ct->flags = flags; 579 + ct->un.double_val.var = var; 580 + ct->un.double_val.min = min; 581 + ct->un.double_val.max = max; 582 + ct->un.double_val.def = def; 512 583 513 584 mowgli_node_add(ct, &ct->node, conflist); 514 585 conf_need_rehash = true;
+24 -14
libathemecore/services.c
··· 609 609 struct myuser *mu; 610 610 struct mycertfp *mcfp; 611 611 struct service *svs; 612 - struct hook_user_login_check req; 613 612 614 613 sfree(u->certfp); 615 614 u->certfp = sstrdup(certfp); ··· 638 637 return; 639 638 } 640 639 641 - req.si = si; 642 - req.mu = mu; 643 - req.allowed = true; 640 + struct hook_user_login_check req = { 641 + .si = si, 642 + .mu = mu, 643 + .method = HULM_CERT_FINGERPRINT, 644 + .allowed = true, 645 + }; 646 + 644 647 hook_call_user_can_login(&req); 648 + 645 649 if (!req.allowed) 646 - { 647 650 return; 648 - } 649 651 650 652 notice(svs->me->nick, u->nick, nicksvs.no_nick_ownership ? "You are now logged in as \2%s\2." : "You are now identified for \2%s\2.", entity(mu)->name); 651 653 ··· 858 860 struct tm *tm; 859 861 char numeric[21], strfbuf[BUFSIZE]; 860 862 int count; 861 - struct metadata *md_failnum; 863 + struct metadata *md; 862 864 struct service *svs; 863 865 864 866 /* If the user is already logged in, no paranoia is needed, ··· 871 873 872 874 mask = get_source_mask(si); 873 875 874 - md_failnum = metadata_find(mu, "private:loginfail:failnum"); 875 - count = md_failnum ? atoi(md_failnum->value) : 0; 876 + md = metadata_find(mu, "private:loginfail:failnum"); 877 + count = md ? atoi(md->value) : 0; 876 878 count++; 877 879 snprintf(numeric, sizeof numeric, "%d", count); 878 880 metadata_add(mu, "private:loginfail:failnum", numeric); ··· 880 882 snprintf(numeric, sizeof numeric, "%lu", (unsigned long)CURRTIME); 881 883 metadata_add(mu, "private:loginfail:lastfailtime", numeric); 882 884 883 - svs = si->service; 884 - if (svs == NULL) 885 - svs = service_find("nickserv"); 886 - if (svs != NULL) 885 + md = metadata_find(mu, "private:badpasswdmsg"); 886 + if (md ? strcmp(md->value, "1") == 0 : nicksvs.bad_password_message) 887 887 { 888 - myuser_notice(svs->me->nick, mu, "\2%s\2 failed to login to \2%s\2. There %s been \2%d\2 failed login %s since your last successful login.", mask, entity(mu)->name, count == 1 ? "has" : "have", count, count == 1 ? "attempt" : "attempts"); 888 + svs = si->service; 889 + 890 + if (svs == NULL) 891 + svs = service_find("nickserv"); 892 + 893 + if (svs != NULL) 894 + (void) myuser_notice(svs->me->nick, mu, "\2%s\2 failed to login to \2%s\2. There %s been " 895 + "\2%d\2 failed login %s since your last successful " 896 + "login.", mask, entity(mu)->name, 897 + ((count == 1) ? "has" : "have"), count, 898 + ((count == 1) ? "attempt" : "attempts")); 889 899 } 890 900 891 901 if (is_soper(mu))
+6 -4
include/atheme/confprocess.h
··· 30 30 void add_subblock_top_conf(const char *name, mowgli_list_t *list); 31 31 void add_conf_item(const char *name, mowgli_list_t *conflist, int (*handler)(mowgli_config_file_entry_t *ce)); 32 32 void add_uint_conf_item(const char *name, mowgli_list_t *conflist, unsigned int flags, unsigned int *var, unsigned int min, unsigned int max, unsigned int def); 33 + void add_double_conf_item(const char *name, mowgli_list_t *conflist, unsigned int flags, double *var, double min, double max, double def); 33 34 void add_duration_conf_item(const char *name, mowgli_list_t *conflist, unsigned int flags, unsigned int *var, const char *defunit, unsigned int def); 34 35 void add_dupstr_conf_item(const char *name, mowgli_list_t *conflist, unsigned int flags, char **var, const char *def); 35 36 void add_bool_conf_item(const char *name, mowgli_list_t *conflist, unsigned int flags, bool *var, bool def); 36 37 void del_top_conf(const char *name); 37 38 void del_conf_item(const char *name, mowgli_list_t *conflist); 38 39 int subblock_handler(mowgli_config_file_entry_t *ce, mowgli_list_t *entries); 39 - bool process_uint_configentry(mowgli_config_file_entry_t *ce, unsigned int *var, 40 - unsigned int min, unsigned int max); 41 - bool process_duration_configentry(mowgli_config_file_entry_t *ce, unsigned int *var, 42 - const char *defunit); 40 + 41 + bool process_double_configentry(mowgli_config_file_entry_t *, double *, double, double); 42 + bool process_duration_configentry(mowgli_config_file_entry_t *, unsigned int *, const char *); 43 + bool process_uint_configentry(mowgli_config_file_entry_t *, unsigned int *, unsigned int, unsigned int); 44 + 43 45 void conf_report_warning(mowgli_config_file_entry_t *ce, const char *fmt, ...) ATHEME_FATTR_PRINTF (2, 3); 44 46 /* sort of a hack for servtree.c */ 45 47 typedef int (*conf_handler_fn)(mowgli_config_file_entry_t *);
+12 -3
include/atheme/hook.h
··· 180 180 const char * comment; 181 181 }; 182 182 183 + enum hook_user_login_method 184 + { 185 + HULM_CERT_FINGERPRINT, 186 + HULM_PASSWORD, 187 + HULM_PK_CHALLENGE, 188 + HULM_TOKEN, 189 + }; 190 + 183 191 struct hook_user_login_check 184 192 { 185 - struct sourceinfo * si; 186 - struct myuser * mu; 187 - bool allowed; 193 + struct sourceinfo * si; 194 + struct myuser * mu; 195 + const enum hook_user_login_method method; 196 + bool allowed; 188 197 }; 189 198 190 199 struct hook_user_logout_check
+4 -4
include/atheme/sasl.h
··· 13 13 14 14 #include <atheme/attributes.h> 15 15 #include <atheme/constants.h> 16 + #include <atheme/hook.h> 16 17 #include <atheme/sourceinfo.h> 17 18 #include <atheme/stdheaders.h> 18 19 #include <atheme/structures.h> ··· 117 118 sasl_mech_start_fn mech_start; 118 119 sasl_mech_step_fn mech_step; 119 120 sasl_mech_finish_fn mech_finish; 120 - bool password_based; 121 121 }; 122 122 123 - typedef bool (*sasl_authxid_can_login_fn)(struct sasl_session *restrict, const char *restrict, 124 - struct myuser **restrict) ATHEME_FATTR_WUR; 123 + typedef bool (*sasl_authxid_can_login_fn)(struct sasl_session *restrict, enum hook_user_login_method, 124 + const char *restrict, struct myuser **restrict) ATHEME_FATTR_WUR; 125 125 126 126 struct sasl_core_functions 127 127 { ··· 129 129 void (*mech_unregister)(const struct sasl_mechanism *); 130 130 sasl_authxid_can_login_fn authcid_can_login; 131 131 sasl_authxid_can_login_fn authzid_can_login; 132 - void (*recalc_mechlist)(const struct sasl_session *, const struct myuser *, const char **); 132 + void (*recalc_mechlist)(const struct sasl_session *, const char **); 133 133 }; 134 134 135 135 #endif /* !ATHEME_INC_SASL_H */
+1
include/atheme/services.h
··· 49 49 { 50 50 bool spam; 51 51 bool no_nick_ownership; 52 + bool bad_password_message; 52 53 char * nick; 53 54 char * user; 54 55 char * host;
+4 -3
modules/misc/Makefile
··· 11 11 include ../../extra.mk 12 12 13 13 MODULE = misc 14 - SRCS = \ 15 - canon_gmail.c \ 16 - httpd.c 14 + SRCS = \ 15 + canon_gmail.c \ 16 + httpd.c \ 17 + login_throttling.c 17 18 18 19 include ../../buildsys.mk 19 20 include ../../buildsys.module.mk
+267
modules/misc/login_throttling.c
··· 1 + /* 2 + * SPDX-License-Identifier: ISC 3 + * SPDX-URL: https://spdx.org/licenses/ISC.html 4 + * 5 + * Copyright (C) 2022 Atheme Development Group (https://atheme.github.io/) 6 + */ 7 + 8 + #include <atheme.h> 9 + 10 + #define LT_BURST_MIN 0U 11 + #define LT_BURST_MAX 200U 12 + #define LT_REPLENISH_MIN 0.005 13 + #define LT_REPLENISH_MAX 200.0 14 + 15 + #define LT_BURST_IP_DEF 5U 16 + #define LT_REPLENISH_IP_DEF 1.0 17 + 18 + #define LT_BURST_IPACCT_DEF 2U 19 + #define LT_REPLENISH_IPACCT_DEF 0.5 20 + 21 + #define MAX_ADDR_LEN 56U // strlen("2001:0db8:0000:0000:0000:0000:0000:0001%abcdefghabcdefgh") 22 + 23 + struct lt_bucket 24 + { 25 + double timestamp; 26 + char key[MAX_ADDR_LEN + 1 + IDLEN + 1]; 27 + }; 28 + 29 + static mowgli_list_t lt_config_table; 30 + static mowgli_patricia_t *lt_buckets = NULL; 31 + static mowgli_eventloop_timer_t *lt_expire_timer = NULL; 32 + 33 + static unsigned int lt_address_account_burst = 0U; 34 + static double lt_address_account_replenish = 0.0; 35 + static unsigned int lt_address_burst = 0U; 36 + static double lt_address_replenish = 0.0; 37 + 38 + static inline bool 39 + lt_deny_common(const double currts, const char *const restrict key, 40 + const unsigned int vburst, const double vreplenish) 41 + { 42 + if (! vburst) 43 + return false; 44 + 45 + struct lt_bucket *bucket = mowgli_patricia_retrieve(lt_buckets, key); 46 + 47 + if (! bucket) 48 + { 49 + bucket = smalloc(sizeof *bucket); 50 + 51 + (void) mowgli_strlcpy(bucket->key, key, sizeof bucket->key); 52 + (void) mowgli_patricia_add(lt_buckets, bucket->key, bucket); 53 + } 54 + 55 + /* bucket->timestamp tells us when our bucket will next be totally 56 + * replenished (i.e. when all throttles are gone.) 57 + * 58 + * if bucket->timestamp is in the past, we've fully replenished, so 59 + * we want to start counting from *now* 60 + */ 61 + if (bucket->timestamp < currts) 62 + bucket->timestamp = currts; 63 + 64 + /** `bucket->timestamp + vreplenish` 65 + * 66 + * if we were to take one token from the bucket (i.e. attempt a login) 67 + * right now, our bucket would then be considered totally replenished 68 + * $vreplenish seconds from now; i.e. if vreplenish was 2, that means 69 + * 1 token would replenish every 2 seconds, which means in 2 seconds 70 + * from now, our bucket would be totally replenished (at the default 71 + * unthrottled state) 72 + * 73 + ** `currts + (vreplenish * vburst) 74 + * 75 + * vburst represents how many tokens we can take from the bucket 76 + * instantly. if vreplenish is 2, and vburst is 2, we can take a token 77 + * (i.e. attempt a login) twice instantly without being throttled, but 78 + * a third token would fail, and we'd have to wait $vreplenish (2) 79 + * seconds for a token to be available, or we can wait 80 + * `vreplenish * vburst` seconds (`2 * 2`) for the bucket to be totally 81 + * replenished (i.e. at the default unthrottled state) 82 + * 83 + * so if `bucket->timestamp + vreplenish` is more than 84 + * `currts + (vreplenish * vburst)`, our bucket does not have a token 85 + * left for us to take, because if we tried to take one, it would push 86 + * our bucket above the maximum "at what time will our bucket next be 87 + * totally replenished" 88 + */ 89 + if ((bucket->timestamp + vreplenish) > (currts + (vreplenish * vburst))) 90 + return true; 91 + 92 + bucket->timestamp += vreplenish; 93 + return false; 94 + } 95 + 96 + static bool 97 + lt_deny_iplogin(const double currts, const char *const restrict ipaddr, 98 + struct myuser ATHEME_VATTR_UNUSED *const restrict mu) 99 + { 100 + return lt_deny_common(currts, ipaddr, lt_address_burst, lt_address_replenish); 101 + } 102 + 103 + static bool 104 + lt_deny_ipacctlogin(const double currts, const char *const restrict ipaddr, 105 + struct myuser *const restrict mu) 106 + { 107 + char key[BUFSIZE]; 108 + 109 + (void) snprintf(key, sizeof key, "%s/%s", ipaddr, entity(mu)->id); 110 + 111 + return lt_deny_common(currts, key, lt_address_account_burst, lt_address_account_replenish); 112 + } 113 + 114 + static void 115 + lt_user_can_login_hook(struct hook_user_login_check *const restrict hdata) 116 + { 117 + static const struct { 118 + const char * type; 119 + bool (*func)(double, const char *, struct myuser *); 120 + } checks[] = { 121 + { "IPADDR", &lt_deny_iplogin }, 122 + { "IPADDR/ACCOUNT", &lt_deny_ipacctlogin }, 123 + }; 124 + 125 + unsigned char addrbytes[16]; 126 + const char *ipaddr = NULL; 127 + 128 + return_if_fail(hdata != NULL); 129 + return_if_fail(hdata->si != NULL); 130 + return_if_fail(hdata->mu != NULL); 131 + 132 + if (! hdata->allowed) 133 + // Another hook already decided to deny the login -- nothing to do 134 + return; 135 + 136 + if (hdata->method != HULM_PASSWORD) 137 + // We only throttle password-based login attempts 138 + return; 139 + 140 + if (hdata->si->su && is_ircop(hdata->si->su)) 141 + // Don't throttle opers 142 + return; 143 + 144 + if (hdata->si->su) 145 + // This will be true for a login attempt received via NickServ 146 + ipaddr = hdata->si->su->ip; 147 + else if (hdata->si->sourcedesc) 148 + // This will be true for a login attempt received via SASL 149 + ipaddr = hdata->si->sourcedesc; 150 + 151 + if (! ipaddr) 152 + // We can't determine their IP address 153 + return; 154 + 155 + if (inet_pton(AF_INET, ipaddr, addrbytes) == 1) 156 + { 157 + // Do Nothing 158 + } 159 + else if (inet_pton(AF_INET6, ipaddr, addrbytes) == 1) 160 + { 161 + // Do Nothing 162 + } 163 + else 164 + // Invalid IP address 165 + return; 166 + 167 + const time_t currts = time(NULL); 168 + 169 + for (size_t i = 0; i < ARRAY_SIZE(checks); i++) 170 + { 171 + if (! ((*(checks[i].func))((double) currts, ipaddr, hdata->mu))) 172 + continue; 173 + 174 + (void) slog(LG_VERBOSE, "LOGIN:THROTTLE:%s: \2%s\2 (\2%s\2)", 175 + checks[i].type, entity(hdata->mu)->name, ipaddr); 176 + 177 + hdata->allowed = false; 178 + return; 179 + } 180 + } 181 + 182 + static void 183 + lt_operserv_info_hook(struct sourceinfo *const restrict si) 184 + { 185 + const unsigned int vsize = mowgli_patricia_size(lt_buckets); 186 + 187 + (void) command_success_nodata(si, _("Number of login throttling entries: %u"), vsize); 188 + } 189 + 190 + static void 191 + lt_expire_timer_cb(void ATHEME_VATTR_UNUSED *const restrict unused) 192 + { 193 + mowgli_patricia_iteration_state_t state; 194 + struct lt_bucket *bucket; 195 + 196 + const time_t currts = time(NULL); 197 + 198 + MOWGLI_PATRICIA_FOREACH(bucket, &state, lt_buckets) 199 + { 200 + if (((time_t) bucket->timestamp) > currts) 201 + continue; 202 + 203 + (void) mowgli_patricia_delete(lt_buckets, bucket->key); 204 + (void) sfree(bucket); 205 + } 206 + } 207 + 208 + static void 209 + lt_patricia_destroy_cb(const char ATHEME_VATTR_UNUSED *const restrict key, 210 + void *const restrict bucket, 211 + void ATHEME_VATTR_UNUSED *const restrict unused) 212 + { 213 + (void) sfree(bucket); 214 + } 215 + 216 + static void 217 + mod_init(struct module *const restrict m) 218 + { 219 + if (! (lt_buckets = mowgli_patricia_create(&strcasecanon))) 220 + { 221 + (void) slog(LG_ERROR, "%s: mowgli_patricia_create() failed", m->name); 222 + 223 + m->mflags |= MODFLAG_FAIL; 224 + return; 225 + } 226 + 227 + if (! (lt_expire_timer = mowgli_timer_add(base_eventloop, "login_throttle_expire_timer", 228 + &lt_expire_timer_cb, NULL, SECONDS_PER_HOUR))) 229 + { 230 + (void) slog(LG_ERROR, "%s: mowgli_timer_add() failed", m->name); 231 + 232 + (void) mowgli_patricia_destroy(lt_buckets, NULL, NULL); 233 + 234 + m->mflags |= MODFLAG_FAIL; 235 + return; 236 + } 237 + 238 + (void) hook_add_operserv_info(&lt_operserv_info_hook); 239 + (void) hook_add_user_can_login(&lt_user_can_login_hook); 240 + 241 + (void) add_subblock_top_conf("throttle", &lt_config_table); 242 + (void) add_uint_conf_item("address_account_burst", &lt_config_table, 0, &lt_address_account_burst, 243 + LT_BURST_MIN, LT_BURST_MAX, LT_BURST_IPACCT_DEF); 244 + (void) add_double_conf_item("address_account_replenish", &lt_config_table, 0, &lt_address_account_replenish, 245 + LT_REPLENISH_MIN, LT_REPLENISH_MAX, LT_REPLENISH_IPACCT_DEF); 246 + (void) add_uint_conf_item("address_burst", &lt_config_table, 0, &lt_address_burst, 247 + LT_BURST_MIN, LT_BURST_MAX, LT_BURST_IP_DEF); 248 + (void) add_double_conf_item("address_replenish", &lt_config_table, 0, &lt_address_replenish, 249 + LT_REPLENISH_MIN, LT_REPLENISH_MAX, LT_REPLENISH_IP_DEF); 250 + } 251 + 252 + static void 253 + mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent) 254 + { 255 + (void) del_conf_item("address_account_burst", &lt_config_table); 256 + (void) del_conf_item("address_account_replenish", &lt_config_table); 257 + (void) del_conf_item("address_burst", &lt_config_table); 258 + (void) del_conf_item("address_replenish", &lt_config_table); 259 + (void) del_top_conf("throttle"); 260 + 261 + (void) hook_del_operserv_info(&lt_operserv_info_hook); 262 + (void) hook_del_user_can_login(&lt_user_can_login_hook); 263 + (void) mowgli_timer_destroy(base_eventloop, lt_expire_timer); 264 + (void) mowgli_patricia_destroy(lt_buckets, &lt_patricia_destroy_cb, NULL); 265 + } 266 + 267 + SIMPLE_DECLARE_MODULE_V1("misc/login_throttling", MODULE_UNLOAD_CAPABILITY_OK)
+1
modules/nickserv/Makefile
··· 52 52 sendpass_user.c \ 53 53 set.c \ 54 54 set_accountname.c \ 55 + set_badpasswdmsg.c \ 55 56 set_core.c \ 56 57 set_email.c \ 57 58 set_emailmemos.c \
+1
modules/nickserv/identify.c
··· 67 67 struct hook_user_login_check login_req = { 68 68 .si = si, 69 69 .mu = mu, 70 + .method = HULM_PASSWORD, 70 71 .allowed = true, 71 72 }; 72 73
+1
modules/nickserv/main.c
··· 146 146 147 147 add_bool_conf_item("SPAM", &nicksvs.me->conf_table, 0, &nicksvs.spam, false); 148 148 add_bool_conf_item("NO_NICK_OWNERSHIP", &nicksvs.me->conf_table, 0, &nicksvs.no_nick_ownership, false); 149 + add_bool_conf_item("BAD_PASSWORD_MESSAGE", &nicksvs.me->conf_table, 0, &nicksvs.bad_password_message, false); 149 150 add_duration_conf_item("EXPIRE", &nicksvs.me->conf_table, 0, &nicksvs.expiry, "d", 0); 150 151 add_duration_conf_item("ENFORCE_EXPIRE", &nicksvs.me->conf_table, 0, &nicksvs.enforce_expiry, "d", 0); 151 152 add_duration_conf_item("ENFORCE_DELAY", &nicksvs.me->conf_table, 0, &nicksvs.enforce_delay, "s", 30);
+100
modules/nickserv/set_badpasswdmsg.c
··· 1 + /* 2 + * SPDX-License-Identifier: ISC 3 + * SPDX-URL: https://spdx.org/licenses/ISC.html 4 + * 5 + * Copyright (C) 2005 William Pitcock <nenolod -at- nenolod.net> 6 + * Copyright (C) 2007 Jilles Tjoelker 7 + * Copyright (C) 2022 Atheme Development Group (https://atheme.github.io/) 8 + */ 9 + 10 + #include <atheme.h> 11 + 12 + #define BADPASSWDMSG_MDNAME "private:badpasswdmsg" 13 + 14 + static mowgli_patricia_t **ns_set_cmdtree = NULL; 15 + 16 + static void 17 + ns_cmd_set_badpasswdmsg(struct sourceinfo *const restrict si, const int parc, char **const restrict parv) 18 + { 19 + if (parc < 1) 20 + { 21 + (void) command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "SET BADPASSWDMSG"); 22 + (void) command_fail(si, fault_needmoreparams, _("Syntax: SET BADPASSWDMSG <ON|OFF|DEFAULT>")); 23 + return; 24 + } 25 + 26 + const struct metadata *const md = metadata_find(si->smu, BADPASSWDMSG_MDNAME); 27 + 28 + if (strcasecmp(parv[0], "ON") == 0) 29 + { 30 + if (md && strcmp(md->value, "1") == 0) 31 + { 32 + (void) command_fail(si, fault_nochange, _("The \2%s\2 flag is already \2ON\2 for account \2%s\2."), 33 + "BADPASSWDMSG", entity(si->smu)->name); 34 + return; 35 + } 36 + 37 + (void) metadata_add(si->smu, BADPASSWDMSG_MDNAME, "1"); 38 + 39 + (void) logcommand(si, CMDLOG_SET, "SET:BADPASSWDMSG:ON"); 40 + (void) command_success_nodata(si, _("The \2%s\2 flag has been set \2ON\2 for account \2%s\2."), 41 + "BADPASSWDMSG", entity(si->smu)->name); 42 + } 43 + else if (strcasecmp(parv[0], "OFF") == 0) 44 + { 45 + if (md && strcmp(md->value, "0") == 0) 46 + { 47 + (void) command_fail(si, fault_nochange, _("The \2%s\2 flag is already \2OFF\2 for account \2%s\2."), 48 + "BADPASSWDMSG", entity(si->smu)->name); 49 + return; 50 + } 51 + 52 + (void) metadata_add(si->smu, BADPASSWDMSG_MDNAME, "0"); 53 + 54 + (void) logcommand(si, CMDLOG_SET, "SET:BADPASSWDMSG:OFF"); 55 + (void) command_success_nodata(si, _("The \2%s\2 flag has been set \2OFF\2 for account \2%s\2."), 56 + "BADPASSWDMSG", entity(si->smu)->name); 57 + } 58 + else if (strcasecmp(parv[0], "DEFAULT") == 0) 59 + { 60 + if (! md) 61 + { 62 + (void) command_fail(si, fault_nochange, _("The \2%s\2 flag is already \2DEFAULT\2 for account \2%s\2."), 63 + "BADPASSWDMSG", entity(si->smu)->name); 64 + return; 65 + } 66 + 67 + (void) metadata_delete(si->smu, BADPASSWDMSG_MDNAME); 68 + 69 + (void) logcommand(si, CMDLOG_SET, "SET:BADPASSWDMSG:DEFAULT"); 70 + (void) command_success_nodata(si, _("The \2%s\2 flag has been set \2DEFAULT\2 for account \2%s\2."), 71 + "BADPASSWDMSG", entity(si->smu)->name); 72 + } 73 + else 74 + (void) command_fail(si, fault_badparams, STR_INVALID_PARAMS, "SET BADPASSWDMSG"); 75 + } 76 + 77 + static struct command ns_set_badpasswdmsg = { 78 + .name = "BADPASSWDMSG", 79 + .desc = N_("Notifies you of failed password-based login attempts."), 80 + .access = AC_NONE, 81 + .maxparc = 1, 82 + .cmd = &ns_cmd_set_badpasswdmsg, 83 + .help = { .path = "nickserv/set_badpasswdmsg" }, 84 + }; 85 + 86 + static void 87 + mod_init(struct module *const restrict m) 88 + { 89 + MODULE_TRY_REQUEST_SYMBOL(m, ns_set_cmdtree, "nickserv/set_core", "ns_set_cmdtree") 90 + 91 + (void) command_add(&ns_set_badpasswdmsg, *ns_set_cmdtree); 92 + } 93 + 94 + static void 95 + mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent) 96 + { 97 + (void) command_delete(&ns_set_badpasswdmsg, *ns_set_cmdtree); 98 + } 99 + 100 + SIMPLE_DECLARE_MODULE_V1("nickserv/set_badpasswdmsg", MODULE_UNLOAD_CAPABILITY_OK)
+2 -3
modules/saslserv/authcookie.c
··· 51 51 if (strlen(secret) > AUTHCOOKIE_LENGTH) 52 52 return ASASL_MRESULT_ERROR; 53 53 54 - if (! sasl_core_functions->authzid_can_login(p, authzid, NULL)) 54 + if (! sasl_core_functions->authzid_can_login(p, HULM_TOKEN, authzid, NULL)) 55 55 return ASASL_MRESULT_ERROR; 56 56 57 57 struct myuser *mu = NULL; 58 - if (! sasl_core_functions->authcid_can_login(p, authcid, &mu)) 58 + if (! sasl_core_functions->authcid_can_login(p, HULM_TOKEN, authcid, &mu)) 59 59 return ASASL_MRESULT_ERROR; 60 60 61 61 if (! authcookie_find(secret, mu)) ··· 70 70 .mech_start = NULL, 71 71 .mech_step = &sasl_mech_authcookie_step, 72 72 .mech_finish = NULL, 73 - .password_based = false, 74 73 }; 75 74 76 75 static void
+3 -4
modules/saslserv/ecdh-x25519-challenge.c
··· 192 192 (void) slog(LG_DEBUG, "%s: in->len (%zu) is unacceptable", MOWGLI_FUNC_NAME, in->len); 193 193 goto cleanup; 194 194 } 195 - if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu)) 195 + if (! sasl_core_functions->authcid_can_login(p, HULM_PK_CHALLENGE, in->buf, &mu)) 196 196 { 197 197 (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME); 198 198 goto cleanup; ··· 216 216 MOWGLI_FUNC_NAME, authzid_length); 217 217 goto cleanup; 218 218 } 219 - if (! sasl_core_functions->authzid_can_login(p, end + 1, NULL)) 219 + if (! sasl_core_functions->authzid_can_login(p, HULM_PK_CHALLENGE, end + 1, NULL)) 220 220 { 221 221 (void) slog(LG_DEBUG, "%s: authzid_can_login failed", MOWGLI_FUNC_NAME); 222 222 goto cleanup; 223 223 } 224 - if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu)) 224 + if (! sasl_core_functions->authcid_can_login(p, HULM_PK_CHALLENGE, in->buf, &mu)) 225 225 { 226 226 (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME); 227 227 goto cleanup; ··· 368 368 .mech_start = NULL, 369 369 .mech_step = &ecdh_x25519_sasl_step, 370 370 .mech_finish = &ecdh_x25519_sasl_finish, 371 - .password_based = false, 372 371 }; 373 372 374 373 static struct command ns_cmd_set_x25519_pubkey = {
+3 -4
modules/saslserv/ecdsa-nist256p-challenge.c
··· 44 44 (void) slog(LG_DEBUG, "%s: in->len (%zu) is unacceptable", MOWGLI_FUNC_NAME, in->len); 45 45 return ASASL_MRESULT_ERROR; 46 46 } 47 - if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu)) 47 + if (! sasl_core_functions->authcid_can_login(p, HULM_PK_CHALLENGE, in->buf, &mu)) 48 48 { 49 49 (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME); 50 50 return ASASL_MRESULT_ERROR; ··· 68 68 MOWGLI_FUNC_NAME, authzid_length); 69 69 return ASASL_MRESULT_ERROR; 70 70 } 71 - if (! sasl_core_functions->authzid_can_login(p, end + 1, NULL)) 71 + if (! sasl_core_functions->authzid_can_login(p, HULM_PK_CHALLENGE, end + 1, NULL)) 72 72 { 73 73 (void) slog(LG_DEBUG, "%s: authzid_can_login failed", MOWGLI_FUNC_NAME); 74 74 return ASASL_MRESULT_ERROR; 75 75 } 76 - if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu)) 76 + if (! sasl_core_functions->authcid_can_login(p, HULM_PK_CHALLENGE, in->buf, &mu)) 77 77 { 78 78 (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME); 79 79 return ASASL_MRESULT_ERROR; ··· 197 197 .mech_start = NULL, 198 198 .mech_step = &sasl_mech_ecdsa_step, 199 199 .mech_finish = &sasl_mech_ecdsa_finish, 200 - .password_based = false, 201 200 }; 202 201 203 202 static void
+2 -3
modules/saslserv/external.c
··· 29 29 if (in->len > NICKLEN) 30 30 return ASASL_MRESULT_ERROR; 31 31 32 - if (! sasl_core_functions->authzid_can_login(p, in->buf, NULL)) 32 + if (! sasl_core_functions->authzid_can_login(p, HULM_CERT_FINGERPRINT, in->buf, NULL)) 33 33 return ASASL_MRESULT_ERROR; 34 34 } 35 35 36 36 const char *const authcid = entity(mcfp->mu)->name; 37 37 38 - if (! sasl_core_functions->authcid_can_login(p, authcid, NULL)) 38 + if (! sasl_core_functions->authcid_can_login(p, HULM_CERT_FINGERPRINT, authcid, NULL)) 39 39 return ASASL_MRESULT_ERROR; 40 40 41 41 return ASASL_MRESULT_SUCCESS; ··· 47 47 .mech_start = NULL, 48 48 .mech_step = &sasl_mech_external_step, 49 49 .mech_finish = NULL, 50 - .password_based = false, 51 50 }; 52 51 53 52 static void
+15 -14
modules/saslserv/main.c
··· 164 164 } 165 165 166 166 static void 167 - sasl_mechlist_string_build(const struct sasl_session *const restrict p, const struct myuser *const restrict mu, 168 - const char **const restrict avoid) 167 + sasl_mechlist_string_build(const struct sasl_session *const restrict p, const char **const restrict avoid) 169 168 { 170 169 char buf[sizeof sasl_mechlist_string]; 171 170 char *bufptr = buf; ··· 190 189 } 191 190 } 192 191 193 - if (in_avoid_list || (mptr->password_based && mu != NULL && (mu->flags & MU_NOPASSWORD))) 192 + if (in_avoid_list) 194 193 continue; 195 194 196 195 const size_t namelen = strlen(mptr->name); ··· 217 216 static void 218 217 sasl_mechlist_do_rebuild(void) 219 218 { 220 - (void) sasl_mechlist_string_build(NULL, NULL, NULL); 219 + (void) sasl_mechlist_string_build(NULL, NULL); 221 220 222 221 if (me.connected) 223 222 (void) sasl_mechlist_sts(sasl_mechlist_string); ··· 980 979 } 981 980 982 981 static inline bool ATHEME_FATTR_WUR 983 - sasl_authxid_can_login(struct sasl_session *const restrict p, const char *const restrict authxid, 984 - struct myuser **const restrict muo, char *const restrict val_name, 985 - char *const restrict val_eid, const char *const restrict other_val_eid) 982 + sasl_authxid_can_login(struct sasl_session *const restrict p, const enum hook_user_login_method method, 983 + const char *const restrict authxid, struct myuser **const restrict muo, 984 + char *const restrict val_name, char *const restrict val_eid, 985 + const char *const restrict other_val_eid) 986 986 { 987 987 return_val_if_fail(p != NULL, false); 988 988 return_val_if_fail(p->si != NULL, false); ··· 1007 1007 (void) mowgli_strlcpy(val_name, entity(mu)->name, NICKLEN + 1); 1008 1008 (void) mowgli_strlcpy(val_eid, entity(mu)->id, IDLEN + 1); 1009 1009 1010 - if (p->mechptr->password_based && (mu->flags & MU_NOPASSWORD)) 1010 + if (method == HULM_PASSWORD && (mu->flags & MU_NOPASSWORD)) 1011 1011 { 1012 1012 (void) logcommand(p->si, CMDLOG_LOGIN, "failed LOGIN %s to \2%s\2 (password authentication disabled)", 1013 1013 p->mechptr->name, entity(mu)->name); ··· 1023 1023 1024 1024 .si = p->si, 1025 1025 .mu = mu, 1026 + .method = method, 1026 1027 .allowed = true, 1027 1028 }; 1028 1029 ··· 1035 1036 } 1036 1037 1037 1038 static bool ATHEME_FATTR_WUR 1038 - sasl_authcid_can_login(struct sasl_session *const restrict p, const char *const restrict authcid, 1039 - struct myuser **const restrict muo) 1039 + sasl_authcid_can_login(struct sasl_session *const restrict p, const enum hook_user_login_method method, 1040 + const char *const restrict authcid, struct myuser **const restrict muo) 1040 1041 { 1041 - return sasl_authxid_can_login(p, authcid, muo, p->authcid, p->authceid, p->authzeid); 1042 + return sasl_authxid_can_login(p, method, authcid, muo, p->authcid, p->authceid, p->authzeid); 1042 1043 } 1043 1044 1044 1045 static bool ATHEME_FATTR_WUR 1045 - sasl_authzid_can_login(struct sasl_session *const restrict p, const char *const restrict authzid, 1046 - struct myuser **const restrict muo) 1046 + sasl_authzid_can_login(struct sasl_session *const restrict p, const enum hook_user_login_method method, 1047 + const char *const restrict authzid, struct myuser **const restrict muo) 1047 1048 { 1048 - return sasl_authxid_can_login(p, authzid, muo, p->authzid, p->authzeid, p->authceid); 1049 + return sasl_authxid_can_login(p, method, authzid, muo, p->authzid, p->authzeid, p->authceid); 1049 1050 } 1050 1051 1051 1052 extern const struct sasl_core_functions sasl_core_functions;
+2 -3
modules/saslserv/plain.c
··· 49 49 if (strlen(secret) > PASSLEN) 50 50 return ASASL_MRESULT_ERROR; 51 51 52 - if (*authzid && ! sasl_core_functions->authzid_can_login(p, authzid, NULL)) 52 + if (*authzid && ! sasl_core_functions->authzid_can_login(p, HULM_PASSWORD, authzid, NULL)) 53 53 return ASASL_MRESULT_ERROR; 54 54 55 55 struct myuser *mu = NULL; 56 - if (! sasl_core_functions->authcid_can_login(p, authcid, &mu)) 56 + if (! sasl_core_functions->authcid_can_login(p, HULM_PASSWORD, authcid, &mu)) 57 57 return ASASL_MRESULT_ERROR; 58 58 59 59 if (! verify_password(mu, secret)) ··· 68 68 .mech_start = NULL, 69 69 .mech_step = &sasl_mech_plain_step, 70 70 .mech_finish = NULL, 71 - .password_based = true, 72 71 }; 73 72 74 73 static void
+3 -6
modules/saslserv/scram.c
··· 306 306 (void) slog(LG_DEBUG, "%s: parsed authzid '%s'", MOWGLI_FUNC_NAME, authzid); 307 307 308 308 // Check it exists and can login 309 - if (! sasl_core_functions->authzid_can_login(p, authzid, NULL)) 309 + if (! sasl_core_functions->authzid_can_login(p, HULM_PASSWORD, authzid, NULL)) 310 310 { 311 311 (void) slog(LG_DEBUG, "%s: authzid_can_login failed", MOWGLI_FUNC_NAME); 312 312 (void) scram_error("other-error", out); ··· 385 385 386 386 (void) slog(LG_DEBUG, "%s: parsed authcid '%s'", MOWGLI_FUNC_NAME, authcid); 387 387 388 - if (! sasl_core_functions->authcid_can_login(p, authcid, &mu)) 388 + if (! sasl_core_functions->authcid_can_login(p, HULM_PASSWORD, authcid, &mu)) 389 389 { 390 390 (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME); 391 391 (void) scram_error("other-error", out); ··· 412 412 if (db.a != prf) 413 413 { 414 414 (void) slog(LG_DEBUG, "%s: PRF ID mismatch: server(%u) != client(%u)", MOWGLI_FUNC_NAME, db.a, prf); 415 - (void) sasl_core_functions->recalc_mechlist(p, mu, scram_get_avoidance_mechlist(db.a)); 415 + (void) sasl_core_functions->recalc_mechlist(p, scram_get_avoidance_mechlist(db.a)); 416 416 (void) scram_error("digest-algorithm-mismatch", out); 417 417 goto error; 418 418 } ··· 772 772 .mech_start = NULL, 773 773 .mech_step = &sasl_mech_scram_sha1_step, 774 774 .mech_finish = &sasl_mech_scram_finish, 775 - .password_based = true, 776 775 }; 777 776 778 777 static const struct sasl_mechanism sasl_mech_scram_sha2_256 = { ··· 780 779 .mech_start = NULL, 781 780 .mech_step = &sasl_mech_scram_sha2_256_step, 782 781 .mech_finish = &sasl_mech_scram_finish, 783 - .password_based = true, 784 782 }; 785 783 786 784 static const struct sasl_mechanism sasl_mech_scram_sha2_512 = { ··· 788 786 .mech_start = NULL, 789 787 .mech_step = &sasl_mech_scram_sha2_512_step, 790 788 .mech_finish = &sasl_mech_scram_finish, 791 - .password_based = true, 792 789 }; 793 790 794 791 static inline void