From b79b315dd4b5fcefb781d1e1e012f71e578a5346 Mon Sep 17 00:00:00 2001 From: Alexander Barton Date: Sat, 27 Dec 2003 13:01:12 +0000 Subject: [PATCH] Added optional support for IDENT lookups (configure switch "--with-ident"). --- ChangeLog | 5 +- NEWS | 8 ++- configure.in | 27 +++++++++- src/ngircd/conn.c | 40 ++++++++++---- src/ngircd/irc-login.c | 10 +++- src/ngircd/resolve.c | 101 +++++++++++++++++++++++++---------- src/ngircd/resolve.h | 9 +++- src/testsuite/channel-test.e | 14 +++-- src/testsuite/mode-test.e | 20 +++---- 9 files changed, 174 insertions(+), 60 deletions(-) diff --git a/ChangeLog b/ChangeLog index 84e8a651..c9c15c1f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,9 @@ ngIRCd CVSHEAD + - Added optional support for "IDENT" lookups on incoming connections. You + have to enable this function with the ./configure switch "--with-ident". + The default is not to do IDENT lookups. - Removed "USE_" prefixes of configuration #defines. ngIRCd 0.7.6 (2003-12-05) @@ -488,4 +491,4 @@ ngIRCd 0.0.1, 31.12.2001 -- -$Id: ChangeLog,v 1.219 2003/12/26 15:55:07 alex Exp $ +$Id: ChangeLog,v 1.220 2003/12/27 13:01:12 alex Exp $ diff --git a/NEWS b/NEWS index c7ecdd3b..a7fa785f 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,12 @@ -- NEWS -- +ngIRCd CVSHEAD + + - Added optional support for "IDENT" lookups on incoming connections. You + have to enable this function with the ./configure switch "--with-ident". + The default is not to do IDENT lookups. + ngIRCd 0.7.5 (2003-07-11) - New configuration variable "MaxConnectionsIP" to limit the number of @@ -174,4 +180,4 @@ ngIRCd 0.0.1, 31.12.2001 -- -$Id: NEWS,v 1.60 2003/11/07 21:32:15 alex Exp $ +$Id: NEWS,v 1.61 2003/12/27 13:01:12 alex Exp $ diff --git a/configure.in b/configure.in index 43d57343..324fd335 100644 --- a/configure.in +++ b/configure.in @@ -8,7 +8,7 @@ # (at your option) any later version. # Please read the file COPYING, README and AUTHORS for more information. # -# $Id: configure.in,v 1.97 2003/12/26 15:55:07 alex Exp $ +# $Id: configure.in,v 1.98 2003/12/27 13:01:12 alex Exp $ # # -- Initialisierung -- @@ -31,6 +31,7 @@ AH_TEMPLATE([ZLIB], [Define if zlib compression should be enabled]) AH_TEMPLATE([TCPWRAP], [Define if TCP wrappers should be used]) AH_TEMPLATE([IRCPLUS], [Define if IRC+ protocol should be used]) AH_TEMPLATE([RENDEZVOUS], [Define if Rendezvous support should be included]) +AH_TEMPLATE([IDENTAUTH], [Define if the server should do IDENT requests]) AH_TEMPLATE([TARGET_OS], [Target operating system name]) AH_TEMPLATE([TARGET_VENDOR], [Target system vendor]) @@ -223,6 +224,22 @@ if test "$x_rendezvous_on" = "yes"; then AC_CHECK_HEADERS(DNSServiceDiscovery/DNSServiceDiscovery.h mach/port.h) fi +x_identauth_on=no +AC_ARG_WITH(ident, + [ --with-ident enable "IDENT" ("AUTH") protocol support], + [ if test "$withval" = "yes"; then + AC_CHECK_LIB(ident, ident_id) + AC_CHECK_FUNCS(ident_id, x_identauth_on=yes, + AC_MSG_ERROR([Can't enable IDENT support!]) + ) + fi + ] +) +if test "$x_identauth_on" = "yes"; then + AC_DEFINE(IDENTAUTH, 1) + AC_CHECK_HEADERS(ident.h) +fi + x_ircplus_on=yes AC_ARG_ENABLE(ircplus, [ --disable-ircplus disable IRC+ protocol], @@ -384,6 +401,12 @@ echo $ECHO_N " IRC+ protocol: $ECHO_C" test "$x_ircplus_on" = "yes" \ && echo "yes" \ || echo "no" -echo + +echo $ECHO_N " IDENT support: $ECHO_C" +test "$x_identauth_on" = "yes" \ + && echo $ECHO_N "yes $ECHO_C" \ + || echo $ECHO_N "no $ECHO_C" + +echo; echo # -eof- diff --git a/src/ngircd/conn.c b/src/ngircd/conn.c index 48c1f468..7b9dcdb3 100644 --- a/src/ngircd/conn.c +++ b/src/ngircd/conn.c @@ -16,7 +16,7 @@ #include "portab.h" -static char UNUSED id[] = "$Id: conn.c,v 1.128 2003/12/26 15:55:07 alex Exp $"; +static char UNUSED id[] = "$Id: conn.c,v 1.129 2003/12/27 13:01:12 alex Exp $"; #include "imp.h" #include @@ -1054,7 +1054,11 @@ New_Connection( INT Sock ) /* Hostnamen ermitteln */ strlcpy( My_Connections[idx].host, inet_ntoa( new_addr.sin_addr ), sizeof( My_Connections[idx].host )); Client_SetHostname( c, My_Connections[idx].host ); +#ifdef IDENTAUTH + s = Resolve_Addr( &new_addr, My_Connections[idx].sock ); +#else s = Resolve_Addr( &new_addr ); +#endif if( s ) { /* Sub-Prozess wurde asyncron gestartet */ @@ -1579,26 +1583,26 @@ Read_Resolver_Result( INT r_fd ) FD_CLR( r_fd, &Resolver_FDs ); - /* Anfrage vom Parent lesen */ + /* Read result from pipe */ len = read( r_fd, result, HOST_LEN - 1 ); if( len < 0 ) { - /* Fehler beim Lesen aus der Pipe */ + /* Error! */ close( r_fd ); Log( LOG_CRIT, "Resolver: Can't read result: %s!", strerror( errno )); return; } result[len] = '\0'; - /* zugehoerige Connection suchen */ + /* Search associated connection ... */ for( i = 0; i < Pool_Size; i++ ) { if(( My_Connections[i].sock != NONE ) && ( My_Connections[i].res_stat ) && ( My_Connections[i].res_stat->pipe[0] == r_fd )) break; } if( i >= Pool_Size ) { - /* Opsa! Keine passende Connection gefunden!? Vermutlich - * wurde sie schon wieder geschlossen. */ + /* Ops, none found? Probably the connection has already + * been closed. */ close( r_fd ); #ifdef DEBUG Log( LOG_DEBUG, "Resolver: Got result for unknown connection!?" ); @@ -1610,7 +1614,7 @@ Read_Resolver_Result( INT r_fd ) Log( LOG_DEBUG, "Resolver: %s is \"%s\".", My_Connections[i].host, result ); #endif - /* Aufraeumen */ + /* Clean up ... */ close( My_Connections[i].res_stat->pipe[0] ); close( My_Connections[i].res_stat->pipe[1] ); free( My_Connections[i].res_stat ); @@ -1618,21 +1622,37 @@ Read_Resolver_Result( INT r_fd ) if( My_Connections[i].sock > NONE ) { - /* Eingehende Verbindung: Hostnamen setzen */ +#ifdef IDENTAUTH + CHAR *ident; +#endif + /* Incoming connection: set hostname */ c = Client_GetFromConn( i ); assert( c != NULL ); strlcpy( My_Connections[i].host, result, sizeof( My_Connections[i].host )); Client_SetHostname( c, result ); + +#ifdef IDENTAUTH + ident = strchr( result, 0 ); + ident++; + + /* Do we have a result of the IDENT lookup? */ + if( *ident ) + { + Log( LOG_INFO, "IDENT lookup on connection %ld: \"%s\".", i, ident ); + Client_SetUser( c, ident, TRUE ); + } + else Log( LOG_INFO, "IDENT lookup on connection %ld: no result.", i ); +#endif } else { - /* Ausgehende Verbindung (=Server): IP setzen */ + /* Outgoing connection (server link!): set IP address */ n = Conf_GetServer( i ); if( n > NONE ) strlcpy( Conf_Server[n].ip, result, sizeof( Conf_Server[n].ip )); else Log( LOG_ERR, "Got resolver result for non-configured server!?" ); } - /* Penalty-Zeit zurueck setzen */ + /* Reset penalty time */ Conn_ResetPenalty( i ); } /* Read_Resolver_Result */ diff --git a/src/ngircd/irc-login.c b/src/ngircd/irc-login.c index a077f16a..10188c5e 100644 --- a/src/ngircd/irc-login.c +++ b/src/ngircd/irc-login.c @@ -14,7 +14,7 @@ #include "portab.h" -static char UNUSED id[] = "$Id: irc-login.c,v 1.36 2003/12/04 14:05:16 alex Exp $"; +static char UNUSED id[] = "$Id: irc-login.c,v 1.37 2003/12/27 13:01:12 alex Exp $"; #include "imp.h" #include @@ -285,6 +285,10 @@ IRC_NICK( CLIENT *Client, REQUEST *Req ) GLOBAL BOOLEAN IRC_USER( CLIENT *Client, REQUEST *Req ) { +#ifdef IDENTAUTH + CHAR *ptr; +#endif + assert( Client != NULL ); assert( Req != NULL ); @@ -297,6 +301,10 @@ IRC_USER( CLIENT *Client, REQUEST *Req ) /* Falsche Anzahl Parameter? */ if( Req->argc != 4 ) return IRC_WriteStrClient( Client, ERR_NEEDMOREPARAMS_MSG, Client_ID( Client ), Req->command ); +#ifdef IDENTAUTH + ptr = Client_User( Client ); + if( ! ptr || ! *ptr || *ptr == '~' ) +#endif Client_SetUser( Client, Req->argv[0], FALSE ); Client_SetInfo( Client, Req->argv[3] ); diff --git a/src/ngircd/resolve.c b/src/ngircd/resolve.c index aeb98ba6..69705db5 100644 --- a/src/ngircd/resolve.c +++ b/src/ngircd/resolve.c @@ -1,6 +1,6 @@ /* * ngIRCd -- The Next Generation IRC Daemon - * Copyright (c)2001,2002 by Alexander Barton (alex@barton.de) + * Copyright (c)2001-2003 by Alexander Barton (alex@barton.de) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ #include "portab.h" -static char UNUSED id[] = "$Id: resolve.c,v 1.6 2003/04/21 10:52:51 alex Exp $"; +static char UNUSED id[] = "$Id: resolve.c,v 1.7 2003/12/27 13:01:12 alex Exp $"; #include "imp.h" #include @@ -27,6 +27,12 @@ static char UNUSED id[] = "$Id: resolve.c,v 1.6 2003/04/21 10:52:51 alex Exp $"; #include #include +#ifdef IDENTAUTH +#ifdef HAVE_IDENT_H +#include +#endif +#endif + #include "conn.h" #include "defines.h" #include "log.h" @@ -35,7 +41,12 @@ static char UNUSED id[] = "$Id: resolve.c,v 1.6 2003/04/21 10:52:51 alex Exp $"; #include "resolve.h" +#ifdef IDENTAUTH +LOCAL VOID Do_ResolveAddr PARAMS(( struct sockaddr_in *Addr, INT Sock, INT w_fd )); +#else LOCAL VOID Do_ResolveAddr PARAMS(( struct sockaddr_in *Addr, INT w_fd )); +#endif + LOCAL VOID Do_ResolveName PARAMS(( CHAR *Host, INT w_fd )); #ifdef h_errno @@ -46,23 +57,27 @@ LOCAL CHAR *Get_Error PARAMS(( INT H_Error )); GLOBAL VOID Resolve_Init( VOID ) { - /* Modul initialisieren */ + /* Initialize module */ FD_ZERO( &Resolver_FDs ); } /* Resolve_Init */ +#ifdef IDENTAUTH +GLOBAL RES_STAT * +Resolve_Addr( struct sockaddr_in *Addr, int Sock ) +#else GLOBAL RES_STAT * Resolve_Addr( struct sockaddr_in *Addr ) +#endif { - /* IP (asyncron!) aufloesen. Bei Fehler, z.B. wenn der - * Child-Prozess nicht erzeugt werden kann, wird NULL geliefert. - * Der Host kann dann nicht aufgeloest werden. */ + /* Resolve IP (asynchronous!). On errors, e.g. if the child process + * can't be forked, this functions returns NULL. */ RES_STAT *s; INT pid; - /* Speicher anfordern */ + /* Allocate memory */ s = malloc( sizeof( RES_STAT )); if( ! s ) { @@ -70,7 +85,7 @@ Resolve_Addr( struct sockaddr_in *Addr ) return NULL; } - /* Pipe fuer Antwort initialisieren */ + /* Initialize pipe for result */ if( pipe( s->pipe ) != 0 ) { free( s ); @@ -78,11 +93,11 @@ Resolve_Addr( struct sockaddr_in *Addr ) return NULL; } - /* Sub-Prozess erzeugen */ + /* For sub-process */ pid = fork( ); if( pid > 0 ) { - /* Haupt-Prozess */ + /* Main process */ Log( LOG_DEBUG, "Resolver for %s created (PID %d).", inet_ntoa( Addr->sin_addr ), pid ); FD_SET( s->pipe[0], &Resolver_FDs ); if( s->pipe[0] > Conn_MaxFD ) Conn_MaxFD = s->pipe[0]; @@ -91,15 +106,19 @@ Resolve_Addr( struct sockaddr_in *Addr ) } else if( pid == 0 ) { - /* Sub-Prozess */ + /* Sub process */ Log_Init_Resolver( ); +#ifdef IDENTAUTH + Do_ResolveAddr( Addr, Sock, s->pipe[1] ); +#else Do_ResolveAddr( Addr, s->pipe[1] ); +#endif Log_Exit_Resolver( ); exit( 0 ); } else { - /* Fehler */ + /* Error! */ free( s ); Log( LOG_CRIT, "Resolver: Can't fork: %s!", strerror( errno )); return NULL; @@ -110,14 +129,13 @@ Resolve_Addr( struct sockaddr_in *Addr ) GLOBAL RES_STAT * Resolve_Name( CHAR *Host ) { - /* Hostnamen (asyncron!) aufloesen. Bei Fehler, z.B. wenn der - * Child-Prozess nicht erzeugt werden kann, wird NULL geliefert. - * Der Host kann dann nicht aufgeloest werden. */ + /* Resolve hostname (asynchronous!). On errors, e.g. if the child + * process can't be forked, this functions returns NULL. */ RES_STAT *s; INT pid; - /* Speicher anfordern */ + /* Allocate memory */ s = malloc( sizeof( RES_STAT )); if( ! s ) { @@ -125,7 +143,7 @@ Resolve_Name( CHAR *Host ) return NULL; } - /* Pipe fuer Antwort initialisieren */ + /* Initialize the pipe for the result */ if( pipe( s->pipe ) != 0 ) { free( s ); @@ -133,11 +151,11 @@ Resolve_Name( CHAR *Host ) return NULL; } - /* Sub-Prozess erzeugen */ + /* Fork sub-process */ pid = fork( ); if( pid > 0 ) { - /* Haupt-Prozess */ + /* Main process */ Log( LOG_DEBUG, "Resolver for \"%s\" created (PID %d).", Host, pid ); FD_SET( s->pipe[0], &Resolver_FDs ); if( s->pipe[0] > Conn_MaxFD ) Conn_MaxFD = s->pipe[0]; @@ -146,7 +164,7 @@ Resolve_Name( CHAR *Host ) } else if( pid == 0 ) { - /* Sub-Prozess */ + /* Sub process */ Log_Init_Resolver( ); Do_ResolveName( Host, s->pipe[1] ); Log_Exit_Resolver( ); @@ -154,7 +172,7 @@ Resolve_Name( CHAR *Host ) } else { - /* Fehler */ + /* Error! */ free( s ); Log( LOG_CRIT, "Resolver: Can't fork: %s!", strerror( errno )); return NULL; @@ -162,17 +180,26 @@ Resolve_Name( CHAR *Host ) } /* Resolve_Name */ +#ifdef IDENTAUTH +LOCAL VOID +Do_ResolveAddr( struct sockaddr_in *Addr, int Sock, INT w_fd ) +#else LOCAL VOID Do_ResolveAddr( struct sockaddr_in *Addr, INT w_fd ) +#endif { - /* Resolver Sub-Prozess: IP aufloesen und Ergebnis in Pipe schreiben. */ + /* Resolver sub-process: resolve IP address and write result into + * pipe to parent. */ CHAR hostname[HOST_LEN]; struct hostent *h; +#ifdef IDENTAUTH + CHAR *res; +#endif Log_Resolver( LOG_DEBUG, "Now resolving %s ...", inet_ntoa( Addr->sin_addr )); - /* Namen aufloesen */ + /* Resolve IP address */ h = gethostbyaddr( (CHAR *)&Addr->sin_addr, sizeof( Addr->sin_addr ), AF_INET ); if( h ) strlcpy( hostname, h->h_name, sizeof( hostname )); else @@ -185,13 +212,30 @@ Do_ResolveAddr( struct sockaddr_in *Addr, INT w_fd ) strlcpy( hostname, inet_ntoa( Addr->sin_addr ), sizeof( hostname )); } - /* Antwort an Parent schreiben */ +#ifdef IDENTAUTH + /* Do "IDENT" (aka "AUTH") lookup and write result to parent */ + Log_Resolver( LOG_DEBUG, "Doing IDENT lookup on socket %d ...", Sock ); + res = ident_id( Sock, 10 ); + Log_Resolver( LOG_DEBUG, "IDENT lookup on socket %d done.", Sock ); +#endif + + /* Write result into pipe to parent */ if( (size_t)write( w_fd, hostname, strlen( hostname ) + 1 ) != (size_t)( strlen( hostname ) + 1 )) { Log_Resolver( LOG_CRIT, "Resolver: Can't write to parent: %s!", strerror( errno )); close( w_fd ); return; } +#ifdef IDENTAUTH + if( (size_t)write( w_fd, res ? res : "", strlen( res ? res : "" ) + 1 ) != (size_t)( strlen( res ? res : "" ) + 1 )) + { + Log_Resolver( LOG_CRIT, "Resolver: Can't write to parent (IDENT): %s!", strerror( errno )); + close( w_fd ); + free( res ); + return; + } + free( res ); +#endif Log_Resolver( LOG_DEBUG, "Ok, translated %s to \"%s\".", inet_ntoa( Addr->sin_addr ), hostname ); } /* Do_ResolveAddr */ @@ -200,7 +244,8 @@ Do_ResolveAddr( struct sockaddr_in *Addr, INT w_fd ) LOCAL VOID Do_ResolveName( CHAR *Host, INT w_fd ) { - /* Resolver Sub-Prozess: Name aufloesen und Ergebnis in Pipe schreiben. */ + /* Resolver sub-process: resolve name and write result into pipe + * to parent. */ CHAR ip[16]; struct hostent *h; @@ -208,7 +253,7 @@ Do_ResolveName( CHAR *Host, INT w_fd ) Log_Resolver( LOG_DEBUG, "Now resolving \"%s\" ...", Host ); - /* Namen aufloesen */ + /* Resolve hostname */ h = gethostbyname( Host ); if( h ) { @@ -225,7 +270,7 @@ Do_ResolveName( CHAR *Host, INT w_fd ) strcpy( ip, "" ); } - /* Antwort an Parent schreiben */ + /* Write result into pipe to parent */ if( (size_t)write( w_fd, ip, strlen( ip ) + 1 ) != (size_t)( strlen( ip ) + 1 )) { Log_Resolver( LOG_CRIT, "Resolver: Can't write to parent: %s!", strerror( errno )); @@ -242,7 +287,7 @@ Do_ResolveName( CHAR *Host, INT w_fd ) LOCAL CHAR * Get_Error( INT H_Error ) { - /* Fehlerbeschreibung fuer H_Error liefern */ + /* Get error message for H_Error */ switch( H_Error ) { diff --git a/src/ngircd/resolve.h b/src/ngircd/resolve.h index f823110f..716dd51f 100644 --- a/src/ngircd/resolve.h +++ b/src/ngircd/resolve.h @@ -1,6 +1,6 @@ /* * ngIRCd -- The Next Generation IRC Daemon - * Copyright (c)2001,2002 by Alexander Barton (alex@barton.de) + * Copyright (c)2001-2003 by Alexander Barton (alex@barton.de) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -8,7 +8,7 @@ * (at your option) any later version. * Please read the file COPYING, README and AUTHORS for more information. * - * $Id: resolve.h,v 1.5 2003/04/21 10:53:10 alex Exp $ + * $Id: resolve.h,v 1.6 2003/12/27 13:01:12 alex Exp $ * * Asynchronous resolver (header) */ @@ -37,7 +37,12 @@ GLOBAL fd_set Resolver_FDs; GLOBAL VOID Resolve_Init PARAMS(( VOID )); +#ifdef IDENTAUTH +GLOBAL RES_STAT *Resolve_Addr PARAMS(( struct sockaddr_in *Addr, int Sock )); +#else GLOBAL RES_STAT *Resolve_Addr PARAMS(( struct sockaddr_in *Addr )); +#endif + GLOBAL RES_STAT *Resolve_Name PARAMS(( CHAR *Host )); diff --git a/src/testsuite/channel-test.e b/src/testsuite/channel-test.e index 19bed869..240abf64 100644 --- a/src/testsuite/channel-test.e +++ b/src/testsuite/channel-test.e @@ -1,4 +1,4 @@ -# $Id: channel-test.e,v 1.2 2002/09/09 21:26:00 alex Exp $ +# $Id: channel-test.e,v 1.3 2003/12/27 13:01:12 alex Exp $ spawn telnet localhost 6789 expect { @@ -16,7 +16,7 @@ expect { send "join #channel\r" expect { timeout { exit 1 } - ":nick!~user@* JOIN :#channel" + "@* JOIN :#channel" } expect { timeout { exit 1 } @@ -26,13 +26,17 @@ expect { send "topic #channel :Test-Topic\r" expect { timeout { exit 1 } - ":nick!~user@* TOPIC #channel :Test-Topic" + "@* TOPIC #channel :Test-Topic" } send "who #channel\r" expect { timeout { exit 1 } - "352 nick #channel ~user * nick H@ :0 User" + "352 nick #channel" +} +expect { + timeout { exit 1 } + "* nick H@ :0 User" } expect { timeout { exit 1 } @@ -62,7 +66,7 @@ expect { send "part #channel\r" expect { timeout { exit 1 } - ":nick!~user@* PART #channel :nick" + "@* PART #channel :nick" } send "quit\r" diff --git a/src/testsuite/mode-test.e b/src/testsuite/mode-test.e index db564086..b8fff010 100644 --- a/src/testsuite/mode-test.e +++ b/src/testsuite/mode-test.e @@ -1,4 +1,4 @@ -# $Id: mode-test.e,v 1.4 2002/12/15 15:52:34 alex Exp $ +# $Id: mode-test.e,v 1.5 2003/12/27 13:01:12 alex Exp $ spawn telnet localhost 6789 expect { @@ -16,7 +16,7 @@ expect { send "mode nick +i\r" expect { timeout { exit 1 } - ":nick!~user@* MODE nick +i" + "@* MODE nick +i" } send "mode nick\r" @@ -28,7 +28,7 @@ expect { send "mode nick -i\r" expect { timeout { exit 1 } - ":nick!~user@* MODE nick -i" + "@* MODE nick -i" } send "oper TestOp 123\r" @@ -50,7 +50,7 @@ expect { send "join #channel\r" expect { timeout { exit 1 } - ":nick!~user@* JOIN :#channel" + "@* JOIN :#channel" } expect { timeout { exit 1 } @@ -60,7 +60,7 @@ expect { send "mode #channel +tn\r" expect { timeout { exit 1 } - ":nick!~user@* MODE #channel +tn" + "@* MODE #channel +tn" } send "mode #channel\r" @@ -72,31 +72,31 @@ expect { send "mode #channel +v nick\r" expect { timeout { exit 1 } - ":nick!~user@* MODE #channel +v nick" + "@* MODE #channel +v nick" } send "mode #channel +I nick1\r" expect { timeout { exit 1 } - ":nick!~user@* MODE #channel +I nick1!*@*" + "@* MODE #channel +I nick1!*@*" } send "mode #channel +b nick2@domain\r" expect { timeout { exit 1 } - ":nick!~user@* MODE #channel +b nick2!*@domain" + "@* MODE #channel +b nick2!*@domain" } send "mode #channel +I nick3!user\r" expect { timeout { exit 1 } - ":nick!~user@* MODE #channel +I nick3!user@*" + "@* MODE #channel +I nick3!user@*" } send "mode #channel -vo nick nick\r" expect { timeout { exit 1 } - ":nick!~user@* MODE #channel -vo nick nick" + "@* MODE #channel -vo nick nick" } send "quit\r" -- 2.39.2