diff --git a/doc/iauthd-c.conf.example b/doc/iauthd-c.conf.example index 0c86f20..4cd60c6 100644 --- a/doc/iauthd-c.conf.example +++ b/doc/iauthd-c.conf.example @@ -50,7 +50,13 @@ iauth { // can safely be removed; with earlier versions, that will cause // Authorization Timeout rejections instead of accepting the // clients. + // + // This should not be used together with "verify". timeout 30 + + // If set, IAuth will kill a connection using +x!. + // Useful when transitioning away from LoC to SASL. + kill_loc "Login-on-Connect is disabled. You may authenticate using SASL. Visit http://undernet.org/sasl for more information." } iauth_xquery { @@ -62,8 +68,10 @@ iauth_xquery { // - login-ipr // - dronecheck // - combined (almost like dronecheck plus login-ipr) + // - verify channels.example.org login-ipr botcheck.example.org dronecheck + verify.example.org verify } iauth_class { @@ -83,7 +91,7 @@ iauth_class { username "joe-oper" address "127.0.0.0/8" } - "r002" { class trusted; account ircoper } + "r002" { class trusted; account "ircoper" } "r003" { class clients; xreply_ok "euworld.example.org" } "r004" { class default_clients } } diff --git a/modules/iauth.h b/modules/iauth.h index 4d135a7..0457d05 100644 --- a/modules/iauth.h +++ b/modules/iauth.h @@ -97,6 +97,9 @@ unsigned int irc_check_mask(const irc_inaddr *check, const irc_inaddr *mask, uns /** Maximum length of an iauthd-c standard routing string. */ #define ROUTINGLEN (IRC_NTOP_MAX + 20) +/** Maximum length of a TLS fingerprint. */ +#define CERTLEN 64 + /** Possible states for a client (with respect to IAuth). */ enum iauth_client_state { IAUTH_REGISTER, @@ -109,6 +112,10 @@ enum iauth_client_state { enum iauth_flags { /** Set when we have made a decision for this request. */ IAUTH_RESPONDED, + /** Set when the client has sent a CAP LS. */ + IAUTH_GOT_CAP_START, + /** Set when the client has sent CAP END. */ + IAUTH_GOT_CAP_END, /** Set when we have sent a "soft done" for this request. */ IAUTH_SOFT_DONE, /** Set when we get an 'N' or 'd' message. */ @@ -121,6 +128,10 @@ enum iauth_flags { IAUTH_GOT_USER_INFO, /** Set when we get a 'P' message. */ IAUTH_GOT_PASSWORD, + /** Set when we get a 'Z' message. */ + IAUTH_GOT_FINGERPRINT, + /** Set when we get an 'A' message. */ + IAUTH_GOT_ACCOUNT, /** Set when we get a 'H' message. */ IAUTH_GOT_HURRY_UP, /** Set when we get blank 'u' message, but have not gotten 'U'. */ @@ -212,6 +223,9 @@ struct iauth_request { /** Text form of #remote_addr. */ char text_addr[IRC_NTOP_MAX]; + /** TLS fingerprint. */ + char tls_fingerprint[CERTLEN + 1]; + /** Contains submodule-specific data. * * No special cleanup of the data is performed. The first element @@ -274,6 +288,13 @@ struct iauth_module { */ void (*error)(struct iauth_request *req, const char type[], const char info[]); + /** Handler for calculating effective flags that may change dynamically. + * If this callback is provided, it will be called during iauth_check_request() + * to compute the current effective flags for this module, which are OR'd into + * the global effective_flags used for checking request readiness. + */ + void (*calc_effective_flags)(const struct iauth_request *req, struct iauth_flagset *flags_out); + /** Handler for simple field changes. * * In particular, \a flag gets #IAUTH_GOT_HOSTNAME for a 'N' or 'd' @@ -332,6 +353,8 @@ struct iauth_module { void iauth_register_module(struct iauth_module *plugin); void iauth_unregister_module(struct iauth_module *plugin); +const char* iauth_get_kill_loc(void); + /* These functions generate IAuth messages to the server. */ void iauth_accept(struct iauth_request *req); void iauth_soft_done(struct iauth_request *req); diff --git a/modules/iauth_core.c b/modules/iauth_core.c index 1fc4327..fb4e013 100644 --- a/modules/iauth_core.c +++ b/modules/iauth_core.c @@ -54,6 +54,9 @@ static struct conf_node_object *iauth_conf; /** Duration of the request timeout. */ static struct conf_node_string *iauth_conf_timeout; +/** Whether to kill connections using +x! when login, login-ipr or combined is not enabled. */ +static struct conf_node_string *iauth_conf_kill_loc; + /** Last assigned serial number. */ static unsigned int iauth_serial; @@ -157,6 +160,15 @@ void iauth_unregister_module(struct iauth_module *plugin) calc_iauth_flags(); } +/** Returns the kill_loc configuration value. Empty if unset. + * + * \return The kill_loc configuration value. Empty string if unset. + */ +const char* iauth_get_kill_loc(void) +{ + return iauth_conf_kill_loc ? iauth_conf_kill_loc->parsed.p_string : ""; +} + /** Looks up the request for \a client_id. * * \param[in] client_id ircd-assigned client identifier @@ -244,9 +256,23 @@ struct iauth_request *iauth_validate_request(const char routing[]) */ void iauth_check_request(struct iauth_request *request) { + struct iauth_module *plugin; + struct set_node *node; + struct iauth_flagset effective_flags = iauth_flags; + + /* Allow modules to adjust their effective requirements dynamically */ + for (node = set_first(iauth_modules); node; node = set_next(node)) { + plugin = ENCLOSING_STRUCT(node, struct iauth_module, node); + if (plugin->calc_effective_flags != NULL) { + struct iauth_flagset module_flags; + plugin->calc_effective_flags(request, &module_flags); + BITSET_OR(effective_flags, effective_flags, module_flags); + } + } + if (request->holds == 0 && !BITSET_GET(request->flags, IAUTH_RESPONDED) - && !BITSET_H_ANDNOT(iauth_flags, request->flags)) { + && !BITSET_H_ANDNOT(effective_flags, request->flags)) { if (request->soft_holds == 0) iauth_accept(request); else if (!BITSET_GET(request->flags, IAUTH_SOFT_DONE)) { @@ -263,7 +289,7 @@ void iauth_check_request(struct iauth_request *request) request->client); } else { log_message(iauth_log, LOG_DEBUG, " -> client %d still waiting: %#x & ~%#x (plus %d soft holds)", - request->client, iauth_flags.bits[0], request->flags.bits[0], + request->client, effective_flags.bits[0], request->flags.bits[0], request->soft_holds); } } @@ -408,6 +434,11 @@ static void notify_pre_registered(struct iauth_request *req) void iauth_accept(struct iauth_request *req) { + if (BITSET_GET(req->flags, IAUTH_GOT_CAP_START) && !BITSET_GET(req->flags, IAUTH_GOT_CAP_END)) { + log_message(iauth_log, LOG_DEBUG, " -> client %d has CAP pending, delaying registration", req->client); + return; + } + assert(!BITSET_GET(req->flags, IAUTH_RESPONDED)); notify_pre_registered(req); BITSET_SET(req->flags, IAUTH_RESPONDED); @@ -691,6 +722,52 @@ static void parse_nick(struct iauth_request *req, char nick[]) iauth_check_request(req); } +static void parse_fingerprint(struct iauth_request *req, char fingerprint[]) +{ + struct iauth_module *plugin; + struct set_node *node; + + if (!req) { + iauth_send_opers("ircd sent garbage: -1 Z ..."); + return; + } + if (fingerprint) { + strncpy(req->tls_fingerprint, fingerprint, CERTLEN); + req->tls_fingerprint[CERTLEN] = '\0'; + BITSET_SET(req->flags, IAUTH_GOT_FINGERPRINT); + } + + for (node = set_first(iauth_modules); node; node = set_next(node)) { + plugin = ENCLOSING_STRUCT(node, struct iauth_module, node); + if (plugin->field_change != NULL) + plugin->field_change(req, IAUTH_GOT_FINGERPRINT); + } + iauth_check_request(req); +} + +static void parse_account(struct iauth_request *req, char account[]) +{ + struct iauth_module *plugin; + struct set_node *node; + + if (!req) { + iauth_send_opers("ircd sent garbage: -1 A ..."); + return; + } + if (account) { + strncpy(req->account, account, ACCOUNTLEN); + req->account[ACCOUNTLEN] = '\0'; + BITSET_SET(req->flags, IAUTH_GOT_ACCOUNT); + } + + for (node = set_first(iauth_modules); node; node = set_next(node)) { + plugin = ENCLOSING_STRUCT(node, struct iauth_module, node); + if (plugin->field_change != NULL) + plugin->field_change(req, IAUTH_GOT_ACCOUNT); + } + iauth_check_request(req); +} + static void parse_hurry_up(struct iauth_request *req) { struct iauth_module *plugin; @@ -744,6 +821,22 @@ static void parse_error(struct iauth_request *req, int argc, char *argv[]) } } +static void parse_cap(struct iauth_request *req, enum iauth_flags flag) +{ + struct iauth_module *plugin; + struct set_node *node; + + BITSET_SET(req->flags, flag); + + /* Notify modules that CAP negotiation has ended */ + for (node = set_first(iauth_modules); node; node = set_next(node)) { + plugin = ENCLOSING_STRUCT(node, struct iauth_module, node); + if (plugin->field_change != NULL) + plugin->field_change(req, flag); + } + iauth_check_request(req); +} + static void parse_server_info(int argc, char *argv[]) { struct iauth_module *plugin; @@ -890,12 +983,24 @@ static void iauth_read(evutil_socket_t fd, short events, void *iauth_in_v) case 'n': parse_nick(req, argv[1]); break; + case 'A': + parse_account(req, argv[1]); + break; + case 'Z': + parse_fingerprint(req, argv[1]); + break; case 'H': parse_hurry_up(req); break; case 'T': parse_registered(req, 1); break; + case 'c': + parse_cap(req, IAUTH_GOT_CAP_START); + break; + case 'e': + parse_cap(req, IAUTH_GOT_CAP_END); + break; case 'E': parse_error(req, argc, argv); break; @@ -954,6 +1059,7 @@ void module_constructor(UNUSED_ARG(const char name[])) iauth_modules = set_alloc(set_compare_charp, NULL); iauth_conf = conf_register_object(NULL, "iauth"); iauth_conf_timeout = conf_register_string(iauth_conf, CONF_STRING_INTERVAL, "timeout", "0"); + iauth_conf_kill_loc = conf_register_string(iauth_conf, CONF_STRING_PLAIN, "kill_loc", ""); event_base_once(ev_base, -1, EV_TIMEOUT, iauth_startup, NULL, &tv_zero); iauth_in = evbuffer_new(); diff --git a/modules/iauth_xquery.c b/modules/iauth_xquery.c index a9a29fb..7698dfe 100644 --- a/modules/iauth_xquery.c +++ b/modules/iauth_xquery.c @@ -74,6 +74,7 @@ * login - LOGIN * login-ipr - LOGIN2 * dronecheck - CHECK + * verify - VERIFY : * combined - CHECK , * then LOGIN * @@ -134,6 +135,7 @@ enum iauth_xquery_type { LOGIN, LOGIN_IPR, DRONECHECK, + VERIFY, COMBINED }; @@ -142,6 +144,7 @@ static const char *type_names[] = { "login", "login-ipr", "dronecheck", + "verify", "combined" }; @@ -188,7 +191,7 @@ static struct { static struct iauth_module iauth_xquery; static struct log_type *iauth_xquery_log; static struct iauth_xquery_services iauth_xquery_services; -static struct iauth_flagset iauth_xquery_flags[4]; +static struct iauth_flagset iauth_xquery_flags[5]; static struct { unsigned long n_cli_allocs; @@ -268,6 +271,8 @@ static void iauth_xquery_set_account(struct iauth_request *req, req->account[ii] = account[ii]; for (; ii < ACCOUNTLEN+1; ++ii) req->account[ii] = '\0'; + + BITSET_SET(req->flags, IAUTH_GOT_ACCOUNT); } static void iauth_xquery_x_reply(const char service[], const char routing[], @@ -302,13 +307,19 @@ static void iauth_xquery_x_reply(const char service[], const char routing[], /* Handle the response. */ if (!reply) { srv->unlinked++; - if (srv->type != DRONECHECK) + if (srv->type != DRONECHECK && srv->type != VERIFY) iauth_challenge(req, "The login server is currently disconnected. Please excuse the inconvenience."); } else if (reply[0] == 'O' && reply[1] == 'K' && (reply[2] == '\0' || reply[2] == ' ')) { - cli->ok_mask |= 1u << ii; - cli->more_mask &= ~(1u << ii); - if (reply[2] != ' ') { + if (srv->type == VERIFY) { + if (cli->more_mask & (1u << ii)) { + req->holds--; + log_message(iauth_xquery_log, LOG_DEBUG, + "release VERIFY hold on %s for %d", routing, req->client); + } + if (reply[2] == ' ') + iauth_challenge(req, reply + 3); + } else if (reply[2] != ' ') { srv->good_no_acct++; } else if ((srv->type == LOGIN) || (srv->type == LOGIN_IPR) @@ -333,6 +344,8 @@ static void iauth_xquery_x_reply(const char service[], const char routing[], srv->name); srv->good_no_acct++; } + cli->ok_mask |= 1u << ii; + cli->more_mask &= ~(1u << ii); } else if (0 == strncmp(reply, "NO ", 3)) { srv->bad++; if (req->account[0] != '\0') @@ -341,9 +354,19 @@ static void iauth_xquery_x_reply(const char service[], const char routing[], return; } else if (0 == strncmp(reply, "AGAIN ", 6)) { iauth_challenge(req, reply + 6); + /* Don't clear ref_mask for AGAIN - service is still waiting for client response */ + return; } else if (0 == strncmp(reply, "MORE ", 5)) { cli->more_mask |= 1u << ii; iauth_challenge(req, reply + 5); + /* For VERIFY services, create a hard hold instead of soft hold */ + if (srv->type == VERIFY) { + req->holds++; + log_message(iauth_xquery_log, LOG_DEBUG, + "hard hold for %d for VERIFY MORE", req->client); + } + /* Don't clear ref_mask for MORE - service is still waiting for client response */ + return; } else { log_message(iauth_xquery_log, LOG_WARNING, "Unexpected XR reply: %s", reply); return; @@ -405,13 +428,20 @@ static void iauth_xquery_check(struct iauth_request *req, if ((cli->sent_mask & (1u << ii)) && ((flag != IAUTH_GOT_PASSWORD) - || (srv->type == DRONECHECK))) + || (srv->type == DRONECHECK || srv->type == VERIFY))) continue; /* already asked this server */ if ((srv->type == LOGIN || srv->type == LOGIN_IPR) && !cli->password[0]) continue; /* do not send a login-type request with no password */ + /* For VERIFY services, require IAUTH_GOT_ACCOUNT during CAP negotiation */ + if (srv->type == VERIFY + && BITSET_GET(req->flags, IAUTH_GOT_CAP_START) + && !BITSET_GET(req->flags, IAUTH_GOT_CAP_END) + && !BITSET_GET(req->flags, IAUTH_GOT_ACCOUNT)) + continue; /* wait for account during CAP negotiation */ + if (BITSET_H_ANDNOT(iauth_xquery_flags[srv->type], req->flags)) continue; /* missing necessary information */ @@ -433,6 +463,14 @@ static void iauth_xquery_check(struct iauth_request *req, hostname = req->hostname[0] ? req->hostname : req->text_addr; + if (srv->type == VERIFY) + iauth_x_query(srv->name, routing, + "VERIFY %s %s %s %s %s :%s", + req->nickname, username, req->text_addr, + hostname, + req->account[0] == '\0' ? "*" : req->account, + req->realname); + if (srv->type == DRONECHECK || srv->type == COMBINED) iauth_x_query(srv->name, routing, "CHECK %s %s %s %s :%s", @@ -518,6 +556,9 @@ static void iauth_xquery_check_password(struct iauth_request *req, req->holds++; log_message(iauth_xquery_log, LOG_DEBUG, "hold for %d for !+x", req->client); + if (iauth_get_kill_loc()[0] != '\0') { + iauth_kill(req, iauth_get_kill_loc()); + } } else if (!is_hidden_only && was_hidden_only && no_account) { req->holds--; log_message(iauth_xquery_log, LOG_DEBUG, @@ -560,9 +601,15 @@ static void iauth_xquery_password(struct iauth_request *req, iauth_x_query(srv->name, routing, "MORE %s", password); cli->more_mask &= ~(1u << ii); if (!cli->ref_mask) { - req->soft_holds++; - log_message(iauth_xquery_log, LOG_DEBUG, - "adding soft hold on %s for MORE %s", routing, srv->name); + if (srv->type == VERIFY) { + req->holds++; + log_message(iauth_xquery_log, LOG_DEBUG, + "adding hard hold on %s for VERIFY MORE %s", routing, srv->name); + } else { + req->soft_holds++; + log_message(iauth_xquery_log, LOG_DEBUG, + "adding soft hold on %s for MORE %s", routing, srv->name); + } } cli->ref_mask |= 1u << ii; srv->refs++; @@ -575,8 +622,66 @@ static void iauth_xquery_user_info(struct iauth_request *req) iauth_xquery_check(req, IAUTH_GOT_USER_INFO); } +static void iauth_xquery_disconnect(struct iauth_request *req) +{ + struct iauth_xquery_client *cli; + struct iauth_xquery_service *srv; + void *ptr; + unsigned int ii; + char routing[ROUTINGLEN]; + + /* Find the client's state struct. */ + ptr = &iauth_xquery; + cli = set_find(&req->data, &ptr); + if (!cli) + return; + + /* Build routing string */ + iauth_routing(req, routing, sizeof(routing)); + + /* Send DISCONNECT to all services that have an outstanding query */ + for (ii = 0; ii < iauth_xquery_services.used; ++ii) { + srv = iauth_xquery_services.vec[ii]; + if (!srv || !srv->configured) + continue; + + /* Check if this service has an outstanding query (ref_mask bit set) */ + if (cli->ref_mask & (1u << ii)) { + iauth_x_query(srv->name, routing, "DISCONNECT"); + log_message(iauth_xquery_log, LOG_DEBUG, + "Sent DISCONNECT to %s for client %d", srv->name, req->client); + } + } +} + +static void iauth_xquery_calc_effective_flags(const struct iauth_request *req, struct iauth_flagset *flags_out) +{ + unsigned int ii; + + BITSET_ZERO(*flags_out); + + /* Compute effective flags based on service requirements and CAP negotiation state. + * For VERIFY services, we require IAUTH_GOT_ACCOUNT during CAP negotiation. + */ + if (BITSET_GET(req->flags, IAUTH_GOT_CAP_START) && !BITSET_GET(req->flags, IAUTH_GOT_CAP_END)) { + for (ii = 0; ii < iauth_xquery_services.used; ++ii) { + struct iauth_xquery_service *srv = iauth_xquery_services.vec[ii]; + if (!srv || !srv->configured) + continue; + + /* If we have a VERIFY service configured, add IAUTH_GOT_ACCOUNT as a required flag during CAP */ + if (srv->type == VERIFY) { + BITSET_SET(*flags_out, IAUTH_GOT_ACCOUNT); + break; + } + } + } +} + static struct iauth_module iauth_xquery = { .owner = "iauth_xquery", + .calc_effective_flags = iauth_xquery_calc_effective_flags, + .disconnect = iauth_xquery_disconnect, .field_change = iauth_xquery_check, .get_config = iauth_xquery_report_config, .get_stats = iauth_xquery_report_stats, @@ -687,6 +792,11 @@ void module_constructor(UNUSED_ARG(const char name[])) IAUTH_GOT_IDENT, IAUTH_GOT_NICK, IAUTH_GOT_USER_INFO); + BITSET_MULTI_SET(iauth_xquery_flags[VERIFY], + IAUTH_GOT_HOSTNAME, + IAUTH_GOT_IDENT, + IAUTH_GOT_NICK, + IAUTH_GOT_USER_INFO); BITSET_OR(iauth_xquery_flags[COMBINED], iauth_xquery_flags[LOGIN], iauth_xquery_flags[DRONECHECK]);