diff options
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | Makefile | 149 | ||||
-rw-r--r-- | deps.mk | 0 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rwxr-xr-x | mkdeps.sh | 10 | ||||
-rw-r--r-- | src/main.go | 7 | ||||
-rw-r--r-- | src/untls.go | 112 | ||||
-rwxr-xr-x | tests/cli-opts.sh | 47 | ||||
-rwxr-xr-x | tests/integration.sh | 73 | ||||
-rw-r--r-- | tests/lib.sh | 119 | ||||
-rw-r--r-- | tests/main.go | 7 | ||||
-rw-r--r-- | tests/untls.go | 17 |
12 files changed, 549 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75d542a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/*.bin +/src/*.a +/src/*.bin +/tests/*.a +/tests/*.bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6ef381b --- /dev/null +++ b/Makefile @@ -0,0 +1,149 @@ +.POSIX: +DATE = 1970-01-01 +VERSION = 0.1.0 +NAME = untls +NAME_UC = $(NAME) +LANGUAGES = en +## Installation prefix. Defaults to "/usr". +PREFIX = /usr +BINDIR = $(PREFIX)/bin +LIBDIR = $(PREFIX)/lib +GOLIBDIR = $(LIBDIR)/go +INCLUDEDIR = $(PREFIX)/include +SRCDIR = $(PREFIX)/src/$(NAME) +SHAREDIR = $(PREFIX)/share +LOCALEDIR = $(SHAREDIR)/locale +MANDIR = $(SHAREDIR)/man +EXEC = ./ +## Where to store the installation. Empty by default. +DESTDIR = +LDLIBS = + + + +.SUFFIXES: +.SUFFIXES: .go .a .bin .bin-check + + + +all: +include deps.mk + + +objects = \ + src/$(NAME).a \ + src/main.a \ + tests/$(NAME).a \ + tests/main.a \ + +sources = \ + src/$(NAME).go \ + src/main.go \ + + +derived-assets = \ + $(objects) \ + src/main.bin \ + tests/main.bin \ + $(NAME).bin \ + +side-assets = \ + $(NAME).socket \ + glaze.socket \ + + + +## Default target. Builds all artifacts required for testing +## and installation. +all: $(derived-assets) + + +$(objects): Makefile + +src/$(NAME).a: src/$(NAME).go + go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go + +tests/$(NAME).a: tests/$(NAME).go src/$(NAME).go + go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go src/$(*F).go + +src/main.a: src/main.go src/$(NAME).a +tests/main.a: tests/main.go tests/$(NAME).a +src/main.a tests/main.a: + go tool compile $(GOCFLAGS) -o $@ -I $(@D) $*.go + +src/main.bin: src/main.a +tests/main.bin: tests/main.a +src/main.bin tests/main.bin: + go tool link $(GOLDFLAGS) -o $@ -L $(@D) $*.a + +$(NAME).bin: src/main.bin + ln -fs $? $@ + + + +tests.bin-check = \ + tests/main.bin-check \ + +tests/main.bin-check: tests/main.bin +$(tests.bin-check): + $(EXEC)$*.bin + +check-unit: $(tests.bin-check) + + +integration-tests = \ + tests/cli-opts.sh \ + tests/integration.sh \ + +.PRECIOUS: $(integration-tests) +$(integration-tests): $(NAME).bin +$(integration-tests): ALWAYS + sh $@ + +check-integration: $(integration-tests) + + +## Run all tests. Each test suite is isolated, so that a parallel +## build can run tests at the same time. The required artifacts +## are created if missing. +check: check-unit check-integration + + + +## Remove *all* derived artifacts produced during the build. +## A dedicated test asserts that this is always true. +clean: + rm -rf $(derived-assets) $(side-assets) + + +## Installs into $(DESTDIR)$(PREFIX). Its dependency target +## ensures that all installable artifacts are crafted beforehand. +install: all + mkdir -p \ + '$(DESTDIR)$(BINDIR)' \ + '$(DESTDIR)$(GOLIBDIR)' \ + '$(DESTDIR)$(SRCDIR)' \ + + cp $(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME) + cp src/$(NAME).a '$(DESTDIR)$(GOLIBDIR)' + cp $(sources) '$(DESTDIR)$(SRCDIR)' + +## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror +## of the "install" target, and removes *all* that was installed. +## A dedicated test asserts that this is always true. +uninstall: + rm -rf \ + '$(DESTDIR)$(BINDIR)'/$(NAME) \ + '$(DESTDIR)$(GOLIBDIR)'/$(NAME).a \ + '$(DESTDIR)$(SRCDIR)' \ + + + +## Run it locally. +run: all + rm -f $(NAME).socket + rm -f glaze.socket + ./$(NAME).bin cert.pem key.pem tls.socket glaze.socket + + +ALWAYS: @@ -0,0 +1,3 @@ +module euandre.org/untls + +go 1.21.5 diff --git a/mkdeps.sh b/mkdeps.sh new file mode 100755 index 0000000..a6b23d5 --- /dev/null +++ b/mkdeps.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu + +export LANG=POSIX.UTF-8 + +varlist() { + printf '%s = \\\n' "$1" + sed 's|^\(.*\)$|\t\1 \\|' + printf '\n' +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..5676910 --- /dev/null +++ b/src/main.go @@ -0,0 +1,7 @@ +package main + +import "untls" + +func main() { + untls.Main() +} diff --git a/src/untls.go b/src/untls.go new file mode 100644 index 0000000..545ee2e --- /dev/null +++ b/src/untls.go @@ -0,0 +1,112 @@ +package untls + +import ( + "crypto/tls" + "fmt" + "io" + "net" + "os" + + g "gobang" +) + + + +type _CLIArgs struct { + certFile string + keyFile string + fromAddr string + toAddr string +} + + + +var emitActiveConnection = g.MakeGauge("active-connections") + +const X = 1 + + + +func parseArgs(args []string) _CLIArgs { + if len(args) != 5 { + fmt.Fprintf( + os.Stderr, + "Usage: %s CERT.pem KEY.pem FROM.socket TO.socket\n", + args[0], + ) + os.Exit(2) + } + return _CLIArgs { + certFile: args[1], + keyFile: args[2], + fromAddr: args[3], + toAddr: args[4], + } +} + +func listen(certFile string, keyFile string, fromAddr string) net.Listener { + certificate, err := tls.LoadX509KeyPair(certFile, keyFile) + g.FatalIf(err) + + config := &tls.Config { + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate { + certificate, + }, + } + + listener, err := tls.Listen("unix", fromAddr, config) + g.FatalIf(err) + g.Info("Started listening", "listen-start", "from-address", fromAddr) + return listener +} + +func copyData(c chan struct {}, from io.Reader, to io.WriteCloser) { + io.Copy(to, from) + c <- struct {} {} + // connection is closed, send signal to stop proxy FIXME +} + +func start(toAddr string, listener net.Listener) { + for { + connFrom, err := listener.Accept() + if err != nil { + g.Warning( + "Error accepting connection", + "accept-connection-error", + "err", err, + ) + continue + } + defer connFrom.Close() + emitActiveConnection.Inc() + + connTo, err := net.Dial("unix", toAddr) + if err != nil { + g.Warning( + "Error dialing connection", + "dial-connection-error", + "err", err, + ) + continue + } + defer connTo.Close() + + c := make(chan struct {}) + go copyData(c, connFrom, connTo) + go copyData(c, connTo, connFrom) + go func() { + <- c + emitActiveConnection.Dec() + }() + } +} + + + +func Main() { + g.Init() + args := parseArgs(os.Args) + listener := listen(args.certFile, args.keyFile, args.fromAddr) + start(args.toAddr, listener) +} diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh new file mode 100755 index 0000000..bcceaa2 --- /dev/null +++ b/tests/cli-opts.sh @@ -0,0 +1,47 @@ +#!/bin/sh +set -eu + +. tests/lib.sh + + +test_needs_4_arguments() { + testing 'needs 4 arguments' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./untls.bin 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_usage "$ERR" + rm -f "$OUT" "$ERR" + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./untls.bin FROM-ADDR 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_usage "$ERR" + rm -f "$OUT" "$ERR" + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./untls.bin cert.pem key.pem TO-ADDR 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_usage "$ERR" + rm -f "$OUT" "$ERR" + + test_ok +} + + +test_needs_4_arguments diff --git a/tests/integration.sh b/tests/integration.sh new file mode 100755 index 0000000..e0a92dc --- /dev/null +++ b/tests/integration.sh @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +. tests/lib.sh + + +test_exits_when_upstream_errors() { + testing 'exits when upstream errors' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR" s1.socket client.txt' EXIT + + rm -f s1.socket + ./binder.bin localhost:1234 s1.socket 1>"$OUT" 2>"$ERR" & + pid=$! + while ! lsof -s TCP:LISTEN -i :1234 > /dev/null; do + true + done + + echo request | socat tcp-connect:localhost:1234 stdio > client.txt + kill $pid + wait + + assert_fgrep_stdout 'listen-start' + assert_fgrep_stdout 'active-connections' + assert_fgrep_stdout 'dial-connection' + assert_empty_stderr + assert_empty_stream 'client.txt' client.txt + rm -f "$OUT" "$ERR" s1.socket client.txt + + test_ok +} + +test_works_from_client_to_server() { + testing 'works from client to server' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR" s2.socket client.txt server.txt' EXIT + + rm -f s2.socket + ./binder.bin localhost:1234 s2.socket 1>"$OUT" 2>"$ERR" & + pid=$! + while ! lsof -s TCP:LISTEN -i :1234 > /dev/null; do + true + done + + echo response | socat unix-listen:s2.socket stdio > server.txt & + while [ ! -S s2.socket ]; do + true + done + + echo request | socat tcp-connect:localhost:1234 stdio > client.txt + kill $pid + wait + + assert_fgrep_stdout 'listen-start' + assert_fgrep_stdout 'active-connections' + assert_empty_stderr + assert_fgrep_stream 'client.txt' client.txt 'response' + assert_fgrep_stream 'server.txt' server.txt 'request' + rm -f "$OUT" "$ERR" s2.socket client.txt server.txt + + test_ok +} + + +exit # FIXME +test_exits_when_upstream_errors +test_works_from_client_to_server diff --git a/tests/lib.sh b/tests/lib.sh new file mode 100644 index 0000000..07ecbef --- /dev/null +++ b/tests/lib.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +end="\033[0m" +red="\033[0;31m" +green="\033[0;32m" +yellow="\033[0;33m" + +N= +OUT= +ERR= +STATUS= + +ERROR() { + # shellcheck disable=2059 + printf "${red}ERROR${end}" +} + +print_debug_info() { + # shellcheck disable=2016 + printf 'LINENO: %s\n$OUT: %s\n$ERR: %s\n' \ + "$N" "$OUT" "$ERR" >&2 +} + +assert_status() { + if [ "$STATUS" != "$1" ]; then + printf '\n%s: Bad status.\n\nexpected: %s\ngot: %s\n' \ + "$(ERROR)" "$1" "$STATUS" >&2 + print_debug_info + exit 1 + fi +} + +assert_usage() { + if ! grep -Fq 'Usage' "$1"; then + echo 'Expected to find "Usage" text, it was missing:' >&2 + cat "$1" >&2 + print_debug_info + exit 1 + fi +} + +assert_empty_stream() { + if [ -s "$2" ]; then + FMT='\n%s: Expected %s (%s) to be empty, but has content:\n%s\n' + # shellcheck disable=2059 + printf "$FMT" \ + "$(ERROR)" "$1" "$2" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_empty_stdout() { + assert_empty_stream STDOUT "$OUT" +} + +assert_empty_stderr() { + assert_empty_stream STDERR "$ERR" +} + +assert_stream() { + if [ "$(cat "$2")" != "$3" ]; then + printf '\n%s: Bad %s (%s)\n\nexpected: %s\ngot: %s\n' \ + "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_stdout() { + assert_stream STDOUT "$OUT" "$1" +} + +assert_stderr() { + assert_stream STDERR "$ERR" "$1" +} + +assert_grep_stream() { + if ! grep -qE "$3" "$2"; then + printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \ + "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_grep_stdout() { + assert_grep_stream STDOUT "$OUT" "$1" +} + +assert_grep_stderr() { + assert_grep_stream STDERR "$ERR" "$1" +} + +assert_fgrep_stream() { + if ! grep -Fq -- "$3" "$2"; then + printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \ + "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_fgrep_stdout() { + assert_fgrep_stream STDOUT "$OUT" "$1" +} + +assert_fgrep_stderr() { + assert_fgrep_stream STDERR "$ERR" "$1" +} + +testing() { + printf "${yellow}testing${end}: %s..." "$1" >&2 +} + +test_ok() { + # shellcheck disable=2059 + printf " ${green}OK${end}.\n" >&2 +} diff --git a/tests/main.go b/tests/main.go new file mode 100644 index 0000000..9355e50 --- /dev/null +++ b/tests/main.go @@ -0,0 +1,7 @@ +package main + +import "untls" + +func main() { + untls.MainTest() +} diff --git a/tests/untls.go b/tests/untls.go new file mode 100644 index 0000000..767ad31 --- /dev/null +++ b/tests/untls.go @@ -0,0 +1,17 @@ +package untls + +import ( + g "gobang" +) + + + +func test_Stub() { + g.AssertEqual(X, 1) +} + + + +func MainTest() { + test_Stub() +} |