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 },