penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit 9e5f87fc60536d280097a6da9171afc181146175

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-10T16:19:17Z
subjectAdded fuzzer for stream_sniff.c
commit 9e5f87fc60536d280097a6da9171afc181146175
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-06-10T16:19:17Z

    Added fuzzer for stream_sniff.c
---
 fuzz/Makefile                                      |  20 +--
 fuzz/README.md                                     |   6 +-
 fuzz/devtools/gen_seeds_stream_sniff.py            | 140 +++++++++++++++++++++
 fuzz/fuzz_stream_sniff.c                           |  71 +++++++++++
 fuzz/seeds/stream_sniff/junk-empty                 |   0
 fuzz/seeds/stream_sniff/junk-http-get              |   3 +
 fuzz/seeds/stream_sniff/junk-random-small          | Bin 0 -> 5 bytes
 fuzz/seeds/stream_sniff/junk-ssh-banner            |   1 +
 fuzz/seeds/stream_sniff/partial-1                  |   1 +
 fuzz/seeds/stream_sniff/partial-16                 | Bin 0 -> 16 bytes
 fuzz/seeds/stream_sniff/partial-2                  |   1 +
 fuzz/seeds/stream_sniff/partial-3                  |   1 +
 fuzz/seeds/stream_sniff/partial-32                 | Bin 0 -> 32 bytes
 fuzz/seeds/stream_sniff/partial-4                  | Bin 0 -> 4 bytes
 fuzz/seeds/stream_sniff/partial-5                  | Bin 0 -> 5 bytes
 fuzz/seeds/stream_sniff/partial-6                  | Bin 0 -> 6 bytes
 fuzz/seeds/stream_sniff/partial-64                 | Bin 0 -> 64 bytes
 fuzz/seeds/stream_sniff/partial-9                  | Bin 0 -> 9 bytes
 fuzz/seeds/stream_sniff/tls-alert-not-handshake    | Bin 0 -> 7 bytes
 fuzz/seeds/stream_sniff/tls-appdata-not-handshake  | Bin 0 -> 10 bytes
 fuzz/seeds/stream_sniff/tls-bad-handshake-len-huge | Bin 0 -> 100 bytes
 .../seeds/stream_sniff/tls-bad-handshake-len-small | Bin 0 -> 100 bytes
 fuzz/seeds/stream_sniff/tls-no-sni                 | Bin 0 -> 80 bytes
 fuzz/seeds/stream_sniff/tls-record-says-too-big    | Bin 0 -> 9 bytes
 fuzz/seeds/stream_sniff/tls-record-says-zero-len   | Bin 0 -> 5 bytes
 .../stream_sniff/tls-sni-declared-len-too-large    | Bin 0 -> 92 bytes
 fuzz/seeds/stream_sniff/tls-sni-empty              | Bin 0 -> 89 bytes
 fuzz/seeds/stream_sniff/tls-sni-example-com        | Bin 0 -> 100 bytes
 fuzz/seeds/stream_sniff/tls-sni-idn-punycode       | Bin 0 -> 111 bytes
 fuzz/seeds/stream_sniff/tls-sni-localhost          | Bin 0 -> 98 bytes
 fuzz/seeds/stream_sniff/tls-sni-long-valid-253     | Bin 0 -> 342 bytes
 fuzz/seeds/stream_sniff/tls-sni-too-long-254       | Bin 0 -> 343 bytes
 32 files changed, 227 insertions(+), 17 deletions(-)

diff --git a/fuzz/Makefile b/fuzz/Makefile
index e6aebe7..ae91969 100644
--- a/fuzz/Makefile
+++ b/fuzz/Makefile
@@ -12,7 +12,7 @@ OUT := $(PROJECT_ROOT)/bin/fuzz
 CORPUS := $(PROJECT_ROOT)/corpus
 SEEDS := seeds
 
-TARGETS := proxy_v2_build file_conf stream_conn
+TARGETS := proxy_v2_build file_conf stream_conn stream_sniff
 BINS := $(addprefix $(OUT)/fuzz_,$(TARGETS))
 
 PROJECT_SRCS := $(filter-out $(SRC_DIR)/tinyproxy.c,$(wildcard $(SRC_DIR)/*.c))
@@ -28,27 +28,16 @@ COMMON_FUZZ_SRC := \
 
 all: $(BINS)
 
-$(OUT)/fuzz_proxy_v2_build: fuzz_proxy_v2_build.c $(PROJECT_SRCS)
-	$(CC) $(CFLAGS) $(FUZZ_CFLAGS) $(CPPFLAGS) \
-		-I$(SRC_DIR) \
-		$^ \
-		-o $@ \
-		$(LDFLAGS) $(FUZZ_LDFLAGS) $(LDLIBS)
+$(OUT):
+	mkdir -p $(OUT)
 
-$(OUT)/fuzz_file_conf: fuzz_file_conf.c $(PROJECT_SRCS)
+$(OUT)/fuzz_%: fuzz_%.c $(PROJECT_SRCS) | $(OUT)
 	$(CC) $(CFLAGS) $(FUZZ_CFLAGS) $(CPPFLAGS) \
 		-I$(SRC_DIR) \
 		$^ \
 		-o $@ \
 		$(LDFLAGS) $(FUZZ_LDFLAGS) $(LDLIBS)
 
-$(OUT)/fuzz_stream_conn: fuzz_stream_conn.c $(PROJECT_SRCS)
-	$(CC) $(CFLAGS) $(FUZZ_CFLAGS) $(CPPFLAGS) \
-		-I$(SRC_DIR) \
-		$^ \
-		-o $@ \
-		$(LDFLAGS) $(LDLIBS)
-
 seed-%: FORCE
 	mkdir -p $(CORPUS)/$*
 	if [ -d "$(SEEDS)/$*" ]; then \
@@ -56,7 +45,6 @@ seed-%: FORCE
 	fi
 
 run-%: $(OUT)/fuzz_% seed-%
-	mkdir -p $(OUT)
 	$(OUT)/fuzz_$* \
 		$(CORPUS)/$* \
 		-max_total_time=$${MAX_TOTAL_TIME:-60} \
diff --git a/fuzz/README.md b/fuzz/README.md
index 8e28774..dcc742e 100755
--- a/fuzz/README.md
+++ b/fuzz/README.md
@@ -2,5 +2,9 @@ macos
 
 Fuzz for 8 hours
 ```
-LIBEVENT_SRC=libevent2/ MAX_TOTAL_TIME=28800 MAX_LEN=65536 CC="$(brew --prefix llvm)/bin/clang" caffeinate make -C fuzz run-file_conf
+caffeinate make -C fuzz run-file_conf \
+    LIBEVENT_SRC=libevent2/ \
+    MAX_TOTAL_TIME=28800 \
+    MAX_LEN=65536 \
+    CC="$(brew --prefix llvm)/bin/clang"
 ```
diff --git a/fuzz/devtools/gen_seeds_stream_sniff.py b/fuzz/devtools/gen_seeds_stream_sniff.py
new file mode 100755
index 0000000..c3b55ad
--- /dev/null
+++ b/fuzz/devtools/gen_seeds_stream_sniff.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+import struct
+
+OUT = Path("seeds/stream_sniff")
+OUT.mkdir(parents=True, exist_ok=True)
+
+
+def write(name, data):
+    (OUT / name).write_bytes(data)
+
+
+def ext_sni(hostname: bytes) -> bytes:
+    # extension_type: server_name = 0
+    # extension_data:
+    #   server_name_list length
+    #   name_type = host_name
+    #   host_name length
+    #   host_name bytes
+    server_name = b"\x00" + struct.pack("!H", len(hostname)) + hostname
+    server_name_list = struct.pack("!H", len(server_name)) + server_name
+    return struct.pack("!HH", 0, len(server_name_list)) + server_name_list
+
+
+def ext_supported_versions() -> bytes:
+    # supported_versions extension, client side:
+    #   extension_type = 43
+    #   extension_data = length-prefixed list of versions
+    body = b"\x04\x03\x04\x03\x03"  # TLS 1.3, TLS 1.2
+    return struct.pack("!HH", 43, len(body)) + body
+
+
+def ext_alpn(proto: bytes = b"http/1.1") -> bytes:
+    # ALPN extension:
+    #   protocol_name_list length
+    #   protocol_name length
+    #   protocol_name
+    name = bytes([len(proto)]) + proto
+    body = struct.pack("!H", len(name)) + name
+    return struct.pack("!HH", 16, len(body)) + body
+
+
+def client_hello(hostname: bytes | None = b"example.com", extra_exts: bytes = b"") -> bytes:
+    legacy_version = b"\x03\x03"  # TLS 1.2
+    random = bytes(range(32))
+    session_id = b""
+
+    cipher_suites = b"".join([
+        b"\x13\x01",  # TLS_AES_128_GCM_SHA256
+        b"\x13\x02",  # TLS_AES_256_GCM_SHA384
+        b"\xc0\x2f",  # TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    ])
+
+    compression_methods = b"\x00"
+
+    extensions = b""
+    if hostname is not None:
+        extensions += ext_sni(hostname)
+    extensions += ext_supported_versions()
+    extensions += ext_alpn()
+    extensions += extra_exts
+
+    body = (
+        legacy_version +
+        random +
+        bytes([len(session_id)]) + session_id +
+        struct.pack("!H", len(cipher_suites)) + cipher_suites +
+        bytes([len(compression_methods)]) + compression_methods +
+        struct.pack("!H", len(extensions)) + extensions
+    )
+
+    handshake = (
+        b"\x01" +                         # HandshakeType client_hello
+        len(body).to_bytes(3, "big") +     # handshake length
+        body
+    )
+
+    record = (
+        b"\x16" +                         # ContentType handshake
+        b"\x03\x01" +                     # TLS record legacy version
+        struct.pack("!H", len(handshake)) +
+        handshake
+    )
+
+    return record
+
+
+# 1. Non-TLS / junk
+write("junk-empty", b"")
+write("junk-http-get", b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
+write("junk-ssh-banner", b"SSH-2.0-OpenSSH_9.9\r\n")
+write("junk-random-small", bytes([0x00, 0xff, 0x13, 0x37, 0x42]))
+
+# 2. Partial TLS records
+good = client_hello(b"example.com")
+for n in [1, 2, 3, 4, 5, 6, 9, 16, 32, 64]:
+    write(f"partial-{n}", good[:n])
+
+# 3. Valid-ish ClientHello with SNI
+write("tls-sni-example-com", client_hello(b"example.com"))
+write("tls-sni-localhost", client_hello(b"localhost"))
+write("tls-sni-long-valid-253", client_hello(b"a" * 253))
+write("tls-sni-idn-punycode", client_hello(b"xn--r8jz45g.xn--zckzah"))
+
+# 4. ClientHello without SNI
+write("tls-no-sni", client_hello(None))
+
+# 5. Malformed TLS-ish things
+write("tls-record-says-zero-len", b"\x16\x03\x01\x00\x00")
+write("tls-record-says-too-big", b"\x16\x03\x01\xff\xff" + b"\x01\x00\x00\x00")
+write("tls-alert-not-handshake", b"\x15\x03\x03\x00\x02\x02\x28")
+write("tls-appdata-not-handshake", b"\x17\x03\x03\x00\x05hello")
+
+# 6. Bad handshake length fields
+bad = bytearray(good)
+bad[6:9] = b"\xff\xff\xff"  # handshake length too large
+write("tls-bad-handshake-len-huge", bytes(bad))
+
+bad = bytearray(good)
+bad[6:9] = b"\x00\x00\x01"  # handshake length too small
+write("tls-bad-handshake-len-small", bytes(bad))
+
+# 7. SNI edge cases
+
+# Declared SNI hostname length larger than available.
+trunc_sni = bytearray(client_hello(b"abc"))
+idx = trunc_sni.find(b"\x00\x03abc")
+if idx != -1:
+    trunc_sni[idx:idx + 2] = b"\x01\x00"
+write("tls-sni-declared-len-too-large", bytes(trunc_sni))
+
+# Actual hostname is 254 bytes; should hit NAME_TOO_LONG or parse error,
+# depending on your implementation.
+write("tls-sni-too-long-254", client_hello(b"a" * 254))
+
+# Empty hostname.
+write("tls-sni-empty", client_hello(b""))
+
+print(f"wrote seeds to {OUT}")
\ No newline at end of file
diff --git a/fuzz/fuzz_stream_sniff.c b/fuzz/fuzz_stream_sniff.c
new file mode 100644
index 0000000..f17f1e8
--- /dev/null
+++ b/fuzz/fuzz_stream_sniff.c
@@ -0,0 +1,71 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <event2/buffer.h>
+
+#include "stream_sniff.h"
+
+static void check_invariants(const struct stream_sniff *s)
+{
+	if (s->sni[STREAM_SNI_MAX] != '\0') {
+		abort();
+	}
+
+	if (strlen(s->sni) > STREAM_SNI_MAX) {
+		abort();
+	}
+
+	if (s->observed > STREAM_SNIFF_MAX) {
+		abort();
+	}
+
+	if (s->done) {
+		switch (s->status) {
+		case STREAM_SNIFF_PARSED:
+		case STREAM_SNIFF_MISSING:
+		case STREAM_SNIFF_NOT_TLS:
+		case STREAM_SNIFF_TRUNCATED:
+		case STREAM_SNIFF_NAME_TOO_LONG:
+		case STREAM_SNIFF_PARSE_ERROR:
+			break;
+
+		case STREAM_SNIFF_NOT_OBSERVED:
+		case STREAM_SNIFF_NEED_MORE:
+		default:
+			abort();
+		}
+	}
+}
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+	struct evbuffer *input;
+	struct stream_sniff s;
+
+	memset(&s, 0, sizeof(s));
+
+	input = evbuffer_new();
+	if (input == NULL) {
+		return 0;
+	}
+
+	if (size > 0 && evbuffer_add(input, data, size) != 0) {
+		evbuffer_free(input);
+		return 0;
+	}
+
+	stream_sniff_peek_client_input(&s, input);
+
+	/*
+	 * Also fuzz the log helper after parsing.
+	 * This catches cases where status/sni state is inconsistent.
+	 */
+	(void)stream_sniff_log_sni(&s);
+
+	check_invariants(&s);
+
+	evbuffer_free(input);
+	return 0;
+}
diff --git a/fuzz/seeds/stream_sniff/junk-empty b/fuzz/seeds/stream_sniff/junk-empty
new file mode 100644
index 0000000..e69de29
diff --git a/fuzz/seeds/stream_sniff/junk-http-get b/fuzz/seeds/stream_sniff/junk-http-get
new file mode 100644
index 0000000..7fba85d
--- /dev/null
+++ b/fuzz/seeds/stream_sniff/junk-http-get
@@ -0,0 +1,3 @@
+GET / HTTP/1.1
+Host: example.com
+
diff --git a/fuzz/seeds/stream_sniff/junk-random-small b/fuzz/seeds/stream_sniff/junk-random-small
new file mode 100644
index 0000000..1b978ae
Binary files /dev/null and b/fuzz/seeds/stream_sniff/junk-random-small differ
diff --git a/fuzz/seeds/stream_sniff/junk-ssh-banner b/fuzz/seeds/stream_sniff/junk-ssh-banner
new file mode 100644
index 0000000..3aea5ae
--- /dev/null
+++ b/fuzz/seeds/stream_sniff/junk-ssh-banner
@@ -0,0 +1 @@
+SSH-2.0-OpenSSH_9.9
diff --git a/fuzz/seeds/stream_sniff/partial-1 b/fuzz/seeds/stream_sniff/partial-1
new file mode 100644
index 0000000..127620c
--- /dev/null
+++ b/fuzz/seeds/stream_sniff/partial-1
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fuzz/seeds/stream_sniff/partial-16 b/fuzz/seeds/stream_sniff/partial-16
new file mode 100644
index 0000000..6f03231
Binary files /dev/null and b/fuzz/seeds/stream_sniff/partial-16 differ
diff --git a/fuzz/seeds/stream_sniff/partial-2 b/fuzz/seeds/stream_sniff/partial-2
new file mode 100644
index 0000000..10cc57b
--- /dev/null
+++ b/fuzz/seeds/stream_sniff/partial-2
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fuzz/seeds/stream_sniff/partial-3 b/fuzz/seeds/stream_sniff/partial-3
new file mode 100644
index 0000000..94db382
--- /dev/null
+++ b/fuzz/seeds/stream_sniff/partial-3
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fuzz/seeds/stream_sniff/partial-32 b/fuzz/seeds/stream_sniff/partial-32
new file mode 100644
index 0000000..52de33e
Binary files /dev/null and b/fuzz/seeds/stream_sniff/partial-32 differ
diff --git a/fuzz/seeds/stream_sniff/partial-4 b/fuzz/seeds/stream_sniff/partial-4
new file mode 100644
index 0000000..bb7fa08
Binary files /dev/null and b/fuzz/seeds/stream_sniff/partial-4 differ
diff --git a/fuzz/seeds/stream_sniff/partial-5 b/fuzz/seeds/stream_sniff/partial-5
new file mode 100644
index 0000000..a5bf948
Binary files /dev/null and b/fuzz/seeds/stream_sniff/partial-5 differ
diff --git a/fuzz/seeds/stream_sniff/partial-6 b/fuzz/seeds/stream_sniff/partial-6
new file mode 100644
index 0000000..c3a4b3e
Binary files /dev/null and b/fuzz/seeds/stream_sniff/partial-6 differ
diff --git a/fuzz/seeds/stream_sniff/partial-64 b/fuzz/seeds/stream_sniff/partial-64
new file mode 100644
index 0000000..53795db
Binary files /dev/null and b/fuzz/seeds/stream_sniff/partial-64 differ
diff --git a/fuzz/seeds/stream_sniff/partial-9 b/fuzz/seeds/stream_sniff/partial-9
new file mode 100644
index 0000000..564828d
Binary files /dev/null and b/fuzz/seeds/stream_sniff/partial-9 differ
diff --git a/fuzz/seeds/stream_sniff/tls-alert-not-handshake b/fuzz/seeds/stream_sniff/tls-alert-not-handshake
new file mode 100644
index 0000000..f3a5df3
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-alert-not-handshake differ
diff --git a/fuzz/seeds/stream_sniff/tls-appdata-not-handshake b/fuzz/seeds/stream_sniff/tls-appdata-not-handshake
new file mode 100644
index 0000000..4b1c117
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-appdata-not-handshake differ
diff --git a/fuzz/seeds/stream_sniff/tls-bad-handshake-len-huge b/fuzz/seeds/stream_sniff/tls-bad-handshake-len-huge
new file mode 100644
index 0000000..63cfb04
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-bad-handshake-len-huge differ
diff --git a/fuzz/seeds/stream_sniff/tls-bad-handshake-len-small b/fuzz/seeds/stream_sniff/tls-bad-handshake-len-small
new file mode 100644
index 0000000..ea7513f
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-bad-handshake-len-small differ
diff --git a/fuzz/seeds/stream_sniff/tls-no-sni b/fuzz/seeds/stream_sniff/tls-no-sni
new file mode 100644
index 0000000..5d4fa02
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-no-sni differ
diff --git a/fuzz/seeds/stream_sniff/tls-record-says-too-big b/fuzz/seeds/stream_sniff/tls-record-says-too-big
new file mode 100644
index 0000000..711a7eb
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-record-says-too-big differ
diff --git a/fuzz/seeds/stream_sniff/tls-record-says-zero-len b/fuzz/seeds/stream_sniff/tls-record-says-zero-len
new file mode 100644
index 0000000..f582c8b
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-record-says-zero-len differ
diff --git a/fuzz/seeds/stream_sniff/tls-sni-declared-len-too-large b/fuzz/seeds/stream_sniff/tls-sni-declared-len-too-large
new file mode 100644
index 0000000..b43eee2
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-sni-declared-len-too-large differ
diff --git a/fuzz/seeds/stream_sniff/tls-sni-empty b/fuzz/seeds/stream_sniff/tls-sni-empty
new file mode 100644
index 0000000..e253b91
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-sni-empty differ
diff --git a/fuzz/seeds/stream_sniff/tls-sni-example-com b/fuzz/seeds/stream_sniff/tls-sni-example-com
new file mode 100644
index 0000000..5cd4956
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-sni-example-com differ
diff --git a/fuzz/seeds/stream_sniff/tls-sni-idn-punycode b/fuzz/seeds/stream_sniff/tls-sni-idn-punycode
new file mode 100644
index 0000000..fc17f92
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-sni-idn-punycode differ
diff --git a/fuzz/seeds/stream_sniff/tls-sni-localhost b/fuzz/seeds/stream_sniff/tls-sni-localhost
new file mode 100644
index 0000000..e4ed54d
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-sni-localhost differ
diff --git a/fuzz/seeds/stream_sniff/tls-sni-long-valid-253 b/fuzz/seeds/stream_sniff/tls-sni-long-valid-253
new file mode 100644
index 0000000..18f06fc
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-sni-long-valid-253 differ
diff --git a/fuzz/seeds/stream_sniff/tls-sni-too-long-254 b/fuzz/seeds/stream_sniff/tls-sni-too-long-254
new file mode 100644
index 0000000..246ecf6
Binary files /dev/null and b/fuzz/seeds/stream_sniff/tls-sni-too-long-254 differ