diff --git a/zyklonb.c b/zyklonb.c index d4e66ca..7c00583 100644 --- a/zyklonb.c +++ b/zyklonb.c @@ -22,6 +22,7 @@ #define PROGRAM_VERSION "alpha" #include "common.c" +#include // --- Configuration (application-specific) ------------------------------------ @@ -39,6 +40,11 @@ static struct config_item g_config_table[] = { "reconnect", "on", "Whether to reconnect on error" }, { "reconnect_delay", "5", "Time between reconnecting" }, + { "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" }, + { "socks_port", "1080", "SOCKS port number" }, + { "socks_username", NULL, "SOCKS auth. username" }, + { "socks_password", NULL, "SOCKS auth. password" }, + { "prefix", ":", "The prefix for bot commands" }, { "admin", NULL, "Host mask for administrators" }, { "plugins", NULL, "The plugins to load on startup" }, @@ -313,7 +319,7 @@ error_ssl_1: static bool irc_establish_connection (struct bot_context *ctx, - const char *host, const char *port, bool use_ssl, struct error **e) + const char *host, const char *port, struct error **e) { struct addrinfo gai_hints, *gai_result, *gai_iter; memset (&gai_hints, 0, sizeof gai_hints); @@ -374,14 +380,6 @@ irc_establish_connection (struct bot_context *ctx, } ctx->irc_fd = sockfd; - if (use_ssl && !irc_initialize_ssl (ctx, e)) - { - xclose (ctx->irc_fd); - ctx->irc_fd = -1; - return false; - } - - print_status ("connection established"); return true; } @@ -624,6 +622,409 @@ setup_recovery_handler (struct bot_context *ctx) print_error ("sigaction: %s", strerror (errno)); } +// --- SOCKS 5/4a (blocking implementation) ------------------------------------ + +// These are awkward protocols. Note that the `username' is used differently +// in SOCKS 4a and 5. In the former version, it is the username that you can +// get ident'ed against. In the latter version, it forms a pair with the +// password field and doesn't need to be an actual user on your machine. + +// TODO: make a non-blocking poller-based version of this; we need c-ares + +struct socks_addr +{ + enum socks_addr_type + { + SOCKS_IPV4 = 1, ///< IPv4 address + SOCKS_DOMAIN = 3, ///< Domain name to be resolved + SOCKS_IPV6 = 4 ///< IPv6 address + } + type; ///< The type of this address + union + { + uint8_t ipv4[4]; ///< IPv4 address, network octet order + const char *domain; ///< Domain name + uint8_t ipv6[16]; ///< IPv6 address, network octet order + } + data; ///< The address itself +}; + +struct socks_data +{ + struct socks_addr address; ///< Target address + uint16_t port; ///< Target port + const char *username; ///< Authentication username + const char *password; ///< Authentication password + + struct socks_addr bound_address; ///< Bound address at the server + uint16_t bound_port; ///< Bound port at the server +}; + +static bool +socks_get_socket (struct addrinfo *addresses, int *fd, struct error **e) +{ + int sockfd; + for (; addresses; addresses = addresses->ai_next) + { + sockfd = socket (addresses->ai_family, + addresses->ai_socktype, addresses->ai_protocol); + if (sockfd == -1) + continue; + set_cloexec (sockfd); + + int yes = 1; + soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, + &yes, sizeof yes) != -1); + + if (!connect (sockfd, addresses->ai_addr, addresses->ai_addrlen)) + break; + xclose (sockfd); + } + if (!addresses) + { + error_set (e, "couldn't connect to the SOCKS server"); + return false; + } + *fd = sockfd; + return true; +} + +#define SOCKS_FAIL(...) \ + BLOCK_START \ + error_set (e, __VA_ARGS__); \ + goto fail; \ + BLOCK_END +#define SOCKS_RECV(buf, len) \ + BLOCK_START \ + if ((n = recv (sockfd, (buf), (len), 0)) == -1) \ + SOCKS_FAIL ("%s: %s", "recv", strerror (errno)); \ + if (n != (len)) \ + SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF"); \ + BLOCK_END + +static bool +socks_4a_connect (struct addrinfo *addresses, struct socks_data *data, + int *fd, struct error **e) +{ + int sockfd; + if (!socks_get_socket (addresses, &sockfd, e)) + return false; + + const void *dest_ipv4 = "\x00\x00\x00\x01"; + const char *dest_domain = NULL; + + char buf[INET6_ADDRSTRLEN]; + switch (data->address.type) + { + case SOCKS_IPV4: + dest_ipv4 = data->address.data.ipv4; + break; + case SOCKS_IPV6: + // About the best thing we can do, not sure if it works anywhere at all + if (!inet_ntop (AF_INET6, &data->address.data.ipv6, buf, sizeof buf)) + SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno)); + dest_domain = buf; + break; + case SOCKS_DOMAIN: + dest_domain = data->address.data.domain; + } + + struct str req; + str_init (&req); + str_append_c (&req, 4); // version + str_append_c (&req, 1); // connect + + str_append_c (&req, data->port >> 8); // higher bits of port + str_append_c (&req, data->port); // lower bits of port + str_append_data (&req, dest_ipv4, 4); // destination address + + if (data->username) + str_append (&req, data->username); + str_append_c (&req, '\0'); + + if (dest_domain) + { + str_append (&req, dest_domain); + str_append_c (&req, '\0'); + } + + ssize_t n = send (sockfd, req.str, req.len, 0); + str_free (&req); + if (n == -1) + SOCKS_FAIL ("%s: %s", "send", strerror (errno)); + + uint8_t resp[8]; + SOCKS_RECV (resp, sizeof resp); + if (resp[0] != 0) + SOCKS_FAIL ("protocol error"); + + switch (resp[1]) + { + case 90: + break; + case 91: + SOCKS_FAIL ("request rejected or failed"); + case 92: + SOCKS_FAIL ("%s: %s", "request rejected", + "SOCKS server cannot connect to identd on the client"); + case 93: + SOCKS_FAIL ("%s: %s", "request rejected", + "identd reports different user-id"); + default: + SOCKS_FAIL ("protocol error"); + } + + *fd = sockfd; + return true; + +fail: + xclose (sockfd); + return false; +} + +#undef SOCKS_FAIL +#define SOCKS_FAIL(...) \ + BLOCK_START \ + error_set (e, __VA_ARGS__); \ + return false; \ + BLOCK_END + +static bool +socks_5_userpass_auth (int sockfd, struct socks_data *data, struct error **e) +{ + size_t ulen = strlen (data->username); + if (ulen > 255) + ulen = 255; + + size_t plen = strlen (data->password); + if (plen > 255) + plen = 255; + + uint8_t req[3 + ulen + plen], *p = req; + *p++ = 0x01; // version + *p++ = ulen; // username length + memcpy (p, data->username, ulen); + p += ulen; + *p++ = plen; // password length + memcpy (p, data->password, plen); + p += plen; + + ssize_t n = send (sockfd, req, p - req, 0); + if (n == -1) + SOCKS_FAIL ("%s: %s", "send", strerror (errno)); + + uint8_t resp[2]; + SOCKS_RECV (resp, sizeof resp); + if (resp[0] != 0x01) + SOCKS_FAIL ("protocol error"); + if (resp[1] != 0x00) + SOCKS_FAIL ("authentication failure"); + return true; +} + +static bool +socks_5_auth (int sockfd, struct socks_data *data, struct error **e) +{ + bool can_auth = data->username && data->password; + + uint8_t hello[4]; + hello[0] = 0x05; // version + hello[1] = 1 + can_auth; // number of authentication methods + hello[2] = 0x00; // no authentication required + hello[3] = 0x02; // username/password + + ssize_t n = send (sockfd, hello, 3 + can_auth, 0); + if (n == -1) + SOCKS_FAIL ("%s: %s", "send", strerror (errno)); + + uint8_t resp[2]; + SOCKS_RECV (resp, sizeof resp); + if (resp[0] != 0x05) + SOCKS_FAIL ("protocol error"); + + switch (resp[1]) + { + case 0x02: + if (!can_auth) + SOCKS_FAIL ("protocol error"); + if (!socks_5_userpass_auth (sockfd, data, e)) + return false; + case 0x00: + break; + case 0xFF: + SOCKS_FAIL ("no acceptable authentication methods"); + default: + SOCKS_FAIL ("protocol error"); + } + return true; +} + +static bool +socks_5_send_req (int sockfd, struct socks_data *data, struct error **e) +{ + uint8_t req[4 + 256 + 2], *p = req; + *p++ = 0x05; // version + *p++ = 0x01; // connect + *p++ = 0x00; // reserved + *p++ = data->address.type; + + switch (data->address.type) + { + case SOCKS_IPV4: + memcpy (p, data->address.data.ipv4, sizeof data->address.data.ipv4); + p += sizeof data->address.data.ipv4; + break; + case SOCKS_DOMAIN: + { + size_t dlen = strlen (data->address.data.domain); + if (dlen > 255) + dlen = 255; + + *p++ = dlen; + memcpy (p, data->address.data.domain, dlen); + p += dlen; + break; + } + case SOCKS_IPV6: + memcpy (p, data->address.data.ipv6, sizeof data->address.data.ipv6); + p += sizeof data->address.data.ipv6; + break; + } + *p++ = data->port >> 8; + *p++ = data->port; + + if (send (sockfd, req, p - req, 0) == -1) + SOCKS_FAIL ("%s: %s", "send", strerror (errno)); + return true; +} + +static bool +socks_5_process_resp (int sockfd, struct socks_data *data, struct error **e) +{ + uint8_t resp_header[4]; + ssize_t n; + SOCKS_RECV (resp_header, sizeof resp_header); + if (resp_header[0] != 0x05) + SOCKS_FAIL ("protocol error"); + + switch (resp_header[1]) + { + case 0x00: + break; + case 0x01: SOCKS_FAIL ("general SOCKS server failure"); + case 0x02: SOCKS_FAIL ("connection not allowed by ruleset"); + case 0x03: SOCKS_FAIL ("network unreachable"); + case 0x04: SOCKS_FAIL ("host unreachable"); + case 0x05: SOCKS_FAIL ("connection refused"); + case 0x06: SOCKS_FAIL ("TTL expired"); + case 0x07: SOCKS_FAIL ("command not supported"); + case 0x08: SOCKS_FAIL ("address type not supported"); + default: SOCKS_FAIL ("protocol error"); + } + + switch ((data->bound_address.type = resp_header[3])) + { + case SOCKS_IPV4: + SOCKS_RECV (data->bound_address.data.ipv4, + sizeof data->bound_address.data.ipv4); + break; + case SOCKS_IPV6: + SOCKS_RECV (data->bound_address.data.ipv6, + sizeof data->bound_address.data.ipv6); + break; + case SOCKS_DOMAIN: + { + uint8_t len; + SOCKS_RECV (&len, sizeof len); + + char domain[len + 1]; + SOCKS_RECV (domain, len); + domain[len] = '\0'; + + data->bound_address.data.domain = xstrdup (domain); + } + default: + SOCKS_FAIL ("protocol error"); + } + + uint16_t port; + SOCKS_RECV (&port, sizeof port); + data->bound_port = ntohs (port); + return true; +} + +#undef SOCKS_FAIL +#undef SOCKS_RECV + +static bool +socks_5_connect (struct addrinfo *addresses, struct socks_data *data, + int *fd, struct error **e) +{ + int sockfd; + if (!socks_get_socket (addresses, &sockfd, e)) + return false; + + if (!socks_5_auth (sockfd, data, e) + || !socks_5_send_req (sockfd, data, e) + || !socks_5_process_resp (sockfd, data, e)) + { + xclose (sockfd); + return false; + } + + *fd = sockfd; + return true; +} + +static int +socks_connect (const char *socks_host, const char *socks_port, + const char *host, const char *port, + const char *username, const char *password, struct error **e) +{ + struct addrinfo gai_hints, *gai_result; + memset (&gai_hints, 0, sizeof gai_hints); + gai_hints.ai_socktype = SOCK_STREAM; + + int err = getaddrinfo (socks_host, socks_port, &gai_hints, &gai_result); + if (err) + { + error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err)); + return false; + } + + unsigned long port_no; + const struct servent *serv; + if ((serv = getservbyname (port, "tcp"))) + port_no = (uint16_t) serv->s_port; + else if (!xstrtoul (&port_no, port, 10) || !port_no || port_no > UINT16_MAX) + { + error_set (e, "invalid port number"); + return false; + } + + struct socks_data data = + { .username = username, .password = password, .port = port_no }; + + if (inet_pton (AF_INET, host, &data.address.data.ipv4) == 1) + data.address.type = SOCKS_IPV4; + else if (inet_pton (AF_INET6, host, &data.address.data.ipv6) == 1) + data.address.type = SOCKS_IPV6; + else + { + data.address.type = SOCKS_DOMAIN; + data.address.data.domain = host; + } + + int fd; + bool success = socks_5_connect (gai_result, &data, &fd, NULL) + || socks_4a_connect (gai_result, &data, &fd, e); + + freeaddrinfo (gai_result); + if (data.bound_address.type == SOCKS_DOMAIN) + free ((char *) data.bound_address.data.domain); + return success ? fd : -1; +} + // --- Plugins ----------------------------------------------------------------- /// The name of the special IRC command for interprocess communication @@ -1558,12 +1959,17 @@ irc_connect (struct bot_context *ctx, struct error **e) const char *irc_port = str_map_find (&ctx->config, "irc_port"); const char *ssl_use_str = str_map_find (&ctx->config, "ssl_use"); + const char *socks_host = str_map_find (&ctx->config, "socks_host"); + const char *socks_port = str_map_find (&ctx->config, "socks_port"); + const char *socks_username = str_map_find (&ctx->config, "socks_username"); + const char *socks_password = str_map_find (&ctx->config, "socks_password"); + const char *nickname = str_map_find (&ctx->config, "nickname"); const char *username = str_map_find (&ctx->config, "username"); const char *realname = str_map_find (&ctx->config, "realname"); // We have a default value for these - hard_assert (irc_port && ssl_use_str); + hard_assert (irc_port && ssl_use_str && socks_port); hard_assert (nickname && username && realname); // TODO: again, get rid of `struct error' in here. The question is: how @@ -1581,9 +1987,36 @@ irc_connect (struct bot_context *ctx, struct error **e) return false; } - if (!irc_establish_connection (ctx, irc_host, irc_port, use_ssl, e)) + if (socks_host) + { + char *address = format_host_port_pair (irc_host, irc_port); + char *socks_address = format_host_port_pair (socks_host, socks_port); + print_status ("connecting to %s via %s...", address, socks_address); + free (socks_address); + free (address); + + struct error *error = NULL; + int fd = socks_connect (socks_host, socks_port, irc_host, irc_port, + socks_username, socks_password, &error); + if (fd == -1) + { + error_set (e, "%s: %s", "SOCKS connection failed", error->message); + error_free (error); + return false; + } + ctx->irc_fd = fd; + } + else if (!irc_establish_connection (ctx, irc_host, irc_port, e)) return false; + if (use_ssl && !irc_initialize_ssl (ctx, e)) + { + xclose (ctx->irc_fd); + ctx->irc_fd = -1; + return false; + } + print_status ("connection established"); + // TODO: in exec try: 1/ set blocking, 2/ setsockopt() SO_LINGER, // (struct linger) { .l_onoff = true; .l_linger = 1 /* 1s should do */; } // 3/ /* O_CLOEXEC */ But only if the QUIT message proves unreliable.