Verified Commit d588e7b8 authored by ammarfaizi2's avatar ammarfaizi2 🦉
Browse files

Merge tag 'gwproxy-2026-07-16-http-auth' of https://github.com/alviroiskandar/gwproxy

Pull HTTP proxy front-end and authentication from Alviro Iskandar Setiawan:

  - HTTP CONNECT tunnelling with "Basic" proxy authentication (RFC 7617),
    sharing a single credential store with SOCKS5.

  - A forwarding HTTP proxy mode for absolute-form requests
    ("GET http://host/path"), carved out into a standalone, unit-tested
    module, with hop-by-hop header stripping and "Expect: 100-continue"
    handling.

* tag 'gwproxy-2026-07-16-http-auth' of https://github.com/alviroiskandar/gwproxy

:
  t: Test forward-proxy "Expect: 100-continue" handling
  http: Drop the full hop-by-hop header set when forwarding
  http: Add unit tests for the HTTP proxy module
  http: Split the HTTP proxy logic into a standalone module
  docs: Document the forwarding HTTP proxy mode
  gwproxy: Add a forwarding HTTP proxy mode
  http1: Accept absolute-form request targets
  gwproxy: Add HTTP CONNECT "Basic" proxy authentication

Signed-off-by: ammarfaizi2's avatarAmmar Faizi <ammarfaizi2@gnuweeb.org>
parents 4a412e7e 35c4b487
Loading
Loading
Loading
Loading
+19 −5
Original line number Diff line number Diff line
@@ -31,12 +31,13 @@ GWPROXY_CC_SOURCES = \
	$(GWPROXY_DIR)/log.c \
	$(GWPROXY_DIR)/net.c \
	$(GWPROXY_DIR)/ev/epoll.c \
	$(GWPROXY_DIR)/http1.c
	$(GWPROXY_DIR)/http1.c \
	$(GWPROXY_DIR)/http.c

GWPROXY_OBJECTS = $(GWPROXY_CC_SOURCES:%.c=%.c.o)

LIBGWPSOCKS5_TARGET = libgwpsocks5.so
LIBGWPSOCKS5_CC_SOURCES = $(GWPROXY_DIR)/socks5.c
LIBGWPSOCKS5_CC_SOURCES = $(GWPROXY_DIR)/socks5.c $(GWPROXY_DIR)/auth.c
LIBGWPSOCKS5_OBJECTS = $(LIBGWPSOCKS5_CC_SOURCES:%.c=%.c.o)
LIBGWPSOCKS5_TEST_TARGET = $(GWPROXY_DIR)/tests/socks5.t
LIBGWPSOCKS5_TEST_CC_SOURCES = $(GWPROXY_DIR)/tests/socks5.c
@@ -54,9 +55,17 @@ LIBGWDNS_TEST_OBJECTS = $(LIBGWDNS_TEST_CC_SOURCES:%.c=%.c.o)
# is built straight from http1.c rather than a separate test source.
LIBGWHTTP1_TEST_TARGET = $(GWPROXY_DIR)/tests/http1.t

# The HTTP proxy module (http.c) is linked against the HTTP/1 parser (http1.c)
# and the credential store (auth.c); its unit tests live in a separate source.
LIBGWHTTP_TEST_TARGET = $(GWPROXY_DIR)/tests/http.t
LIBGWHTTP_TEST_CC_SOURCES = $(GWPROXY_DIR)/tests/http.c
LIBGWHTTP_TEST_OBJECTS = $(LIBGWHTTP_TEST_CC_SOURCES:%.c=%.c.o)
LIBGWHTTP_OBJECTS = $(GWPROXY_DIR)/http.c.o $(GWPROXY_DIR)/http1.c.o \
		    $(GWPROXY_DIR)/auth.c.o

ALL_TEST_TARGETS = $(LIBGWDNS_TEST_TARGET) $(LIBGWPSOCKS5_TEST_TARGET) \
		   $(LIBGWHTTP1_TEST_TARGET)
ALL_OBJECTS = $(GWPROXY_OBJECTS) $(LIBGWPSOCKS5_OBJECTS) $(LIBGWDNS_OBJECTS) $(LIBGWDNS_TEST_OBJECTS) $(LIBGWPSOCKS5_TEST_OBJECTS)
		   $(LIBGWHTTP1_TEST_TARGET) $(LIBGWHTTP_TEST_TARGET)
ALL_OBJECTS = $(GWPROXY_OBJECTS) $(LIBGWPSOCKS5_OBJECTS) $(LIBGWDNS_OBJECTS) $(LIBGWDNS_TEST_OBJECTS) $(LIBGWPSOCKS5_TEST_OBJECTS) $(LIBGWHTTP_TEST_OBJECTS)
ALL_TARGETS = $(GWPROXY_TARGET) $(LIBGWPSOCKS5_TARGET) $(LIBGWDNS_TARGET) $(ALL_TEST_TARGETS)
ALL_DEPFILES = $(ALL_OBJECTS:.o=.o.d)

@@ -135,6 +144,9 @@ $(LIBGWDNS_TEST_TARGET): $(LIBGWDNS_OBJECTS) $(LIBGWDNS_TEST_OBJECTS) $(LIBGWDNS
$(LIBGWHTTP1_TEST_TARGET): $(GWPROXY_DIR)/http1.c $(GWPROXY_DIR)/http1.h $(GWPROXY_DIR)/common.h $(EXTRA_DEPS)
	$(CC) $(CFLAGS) -DGWNET_HTTP1_TESTS -DGWNET_HTTP1_RUN_TESTS $(LDFLAGS) -o $@ $< $(LIBS)

$(LIBGWHTTP_TEST_TARGET): $(LIBGWHTTP_OBJECTS) $(LIBGWHTTP_TEST_OBJECTS)
	$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

%.c.o: %.c $(EXTRA_DEPS)
	$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@

@@ -152,7 +164,7 @@ IE=LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(shell pwd)
test: test-unit test-integration
	@echo "All tests completed successfully.";

test-unit: $(LIBGWDNS_TEST_TARGET) $(LIBGWPSOCKS5_TEST_TARGET) $(LIBGWHTTP1_TEST_TARGET)
test-unit: $(LIBGWDNS_TEST_TARGET) $(LIBGWPSOCKS5_TEST_TARGET) $(LIBGWHTTP1_TEST_TARGET) $(LIBGWHTTP_TEST_TARGET)
	@echo "Running unit tests...";
	@echo "Testing libgwdns...";
	@$(IE) ./$(LIBGWDNS_TEST_TARGET);
@@ -160,6 +172,8 @@ test-unit: $(LIBGWDNS_TEST_TARGET) $(LIBGWPSOCKS5_TEST_TARGET) $(LIBGWHTTP1_TEST
	@$(IE) ./$(LIBGWPSOCKS5_TEST_TARGET);
	@echo "Testing http1...";
	@$(IE) ./$(LIBGWHTTP1_TEST_TARGET);
	@echo "Testing http...";
	@$(IE) ./$(LIBGWHTTP_TEST_TARGET);
	@echo "Unit tests completed.";

test-integration: $(GWPROXY_TARGET)
+19 −8
Original line number Diff line number Diff line
@@ -10,9 +10,13 @@ Features
  - SOCKS5 CONNECT proxy (RFC 1928):
      - IPv4, IPv6 and domain-name (ATYP) targets.
      - Username/password authentication (RFC 1929), or no authentication.
  - HTTP CONNECT proxy (tunneling).
  - HTTP proxy:
      - CONNECT tunneling.
      - Forwarding of ordinary absolute-form requests
        ("GET http://host/path"), one request per connection.
      - "Basic" proxy authentication (RFC 7617), or no authentication.
  - Combined SOCKS5 + HTTP on a single listening port (auto-detected per
    connection).
    connection), sharing one credential store.
  - Transparent proxy: recover each connection's original destination from
    the SO_ORIGINAL_DST socket option, for use behind an iptables REDIRECT
    rule.
@@ -63,13 +67,17 @@ Run './gwproxy --help' for the complete list of options. A few examples:
  ./gwproxy --bind='[::]:1080' --as-socks5=1

  # SOCKS5 proxy with username/password authentication
  ./gwproxy --bind='[::]:1080' --as-socks5=1 --socks5-auth-file=auth.txt
  ./gwproxy --bind='[::]:1080' --as-socks5=1 --auth-file=auth.txt

  # HTTP CONNECT proxy on port 8080
  # HTTP proxy on port 8080 (handles both CONNECT tunnels and forwarding
  # of ordinary "GET http://host/path" requests)
  ./gwproxy --bind='[::]:8080' --as-http=1

  # SOCKS5 and HTTP CONNECT on the same port
  ./gwproxy --bind='[::]:1080' --as-socks5=1 --as-http=1
  # HTTP CONNECT proxy requiring "Basic" authentication
  ./gwproxy --bind='[::]:8080' --as-http=1 --auth-file=auth.txt

  # SOCKS5 and HTTP CONNECT on the same port, sharing one credential file
  ./gwproxy --bind='[::]:1080' --as-socks5=1 --as-http=1 --auth-file=auth.txt

  # Chain every outgoing connection through an upstream SOCKS5 proxy
  ./gwproxy --bind='[::]:1080' --as-socks5=1 \
@@ -78,8 +86,11 @@ Run './gwproxy --help' for the complete list of options. A few examples:
  # Use the io_uring event loop, and set SO_MARK 100 on outgoing packets
  ./gwproxy --bind='[::]:1080' --as-socks5=1 --event-loop=io_uring --mark=100

The SOCKS5 authentication file holds one "username:password" entry per
line.
The authentication file (--auth-file, also spelled --socks5-auth-file)
holds one "username:password" entry per line. The same file is used for
both SOCKS5 (RFC 1929) and HTTP CONNECT ("Basic", RFC 7617) auth; when it
is set, clients of either protocol must present valid credentials. It is
re-read automatically when the file changes.

Transparent proxy mode takes each connection's original destination from
SO_ORIGINAL_DST, so it must sit behind an iptables REDIRECT rule. gwproxy

src/gwproxy/auth.c

0 → 100644
+356 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0-only
/*
 * auth.c - Shared username/password credential store.
 *
 * Copyright (C) 2026  Alviro Iskandar Setiawan <alviro.iskandar@gnuweeb.org>
 */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdint.h>
#include <errno.h>
#include <pthread.h>

#include "auth.h"

struct auth_entry {
	char	*u, *p;
	uint8_t	ulen, plen;
};

struct gwp_auth {
	FILE			*fp;
	pthread_rwlock_t	lock;
	struct auth_entry	*entries;
	size_t			nr;
	size_t			cap;
};

/*
 * Upper bound on the decoded length of an HTTP "Basic" credential
 * ("user:password"). Usernames and passwords are each capped at 255 bytes
 * by add_auth_entry(), so the longest possible match is 255 + 1 + 255.
 */
#define GWP_AUTH_BASIC_DEC_MAX	(255 + 1 + 255)

static bool is_space(unsigned char c)
{
	return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}

static char *trim_str(char *str)
{
	char *end;

	while (is_space((unsigned char)*str))
		str++;

	/*
	 * Nothing left after trimming leading whitespace. Return early so we
	 * don't form the pointer "str - 1" below, which is undefined behaviour
	 * when str already points at the start of the buffer.
	 */
	if (*str == '\0')
		return str;

	end = str + strlen(str) - 1;
	while (end > str && is_space((unsigned char)*end))
		end--;

	end[1] = '\0';
	return str;
}

static void free_auth_entries(struct gwp_auth *auth)
{
	size_t i;

	if (!auth)
		return;

	for (i = 0; i < auth->nr; i++)
		free(auth->entries[i].u);

	free(auth->entries);
	auth->entries = NULL;
	auth->nr = 0;
	auth->cap = 0;
}

static int add_auth_entry(struct gwp_auth *auth, const char *line, size_t len)
{
	struct auth_entry *ae;
	size_t ulen, plen;
	char *u, *p;

	if (auth->nr >= auth->cap) {
		size_t new_cap = auth->cap ? auth->cap * 2 : 16;
		struct auth_entry *new_entries;

		new_entries = realloc(auth->entries,
				      new_cap * sizeof(*new_entries));
		if (!new_entries)
			return -ENOMEM;

		auth->entries = new_entries;
		auth->cap = new_cap;
	}

	u = malloc(len + 1);
	if (!u)
		return -ENOMEM;

	memcpy(u, line, len);
	u[len] = '\0';

	p = strchr(u, ':');
	if (p)
		*p++ = '\0';

	ulen = strlen(u);
	if (ulen > 255)
		goto out_free_u;

	plen = p ? strlen(p) : 0;
	if (plen > 255)
		goto out_free_u;

	ae = &auth->entries[auth->nr++];
	ae->u = u;
	ae->p = p;
	ae->ulen = ulen;
	ae->plen = plen;
	return 0;

out_free_u:
	free(u);
	return -EINVAL;
}

/*
 * Constant-time byte comparison for credential checks. Unlike memcmp() it does
 * not short-circuit on the first mismatch, so it does not leak (via timing) how
 * many leading bytes of a supplied username/password are correct. A zero length
 * compares equal without dereferencing either pointer, which also makes the
 * empty-password case (p == NULL) well-defined rather than UB.
 */
static bool ct_bytes_eq(const void *a, const void *b, size_t len)
{
	const volatile unsigned char *pa = a;
	const volatile unsigned char *pb = b;
	unsigned char diff = 0;
	size_t i;

	for (i = 0; i < len; i++)
		diff |= pa[i] ^ pb[i];

	return diff == 0;
}

bool gwp_auth_check(struct gwp_auth *auth, const char *u, size_t ulen,
		    const char *p, size_t plen)
{
	bool ret = false;
	size_t i;

	if (!auth)
		return false;

	/*
	 * Read the entry set under the lock; a concurrent gwp_auth_reload()
	 * frees and rebuilds it under the write lock. When there are no
	 * entries nr is 0 and the loop simply does not run.
	 */
	pthread_rwlock_rdlock(&auth->lock);
	for (i = 0; i < auth->nr; i++) {
		const struct auth_entry *ae = &auth->entries[i];
		if (ulen != ae->ulen)
			continue;
		if (plen != ae->plen)
			continue;
		if (!ct_bytes_eq(u, ae->u, ulen))
			continue;
		if (!ct_bytes_eq(p, ae->p, plen))
			continue;
		ret = true;
		break;
	}
	pthread_rwlock_unlock(&auth->lock);
	return ret;
}

int gwp_auth_reload(struct gwp_auth *auth)
{
	char buf[4096], *t;
	size_t l;
	int r = 0;

	if (!auth || !auth->fp)
		return -ENOSYS;

	pthread_rwlock_wrlock(&auth->lock);
	free_auth_entries(auth);
	while (1) {
		t = fgets(buf, sizeof(buf), auth->fp);
		if (!t)
			break;

		t = trim_str(buf);
		l = strlen(t);
		if (!l)
			continue;

		r = add_auth_entry(auth, t, l);
		if (r < 0)
			break;
	}
	rewind(auth->fp);
	pthread_rwlock_unlock(&auth->lock);
	return r;
}

int gwp_auth_create(struct gwp_auth **out, const char *path)
{
	struct gwp_auth *auth;
	FILE *fp;
	int r;

	if (!path || !*path) {
		*out = NULL;
		return 0;
	}

	auth = calloc(1, sizeof(*auth));
	if (!auth)
		return -ENOMEM;

	r = pthread_rwlock_init(&auth->lock, NULL);
	if (r) {
		free(auth);
		return -r;
	}

	fp = fopen(path, "rb");
	if (!fp) {
		r = -errno;
		goto out_destroy_lock;
	}

	auth->fp = fp;
	r = gwp_auth_reload(auth);
	if (r < 0)
		goto out_free_ent;

	*out = auth;
	return 0;

out_free_ent:
	free_auth_entries(auth);
	fclose(auth->fp);
out_destroy_lock:
	pthread_rwlock_destroy(&auth->lock);
	free(auth);
	return r;
}

void gwp_auth_destroy(struct gwp_auth *auth)
{
	if (!auth)
		return;

	pthread_rwlock_destroy(&auth->lock);
	free_auth_entries(auth);
	fclose(auth->fp);
	free(auth);
}

static int b64_val(unsigned char c)
{
	if (c >= 'A' && c <= 'Z')
		return (int)(c - 'A');
	if (c >= 'a' && c <= 'z')
		return (int)(c - 'a') + 26;
	if (c >= '0' && c <= '9')
		return (int)(c - '0') + 52;
	if (c == '+')
		return 62;
	if (c == '/')
		return 63;
	return -1;
}

/*
 * Decode a base64 (RFC 4648) token into @out. Padding '=' and any trailing
 * whitespace terminate the token. Returns the decoded length on success, or
 * -1 on an invalid character or if the output would exceed @out_cap.
 */
static int base64_decode(const char *in, size_t inlen, unsigned char *out,
			 size_t out_cap)
{
	uint32_t acc = 0;
	size_t olen = 0, i;
	int nbits = 0;

	for (i = 0; i < inlen; i++) {
		unsigned char c = (unsigned char)in[i];
		int v;

		if (c == '=' || c == ' ' || c == '\t' || c == '\r' || c == '\n')
			break;

		v = b64_val(c);
		if (v < 0)
			return -1;

		acc = (acc << 6) | (uint32_t)v;
		nbits += 6;
		if (nbits >= 8) {
			nbits -= 8;
			if (olen >= out_cap)
				return -1;
			out[olen++] = (unsigned char)((acc >> nbits) & 0xffu);
		}
	}

	return (int)olen;
}

bool gwp_auth_check_basic(struct gwp_auth *auth, const char *hdr_val)
{
	unsigned char dec[GWP_AUTH_BASIC_DEC_MAX];
	const char *b64;
	const void *colon;
	size_t ulen, plen;
	int dlen;

	if (!auth || !hdr_val)
		return false;

	/* The scheme token is case-insensitive and followed by whitespace. */
	if (strncasecmp(hdr_val, "Basic", 5))
		return false;
	b64 = hdr_val + 5;
	if (*b64 != ' ' && *b64 != '\t')
		return false;
	while (*b64 == ' ' || *b64 == '\t')
		b64++;

	dlen = base64_decode(b64, strlen(b64), dec, sizeof(dec));
	if (dlen < 0)
		return false;

	/* RFC 7617: split on the first ':'; the password may contain ':'. */
	colon = memchr(dec, ':', (size_t)dlen);
	if (!colon)
		return false;

	ulen = (size_t)((const unsigned char *)colon - dec);
	plen = (size_t)dlen - ulen - 1;
	return gwp_auth_check(auth, (const char *)dec, ulen,
			      (const char *)colon + 1, plen);
}

src/gwproxy/auth.h

0 → 100644
+72 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0-only
/*
 * auth.h - Shared username/password credential store.
 *
 * The store is loaded from a colon-separated "user:password" file (one
 * entry per line) and is shared by the SOCKS5 (RFC 1929) and HTTP CONNECT
 * (RFC 7617 "Basic") proxy front-ends. It is safe for concurrent readers
 * across worker threads; reloads take a write lock.
 *
 * Copyright (C) 2026  Alviro Iskandar Setiawan <alviro.iskandar@gnuweeb.org>
 */
#ifndef GWPROXY__AUTH_H
#define GWPROXY__AUTH_H

#include <stddef.h>
#include <stdbool.h>

struct gwp_auth;

/**
 * Create a credential store from the file at @path.
 *
 * If @path is NULL or empty, no store is created: *@out is set to NULL and
 * 0 is returned (i.e. authentication is disabled). Otherwise the file is
 * opened and parsed, and the resulting store is stored in *@out.
 *
 * @param out	Pointer that receives the new store (or NULL if @path is unset).
 * @param path	Path to the "user:password" credential file, or NULL.
 * @return	0 on success, or a negative error code on failure.
 */
int gwp_auth_create(struct gwp_auth **out, const char *path);

/**
 * Free a credential store. Does nothing if @auth is NULL.
 *
 * @param auth	The store to free.
 */
void gwp_auth_destroy(struct gwp_auth *auth);

/**
 * Re-read the credential file, atomically replacing the in-memory entries.
 *
 * @param auth	The store to reload. Must have been created from a file.
 * @return	0 on success, or a negative error code on failure.
 */
int gwp_auth_reload(struct gwp_auth *auth);

/**
 * Check a username/password pair against the store in constant time.
 *
 * @param auth	The store, or NULL. A NULL store never matches.
 * @param u	Username bytes.
 * @param ulen	Username length.
 * @param p	Password bytes (may be NULL when @plen is 0).
 * @param plen	Password length.
 * @return	true if the credentials match an entry, false otherwise.
 */
bool gwp_auth_check(struct gwp_auth *auth, const char *u, size_t ulen,
		    const char *p, size_t plen);

/**
 * Check an HTTP "Proxy-Authorization"/"Authorization" header value that uses
 * the "Basic" scheme (RFC 7617) against the store.
 *
 * @param auth		The store, or NULL. A NULL store never matches.
 * @param hdr_val	The raw header value, e.g. `Basic dXNlcjpwYXNz`. A NULL
 *			or malformed value never matches.
 * @return		true if the decoded credentials match, false otherwise.
 */
bool gwp_auth_check_basic(struct gwp_auth *auth, const char *hdr_val);

#endif /* #ifndef GWPROXY__AUTH_H */
+20 −12
Original line number Diff line number Diff line
@@ -846,8 +846,10 @@ static int upstream_s5_complete(struct gwp_wrk *w, struct gwp_conn_pair *gcp,
		if (unlikely(r))
			return r;
	} else if (gcp->prot_type == GWP_PROT_TYPE_HTTP) {
		memcpy(rbuf, "HTTP/1.1 200 OK\r\n\r\n", 19);
		rlen = 19;
		r = gwp_http_build_connect_reply(gcp->http_conn, rbuf, sizeof(rbuf));
		if (unlikely(r < 0))
			return r;
		rlen = (size_t)r;
	}

	/* Drop the proxy's CONNECT reply, keep any early destination data. */
@@ -1064,10 +1066,17 @@ static int handle_ev_target_conn_result(struct gwp_wrk *w,
		if (r)
			return r;
	} else if (gcp->conn_state == CONN_STATE_HTTP_CONNECT) {
		if (gcp->target.cap < 19)
			return -ENOBUFS;
		memcpy(gcp->target.buf, "HTTP/1.1 200 OK\r\n\r\n", 19);
		gcp->target.len = 19;
		/*
		 * "200 OK" for a CONNECT tunnel, nothing for a forwarding
		 * request -- whose rewritten origin-form request is already
		 * queued in client.buf and flushed to the origin below, with the
		 * origin's response relayed back.
		 */
		r = gwp_http_build_connect_reply(gcp->http_conn, gcp->target.buf,
						 gcp->target.cap);
		if (r < 0)
			return r;
		gcp->target.len = (uint32_t)r;
	}

	gcp->is_target_alive = true;
@@ -1386,13 +1395,12 @@ static int handle_ev_dns_query(struct gwp_wrk *w, struct gwp_conn_pair *gcp)
	return r;
}

static int handle_ev_socks5_auth_file(struct gwp_wrk *w)
static int handle_ev_auth_file(struct gwp_wrk *w)
{
	static const size_t l = sizeof(struct inotify_event) + NAME_MAX + 1;
	ssize_t r;

	assert(w->ctx->cfg.as_socks5);
	assert(w->ctx->socks5);
	assert(w->ctx->auth);

	r = __sys_read(w->ctx->ino_fd, w->ctx->ino_buf, l);
	if (unlikely(r < 0)) {
@@ -1403,8 +1411,8 @@ static int handle_ev_socks5_auth_file(struct gwp_wrk *w)
		return (int)r;
	}

	gwp_socks5_auth_reload(w->ctx->socks5);
	pr_info(&w->ctx->lh, "Reloaded SOCKS5 authentication file");
	gwp_auth_reload(w->ctx->auth);
	pr_info(&w->ctx->lh, "Reloaded authentication file");
	return 0;
}

@@ -1595,7 +1603,7 @@ static int handle_event(struct gwp_wrk *w, struct epoll_event *ev)
		r = handle_ev_dns_query(w, udata);
		break;
	case EV_BIT_SOCKS5_AUTH_FILE:
		r = handle_ev_socks5_auth_file(w);
		r = handle_ev_auth_file(w);
		break;
	case EV_BIT_RAW_DNS_QUERY:
		r = handle_ev_raw_dns_query(w);
Loading