From 9714da7a76806bac4be44a9dde85af06d37df7cf Mon Sep 17 00:00:00 2001 From: Taylor R Campbell Date: Tue, 2 Jan 2024 02:47:29 +0000 Subject: [PATCH] New option [libdefaults] socks4a_proxy. All network traffic to KDC goes through the SOCKS4a proxy if it is configured. This is deliberately kept simple -- and is not generalized to SOCKS4 or SOCKS5 or other types of proxies -- so it is easy to audit for network and DNS leaks. (SOCKS4 works in IP addresses, and so invites DNS leaks. SOCKS5 can be OK, if used judiciously, but takes more work to implement.) XXX Need to audit Heimdal for other kinds of traffic too outside libkrb5. Subsequent changes on this branch will address other parts of libkrb5. XXX Need to combine with https://github.com/heimdal/heimdal/pull/1155 to plug DNS leaks. XXX Need to figure out where the socks4a.c code should go. fix https://github.com/heimdal/heimdal/issues/1151 --- lib/krb5/Makefile.am | 2 + lib/krb5/context.c | 1 + lib/krb5/krb5.conf.5 | 10 + lib/krb5/krb5_locl.h | 3 + lib/krb5/send_to_kdc.c | 126 +++++++++++- lib/krb5/socks4a.c | 382 ++++++++++++++++++++++++++++++++++++ lib/krb5/socks4a.h | 57 ++++++ lib/krb5/verify_krb5_conf.c | 1 + 8 files changed, 580 insertions(+), 2 deletions(-) create mode 100644 lib/krb5/socks4a.c create mode 100644 lib/krb5/socks4a.h diff --git a/lib/krb5/Makefile.am b/lib/krb5/Makefile.am index ecce461dd8..e9eac8d758 100644 --- a/lib/krb5/Makefile.am +++ b/lib/krb5/Makefile.am @@ -241,6 +241,8 @@ dist_libkrb5_la_SOURCES = \ sendauth.c \ set_default_realm.c \ sock_principal.c \ + socks4a.c \ + socks4a.h \ store.c \ store-int.c \ store-int.h \ diff --git a/lib/krb5/context.c b/lib/krb5/context.c index 9d03a80afe..8987e1f75f 100644 --- a/lib/krb5/context.c +++ b/lib/krb5/context.c @@ -114,6 +114,7 @@ init_context_from_config_file(krb5_context context) INIT_FIELD(context, int, max_retries, 3, "max_retries"); INIT_FIELD(context, string, http_proxy, NULL, "http_proxy"); + INIT_FIELD(context, string, socks4a_proxy, NULL, "socks4a_proxy"); ret = krb5_config_get_bool_default(context, NULL, FALSE, "libdefaults", diff --git a/lib/krb5/krb5.conf.5 b/lib/krb5/krb5.conf.5 index 271a0d455d..c55a70b57d 100644 --- a/lib/krb5/krb5.conf.5 +++ b/lib/krb5/krb5.conf.5 @@ -306,6 +306,16 @@ enable this option unconditionally. .It Li warn_pwexpire = Va time How soon to warn for expiring password. Default is seven days. +.It Li socks4a_proxy = Va host Ns Oo Li : Ns Va port Oc +SOCKS4a proxy to use when talking to the KDC. +Default port is 1080. +.Pp +Only TCP service to KDC is allowed in this case, not UDP or HTTP. +.Pp +The KDC hostname is passed to the SOCKS4a proxy verbatim without any +DNS resolution first. +Other DNS resolution, of the proxy address and for any realm mapping or +KDC discovery, may still be done outside the SOCKS4a proxy. .It Li http_proxy = Va proxy-spec A HTTP-proxy to use when talking to the KDC via HTTP. .It Li dns_proxy = Va proxy-spec diff --git a/lib/krb5/krb5_locl.h b/lib/krb5/krb5_locl.h index 75ca24b667..d8394bc8f6 100644 --- a/lib/krb5/krb5_locl.h +++ b/lib/krb5/krb5_locl.h @@ -194,6 +194,8 @@ typedef void (KRB5_LIB_CALL *krb5_gssic_delete_sec_context)( #define KRB5_GSS_IC_FLAG_RELEASE_CRED 1 +struct socks4a; /* XXX */ + #include #include "heim_threads.h" @@ -294,6 +296,7 @@ typedef struct krb5_context_data { const krb5_cc_ops **cc_ops; int num_cc_ops; const char *http_proxy; + const char *socks4a_proxy; const char *time_fmt; krb5_boolean log_utc; const char *default_keytab; diff --git a/lib/krb5/send_to_kdc.c b/lib/krb5/send_to_kdc.c index 5d8ec42156..a142475301 100644 --- a/lib/krb5/send_to_kdc.c +++ b/lib/krb5/send_to_kdc.c @@ -35,6 +35,7 @@ #include "krb5_locl.h" #include "send_to_kdc_plugin.h" +#include "socks4a.h" /** * @section send_to_kdc Locating and sending packets to the KDC @@ -326,11 +327,12 @@ struct host_fun { }; struct host { - enum host_state { CONNECT, CONNECTING, CONNECTED, WAITING_REPLY, DEAD } state; + enum host_state { CONNECT, CONNECTING, PROXYING, CONNECTED, WAITING_REPLY, DEAD } state; krb5_krbhst_info *hi; struct addrinfo *freeai; struct addrinfo *ai; rk_socket_t fd; + struct socks4a *socks4a; const struct host_fun *fun; unsigned int tries; time_t timeout; @@ -393,6 +395,10 @@ deallocate_host(void *ptr) struct host *host = ptr; if (!rk_IS_BAD_SOCKET(host->fd)) rk_closesocket(host->fd); + heim_assert((host->state == PROXYING) == (host->socks4a != NULL), + "inconsistent socks4a proxy state"); + _socks4a_free(host->socks4a); + host->socks4a = NULL; /* paranoia */ krb5_data_free(&host->data); if (host->freeai) freeaddrinfo(host->freeai); @@ -487,6 +493,43 @@ host_connected(krb5_context context, krb5_sendto_ctx ctx, struct host *host) { krb5_error_code ret; + /* + * If we have a SOCKS4a proxy configured, we need to request + * proxying between when the underlying socket connection succeeds + * and when we enter the CONNECTED state meaning we're ready to + * send and receive application data. + */ + if (context->socks4a_proxy) { + if (host->state == CONNECTING) { + /* + * The underlying socket connection has just succeeded. + * Attempt to request proxying and enter the intermediate + * PROXYING state. + */ + debug_host(context, 5, host, "socks4a proxying"); + host->socks4a = NULL; + ret = _socks4a_connect(host->fd, host->fd, + host->hi->hostname, host->hi->port, /*userid*/NULL, + &host->socks4a); + if (ret) { + host_dead(context, host, "socks4a proxy failed"); + return; + } + host->state = PROXYING; + return; + } else { + debug_host(context, 5, host, "socks4a proxied"); + heim_assert(host->state == PROXYING, "bad host_connected state"); + /* + * The proxy has accepted our proxying request. We are now + * ready to enter the CONNECTED state as if we had no proxy + * in the way. + */ + _socks4a_free(host->socks4a); + host->socks4a = NULL; + } + } + host->state = CONNECTED; /* * Now prepare data to send to host @@ -761,6 +804,42 @@ eval_host_state(krb5_context context, if (host->state == CONNECTING && writeable) host_connected(context, ctx, host); + if (host->state == PROXYING) { + /* + * Proxy is still connecting. Do an I/O step to see if we + * can make progress. + */ + debug_host(context, 10, host, "socks4a i/o (reading=%d writing=%d)", + _socks4a_reading(host->socks4a), + _socks4a_writing(host->socks4a)); + heim_assert(context->socks4a_proxy, "proxying without proxy"); + heim_assert(_socks4a_reading(host->socks4a) ? readable : 1, + "woken for read when not readable"); + heim_assert(_socks4a_writing(host->socks4a) ? writeable : 1, + "woken for read when not readable"); + ret = _socks4a_io(host->socks4a); + if (ret) { + _socks4a_free(host->socks4a); + host->socks4a = NULL; + host_dead(context, host, "socks4a proxy failed"); + return 0; /* no reply yet */ + } + if (!_socks4a_connected(host->socks4a)) { + /* + * Proxy is still connecting. Wait until we can do another + * I/O step. + */ + debug_host(context, 10, host, "socks4a still connecting"); + return 0; + } + /* + * Proxy has connected on our behalf, transition to CONNECTED + * and start over since _socks4a_io has consumed the I/O state. + */ + host_connected(context, ctx, host); + return 0; + } + if (readable) { debug_host(context, 5, host, "reading packet"); @@ -827,7 +906,44 @@ submit_request(krb5_context context, krb5_sendto_ctx ctx, krb5_krbhst_info *hi) gettimeofday(&nrstart, NULL); - if (hi->proto == KRB5_KRBHST_HTTP && context->http_proxy) { + if (context->socks4a_proxy) { + char *proxy, *proxyhost, *proxyport; + struct addrinfo hints; + + /* + * Refuse anything but TCP connections when we have a SOCKS4a + * proxy configured. + */ + if (hi->proto != KRB5_KRBHST_TCP) + return KRB5_KDC_UNREACH; + + /* + * Parse `[:]' into parts. + */ + if ((proxy = strdup(context->socks4a_proxy)) == NULL) + return ENOMEM; + proxyhost = proxy; + if ((proxyport = strchr(proxy, ':')) != NULL) + *proxyport++ = '\0'; + + /* + * Set up getaddrinfo hints for stream connection. + */ + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + /* + * Resolve the proxy's address. + */ + ret = getaddrinfo(proxyhost, proxyport ? proxyport : "1080", &hints, + &ai); + free(proxy); + if (ret) + return krb5_eai_to_heim_errno(ret, errno); + freeai = ai; + + } else if (hi->proto == KRB5_KRBHST_HTTP && context->http_proxy) { char *proxy2 = strdup(context->http_proxy); char *el, *proxy = proxy2; struct addrinfo hints; @@ -1012,6 +1128,12 @@ wait_setup(heim_object_t obj, void *iter_ctx, int *stop) FD_SET(h->fd, &wait_ctx->rfds); FD_SET(h->fd, &wait_ctx->wfds); break; + case PROXYING: + if (_socks4a_reading(h->socks4a)) + FD_SET(h->fd, &wait_ctx->rfds); + if (_socks4a_writing(h->socks4a)) + FD_SET(h->fd, &wait_ctx->wfds); + break; default: debug_host(wait_ctx->context, 5, h, "invalid sendto host state"); heim_abort("invalid sendto host state"); diff --git a/lib/krb5/socks4a.c b/lib/krb5/socks4a.c new file mode 100644 index 0000000000..642b1d911d --- /dev/null +++ b/lib/krb5/socks4a.c @@ -0,0 +1,382 @@ +/*- + * Copyright (c) 2024 Taylor R. Campbell + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* https://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol */ + +#define _POSIX_C_SOURCE 200809L + +#include "socks4a.h" + +#include +#include +#include +#include +#include + +/* + * enc16be(buf, x) + * + * Encode the 16-bit integer x in big-endian at buf. + */ +static void +enc16be(void *buf, uint16_t x) +{ + uint8_t *p = buf; + + p[0] = x >> 8; + p[1] = x; +} + +/* + * enc32be(buf, x) + * + * Encode the 32-bit integer x in big-endian at buf. + */ +static void +enc32be(void *buf, uint32_t x) +{ + uint8_t *p = buf; + + p[0] = x >> 24; + p[1] = x >> 16; + p[2] = x >> 8; + p[3] = x; +} + +#define SOCKS4A_MAXUSERHOST0 \ + (SOCKS4A_MAXUSERID + 1 + SOCKS4A_MAXHOSTNAME + 1) + +struct socks4a_request { + uint8_t vn; + uint8_t cd; + uint8_t dstport[2]; + uint8_t dstip[4]; + char userhost[SOCKS4A_MAXUSERHOST0]; +}; + +struct socks4a_reply { + uint8_t vn; + uint8_t cd; + uint8_t dstport[2]; + uint8_t dstip[4]; +}; + +struct socks4a { + int readfd; + int writefd; + char *p; + size_t n; + enum { NO, RD, WR } io; + enum socks4a_state { + CONNECTING_REQ, + CONNECTING_REPLY, + CONNECTED, + } state; + union { + struct socks4a_request request; + struct socks4a_reply reply; + } u; +}; + +/* + * _socks4a_free(S) + * + * Free a SOCKS4a connection state yielded by _socks4a_connect. + */ +void +_socks4a_free(struct socks4a *S) +{ + + free(S); +} + +/* + * strmove0(&p, &n, s) + * + * If the NUL-terminated string s has at most n bytes, including + * NUL terminator, then: + * 1. copy it (including the NUL terminator) to p, + * 2. advance p by the number of bytes copied (including NUL + * terminator), + * 3. reduce n by the number of bytes copied (including NUL + * terminator), and + * 4. return 0. + * + * Otherwise, return E2BIG with no side effects. + */ +static int +strmove0(char **pp, size_t *np, const char *s) +{ + size_t k = strlen(s) + 1; /* count NUL terminator */ + + if (k > *np) + return E2BIG; + memcpy(*pp, s, k); + *pp += k; + *np -= k; + return 0; +} + +/* + * _socks4a_connect(readfd, writefd, hostname, port, userid, &S) + * + * Allocate and initialize state to request a SOCKS4a proxy + * connection, to read from readfd and write to writefd. + * + * On success, store a struct socks4a pointer at S and return 0. + * Caller must free S with _socks4a_free(S) when done. + * + * On failure, return an errno error code. + * + * Only allocates and initializes memory; does not perform I/O. + */ +int +_socks4a_connect(int readfd, int writefd, + const char *hostname, uint16_t port, const char *userid, + struct socks4a **socks4a_ret) +{ + struct socks4a *S = NULL; + char *p; + size_t n; + int error; + + /* + * Validate the userid and hostname input lengths. + */ + if (userid && strlen(userid) > SOCKS4A_MAXUSERID) { + error = EINVAL; + goto out; + } + if (strlen(hostname) > SOCKS4A_MAXHOSTNAME) { + error = EINVAL; + goto out; + } + + /* + * Allocate state for the SOCKS connection. + */ + S = calloc(1, sizeof(*S)); + if (S == NULL) { + error = errno; + goto out; + } + S->readfd = readfd; + S->writefd = writefd; + + /* + * Format the CONNECT request. + */ + memset(&S->u.request, 0, sizeof S->u.request); /* paranoia */ + S->u.request.vn = 4; /* version -- SOCKS4 */ + S->u.request.cd = 1; /* command -- CONNECT */ + enc16be(S->u.request.dstport, port); + enc32be(S->u.request.dstip, 0x00000001); /* 0.0.0.1 -- SOCKS4a */ + p = S->u.request.userhost; + n = sizeof S->u.request.userhost; + error = strmove0(&p, &n, userid ? userid : ""); + if (error) + goto out; + error = strmove0(&p, &n, hostname); + if (error) + goto out; + + /* + * Prepare I/O to send the CONNECT request. + */ + S->state = CONNECTING_REQ; + S->p = (void *)&S->u.request; + S->n = p - (char *)S->p; + S->io = WR; + error = 0; + +out: if (error) { + free(S); + S = NULL; + } + *socks4a_ret = S; + return error; +} + +/* + * _socks4a_connected(S) + * + * True if and only if the SOCKS4a proxy connection has been + * established. + * + * If this returns false, the caller should wait with select/poll + * or equivalent until it can read or write data on readfd or + * writefd, according to _socks4a_reading(S) or + * _socks4a_writing(S), and then call _socks4a_io(S) before + * testing _socks4a_connected(S) again. + * + * Once this is true, bytes written to writefd will be sent by the + * proxy to the remote host, and bytes received by the proxy from + * the remote host will come flying out of readfd. + */ +int +_socks4a_connected(const struct socks4a *S) +{ + + return S->state == CONNECTED; +} + +/* + * _socks4a_reading(S) + * + * If the SOCKS4a proxy connection is not yet established, true + * iff we are waiting to read a reply from the proxy. + */ +int +_socks4a_reading(const struct socks4a *S) +{ + + return S->io == RD; +} + +/* + * _socks4a_writing(S) + * + * If the SOCKS4a proxy connection is not yet established, true + * iff we are waiting to write a request to the proxy. + */ +int +_socks4a_writing(const struct socks4a *S) +{ + + return S->io == WR; +} + +/* + * _socks4a_io(S) + * + * Do an I/O step to establish a SOCKS4a proxy connection. To be + * called when (a) the SOCKS4a proxy connection has yet to be + * established, and (b) the I/O needed by the SOCKS4a protocol -- + * reads if _socks4a_reading(S), writes if _socks4a_writing(S) -- + * is ready. Caller should call _socks4a_connected(S) when this + * succeeds to see if the connection has completed. + * + * Returns 0 on success, errno code on error. EINTR and EAGAIN + * are transient errors; others such as EIO are fatal. + */ +int +_socks4a_io(struct socks4a *S) +{ + enum socks4a_state state = S->state; + ssize_t k; + + /* + * Verify we're in a state where I/O is relevant. Otherwise, + * fail with EINVAL -- this is an application error, most + * likely calling socks4a_io(S) when socks4a_connected(S) is + * already true. + */ + switch (state) { + case CONNECTING_REQ: + case CONNECTING_REPLY: + break; + case CONNECTED: + default: + return EINVAL; + } + + /* + * Do an increment of I/O by reading from or writing to the + * appropriate fd. + */ + switch (S->io) { + case NO: + default: + return EINVAL; /* paranoia */ + case RD: + k = read(S->readfd, S->p, S->n); + if (k == 0) /* EOF */ + return EIO; + break; + case WR: + k = write(S->writefd, S->p, S->n); + break; + } + + /* + * If the read or write failed, it returned an error code in + * errno, so return that. + */ + if (k == -1) + return errno; + + /* + * If the read or write returned more bytes than we asked for, + * something is amiss, so fail with EIO. + */ + if ((size_t)k > S->n) + return EIO; + + /* + * Advance the I/O pointer. If there's more I/O to do, stop + * here and let the caller wait before calling socks4a_io(S) + * again. + */ + S->p += (size_t)k; + S->n -= (size_t)k; + if (S->n > 0) + return 0; + S->io = NO; /* paranoia */ + + /* + * One I/O transfer has completed. Clear the I/O direction out + * of paranoia and transition to the next state. + */ + switch (state) { + case CONNECTING_REQ: /* + * CONNECT request sent. Start reading + * a reply. + */ + S->p = (void *)&S->u.reply; + S->n = sizeof S->u.reply; + S->io = RD; + S->state = CONNECTING_REPLY; + return 0; + case CONNECTING_REPLY: /* + * CONNECT reply received. Parse it + * and determine whether we're + * sucessfully connected or not. + * + * Ignore dstport and dstip -- not + * relevant to CONNECT, only to BIND. + */ + if (S->u.reply.vn != 0) + return EIO; + if (S->u.reply.cd != 0x5a) /* 0x5a: request granted */ + /* XXX report more specific error */ + return ECONNREFUSED; + S->state = CONNECTED; + return 0; + case CONNECTED: + default: /* XXX unreachable */ + return EIO; + } +} diff --git a/lib/krb5/socks4a.h b/lib/krb5/socks4a.h new file mode 100644 index 0000000000..7ab421a6b7 --- /dev/null +++ b/lib/krb5/socks4a.h @@ -0,0 +1,57 @@ +/*- + * Copyright (c) 2024 Taylor R. Campbell + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef SOCKS4A_H +#define SOCKS4A_H + +#include + +/* + * Arbitrary but matches SOCKS5. + */ +#define SOCKS4A_MAXUSERID 255 + +/* + * Binary DNS name -- *(n(1 byte), label(n bytes)), 0(1 byte) -- is + * limited to 255 bytes. Hostname text notation with dots doesn't have + * the zero length byte for the trailing empty label, so that's limited + * to 254 bytes with a trailing dot, or 253 bytes without. To keep it + * simple and allow the trailing dot or not, we'll just take 254 as the + * maximum length. + */ +#define SOCKS4A_MAXHOSTNAME 254 + +struct socks4a; + +int _socks4a_connect(int, int, const char *, uint16_t, const char *, + struct socks4a **); +int _socks4a_connected(const struct socks4a *); +int _socks4a_reading(const struct socks4a *); +int _socks4a_writing(const struct socks4a *); +int _socks4a_io(struct socks4a *); +void _socks4a_free(struct socks4a *); + +#endif /* SOCKS4A_H */ diff --git a/lib/krb5/verify_krb5_conf.c b/lib/krb5/verify_krb5_conf.c index c258a2bd3b..39041eb406 100644 --- a/lib/krb5/verify_krb5_conf.c +++ b/lib/krb5/verify_krb5_conf.c @@ -448,6 +448,7 @@ struct entry libdefaults_entries[] = { { "proxiable", krb5_config_string, check_boolean, 0 }, { "renew_lifetime", krb5_config_string, check_time, 0 }, { "scan_interfaces", krb5_config_string, check_boolean, 0 }, + { "socks4a_proxy", krb5_config_string, check_host, 0 }, { "srv_lookup", krb5_config_string, check_boolean, 0 }, { "srv_try_txt", krb5_config_string, check_boolean, 0 }, { "ticket_lifetime", krb5_config_string, check_time, 0 },