Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/readme.iauth
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,17 @@ Next State: -
Comments: Indicates that the iauth instance does not strongly trust
<username> to be accurate, but has no more trusted username.

f - Forced Nickname
Syntax: f <id> <remoteip> <remoteport> <nickname>
Example: f 5 192.168.1.10 23367 Buddha
States: REGISTER, HURRY
Next State: -
Comments: Indicates that the iauth instance wants the client to use
the specified nickname during registration, even if the client
requested a different one. If the nickname is missing, invalid,
juped, or already in use, the server reports an E error and does
not complete registration until iauth sends a subsequent valid f.

N - Client Hostname
Syntax: N <id> <remoteip> <remoteport> <hostname>
Example: N 5 192.168.1.10 23367 buddha.example.org
Expand Down
1 change: 1 addition & 0 deletions include/s_user.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ extern int register_user(struct Client* cptr, struct Client *sptr);

extern void user_count_memory(size_t* count_out, size_t* bytes_out);

extern int do_nick_name(char* nick);
extern int set_nick_name(struct Client* cptr, struct Client* sptr,
const char* nick, int parc, char* parv[]);
extern void send_umode_out(struct Client* cptr, struct Client* sptr,
Expand Down
2 changes: 1 addition & 1 deletion ircd/m_nick.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
* The '~'-character should be allowed, but a change should be global,
* some confusion would result if only few servers allowed it...
*/
static int do_nick_name(char* nick)
int do_nick_name(char* nick)
{
char* ch = nick;
char* end = ch + NICKLEN;
Expand Down
123 changes: 112 additions & 11 deletions ircd/s_auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "ircd.h"
#include "ircd_alloc.h"
#include "ircd_chattr.h"
#include "hash.h"
#include "ircd_events.h"
#include "ircd_features.h"
#include "ircd_log.h"
Expand Down Expand Up @@ -86,6 +87,7 @@ enum AuthRequestFlag {
AR_NEEDS_NICK, /**< user must send NICK command */
AR_LAST_SCAN = AR_NEEDS_NICK, /**< maximum flag to scan through */
AR_IAUTH_PENDING, /**< iauth request sent, waiting for response */
AR_IAUTH_NEEDS_NICK,/**< iauth f failed; wait for a valid forced nick */
AR_IAUTH_HURRY, /**< we told iauth to hurry up */
AR_IAUTH_USERNAME, /**< iauth sent a username (preferred or forced) */
AR_IAUTH_FUSERNAME, /**< iauth sent a forced username */
Expand Down Expand Up @@ -382,23 +384,47 @@ static int auth_set_username(struct AuthRequest *auth)
int auth_set_account(struct AuthRequest *auth, const char *account_info)
{
struct Client *sptr;
char *account_copy = NULL, *account = NULL, *id_str = NULL, *flags_str = NULL, *extra = NULL;
char *account_copy = NULL, *account = NULL, *id_str = NULL, *flags_str = NULL;
char *first_word, *rest, *extra = NULL, *p;

assert(auth != NULL);

sptr = auth->client;
if (!cli_user(sptr) || EmptyString(account_info))
return 1;

/* Parse account information: username:id:flags */
/*
* Payload shape (whitespace-separated):
* <account>[:<id>[:<flags>[:...]]] [+x [...]]
*
* Only the first three colon fields of the first word are used locally
* (account / id / flags). Further colon fields and further words after
* the first extra token are ignored for local parsing but the original
* string is still forwarded to iauth in full.
*/
DupString(account_copy, account_info);
if (!account_copy)
return 1;

account = strtok(account_copy, ":");
first_word = account_copy;
rest = strchr(account_copy, ' ');
if (rest) {
*rest++ = '\0';
while (*rest == ' ')
rest++;
if (*rest) {
/* First extra token only (e.g. "+x"); ignore friends. */
extra = rest;
p = strchr(extra, ' ');
if (p)
*p = '\0';
}
}

account = strtok(first_word, ":");
id_str = strtok(NULL, ":");
flags_str = strtok(NULL, " ");
extra = strtok(NULL, "");
flags_str = strtok(NULL, ":");
/* strtok(NULL, ":") would be ":something"; intentionally unused. */

/* A malformed reply may contain no account name at all. */
if (EmptyString(account)) {
Expand All @@ -420,12 +446,15 @@ int auth_set_account(struct AuthRequest *auth, const char *account_info)

SetAccount(sptr);

/* Check for +x flag (host hiding) */
if (extra && strstr(extra, "+x") && feature_bool(FEAT_HOST_HIDING)) {
/*
* Second word is umode-like if it starts with '+'. Presence of 'x'
* requests host hiding (e.g. "+x", "+xo").
*/
if (extra && *extra == '+' && strchr(extra, 'x')
&& feature_bool(FEAT_HOST_HIDING))
SetHiddenHost(sptr);
}

sendto_iauth(sptr, "A %s", cli_user(sptr)->account);
sendto_iauth(sptr, "A %s", account_info);
MyFree(account_copy);
return 0;
}
Expand Down Expand Up @@ -605,6 +634,15 @@ static int check_auth_finished(struct AuthRequest *auth, int bitclr)
else
FlagSet(&auth->flags, AR_IAUTH_HURRY);

/* A failed iauth "f" must be followed by a valid forced nick before
* registration can complete (even if iauth already sent D). */
if (FlagHas(&auth->flags, AR_IAUTH_NEEDS_NICK))
{
Debug((DEBUG_INFO, "Auth %p [%d] waiting for iauth forced nick", auth,
cli_fd(auth->client)));
return 0;
}

res = 0;
if (IsUserPort(auth->client) || IsWebsocketPort(auth->client))
{
Expand Down Expand Up @@ -2078,6 +2116,66 @@ static int iauth_cmd_username_bad(struct IAuth *iauth, struct Client *cli,
return AR_AUTH_PENDING;
}

/** Set client's nickname from iauth.
* @param[in] iauth Active IAuth session.
* @param[in] cli Client referenced by command.
* @param[in] parc Number of parameters (1).
* @param[in] params New nickname for client.
* @return Zero (auth_set_nick() handles registration progress).
*/
static int iauth_cmd_nick_forced(struct IAuth *iauth, struct Client *cli,
int parc, char **params)
{
struct AuthRequest *auth;
struct Client *acptr;
char nick[NICKLEN + 2];
char *tilde;

auth = cli_auth(cli);
assert(auth != NULL);

if (EmptyString(params[0])) {
FlagSet(&auth->flags, AR_IAUTH_NEEDS_NICK);
sendto_iauth(cli, "E Missing :Missing nickname parameter");
return 0;
}

ircd_strncpy(nick, params[0], NICKLEN);
if ((tilde = strchr(nick, '~')))
*tilde = '\0';
if (!do_nick_name(nick)) {
FlagSet(&auth->flags, AR_IAUTH_NEEDS_NICK);
sendto_iauth(cli, "E Invalid :Invalid nickname [%s]", params[0]);
return 0;
}

if (isNickJuped(nick)) {
FlagSet(&auth->flags, AR_IAUTH_NEEDS_NICK);
sendto_iauth(cli, "E Invalid :Nickname is juped [%s]", nick);
return 0;
}

acptr = FindClient(nick);
if (acptr && acptr != cli) {
FlagSet(&auth->flags, AR_IAUTH_NEEDS_NICK);
sendto_iauth(cli, "E InUse :Nickname in use [%s]", nick);
return 0;
}

/* Tell the client about the assignment before renaming locally. */
if (cli_name(cli)[0] && 0 != ircd_strcmp(cli_name(cli), nick))
sendcmdto_one(cli, CMD_NICK, cli, ":%s", nick);

if (cli_name(cli)[0])
hRemClient(cli);
strcpy(cli_name(cli), nick);
hAddClient(cli);

FlagClr(&auth->flags, AR_IAUTH_NEEDS_NICK);
auth_set_nick(auth, nick);
return 0;
}

/** Set client's hostname.
* @param[in] iauth Active IAuth session.
* @param[in] cli Client referenced by command.
Expand Down Expand Up @@ -2441,6 +2539,7 @@ static void iauth_parse(struct IAuth *iauth, char *message)
case 'o': handler = iauth_cmd_username_forced; has_cli = 1; break;
case 'U': handler = iauth_cmd_username_good; has_cli = 1; break;
case 'u': handler = iauth_cmd_username_bad; has_cli = 1; break;
case 'f': handler = iauth_cmd_nick_forced; has_cli = 1; break;
case 'N': handler = iauth_cmd_hostname; has_cli = 1; break;
case 'I': handler = iauth_cmd_ip_address; has_cli = 1; break;
case 'M': handler = iauth_cmd_usermode; has_cli = 1; break;
Expand Down Expand Up @@ -2494,9 +2593,11 @@ static void iauth_parse(struct IAuth *iauth, char *message)
sendto_iauth(NULL, "E Gone :[%s %s %s]", params[0], params[1],
params[2]);
else if ((!(auth = cli_auth(cli)) ||
!FlagHas(&auth->flags, AR_IAUTH_PENDING)) &&
(!FlagHas(&auth->flags, AR_IAUTH_PENDING) &&
!(handler == iauth_cmd_nick_forced &&
FlagHas(&auth->flags, AR_IAUTH_NEEDS_NICK)))) &&
has_cli == 1)
/* Client is done with IAuth checks. */
/* Client is done with IAuth checks (unless waiting for a valid f). */
sendto_iauth(cli, "E Done :[%s %s %s]", params[0], params[1], params[2]);
else {
struct irc_sockaddr addr;
Expand Down
7 changes: 7 additions & 0 deletions ircd/s_user.c
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,13 @@ int set_nick_name(struct Client* cptr, struct Client* sptr,
/*
* Client changing its nick
*
* Unregistered local clients may not change nick once one is set:
* the chosen nick is what iauth sees (and may force via "f"). Allowing
* a second NICK before registration would bypass those checks.
*/
if (MyConnect(sptr) && !IsUser(sptr))
return 0; /* nick locked for iauth until registration */
/*
* If the client belongs to me, then check to see
* if client is on any channels where it is currently
* banned. If so, do not allow the nick change to occur.
Expand Down
1 change: 1 addition & 0 deletions tests/iauth_nick/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

92 changes: 92 additions & 0 deletions tests/iauth_nick/iauth_stub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""IAuth stub for nickname assignment tests.

Logs every line from the ircd to the file given as argv[1]. Client
nicknames drive stub behaviour:

testnick -> force nick to "Guest001", then approve (used with/without SASL)
tmpuser -> force nick to "finaluser", then approve
set_<nick> -> force nick to <nick>, then approve
bad_<nick> -> try invalid nick, then on E retry with "recovered", D
collide -> force "taken"; on E InUse retry with "freenick", D
stuckbad -> force invalid nick and D without retry (should not 001)

All other clients are approved on "n" without changing their nick.

For testnick, f is always sent before D so registration cannot finish on
the client's requested nick. SASL success (A) is independent and may
arrive before or after n; the stub does not wait for it.
"""

import sys


def main():
logf = open(sys.argv[1], "a", buffering=1)

def out(line):
# Prefix outgoing replies so tests can assert f-before-D ordering.
logf.write("> " + line + "\n")
sys.stdout.write(line + "\n")
sys.stdout.flush()

# R: iauth is required; U: enable Undernet extensions (U/u/n/H/T).
out("O RU")

clients = {}
# cid -> ("bad"|"collide", ip, port)
awaiting_retry = {}

for line in sys.stdin:
line = line.rstrip("\r\n")
logf.write(line + "\n")
parts = line.split(" ")
if len(parts) < 2:
continue
cid, cmd = parts[0], parts[1]
if cmd == "C" and len(parts) >= 4:
clients[cid] = (parts[2], parts[3])
elif cmd == "n" and cid in clients:
ip, port = clients[cid]
nick = parts[2] if len(parts) >= 3 else ""
if nick == "testnick":
# Reject requested nick by forcing Guest001 before Done.
out(f"f {cid} {ip} {port} Guest001")
out(f"D {cid} {ip} {port}")
clients.pop(cid, None)
elif nick == "tmpuser":
out(f"f {cid} {ip} {port} finaluser")
out(f"D {cid} {ip} {port}")
clients.pop(cid, None)
elif nick.startswith("set_"):
out(f"f {cid} {ip} {port} {nick[4:]}")
out(f"D {cid} {ip} {port}")
clients.pop(cid, None)
elif nick.startswith("bad_"):
out(f"f {cid} {ip} {port} {nick[4:]}")
awaiting_retry[cid] = ("bad", ip, port)
elif nick == "stuckbad":
out(f"f {cid} {ip} {port} -invalid")
out(f"D {cid} {ip} {port}")
clients.pop(cid, None)
elif nick == "collide":
out(f"f {cid} {ip} {port} taken")
awaiting_retry[cid] = ("collide", ip, port)
else:
out(f"D {cid} {ip} {port}")
clients.pop(cid, None)
elif cmd == "E" and cid in awaiting_retry:
kind, ip, port = awaiting_retry.pop(cid)
if kind == "bad":
out(f"f {cid} {ip} {port} recovered")
else:
out(f"f {cid} {ip} {port} freenick")
out(f"D {cid} {ip} {port}")
clients.pop(cid, None)
elif cmd == "D":
clients.pop(cid, None)
awaiting_retry.pop(cid, None)


if __name__ == "__main__":
main()
Loading
Loading