/* * IRC - Internet Relay Chat, src/modules/server.c * (C) 2004-present The UnrealIRCd Team * * See file AUTHORS in IRC package for additional names of * the programmers. * * 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 * the Free Software Foundation; either version 1, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "unrealircd.h" /* Definitions */ typedef enum AutoConnectStrategy { AUTOCONNECT_PARALLEL = 0, AUTOCONNECT_SEQUENTIAL = 1, AUTOCONNECT_SEQUENTIAL_FALLBACK = 2 } AutoConnectStrategy; typedef struct cfgstruct cfgstruct; struct cfgstruct { AutoConnectStrategy autoconnect_strategy; long connect_timeout; long handshake_timeout; }; /* Forward declarations */ void server_config_setdefaults(cfgstruct *cfg); int server_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); int server_config_run(ConfigFile *cf, ConfigEntry *ce, int type); EVENT(server_autoconnect); EVENT(server_handshake_timeout); void send_channel_modes_sjoin3(Client *to, Channel *channel); CMD_FUNC(cmd_server); CMD_FUNC(cmd_sid); int _verify_link(Client *client, ConfigItem_link **link_out); void _send_protoctl_servers(Client *client, int response); void _send_server_message(Client *client); void _introduce_user(Client *to, Client *acptr); int _check_deny_version(Client *cptr, char *software, int protocol, char *flags); void _broadcast_sinfo(Client *acptr, Client *to, Client *except); int server_sync(Client *cptr, ConfigItem_link *conf, int incoming); void tls_link_notification_verify(Client *acptr, ConfigItem_link *aconf); void server_generic_free(ModData *m); int server_post_connect(Client *client); void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp); static int connect_server_helper(ConfigItem_link *, Client *); /* Global variables */ static char buf[BUFSIZE]; static cfgstruct cfg; static char *last_autoconnect_server = NULL; ModuleHeader MOD_HEADER = { "server", "5.0", "command /server", "UnrealIRCd Team", "unrealircd-6", }; MOD_TEST() { MARK_AS_OFFICIAL_MODULE(modinfo); EfunctionAddVoid(modinfo->handle, EFUNC_SEND_PROTOCTL_SERVERS, _send_protoctl_servers); EfunctionAddVoid(modinfo->handle, EFUNC_SEND_SERVER_MESSAGE, _send_server_message); EfunctionAdd(modinfo->handle, EFUNC_VERIFY_LINK, _verify_link); EfunctionAddVoid(modinfo->handle, EFUNC_INTRODUCE_USER, _introduce_user); EfunctionAdd(modinfo->handle, EFUNC_CHECK_DENY_VERSION, _check_deny_version); EfunctionAddVoid(modinfo->handle, EFUNC_BROADCAST_SINFO, _broadcast_sinfo); EfunctionAddVoid(modinfo->handle, EFUNC_CONNECT_SERVER, _connect_server); HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, server_config_test); return MOD_SUCCESS; } MOD_INIT() { MARK_AS_OFFICIAL_MODULE(modinfo); LoadPersistentPointer(modinfo, last_autoconnect_server, server_generic_free); server_config_setdefaults(&cfg); HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, server_config_run); HookAdd(modinfo->handle, HOOKTYPE_POST_SERVER_CONNECT, 0, server_post_connect); CommandAdd(modinfo->handle, "SERVER", cmd_server, MAXPARA, CMD_UNREGISTERED|CMD_SERVER); CommandAdd(modinfo->handle, "SID", cmd_sid, MAXPARA, CMD_SERVER); return MOD_SUCCESS; } MOD_LOAD() { EventAdd(modinfo->handle, "server_autoconnect", server_autoconnect, NULL, 2000, 0); EventAdd(modinfo->handle, "server_handshake_timeout", server_handshake_timeout, NULL, 1000, 0); return MOD_SUCCESS; } MOD_UNLOAD() { SavePersistentPointer(modinfo, last_autoconnect_server); return MOD_SUCCESS; } /** Convert 'str' to a AutoConnectStrategy value. * @param str The string, eg "parallel" * @returns a valid AutoConnectStrategy value or -1 if not found. */ AutoConnectStrategy autoconnect_strategy_strtoval(char *str) { if (!strcmp(str, "parallel")) return AUTOCONNECT_PARALLEL; if (!strcmp(str, "sequential")) return AUTOCONNECT_SEQUENTIAL; if (!strcmp(str, "sequential-fallback")) return AUTOCONNECT_SEQUENTIAL_FALLBACK; return -1; } /** Convert an AutoConnectStrategy value to a string. * @param val The value to convert to a string * @returns a string, such as "parallel". */ char *autoconnect_strategy_valtostr(AutoConnectStrategy val) { switch (val) { case AUTOCONNECT_PARALLEL: return "parallel"; case AUTOCONNECT_SEQUENTIAL: return "sequential"; case AUTOCONNECT_SEQUENTIAL_FALLBACK: return "sequential-fallback"; default: return "???"; } } void server_config_setdefaults(cfgstruct *cfg) { cfg->autoconnect_strategy = AUTOCONNECT_SEQUENTIAL; cfg->connect_timeout = 10; cfg->handshake_timeout = 20; } int server_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) { int errors = 0; ConfigEntry *cep; if (type != CONFIG_SET) return 0; /* We are only interrested in set::server-linking.. */ if (!ce || strcmp(ce->name, "server-linking")) return 0; for (cep = ce->items; cep; cep = cep->next) { if (!cep->value) { config_error("%s:%i: blank set::server-linking::%s without value", cep->file->filename, cep->line_number, cep->name); errors++; continue; } else if (!strcmp(cep->name, "autoconnect-strategy")) { if (autoconnect_strategy_strtoval(cep->value) < 0) { config_error("%s:%i: set::server-linking::autoconnect-strategy: invalid value '%s'. " "Should be one of: parallel", cep->file->filename, cep->line_number, cep->value); errors++; continue; } } else if (!strcmp(cep->name, "connect-timeout")) { long v = config_checkval(cep->value, CFG_TIME); if ((v < 5) || (v > 30)) { config_error("%s:%i: set::server-linking::connect-timeout should be between 5 and 60 seconds", cep->file->filename, cep->line_number); errors++; continue; } } else if (!strcmp(cep->name, "handshake-timeout")) { long v = config_checkval(cep->value, CFG_TIME); if ((v < 10) || (v > 120)) { config_error("%s:%i: set::server-linking::handshake-timeout should be between 10 and 120 seconds", cep->file->filename, cep->line_number); errors++; continue; } } else { config_error("%s:%i: unknown directive set::server-linking::%s", cep->file->filename, cep->line_number, cep->name); errors++; continue; } } *errs = errors; return errors ? -1 : 1; } int server_config_run(ConfigFile *cf, ConfigEntry *ce, int type) { ConfigEntry *cep; if (type != CONFIG_SET) return 0; /* We are only interrested in set::server-linking.. */ if (!ce || strcmp(ce->name, "server-linking")) return 0; for (cep = ce->items; cep; cep = cep->next) { if (!strcmp(cep->name, "autoconnect-strategy")) { cfg.autoconnect_strategy = autoconnect_strategy_strtoval(cep->value); } else if (!strcmp(cep->name, "connect-timeout")) { cfg.connect_timeout = config_checkval(cep->value, CFG_TIME); } else if (!strcmp(cep->name, "handshake-timeout")) { cfg.handshake_timeout = config_checkval(cep->value, CFG_TIME); } } return 1; } int server_needs_linking(ConfigItem_link *aconf) { ConfigItem_deny_link *deny; Client *client; ConfigItem_class *class; /* We're only interested in autoconnect blocks that are valid. Also, we ignore temporary link blocks. */ if (!(aconf->outgoing.options & CONNECT_AUTO) || !aconf->outgoing.hostname || (aconf->flag.temporary == 1)) return 0; class = aconf->class; /* Never do more than one connection attempt per seconds (for the same server) */ if ((aconf->hold > TStime())) return 0; aconf->hold = TStime() + class->connfreq; client = find_client(aconf->servername, NULL); if (client) return 0; /* Server already connected (or connecting) */ if (class->clients >= class->maxclients) return 0; /* Class is full */ /* Check connect rules to see if we're allowed to try the link */ for (deny = conf_deny_link; deny; deny = deny->next) if (unreal_mask_match_string(aconf->servername, deny->mask) && crule_eval(deny->rule)) return 0; /* Yes, this server is a linking candidate */ return 1; } void server_autoconnect_parallel(void) { ConfigItem_link *aconf; for (aconf = conf_link; aconf; aconf = aconf->next) { if (!server_needs_linking(aconf)) continue; connect_server(aconf, NULL, NULL); } } /** Find first (valid) autoconnect server in link blocks. * This function should not be used directly. It is a helper function * for find_next_autoconnect_server(). */ ConfigItem_link *find_first_autoconnect_server(void) { ConfigItem_link *aconf; for (aconf = conf_link; aconf; aconf = aconf->next) { if (!server_needs_linking(aconf)) continue; return aconf; /* found! */ } return NULL; /* none */ } /** Find next server that we should try to autoconnect to. * Taking into account that we last tried server 'current'. * @param current Server the previous autoconnect attempt was made to * @returns A link block, or NULL if no servers are suitable. */ ConfigItem_link *find_next_autoconnect_server(char *current) { ConfigItem_link *aconf; /* If the current autoconnect server is NULL then * just find whichever valid server is first. */ if (current == NULL) return find_first_autoconnect_server(); /* Next code is a bit convulted, it would have * been easier if conf_link was a circular list ;) */ /* Otherwise, walk the list up to 'current' */ for (aconf = conf_link; aconf; aconf = aconf->next) { if (!strcmp(aconf->servername, current)) break; } /* If the 'current' server dissapeared, then let's * just pick the first one from the list. * It is a rare event to have the link { } block * removed of a server that we just happened to * try to link to before, so we can afford to do * it this way. */ if (!aconf) return find_first_autoconnect_server(); /* Check the remainder for the list, in other words: * check all servers after 'current' if they are * ready for an outgoing connection attempt... */ for (aconf = aconf->next; aconf; aconf = aconf->next) { if (!server_needs_linking(aconf)) continue; return aconf; /* found! */ } /* If we get here then there are no valid servers * after 'current', so now check for before 'current' * (and including 'current', since we may * have to autoconnect to that one again, * eg if it is the only autoconnect server)... */ for (aconf = conf_link; aconf; aconf = aconf->next) { if (!server_needs_linking(aconf)) { if (!strcmp(aconf->servername, current)) break; /* need to stop here */ continue; } return aconf; /* found! */ } return NULL; /* none */ } /** Check if we are currently connecting to a server (outgoing). * This function takes into account not only an outgoing TCP/IP connect * or TLS handshake, but also if we are 'somewhat connected' to that * server but have not completed the full sync, eg we may still need * to receive SIDs or other sync data. * NOTE: This implicitly assumes that outgoing links only go to * servers that will (eventually) send "EOS". * Should be a reasonable assumption given that in nearly all * cases we only connect to UnrealIRCd servers for the outgoing * case, as services are "always" incoming links. * @returns 1 if an outgoing link is in progress, 0 if not. */ int current_outgoing_link_in_process(void) { Client *client; list_for_each_entry(client, &unknown_list, lclient_node) { if (client->server && *client->server->by && client->local->creationtime && (IsConnecting(client) || IsTLSConnectHandshake(client) || !IsSynched(client))) { return 1; } } list_for_each_entry(client, &server_list, special_node) { if (client->server && *client->server->by && client->local->creationtime && (IsConnecting(client) || IsTLSConnectHandshake(client) || !IsSynched(client))) { return 1; } } return 0; } void server_autoconnect_sequential(void) { ConfigItem_link *aconf; if (current_outgoing_link_in_process()) return; /* We are currently not in the process of doing an outgoing connect, * let's see if we need to connect to somewhere... */ aconf = find_next_autoconnect_server(last_autoconnect_server); if (aconf == NULL) return; /* No server to connect to at this time */ /* Start outgoing link attempt */ safe_strdup(last_autoconnect_server, aconf->servername); connect_server(aconf, NULL, NULL); } /** Perform autoconnect to servers that are not linked yet. */ EVENT(server_autoconnect) { switch (cfg.autoconnect_strategy) { case AUTOCONNECT_PARALLEL: server_autoconnect_parallel(); break; case AUTOCONNECT_SEQUENTIAL: /* Fallback is the same as sequential but we reset last_autoconnect_server on connect */ case AUTOCONNECT_SEQUENTIAL_FALLBACK: server_autoconnect_sequential(); break; } } EVENT(server_handshake_timeout) { Client *client, *next; list_for_each_entry_safe(client, next, &unknown_list, lclient_node) { /* We are only interested in outgoing server connects */ if (!client->server || !*client->server->by || !client->local->creationtime) continue; /* Handle set::server-linking::connect-timeout */ if ((IsConnecting(client) || IsTLSConnectHandshake(client)) && ((TStime() - client->local->creationtime) >= cfg.connect_timeout)) { /* If this is a connect timeout to an outgoing server then notify ops & log it */ unreal_log(ULOG_INFO, "link", "LINK_CONNECT_TIMEOUT", client, "Connect timeout while trying to link to server '$client' ($client.ip)"); exit_client(client, NULL, "Connection timeout"); continue; } /* Handle set::server-linking::handshake-timeout */ if ((TStime() - client->local->creationtime) >= cfg.handshake_timeout) { /* If this is a handshake timeout to an outgoing server then notify ops & log it */ unreal_log(ULOG_INFO, "link", "LINK_HANDSHAKE_TIMEOUT", client, "Connect handshake timeout while trying to link to server '$client' ($client.ip)"); exit_client(client, NULL, "Handshake Timeout"); continue; } } } /** Check deny version { } blocks. * @param cptr Client (a server) * @param software Software version in use (can be NULL) * @param protoctol UnrealIRCd protocol version in use (can be 0) * @param flags Server flags (hardly ever used, can be NULL) * @returns 1 if link is denied (client is already killed), 0 if not. */ int _check_deny_version(Client *cptr, char *software, int protocol, char *flags) { ConfigItem_deny_version *vlines; for (vlines = conf_deny_version; vlines; vlines = vlines->next) { if (match_simple(vlines->mask, cptr->name)) break; } if (vlines) { char *proto = vlines->version; char *vflags = vlines->flags; int result = 0, i; switch (*proto) { case '<': proto++; if (protocol < atoi(proto)) result = 1; break; case '>': proto++; if (protocol > atoi(proto)) result = 1; break; case '=': proto++; if (protocol == atoi(proto)) result = 1; break; case '!': proto++; if (protocol != atoi(proto)) result = 1; break; default: if (protocol == atoi(proto)) result = 1; break; } if (protocol == 0 || *proto == '*') result = 0; if (result) { exit_client(cptr, NULL, "Denied by deny version { } block"); return 0; } if (flags) { for (i = 0; vflags[i]; i++) { if (vflags[i] == '!') { i++; if (strchr(flags, vflags[i])) { result = 1; break; } } else if (!strchr(flags, vflags[i])) { result = 1; break; } } if (*vflags == '*' || !strcmp(flags, "0")) result = 0; } if (result) { exit_client(cptr, NULL, "Denied by deny version { } block"); return 0; } } return 1; } /** Send our PROTOCTL SERVERS=x,x,x,x stuff. * When response is set, it will be PROTOCTL SERVERS=*x,x,x (mind the asterisk). */ void _send_protoctl_servers(Client *client, int response) { char buf[512]; Client *acptr; int sendit = 1; sendto_one(client, NULL, "PROTOCTL EAUTH=%s,%d,%s%s,%s", me.name, UnrealProtocol, serveropts, extraflags ? extraflags : "", version); ircsnprintf(buf, sizeof(buf), "PROTOCTL SERVERS=%s", response ? "*" : ""); list_for_each_entry(acptr, &global_server_list, client_node) { snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%s,", acptr->id); sendit = 1; if (strlen(buf) > sizeof(buf)-12) { if (buf[strlen(buf)-1] == ',') buf[strlen(buf)-1] = '\0'; sendto_one(client, NULL, "%s", buf); /* We use the asterisk here too for continuation lines */ ircsnprintf(buf, sizeof(buf), "PROTOCTL SERVERS=*"); sendit = 0; } } /* Remove final comma (if any) */ if (buf[strlen(buf)-1] == ',') buf[strlen(buf)-1] = '\0'; if (sendit) sendto_one(client, NULL, "%s", buf); } void _send_server_message(Client *client) { if (client->server && client->server->flags.server_sent) { #ifdef DEBUGMODE abort(); #endif return; } sendto_one(client, NULL, "SERVER %s 1 :U%d-%s%s-%s %s", me.name, UnrealProtocol, serveropts, extraflags ? extraflags : "", me.id, me.info); if (client->server) client->server->flags.server_sent = 1; } #define LINK_DEFAULT_ERROR_MSG "Link denied (No link block found with your server name or link::incoming::mask did not match)" /** Verify server link. * This does authentication and authorization checks. * @param cptr The client directly connected to us (cptr). * @param client The client which (originally) issued the server command (client). * @param link_out Pointer-to-pointer-to-link block. Will be set when auth OK. Caller may pass NULL if he doesn't care. * @returns This function returns 1 on successful authentication, 0 otherwise - in which case the client has been killed. */ int _verify_link(Client *client, ConfigItem_link **link_out) { ConfigItem_link *link, *orig_link; Client *acptr = NULL, *ocptr = NULL; ConfigItem_ban *bconf; /* We set the sockhost here so you can have incoming masks based on hostnames. * Perhaps a bit late to do it here, but does anyone care? */ if (client->local->hostp && client->local->hostp->h_name) set_sockhost(client, client->local->hostp->h_name); if (link_out) *link_out = NULL; if (!client->local->passwd) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_NO_PASSWORD", client, "Link with server $client.details denied: No password provided. Protocol error."); exit_client(client, NULL, "Missing password"); return 0; } if (client->server && client->server->conf) { /* This is an outgoing connect so we already know what link block we are * dealing with. It's the one in: client->server->conf */ /* Actually we still need to double check the servername to avoid confusion. */ if (strcasecmp(client->name, client->server->conf->servername)) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_SERVERNAME_MISMATCH", client, "Link with server $client.details denied: " "Outgoing connect from link block '$link_block' but server " "introduced itself as '$client'. Server name mismatch.", log_data_link_block(client->server->conf)); exit_client_fmt(client, NULL, "Servername (%s) does not match name in my link block (%s)", client->name, client->server->conf->servername); return 0; } link = client->server->conf; goto skip_host_check; } else { /* Hunt the linkblock down ;) */ for(link = conf_link; link; link = link->next) if (match_simple(link->servername, client->name)) break; } if (!link) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_UNKNOWN_SERVER", client, "Link with server $client.details denied: No link block named '$client'"); exit_client(client, NULL, LINK_DEFAULT_ERROR_MSG); return 0; } if (!link->incoming.mask) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_NO_INCOMING", client, "Link with server $client.details denied: Link block exists, but there is no link::incoming::mask set.", log_data_link_block(link)); exit_client(client, NULL, LINK_DEFAULT_ERROR_MSG); return 0; } orig_link = link; link = find_link(client->name, client); if (!link) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_INCOMING_MASK_MISMATCH", client, "Link with server $client.details denied: Server is in link block but link::incoming::mask didn't match", log_data_link_block(orig_link)); exit_client(client, NULL, LINK_DEFAULT_ERROR_MSG); return 0; } skip_host_check: /* Try to authenticate the server... */ if (!Auth_Check(client, link->auth, client->local->passwd)) { /* Let's help admins a bit with a good error message in case * they mix different authentication systems (plaintext password * vs an "TLS Auth type" like spkifp/tlsclientcert/tlsclientcertfp). * The 'if' statement below is a bit complex but it consists of 2 things: * 1. Check if our side expects a plaintext password but we did not receive one * 2. Check if our side expects a non-plaintext password but we did receive one */ if (((link->auth->type == AUTHTYPE_PLAINTEXT) && client->local->passwd && !strcmp(client->local->passwd, "*")) || ((link->auth->type != AUTHTYPE_PLAINTEXT) && client->local->passwd && strcmp(client->local->passwd, "*"))) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_AUTH_FAILED", client, "Link with server $client.details denied: Authentication failed: $auth_failure_msg", log_data_string("auth_failure_msg", "different password types on both sides of the link\n" "Read https://www.unrealircd.org/docs/FAQ#auth-fail-mixed for more information"), log_data_link_block(link)); } else if (link->auth->type == AUTHTYPE_SPKIFP) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_AUTH_FAILED", client, "Link with server $client.details denied: Authentication failed: $auth_failure_msg", log_data_string("auth_failure_msg", "spkifp mismatch"), log_data_link_block(link)); } else if (link->auth->type == AUTHTYPE_TLS_CLIENTCERT) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_AUTH_FAILED", client, "Link with server $client.details denied: Authentication failed: $auth_failure_msg", log_data_string("auth_failure_msg", "tlsclientcert mismatch"), log_data_link_block(link)); } else if (link->auth->type == AUTHTYPE_TLS_CLIENTCERTFP) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_AUTH_FAILED", client, "Link with server $client.details denied: Authentication failed: $auth_failure_msg", log_data_string("auth_failure_msg", "certfp mismatch"), log_data_link_block(link)); } else { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_AUTH_FAILED", client, "Link with server $client.details denied: Authentication failed: $auth_failure_msg", log_data_string("auth_failure_msg", "bad password"), log_data_link_block(link)); } exit_client(client, NULL, "Link denied (Authentication failed)"); return 0; } /* Verify the TLS certificate (if requested) */ if (link->verify_certificate) { char *errstr = NULL; if (!IsTLS(client)) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_VERIFY_CERTIFICATE_FAILED", client, "Link with server $client.details denied: verify-certificate failed: $certificate_failure_msg", log_data_string("certificate_failure_msg", "not using TLS"), log_data_link_block(link)); exit_client(client, NULL, "Link denied (Not using TLS)"); return 0; } if (!verify_certificate(client->local->ssl, link->servername, &errstr)) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_VERIFY_CERTIFICATE_FAILED", client, "Link with server $client.details denied: verify-certificate failed: $certificate_failure_msg", log_data_string("certificate_failure_msg", errstr), log_data_link_block(link)); exit_client(client, NULL, "Link denied (Certificate verification failed)"); return 0; } } if ((bconf = find_ban(NULL, client->name, CONF_BAN_SERVER))) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_SERVER_BAN", client, "Link with server $client.details denied: " "Server is banned ($ban_reason)", log_data_string("ban_reason", bconf->reason), log_data_link_block(link)); exit_client_fmt(client, NULL, "Banned server: %s", bconf->reason); return 0; } if (link->class->clients + 1 > link->class->maxclients) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_CLASS_FULL", client, "Link with server $client.details denied: " "class '$link_block.class' is full", log_data_link_block(link)); exit_client(client, NULL, "Full class"); return 0; } if (!IsLocalhost(client) && (iConf.plaintext_policy_server == POLICY_DENY) && !IsSecure(client)) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_NO_TLS", client, "Link with server $client.details denied: " "Server needs to use TLS (set::plaintext-policy::server is 'deny')\n" "See https://www.unrealircd.org/docs/FAQ#server-requires-tls", log_data_link_block(link)); exit_client(client, NULL, "Servers need to use TLS (set::plaintext-policy::server is 'deny')"); return 0; } if (IsSecure(client) && (iConf.outdated_tls_policy_server == POLICY_DENY) && outdated_tls_client(client)) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_OUTDATED_TLS", client, "Link with server $client.details denied: " "Server is using an outdated TLS protocol or cipher ($tls_cipher) and set::outdated-tls-policy::server is 'deny'.\n" "See https://www.unrealircd.org/docs/FAQ#server-outdated-tls", log_data_link_block(link), log_data_string("tls_cipher", tls_get_cipher(client))); exit_client(client, NULL, "Server using outdates TLS protocol or cipher (set::outdated-tls-policy::server is 'deny')"); return 0; } /* This one is at the end, because it causes us to delink another server, * so we want to be (reasonably) sure that this one will succeed before * breaking the other one. */ if ((acptr = find_server(client->name, NULL))) { if (IsMe(acptr)) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_SERVER_EXISTS", client, "Link with server $client.details denied: " "Server is trying to link with my name ($me_name)", log_data_string("me_name", me.name), log_data_link_block(link)); exit_client(client, NULL, "Server Exists (server trying to link with same name as myself)"); return 0; } else { unreal_log(ULOG_ERROR, "link", "LINK_DROPPED_REINTRODUCED", client, "Link with server $client.details causes older link " "with same server via $existing_client.server.uplink to be dropped.", log_data_client("existing_client", acptr), log_data_link_block(link)); exit_client_ex(acptr, client->direction, NULL, "Old link dropped, resyncing"); } } if (link_out) *link_out = link; return 1; } /** Server command. Only for locally connected servers!!. * parv[1] = server name * parv[2] = hop count * parv[3] = server description, may include protocol version and other stuff too (VL) */ CMD_FUNC(cmd_server) { const char *servername = NULL; /* Pointer for servername */ char *ch = NULL; /* */ char descbuf[BUFSIZE]; int hop = 0; char info[REALLEN + 61]; ConfigItem_link *aconf = NULL; ConfigItem_deny_link *deny; char *flags = NULL, *protocol = NULL, *inf = NULL, *num = NULL; int incoming; if (IsUser(client)) { sendnumeric(client, ERR_ALREADYREGISTRED); sendnotice(client, "*** Sorry, but your IRC program doesn't appear to support changing servers."); return; } if (parc < 4 || (!*parv[3])) { exit_client(client, NULL, "Not enough SERVER parameters"); return; } servername = parv[1]; /* Remote 'SERVER' command is not possible on a 100% SID network */ if (!MyConnect(client)) { unreal_log(ULOG_ERROR, "link", "LINK_OLD_PROTOCOL", client, "Server link $client tried to introduce $servername using SERVER command. " "Server is using an old and unsupported protocol from UnrealIRCd 3.2.x or earlier. " "See https://www.unrealircd.org/docs/FAQ#old-server-protocol", log_data_string("servername", servername)); exit_client(client->direction, NULL, "Introduced another server with unsupported protocol"); return; } if (client->local->listener && (client->local->listener->options & LISTENER_CLIENTSONLY)) { exit_client(client, NULL, "This port is for clients only"); return; } if (!valid_server_name(servername)) { exit_client(client, NULL, "Bogus server name"); return; } if (!client->local->passwd) { exit_client(client, NULL, "Missing password"); return; } /* We set the client->name early here, even though it is not authenticated yet. * Reason is that it makes the notices and logging more useful. * This should be safe as it is not in the server linked list yet or hash table. * CMTSRV941 -- Syzop. */ strlcpy(client->name, servername, sizeof(client->name)); if (!verify_link(client, &aconf)) return; /* Rejected */ /* From this point the server is authenticated, so we can be more verbose * with notices to ircops and in exit_client() and such. */ if (strlen(client->id) != 3) { unreal_log(ULOG_ERROR, "link", "LINK_OLD_PROTOCOL", client, "Server link $servername rejected. Server is using an old and unsupported protocol from UnrealIRCd 3.2.x or earlier. " "See https://www.unrealircd.org/docs/FAQ#old-server-protocol", log_data_string("servername", servername)); exit_client(client, NULL, "Server using old unsupported protocol from UnrealIRCd 3.2.x or earlier. " "See https://www.unrealircd.org/docs/FAQ#old-server-protocol"); return; } hop = atol(parv[2]); if (hop != 1) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_INVALID_HOPCOUNT", client, "Server link $servername rejected. Directly linked server provided a hopcount of $hopcount, while 1 was expected.", log_data_string("servername", servername), log_data_integer("hopcount", hop)); exit_client(client, NULL, "Invalid SERVER message, hop count must be 1"); return; } client->hopcount = hop; strlcpy(info, parv[parc - 1], sizeof(info)); /* Parse "VL" data in description */ if (SupportVL(client)) { char tmp[REALLEN + 61]; inf = protocol = flags = num = NULL; strlcpy(tmp, info, sizeof(tmp)); /* work on a copy */ /* We are careful here to allow invalid syntax or missing * stuff, which mean that particular variable will stay NULL. */ protocol = strtok(tmp, "-"); if (protocol) flags = strtok(NULL, "-"); if (flags) num = strtok(NULL, " "); if (num) inf = strtok(NULL, ""); if (inf) { strlcpy(client->info, inf[0] ? inf : "server", sizeof(client->info)); /* set real description */ if (!_check_deny_version(client, NULL, atoi(protocol), flags)) return; /* Rejected */ } else { strlcpy(client->info, info[0] ? info : "server", sizeof(client->info)); } } else { strlcpy(client->info, info[0] ? info : "server", sizeof(client->info)); } /* Process deny server { } restrictions */ for (deny = conf_deny_link; deny; deny = deny->next) { if (deny->flag.type == CRULE_ALL && unreal_mask_match_string(servername, deny->mask) && crule_eval(deny->rule)) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_DENY_LINK_BLOCK", client, "Server link $servername rejected by deny link { } block.", log_data_string("servername", servername)); exit_client(client, NULL, "Disallowed by connection rule"); return; } } if (aconf->options & CONNECT_QUARANTINE) SetQuarantined(client); ircsnprintf(descbuf, sizeof descbuf, "Server: %s", servername); fd_desc(client->local->fd, descbuf); incoming = IsUnknown(client) ? 1 : 0; if (client->local->passwd) safe_free(client->local->passwd); /* Set up server structure */ free_pending_net(client); SetServer(client); irccounts.me_servers++; irccounts.servers++; irccounts.unknown--; list_move(&client->client_node, &global_server_list); list_move(&client->lclient_node, &lclient_list); list_add(&client->special_node, &server_list); if (find_uline(client->name)) { if (client->server && client->server->features.software && !strncmp(client->server->features.software, "UnrealIRCd-", 11)) { unreal_log(ULOG_ERROR, "link", "BAD_ULINES", client, "Bad ulines! Server $client matches your ulines { } block, but this server " "is an UnrealIRCd server. UnrealIRCd servers should never be ulined as it " "causes security issues. Ulines should only be added for services! " "See https://www.unrealircd.org/docs/FAQ#bad-ulines."); exit_client(client, NULL, "Bad ulines. See https://www.unrealircd.org/docs/FAQ#bad-ulines"); } SetULine(client); } find_or_add(client->name); if (IsSecure(client)) { unreal_log(ULOG_INFO, "link", "SERVER_LINKED", client, "Server linked: $me -> $client [secure: $tls_cipher]", log_data_string("tls_cipher", tls_get_cipher(client)), log_data_client("me", &me)); tls_link_notification_verify(client, aconf); } else { unreal_log(ULOG_INFO, "link", "SERVER_LINKED", client, "Server linked: $me -> $client", log_data_client("me", &me)); /* Print out a warning if linking to a non-TLS server unless it's localhost. * Yeah.. there are still other cases when non-TLS links are fine (eg: local IP * of the same machine), we won't bother with detecting that. -- Syzop */ if (!IsLocalhost(client) && (iConf.plaintext_policy_server == POLICY_WARN)) { unreal_log(ULOG_WARNING, "link", "LINK_WARNING_NO_TLS", client, "Link with server $client.details is unencrypted (not TLS). " "We highly recommend to use TLS for server linking. " "See https://www.unrealircd.org/docs/Linking_servers", log_data_link_block(aconf)); } if (IsSecure(client) && (iConf.outdated_tls_policy_server == POLICY_WARN) && outdated_tls_client(client)) { unreal_log(ULOG_WARNING, "link", "LINK_WARNING_OUTDATED_TLS", client, "Link with server $client.details is using an outdated " "TLS protocol or cipher ($tls_cipher).", log_data_link_block(aconf), log_data_string("tls_cipher", tls_get_cipher(client))); } } add_to_client_hash_table(client->name, client); /* doesnt duplicate client->server if allocted this struct already */ make_server(client); client->uplink = &me; if (!client->server->conf) client->server->conf = aconf; /* Only set serv->conf to aconf if not set already! Bug #0003913 */ if (incoming) client->server->conf->refcount++; client->server->conf->class->clients++; client->local->class = client->server->conf->class; RunHook(HOOKTYPE_SERVER_CONNECT, client); server_sync(client, aconf, incoming); } /** Remote server command (SID). * parv[1] = server name * parv[2] = hop count (always >1) * parv[3] = SID * parv[4] = server description */ CMD_FUNC(cmd_sid) { Client *acptr, *ocptr; ConfigItem_link *aconf; ConfigItem_ban *bconf; int hop; const char *servername = parv[1]; Client *direction = client->direction; /* lazy, since this function may be removed soon */ /* Only allow this command from server sockets */ if (!IsServer(client->direction)) { sendnumeric(client, ERR_NOTFORUSERS, "SID"); return; } if (parc < 4 || BadPtr(parv[3])) { sendnumeric(client, ERR_NEEDMOREPARAMS, "SID"); return; } /* The SID check is done early because we do all the killing by SID, * so we want to know if that won't work first. */ if (!valid_sid(parv[3])) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_INVALID_SID", client, "Denied remote server $servername which was introduced by $client: " "Invalid SID.", log_data_string("servername", servername), log_data_string("sid", parv[3])); /* Since we cannot SQUIT via SID (since it is invalid), this gives * us huge doubts about the accuracy of the uplink, so in this case * we terminate the entire uplink. */ exit_client(client, NULL, "Trying to introduce a server with an invalid SID"); return; } /* Check if server already exists... */ if ((acptr = find_server(servername, NULL))) { /* Found. Bad. Quit. */ if (IsMe(acptr)) { /* This should never happen, not even due to a race condition. * We cannot send SQUIT here either since it is unclear what * side would be squitted. * As said, not really important, as this does not happen anyway. */ unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_DUPLICATE_SERVER_IS_ME", client, "Denied remote server $servername which was introduced by $client: " "Server is using our servername, this should be impossible!", log_data_string("servername", servername)); sendto_one(client, NULL, "ERROR: Server %s exists (it's me!)", me.name); exit_client(client, NULL, "Server Exists"); return; } unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_DUPLICATE_SERVER", client, "Denied remote server $servername which was introduced by $client: " "Already linked via $existing_client.server.uplink.", log_data_string("servername", servername), log_data_client("existing_client", acptr)); // FIXME: oldest should die. // FIXME: code below looks wrong, it checks direction TS instead of anything else acptr = acptr->direction; ocptr = (direction->local->creationtime > acptr->local->creationtime) ? acptr : direction; acptr = (direction->local->creationtime > acptr->local->creationtime) ? direction : acptr; // FIXME: Wait, this kills entire acptr? Without sending SQUIT even :D exit_client(acptr, NULL, "Server Exists"); return; } if ((acptr = find_client(parv[3], NULL))) { unreal_log(ULOG_ERROR, "link", "LINK_DENIED_DUPLICATE_SID_SERVER", client, "Denied server $servername with SID $sid: Server with SID $existing_client.id ($existing_client) is already linked.", log_data_string("servername", servername), log_data_string("sid", parv[3]), log_data_client("existing_client", acptr)); sendto_one(client, NULL, "SQUIT %s :Server with this SID (%s) already exists (%s)", parv[3], parv[3], acptr->name); return; } /* Check deny server { } */ if ((bconf = find_ban(NULL, servername, CONF_BAN_SERVER))) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_SERVER_BAN", client, "Denied remote server $servername which was introduced by $client: " "Server is banned ($ban_reason)", log_data_string("ban_reason", bconf->reason)); /* Before UnrealIRCd 6 this would SQUIT the server who introduced * this server. That seems a bit of an overreaction, so we now * send a SQUIT instead. */ sendto_one(client, NULL, "SQUIT %s :Banned server: %s", parv[3], bconf->reason); return; } /* OK, let us check the data now */ if (!valid_server_name(servername)) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_INVALID_SERVERNAME", client, "Denied remote server $servername which was introduced by $client: " "Invalid server name.", log_data_string("servername", servername)); sendto_one(client, NULL, "SQUIT %s :Invalid servername", parv[3]); return; } hop = atoi(parv[2]); if (hop < 2) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_INVALID_HOP_COUNT", client, "Denied remote server $servername which was introduced by $client: " "Invalid server name.", log_data_string("servername", servername), log_data_integer("hop_count", hop)); sendto_one(client, NULL, "SQUIT %s :Invalid hop count (%d)", parv[3], hop); return; } if (!client->direction->server->conf) { unreal_log(ULOG_ERROR, "link", "BUG_LOST_CONFIG", client, "[BUG] Lost link conf record for link $direction.", log_data_client("direction", direction)); exit_client(client->direction, NULL, "BUG: lost link configuration"); return; } aconf = client->direction->server->conf; if (!aconf->hub) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_NO_HUB", client, "Denied remote server $servername which was introduced by $client: " "Server may not introduce this server ($direction is not a hub).", log_data_string("servername", servername), log_data_client("direction", client->direction)); sendto_one(client, NULL, "SQUIT %s :Server is not permitted to be a hub: %s", parv[3], client->direction->name); return; } if (!match_simple(aconf->hub, servername)) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_NO_MATCHING_HUB", client, "Denied remote server $servername which was introduced by $client: " "Server may not introduce this server ($direction hubmask does not allow it).", log_data_string("servername", servername), log_data_client("direction", client->direction)); sendto_one(client, NULL, "SQUIT %s :Hub config for %s does not allow introducing this server", parv[3], client->direction->name); return; } if (aconf->leaf) { if (!match_simple(aconf->leaf, servername)) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_NO_MATCHING_LEAF", client, "Denied remote server $servername which was introduced by $client: " "Server may not introduce this server ($direction leaf config does not allow it).", log_data_string("servername", servername), log_data_client("direction", client->direction)); sendto_one(client, NULL, "SQUIT %s :Leaf config for %s does not allow introducing this server", parv[3], client->direction->name); return; } } if (aconf->leaf_depth && (hop > aconf->leaf_depth)) { unreal_log(ULOG_ERROR, "link", "REMOTE_LINK_DENIED_LEAF_DEPTH", client, "Denied remote server $servername which was introduced by $client: " "Server may not introduce this server ($direction leaf depth config does not allow it).", log_data_string("servername", servername), log_data_client("direction", client->direction)); sendto_one(client, NULL, "SQUIT %s :Leaf depth config for %s does not allow introducing this server", parv[3], client->direction->name); return; } /* All approved, add the server */ acptr = make_client(direction, find_server(client->name, direction)); strlcpy(acptr->name, servername, sizeof(acptr->name)); acptr->hopcount = hop; strlcpy(acptr->id, parv[3], sizeof(acptr->id)); strlcpy(acptr->info, parv[parc - 1], sizeof(acptr->info)); make_server(acptr); SetServer(acptr); /* If this server is U-lined, or the parent is, then mark it as U-lined */ if (IsULine(client) || find_uline(acptr->name)) SetULine(acptr); irccounts.servers++; find_or_add(acptr->name); add_client_to_list(acptr); add_to_client_hash_table(acptr->name, acptr); add_to_id_hash_table(acptr->id, acptr); list_move(&acptr->client_node, &global_server_list); if (IsULine(client->direction) || IsSynched(client->direction)) { /* Log these (but don't show when still syncing) */ unreal_log(ULOG_INFO, "link", "SERVER_LINKED_REMOTE", acptr, "Server linked: $client -> $other_server", log_data_client("other_server", client)); } RunHook(HOOKTYPE_SERVER_CONNECT, acptr); sendto_server(client, 0, 0, NULL, ":%s SID %s %d %s :%s", acptr->uplink->id, acptr->name, hop + 1, acptr->id, acptr->info); RunHook(HOOKTYPE_POST_SERVER_CONNECT, acptr); } void _introduce_user(Client *to, Client *acptr) { build_umode_string(acptr, 0, SEND_UMODES, buf); sendto_one_nickcmd(to, NULL, acptr, buf); send_moddata_client(to, acptr); if (acptr->user->away) sendto_one(to, NULL, ":%s AWAY :%s", acptr->id, acptr->user->away); if (acptr->user->swhois) { SWhois *s; for (s = acptr->user->swhois; s; s = s->next) { if (CHECKSERVERPROTO(to, PROTO_EXTSWHOIS)) { sendto_one(to, NULL, ":%s SWHOIS %s + %s %d :%s", me.id, acptr->name, s->setby, s->priority, s->line); } else { sendto_one(to, NULL, ":%s SWHOIS %s :%s", me.id, acptr->name, s->line); } } } } #define SafeStr(x) ((x && *(x)) ? (x) : "*") /** Broadcast SINFO. * @param cptr The server to send the information about. * @param to The server to send the information TO (NULL for broadcast). * @param except The direction NOT to send to. * This function takes into account that the server may not * provide all of the detailed info. If any information is * absent we will send 0 for numbers and * for NULL strings. */ void _broadcast_sinfo(Client *acptr, Client *to, Client *except) { char chanmodes[128], buf[512]; if (acptr->server->features.chanmodes[0]) { snprintf(chanmodes, sizeof(chanmodes), "%s,%s,%s,%s", acptr->server->features.chanmodes[0], acptr->server->features.chanmodes[1], acptr->server->features.chanmodes[2], acptr->server->features.chanmodes[3]); } else { strlcpy(chanmodes, "*", sizeof(chanmodes)); } snprintf(buf, sizeof(buf), "%lld %d %s %s %s :%s", (long long)acptr->server->boottime, acptr->server->features.protocol, SafeStr(acptr->server->features.usermodes), chanmodes, SafeStr(acptr->server->features.nickchars), SafeStr(acptr->server->features.software)); if (to) { /* Targetted to one server */ sendto_one(to, NULL, ":%s SINFO %s", acptr->id, buf); } else { /* Broadcast (except one side...) */ sendto_server(except, 0, 0, NULL, ":%s SINFO %s", acptr->id, buf); } } /** Sync all information with server 'client'. * Eg: users, channels, everything. * @param client The newly linked in server * @param aconf The link block that belongs to this server * @note This function (via cmd_server) is called from both sides, so * from the incoming side and the outgoing side. */ int server_sync(Client *client, ConfigItem_link *aconf, int incoming) { Client *acptr; if (incoming) { /* If this is an incomming connection, then we have just received * their stuff and now send our PASS, PROTOCTL and SERVER messages back. */ if (!IsEAuth(client)) /* if eauth'd then we already sent the passwd */ sendto_one(client, NULL, "PASS :%s", (aconf->auth->type == AUTHTYPE_PLAINTEXT) ? aconf->auth->data : "*"); send_proto(client, aconf); send_server_message(client); } /* Broadcast new server to the rest of the network */ sendto_server(client, 0, 0, NULL, ":%s SID %s 2 %s :%s", client->uplink->id, client->name, client->id, client->info); /* Broadcast the just-linked-in featureset to other servers on our side */ broadcast_sinfo(client, NULL, client); /* Send moddata of &me (if any, likely minimal) */ send_moddata_client(client, &me); list_for_each_entry_reverse(acptr, &global_server_list, client_node) { /* acptr->direction == acptr for acptr == client */ if (acptr->direction == client) continue; if (IsServer(acptr)) { sendto_one(client, NULL, ":%s SID %s %d %s :%s", acptr->uplink->id, acptr->name, acptr->hopcount + 1, acptr->id, acptr->info); /* Also signal to the just-linked server which * servers are fully linked. * Now you might ask yourself "Why don't we just * assume every server you get during link phase * is fully linked?", well.. there's a race condition * if 2 servers link (almost) at the same time, * then you would think the other one is fully linked * while in fact he was not.. -- Syzop. */ if (acptr->server->flags.synced) sendto_one(client, NULL, ":%s EOS", acptr->id); /* Send SINFO of our servers to their side */ broadcast_sinfo(acptr, client, NULL); send_moddata_client(client, acptr); /* send moddata of server 'acptr' (if any, likely minimal) */ } } /* Synching nick information */ list_for_each_entry_reverse(acptr, &client_list, client_node) { /* acptr->direction == acptr for acptr == client */ if (acptr->direction == client) continue; if (IsUser(acptr)) introduce_user(client, acptr); } /* ** Last, pass all channels plus statuses */ { Channel *channel; for (channel = channels; channel; channel = channel->nextch) { send_channel_modes_sjoin3(client, channel); if (channel->topic_time) sendto_one(client, NULL, "TOPIC %s %s %lld :%s", channel->name, channel->topic_nick, (long long)channel->topic_time, channel->topic); send_moddata_channel(client, channel); } } /* Send ModData for all member(ship) structs */ send_moddata_members(client); /* pass on TKLs */ tkl_sync(client); RunHook(HOOKTYPE_SERVER_SYNC, client); sendto_one(client, NULL, "NETINFO %i %lld %i %s 0 0 0 :%s", irccounts.global_max, (long long)TStime(), UnrealProtocol, CLOAK_KEY_CHECKSUM, NETWORK_NAME); /* Send EOS (End Of Sync) to the just linked server... */ sendto_one(client, NULL, ":%s EOS", me.id); RunHook(HOOKTYPE_POST_SERVER_CONNECT, client); return 0; } void tls_link_notification_verify(Client *client, ConfigItem_link *aconf) { const char *spki_fp; const char *tls_fp; char *errstr = NULL; int verify_ok; if (!MyConnect(client) || !client->local->ssl || !aconf) return; if ((aconf->auth->type == AUTHTYPE_TLS_CLIENTCERT) || (aconf->auth->type == AUTHTYPE_TLS_CLIENTCERTFP) || (aconf->auth->type == AUTHTYPE_SPKIFP)) { /* Link verified by certificate or SPKI */ return; } if (aconf->verify_certificate) { /* Link verified by trust chain */ return; } tls_fp = moddata_client_get(client, "certfp"); spki_fp = spki_fingerprint(client); if (!tls_fp || !spki_fp) return; /* wtf ? */ /* Only bother the user if we are linking to UnrealIRCd 4.0.16+, * since only for these versions we can give precise instructions. */ if (!client->server || client->server->features.protocol < 4016) return; verify_ok = verify_certificate(client->local->ssl, aconf->servername, &errstr); if (errstr && strstr(errstr, "not valid for hostname")) { unreal_log(ULOG_INFO, "link", "HINT_VERIFY_LINK", client, "You may want to consider verifying this server link.\n" "More information about this can be found on https://www.unrealircd.org/Link_verification\n" "Unfortunately the certificate of server '$client' has a name mismatch:\n" "$tls_verify_error\n" "This isn't a fatal error but it will prevent you from using verify-certificate yes;", log_data_link_block(aconf), log_data_string("tls_verify_error", errstr)); } else if (!verify_ok) { unreal_log(ULOG_INFO, "link", "HINT_VERIFY_LINK", client, "You may want to consider verifying this server link.\n" "More information about this can be found on https://www.unrealircd.org/Link_verification\n" "In short: in the configuration file, change the 'link $client {' block to use this as a password:\n" "password \"$spki_fingerprint\" { spkifp; };\n" "And follow the instructions on the other side of the link as well (which will be similar, but will use a different hash)", log_data_link_block(aconf), log_data_string("spki_fingerprint", spki_fp)); } else { unreal_log(ULOG_INFO, "link", "HINT_VERIFY_LINK", client, "You may want to consider verifying this server link.\n" "More information about this can be found on https://www.unrealircd.org/Link_verification\n" "In short: in the configuration file, add the following to your 'link $client {' block:\n" "verify-certificate yes;\n" "Alternatively, you could use SPKI fingerprint verification. Then change the password in the link block to be:\n" "password \"$spki_fingerprint\" { spki_fp; };", log_data_link_block(aconf), log_data_string("spki_fingerprint", spki_fp)); } } /** This will send "to" a full list of the modes for channel channel, * * Half of it recoded by Syzop: the whole buffering and size checking stuff * looked weird and just plain inefficient. We now fill up our send-buffer * really as much as we can, without causing any overflows of course. */ void send_channel_modes_sjoin3(Client *to, Channel *channel) { MessageTag *mtags = NULL; Member *members; Member *lp; Ban *ban; short nomode, nopara; char tbuf[512]; /* work buffer, for temporary data */ char buf[1024]; /* send buffer */ char *bufptr; /* points somewhere in 'buf' */ char *p; /* points to somewhere in 'tbuf' */ int prebuflen = 0; /* points to after the > : part */ int sent = 0; /* we need this so we send at least 1 message about the channel (eg if +P and no members, no bans, #4459) */ char modebuf[BUFSIZE], parabuf[BUFSIZE]; if (*channel->name != '#') return; nomode = 0; nopara = 0; members = channel->members; /* First we'll send channel, channel modes and members and status */ *modebuf = *parabuf = '\0'; channel_modes(to, modebuf, parabuf, sizeof(modebuf), sizeof(parabuf), channel, 1); /* Strip final space if needed */ if (*parabuf && (parabuf[strlen(parabuf)-1] == ' ')) parabuf[strlen(parabuf)-1] = '\0'; if (!modebuf[1]) nomode = 1; if (!(*parabuf)) nopara = 1; /* Generate a new message (including msgid). * Due to the way SJOIN works, we will use the same msgid for * multiple SJOIN messages to servers. Rest assured that clients * will never see these duplicate msgid's though. They * will see a 'special' version instead with a suffix. */ new_message(&me, NULL, &mtags); if (nomode && nopara) { ircsnprintf(buf, sizeof(buf), ":%s SJOIN %lld %s :", me.id, (long long)channel->creationtime, channel->name); } if (nopara && !nomode) { ircsnprintf(buf, sizeof(buf), ":%s SJOIN %lld %s %s :", me.id, (long long)channel->creationtime, channel->name, modebuf); } if (!nopara && !nomode) { ircsnprintf(buf, sizeof(buf), ":%s SJOIN %lld %s %s %s :", me.id, (long long)channel->creationtime, channel->name, modebuf, parabuf); } prebuflen = strlen(buf); bufptr = buf + prebuflen; /* RULES: * - Use 'tbuf' as a working buffer, use 'p' to advance in 'tbuf'. * Thus, be sure to do a 'p = tbuf' at the top of the loop. * - When one entry has been build, check if strlen(buf) + strlen(tbuf) > BUFSIZE - 8, * if so, do not concat but send the current result (buf) first to the server * and reset 'buf' to only the prebuf part (all until the ':'). * Then, in both cases, concat 'tbuf' to 'buf' and continue * - Be sure to ALWAYS zero terminate (*p = '\0') when the entry has been build. * - Be sure to add a space after each entry ;) * * For a more illustrated view, take a look at the first for loop, the others * are pretty much the same. * * Follow these rules, and things would be smooth and efficient (network-wise), * if you ignore them, expect crashes and/or heap corruption, aka: HELL. * You have been warned. * * Side note: of course things would be more efficient if the prebuf thing would * not be sent every time, but that's another story * -- Syzop */ for (lp = members; lp; lp = lp->next) { p = mystpcpy(tbuf, modes_to_sjoin_prefix(lp->member_modes)); /* eg @+ */ p = mystpcpy(p, lp->client->id); /* nick (well, id) */ *p++ = ' '; *p = '\0'; /* this is: if (strlen(tbuf) + strlen(buf) > BUFSIZE - 8) */ if ((p - tbuf) + (bufptr - buf) > BUFSIZE - 8) { /* Would overflow, so send our current stuff right now (except new stuff) */ sendto_one(to, mtags, "%s", buf); sent++; ircsnprintf(buf, sizeof(buf), ":%s SJOIN %lld %s :", me.id, (long long)channel->creationtime, channel->name); prebuflen = strlen(buf); bufptr = buf + prebuflen; *bufptr = '\0'; } /* concat our stuff.. */ bufptr = mystpcpy(bufptr, tbuf); } for (ban = channel->banlist; ban; ban = ban->next) { p = tbuf; if (SupportSJSBY(to)) p += add_sjsby(p, ban->who, ban->when); *p++ = '&'; p = mystpcpy(p, ban->banstr); *p++ = ' '; *p = '\0'; /* this is: if (strlen(tbuf) + strlen(buf) > BUFSIZE - 8) */ if ((p - tbuf) + (bufptr - buf) > BUFSIZE - 8) { /* Would overflow, so send our current stuff right now (except new stuff) */ sendto_one(to, mtags, "%s", buf); sent++; ircsnprintf(buf, sizeof(buf), ":%s SJOIN %lld %s :", me.id, (long long)channel->creationtime, channel->name); prebuflen = strlen(buf); bufptr = buf + prebuflen; *bufptr = '\0'; } /* concat our stuff.. */ bufptr = mystpcpy(bufptr, tbuf); } for (ban = channel->exlist; ban; ban = ban->next) { p = tbuf; if (SupportSJSBY(to)) p += add_sjsby(p, ban->who, ban->when); *p++ = '"'; p = mystpcpy(p, ban->banstr); *p++ = ' '; *p = '\0'; /* this is: if (strlen(tbuf) + strlen(buf) > BUFSIZE - 8) */ if ((p - tbuf) + (bufptr - buf) > BUFSIZE - 8) { /* Would overflow, so send our current stuff right now (except new stuff) */ sendto_one(to, mtags, "%s", buf); sent++; ircsnprintf(buf, sizeof(buf), ":%s SJOIN %lld %s :", me.id, (long long)channel->creationtime, channel->name); prebuflen = strlen(buf); bufptr = buf + prebuflen; *bufptr = '\0'; } /* concat our stuff.. */ bufptr = mystpcpy(bufptr, tbuf); } for (ban = channel->invexlist; ban; ban = ban->next) { p = tbuf; if (SupportSJSBY(to)) p += add_sjsby(p, ban->who, ban->when); *p++ = '\''; p = mystpcpy(p, ban->banstr); *p++ = ' '; *p = '\0'; /* this is: if (strlen(tbuf) + strlen(buf) > BUFSIZE - 8) */ if ((p - tbuf) + (bufptr - buf) > BUFSIZE - 8) { /* Would overflow, so send our current stuff right now (except new stuff) */ sendto_one(to, mtags, "%s", buf); sent++; ircsnprintf(buf, sizeof(buf), ":%s SJOIN %lld %s :", me.id, (long long)channel->creationtime, channel->name); prebuflen = strlen(buf); bufptr = buf + prebuflen; *bufptr = '\0'; } /* concat our stuff.. */ bufptr = mystpcpy(bufptr, tbuf); } if (buf[prebuflen] || !sent) sendto_one(to, mtags, "%s", buf); free_message_tags(mtags); } void server_generic_free(ModData *m) { safe_free(m->ptr); } int server_post_connect(Client *client) { if (cfg.autoconnect_strategy == AUTOCONNECT_SEQUENTIAL_FALLBACK && last_autoconnect_server && !strcmp(last_autoconnect_server, client->name)) { last_autoconnect_server = NULL; } return 0; } /** Start an outgoing connection to a server, for server linking. * @param aconf Configuration attached to this server * @param by The user initiating the connection (can be NULL) * @param hp The address to connect to. */ void _connect_server(ConfigItem_link *aconf, Client *by, struct hostent *hp) { Client *client; if (!aconf->outgoing.hostname) { /* Actually the caller should make sure that this doesn't happen, * so this error may never be triggered: */ unreal_log(ULOG_ERROR, "link", "LINK_ERROR_NO_OUTGOING", NULL, "Connect to $link_block failed: link block is for incoming only (no link::outgoing::hostname set)", log_data_link_block(aconf)); return; } if (!hp) { /* Remove "cache" */ safe_free(aconf->connect_ip); } /* * If we dont know the IP# for this host and itis a hostname and * not a ip# string, then try and find the appropriate host record. */ if (!aconf->connect_ip) { if (is_valid_ip(aconf->outgoing.hostname)) { /* link::outgoing::hostname is an IP address. No need to resolve host. */ safe_strdup(aconf->connect_ip, aconf->outgoing.hostname); } else { /* It's a hostname, let the resolver look it up. */ int ipv4_explicit_bind = 0; if (aconf->outgoing.bind_ip && (is_valid_ip(aconf->outgoing.bind_ip) == 4)) ipv4_explicit_bind = 1; /* We need this 'aconf->refcount++' or else there's a race condition between * starting resolving the host and the result of the resolver (we could * REHASH in that timeframe) leading to an invalid (freed!) 'aconf'. * -- Syzop, bug #0003689. */ aconf->refcount++; unrealdns_gethostbyname_link(aconf->outgoing.hostname, aconf, ipv4_explicit_bind); unreal_log(ULOG_INFO, "link", "LINK_RESOLVING", NULL, "Resolving hostname $link_block.hostname...", log_data_link_block(aconf)); /* Going to resolve the hostname, in the meantime we return (asynchronous operation) */ return; } } client = make_client(NULL, &me); client->local->hostp = hp; /* * Copy these in so we have something for error detection. */ strlcpy(client->name, aconf->servername, sizeof(client->name)); strlcpy(client->local->sockhost, aconf->outgoing.hostname, HOSTLEN + 1); if (!connect_server_helper(aconf, client)) { fd_close(client->local->fd); --OpenFiles; client->local->fd = -2; free_client(client); /* Fatal error */ return; } /* The socket has been connected or connect is in progress. */ make_server(client); client->server->conf = aconf; client->server->conf->refcount++; if (by && IsUser(by)) strlcpy(client->server->by, by->name, sizeof(client->server->by)); else strlcpy(client->server->by, "AutoConn.", sizeof client->server->by); SetConnecting(client); SetOutgoing(client); irccounts.unknown++; list_add(&client->lclient_node, &unknown_list); set_sockhost(client, aconf->outgoing.hostname); add_client_to_list(client); if (aconf->outgoing.options & CONNECT_TLS) { SetTLSConnectHandshake(client); fd_setselect(client->local->fd, FD_SELECT_WRITE, unreal_tls_client_handshake, client); } else fd_setselect(client->local->fd, FD_SELECT_WRITE, completed_connection, client); unreal_log(ULOG_INFO, "link", "LINK_CONNECTING", client, "Trying to activate link with server $client ($link_block.ip:$link_block.port)...", log_data_link_block(aconf)); } /** Helper function for connect_server() to prepare the actual bind()'ing and connect(). * This will also take care of logging/sending error messages. * @param aconf Configuration entry of the server. * @param client The client entry that we will use and fill in. * @returns 1 on success, 0 on failure. */ static int connect_server_helper(ConfigItem_link *aconf, Client *client) { char *bindip; char buf[BUFSIZE]; if (!aconf->connect_ip) { unreal_log(ULOG_ERROR, "link", "LINK_ERROR_NOIP", client, "Connect to $client failed: no IP address to connect to", log_data_link_block(aconf)); return 0; /* handled upstream or shouldn't happen */ } if (strchr(aconf->connect_ip, ':')) SetIPV6(client); safe_strdup(client->ip, aconf->connect_ip); snprintf(buf, sizeof buf, "Outgoing connection: %s", get_client_name(client, TRUE)); client->local->fd = fd_socket(IsIPV6(client) ? AF_INET6 : AF_INET, SOCK_STREAM, 0, buf); if (client->local->fd < 0) { if (ERRNO == P_EMFILE) { unreal_log(ULOG_ERROR, "link", "LINK_ERROR_MAXCLIENTS", client, "Connect to $client failed: no more sockets available", log_data_link_block(aconf)); return 0; } unreal_log(ULOG_ERROR, "link", "LINK_ERROR_SOCKET", client, "Connect to $client failed: could not create socket: $socket_error", log_data_socket_error(-1), log_data_link_block(aconf)); return 0; } if (++OpenFiles >= maxclients) { unreal_log(ULOG_ERROR, "link", "LINK_ERROR_MAXCLIENTS", client, "Connect to $client failed: no more connections available", log_data_link_block(aconf)); return 0; } set_sockhost(client, aconf->outgoing.hostname); if (!aconf->outgoing.bind_ip && iConf.link_bindip) bindip = iConf.link_bindip; else bindip = aconf->outgoing.bind_ip; if (bindip && strcmp("*", bindip)) { if (!unreal_bind(client->local->fd, bindip, 0, IsIPV6(client))) { unreal_log(ULOG_ERROR, "link", "LINK_ERROR_SOCKET_BIND", client, "Connect to $client failed: could not bind socket to $link_block.bind_ip: $socket_error -- " "Your link::outgoing::bind-ip is probably incorrect.", log_data_socket_error(client->local->fd), log_data_link_block(aconf)); return 0; } } set_sock_opts(client->local->fd, client, IsIPV6(client)); if (!unreal_connect(client->local->fd, client->ip, aconf->outgoing.port, IsIPV6(client))) { unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client, "Connect to $client ($link_block.ip:$link_block.port) failed: $socket_error", log_data_socket_error(client->local->fd), log_data_link_block(aconf)); return 0; } return 1; }