diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 115 | ||||
-rw-r--r-- | deps.mk | 0 | ||||
-rw-r--r-- | go.mod | 6 | ||||
-rwxr-xr-x | mkdeps.sh | 10 | ||||
-rw-r--r-- | src/cmd/main.go | 7 | ||||
-rw-r--r-- | src/lib.go | 132 | ||||
-rwxr-xr-x | tests/cli-opts.sh | 2 | ||||
-rw-r--r-- | tests/lib_test.go | 23 |
9 files changed, 297 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85a298a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/*.bin +/tests/*.bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a97ee73 --- /dev/null +++ b/Makefile @@ -0,0 +1,115 @@ +.POSIX: +DATE = 1970-01-01 +VERSION = 0.1.0 +NAME = binder +NAME_UC = $(NAME) +LANGUAGES = en +## Installation prefix. Defaults to "/usr". +PREFIX = /usr +BINDIR = $(PREFIX)/bin +LIBDIR = $(PREFIX)/lib +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 = +GOFLAGS = + + + +.SUFFIXES: +.SUFFIXES: .go .bin + + + +all: +include deps.mk + + +sources = \ + src/lib.go \ + src/cmd/main.go \ + + +derived-assets = \ + $(NAME).bin \ + tests/lib_test.bin \ + +side-assets = \ + glaze.socket \ + + + +## Default target. Builds all artifacts required for testing +## and installation. +all: $(derived-assets) + + +$(NAME).bin: src/lib.go src/cmd/main.go Makefile + go build $(GOFLAGS) -v -o $@ src/cmd/main.go + +tests/lib_test.bin: src/lib.go tests/lib_test.go Makefile + go test $(GOFLAGS) -v -o $@ -c $*.go + + + +check-unit: tests/lib_test.bin + ./tests/lib_test.bin + + +integration-tests = \ + tests/cli-opts.sh \ + +$(integration-tests): $(NAME).bin 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)' \ + + cp $(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME) + for f in $(sources); do \ + dir='$(DESTDIR)$(SRCDIR)'/"`dirname "$${f#src/}"`"; \ + mkdir -p "$$dir"; \ + cp -P "$$f" "$$dir"; \ + done + +## 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)$(SRCDIR)' \ + + + +## Run it locally. +run: all + rm -f glaze.socket + ./$(NAME).bin 0.0.0.0:4443 glaze.socket + + +ALWAYS: @@ -0,0 +1,6 @@ +module euandre.org/binder + +go 1.21.5 + +require euandre.org/gobang v0.1.0 +replace euandre.org/gobang => ../gobang 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/cmd/main.go b/src/cmd/main.go new file mode 100644 index 0000000..d9c8379 --- /dev/null +++ b/src/cmd/main.go @@ -0,0 +1,7 @@ +package main + +import "euandre.org/binder/src" + +func main() { + binder.Main() +} diff --git a/src/lib.go b/src/lib.go new file mode 100644 index 0000000..04ac41e --- /dev/null +++ b/src/lib.go @@ -0,0 +1,132 @@ +package binder + +import ( + "fmt" + "io" + "net" + "os" + "os/user" + "strconv" + "syscall" + + g "euandre.org/gobang/src" +) + + + +type CLIArgs struct { + FromAddr string + ToAddr string +} + + + +const USER = "nobody" + + + +var EmitActiveConnection = g.MakeGauge("active-connections") + + + +func dropPrivileges(username string) { + g.Info("Dropping privileges", "drop-root-init") + + user, err := user.Lookup(username) + g.FatalIf(err) + + gid, err := strconv.Atoi(user.Gid) + g.FatalIf(err) + + uid, err := strconv.Atoi(user.Uid) + g.FatalIf(err) + + err = syscall.Setgid(gid) + g.FatalIf(err) + + err = syscall.Setgroups([]int{}) + g.FatalIf(err) + + err = syscall.Setuid(uid) + g.FatalIf(err) + + g.Info("Privileges dropped", "drop-root-end") +} + +func isRunningAsRoot() bool { + return os.Geteuid() == 0 +} + +func copyData(closer chan struct{}, from io.Reader, to io.Writer) { + io.Copy(to, from) + closer <- struct{}{} +} + +func ParseArgs(args []string) CLIArgs { + if len(args) != 3 { + fmt.Fprintf( + os.Stderr, + "Usage: %s FROM-ADDRESS TO-ADDRESS\n", + args[0], + ) + os.Exit(2) + } + return CLIArgs { + FromAddr: args[1], + ToAddr: args[2], + } +} + +func DropRoot() { + if isRunningAsRoot() { + dropPrivileges(USER) + if isRunningAsRoot() { + panic("Failed to drop privileges") + } + } +} + +func Start(toAddr string, listener net.Listener) { + for { + connFrom, err := listener.Accept() + if err != nil { + g.Warning( + "Error accepting connection", + "accept-connection", + "err", err, + ) + continue + } + defer connFrom.Close() + EmitActiveConnection.Inc() + + connTo, err := net.Dial("unix", toAddr) + if err != nil { + g.Error( + "Error dialing connection", + "dial-connection", + "err", err, + ) + return + } + defer connTo.Close() + + closer := make(chan struct{}, 2) + go copyData(closer, connFrom, connTo) + go copyData(closer, connTo, connFrom) + go func() { + <-closer + EmitActiveConnection.Dec() + }() + } +} + + +func Main() { + args := ParseArgs(os.Args) + listener, err := net.Listen("tcp", args.FromAddr) + g.FatalIf(err) + + DropRoot() + Start(args.ToAddr, listener) +} diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh new file mode 100755 index 0000000..92b70ea --- /dev/null +++ b/tests/cli-opts.sh @@ -0,0 +1,2 @@ +#!/bin/sh +set -eu diff --git a/tests/lib_test.go b/tests/lib_test.go new file mode 100644 index 0000000..a857510 --- /dev/null +++ b/tests/lib_test.go @@ -0,0 +1,23 @@ +package binder_test + +import ( + "testing" + + g "euandre.org/gobang/src" + + "euandre.org/binder/src" +) + +func TestPlaceholder(t *testing.T) { + g.AssertEqual(t, binder.USER, "nobody") +} + +func TestParseArgs(t *testing.T) { + given := binder.ParseArgs([]string { "a", "b", "c" }) + expected := binder.CLIArgs { + FromAddr: "b", + ToAddr: "c", + } + + g.AssertEqual(t, given, expected) +} |