diff --git a/gmid.conf.5 b/gmid.conf.5 index 40b2800..f509c5a 100644 --- a/gmid.conf.5 +++ b/gmid.conf.5 @@ -11,7 +11,7 @@ .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -.Dd June 28, 2024 +.Dd August 3, 2024 .Dt GMID.CONF 5 .Os .Sh NAME @@ -521,6 +521,10 @@ Refer to the .Xr tls_config_parse_protocols 3 function for the valid protocol string values. By default, both TLSv1.2 and TLSv1.3 are enabled. +.It Ic proxy-v1 +Use the proxy protocol v1. +If supported by the remote server, this is useful to propagate the +information about the originating IP address and port. .It Ic relay-to Ar host Op Cm port Ar port Relay the request to the given .Ar host diff --git a/gmid.h b/gmid.h index c179f8a..3a28c29 100644 --- a/gmid.h +++ b/gmid.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021, 2022, 2023 Omar Polo + * Copyright (c) 2020-2024 Omar Polo * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -168,6 +168,7 @@ struct proxy { size_t keylen; char *reqca_path; X509_STORE *reqca; + int proxy; /* use the proxy protocol */ TAILQ_ENTRY(proxy) proxies; }; diff --git a/parse.y b/parse.y index 1d1af4f..fffee2a 100644 --- a/parse.y +++ b/parse.y @@ -519,6 +519,9 @@ proxy_opt : CERT string { yyerror("invalid protocols string \"%s\"", $2); free($2); } + | PROXYV1 { + proxy->proxy = 1; + } | RELAY_TO string proxy_port { if (strlcpy(proxy->host, $2, sizeof(proxy->host)) >= sizeof(proxy->host)) diff --git a/proxy.c b/proxy.c index 9c4c568..b419ee9 100644 --- a/proxy.c +++ b/proxy.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022, 2023 Omar Polo + * Copyright (c) 2021-2024 Omar Polo * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -237,6 +237,11 @@ proxy_enqueue_req(struct client *c) if (c->proxybev == NULL) fatal("can't allocate bufferevent"); + evb = EVBUFFER_OUTPUT(c->proxybev); + + if (p->notls && p->proxy) + evbuffer_add(evb, c->buf.data, c->buf.len); + if (!p->notls) { event_set(&c->proxybev->ev_read, c->pfd, EV_READ, proxy_tls_readcb, c->proxybev); @@ -251,7 +256,6 @@ proxy_enqueue_req(struct client *c) serialize_iri(&c->iri, iribuf, sizeof(iribuf)); - evb = EVBUFFER_OUTPUT(c->proxybev); evbuffer_add_printf(evb, "%s\r\n", iribuf); bufferevent_enable(c->proxybev, EV_READ|EV_WRITE); @@ -287,6 +291,40 @@ proxy_handshake(int fd, short event, void *d) proxy_enqueue_req(c); } +static ssize_t +proxy_read_cb(struct tls *ctx, void *buf, size_t buflen, void *cb_arg) +{ + struct client *c = cb_arg; + ssize_t ret; + + ret = read(c->pfd, buf, buflen); + if (ret == -1 && errno == EAGAIN) + ret = TLS_WANT_POLLIN; + return ret; +} + +static ssize_t +proxy_write_cb(struct tls *ctx, const void *buf, size_t len, void *cb_arg) +{ + struct client *c = cb_arg; + ssize_t ret; + size_t left; + + while ((left = c->buf.len - c->buf.read_pos) > 0) { + ret = write(c->pfd, c->buf.data + c->buf.read_pos, left); + if (ret == -1 && errno == EAGAIN) + return (TLS_WANT_POLLOUT); + if (ret <= 0) + return (ret); + c->buf.read_pos += ret; + } + + ret = write(c->pfd, buf, len); + if (ret == -1 && errno == EAGAIN) + ret = TLS_WANT_POLLOUT; + return (ret); +} + static int proxy_setup_tls(struct client *c) { @@ -323,7 +361,8 @@ proxy_setup_tls(struct client *c) if (*(hn = p->sni) == '\0') hn = p->host; - if (tls_connect_socket(c->proxyctx, c->pfd, hn) == -1) + if (tls_connect_cbs(c->proxyctx, proxy_read_cb, proxy_write_cb, c, hn) + == -1) goto err; c->proxyevset = 1; diff --git a/regress/lib.sh b/regress/lib.sh index 6116cd7..561f9bf 100644 --- a/regress/lib.sh +++ b/regress/lib.sh @@ -14,6 +14,7 @@ run_test() { host="$REGRESS_HOST" port=10965 proxy_port=10966 + proxy= config_common="log syslog off" hdr= body= @@ -72,7 +73,7 @@ server "$server_name" { cert "$PWD/localhost.pem" key "$PWD/localhost.key" root "$PWD/testdata" - listen on $host port $port + listen on $host port $port $proxy $2 } EOF diff --git a/regress/regress b/regress/regress index 82d17a9..c3bbed8 100755 --- a/regress/regress +++ b/regress/regress @@ -69,6 +69,7 @@ run_test test_ipv4_addr run_test test_ipv6_addr need_ipv6 run_test test_ipv6_server need_ipv6 run_test test_high_prefork +run_test test_proxy_protocol_v1 # TODO: add test that uses only a TLSv1.2 or TLSv1.3 # TODO: add a test that attempt to serve a non-regular file diff --git a/regress/tests.sh b/regress/tests.sh index 4905dbc..cfc7c6a 100644 --- a/regress/tests.sh +++ b/regress/tests.sh @@ -533,3 +533,32 @@ test_high_prefork() { dont_check_server_alive=yes kill "$(cat gmid.pid)" 2>/dev/null || true } + +test_proxy_protocol_v1() { + rm -f log + proxy=proxy-v1 + gen_config ' +log access "'$PWD'/log" +log style legacy' '' + set_proxy 'proxy-v1' + run + + ggflags="-P localhost:$proxy_port -H localhost.local" + + # This will generate two log entry: one for the "frontend" + # and one for the proxied request. Both should have exactly + # the same IP and port. + fetch_hdr / + check_reply '20 text/gemini' + + n=$(awk '{print $1}' log | uniq | wc -l) + if [ "$n" -ne 1 ]; then + # keep the log for post-mortem analysis + echo + echo "n is $n" + return 1 + fi + + rm -f log + return 0 +} diff --git a/server.c b/server.c index faa8c29..aed958d 100644 --- a/server.c +++ b/server.c @@ -556,21 +556,23 @@ check_matching_certificate(X509_STORE *store, struct client *c) } static int -proxy_socket(struct client *c, const char *host, const char *port) +proxy_socket(struct client *c, struct proxy *p) { struct addrinfo hints, *res, *res0; int r, sock, save_errno; const char *cause = NULL; + char to[NI_MAXHOST], to_port[NI_MAXSERV]; + int err; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; /* XXX: asr_run? :> */ - r = getaddrinfo(host, port, &hints, &res0); + r = getaddrinfo(p->host, p->port, &hints, &res0); if (r != 0) { log_warnx("getaddrinfo(\"%s\", \"%s\"): %s", - host, port, gai_strerror(r)); + p->host, p->port, gai_strerror(r)); return -1; } @@ -595,10 +597,30 @@ proxy_socket(struct client *c, const char *host, const char *port) } if (sock == -1) - log_warn("can't connect to %s:%s: %s", host, port, cause); + log_warn("can't connect to %s:%s: %s", p->host, p->port, cause); + + if (res && sock != -1 && p->proxy) { + err = getnameinfo(res->ai_addr, res->ai_addrlen, + to, sizeof(to), to_port, sizeof(to_port), + NI_NUMERICHOST|NI_NUMERICSERV); + if (err != 0) { + log_warnx("getnameinfo failed: %s", gai_strerror(err)); + strlcpy(to, c->rhost, sizeof(to)); + strlcpy(to_port, c->rserv, sizeof(to_port)); + } - freeaddrinfo(res0); + r = snprintf(c->buf.data, sizeof(c->buf.data), + "PROXY TCP%c %s %s %s %s\r\n", + c->addr->ai_family == AF_INET ? '4' : '6', + c->rhost, to, c->rserv, to_port); + if (r < 0 || (size_t)r >= sizeof(c->buf.data)) { + log_warnx("failed serialize info for the proxy protocol"); + c->buf.data[0] = '\0'; + } else + c->buf.len = r; + } + freeaddrinfo(res0); return sock; } @@ -618,7 +640,7 @@ apply_reverse_proxy(struct client *c) log_debug("opening proxy connection for %s:%s", p->host, p->port); - if ((c->pfd = proxy_socket(c, p->host, p->port)) == -1) { + if ((c->pfd = proxy_socket(c, p)) == -1) { start_reply(c, PROXY_ERROR, "proxy error"); return 1; }