summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--args/args.go7
-rw-r--r--conf.d/penguins16
-rw-r--r--init.d/penguins45
-rw-r--r--main.go41
5 files changed, 105 insertions, 7 deletions
diff --git a/.gitignore b/.gitignore
index 0bba11b..d8dd5e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.env
.DS_Store
-penguins
+/penguins
+/build/
*.db
/db/*.db
.claude
diff --git a/args/args.go b/args/args.go
index 6c79fe9..a488929 100644
--- a/args/args.go
+++ b/args/args.go
@@ -17,6 +17,11 @@ type Arguments struct {
Port int
Server bool
+ // Socket, when set, makes the HTTP server listen on this unix socket path
+ // instead of the TCP Port — handy behind a reverse proxy. Port is ignored
+ // when Socket is set.
+ Socket string
+
// Dns runs the authoritative DNS server for the LAN. DnsZone is the suffix
// it owns (e.g. penguins.lan); queries outside it are forwarded to
// DnsResolvers (empty = no forwarding, since the LAN has no outside
@@ -51,6 +56,7 @@ func GetArgs() (*Arguments, error) {
port := flag.Int("port", 8080, "Port to listen on")
server := flag.Bool("server", false, "Run the HTTP server")
+ socket := flag.String("socket", "", "Serve HTTP on this unix socket instead of the TCP port")
dns := flag.Bool("dns", false, "Run the authoritative DNS server")
dnsPort := flag.Int("dns-port", 8053, "Port for the DNS server to listen on")
@@ -74,6 +80,7 @@ func GetArgs() (*Arguments, error) {
Scheduler: *scheduler,
Port: *port,
Server: *server,
+ Socket: *socket,
Dns: *dns,
DnsPort: *dnsPort,
DnsResolvers: resolvers,
diff --git a/conf.d/penguins b/conf.d/penguins
new file mode 100644
index 0000000..b6c95e2
--- /dev/null
+++ b/conf.d/penguins
@@ -0,0 +1,16 @@
+# Configuration for the penguins.lan OpenRC service (/etc/init.d/penguins).
+
+# User/group the daemon runs as.
+penguins_user="penguins"
+penguins_group="penguins"
+
+# Deploy tree. Must contain the binary plus .env, db/, templates/ and static/.
+penguins_dir="/opt/penguins.lan"
+
+# Path to the compiled binary (defaults to ${penguins_dir}/penguins).
+#penguins_bin="/opt/penguins.lan/penguins"
+
+# Subcommand flags. Add e.g. -dns-port 53 to serve real DNS, or -port 80 for
+# HTTP on the privileged port (both rely on cap_net_bind_service). To sit behind
+# a reverse proxy, swap the TCP port for a unix socket: -socket /run/penguins.sock
+penguins_flags="-server -scheduler -dns -dns-port 53 -port 9000 -migrate"
diff --git a/init.d/penguins b/init.d/penguins
new file mode 100644
index 0000000..03835a3
--- /dev/null
+++ b/init.d/penguins
@@ -0,0 +1,45 @@
+#!/sbin/openrc-run
+
+# OpenRC service for penguins.lan.
+#
+# Install:
+# install -Dm755 init.d/penguins /etc/init.d/penguins
+# install -Dm644 conf.d/penguins /etc/conf.d/penguins
+# rc-update add penguins default
+# rc-service penguins start
+#
+# The binary resolves .env, ./db, ./templates and ./static relative to its
+# working directory, so directory= must point at the deploy tree.
+
+name="penguins.lan"
+description="penguins.lan portal, scheduler and DNS server"
+
+: "${penguins_user:=penguins}"
+: "${penguins_group:=penguins}"
+: "${penguins_dir:=/opt/penguins.lan}"
+: "${penguins_bin:=${penguins_dir}/penguins}"
+: "${penguins_flags:=-server -scheduler -dns -migrate}"
+
+command="${penguins_bin}"
+command_args="${penguins_flags}"
+command_user="${penguins_user}:${penguins_group}"
+directory="${penguins_dir}"
+
+supervisor="supervise-daemon"
+respawn_delay=5
+respawn_max=0
+output_log="/var/log/penguins.log"
+error_log="/var/log/penguins.log"
+
+# Binding DNS on port 53 (and HTTP on 80) needs this capability when not root.
+capabilities="^cap_net_bind_service"
+
+depend() {
+ need net
+ after firewall
+}
+
+start_pre() {
+ checkpath --directory --owner "${penguins_user}:${penguins_group}" --mode 0750 "${penguins_dir}/db"
+ checkpath --file --owner "${penguins_user}:${penguins_group}" --mode 0640 "${output_log}"
+}
diff --git a/main.go b/main.go
index 872749d..9ec1cd9 100644
--- a/main.go
+++ b/main.go
@@ -2,6 +2,8 @@ package main
import (
"log"
+ "net"
+ "os"
"github.com/joho/godotenv"
@@ -41,12 +43,22 @@ func main() {
if argv.Server {
server := api.MakeServer(argv, dbConn)
- log.Printf("listening on :%d", argv.Port)
- go func() {
- if err := server.ListenAndServe(); err != nil {
- log.Fatal(err)
- }
- }()
+ if argv.Socket != "" {
+ listener := unixListener(argv.Socket)
+ log.Printf("listening on unix socket %s", argv.Socket)
+ go func() {
+ if err := server.Serve(listener); err != nil {
+ log.Fatal(err)
+ }
+ }()
+ } else {
+ log.Printf("listening on :%d", argv.Port)
+ go func() {
+ if err := server.ListenAndServe(); err != nil {
+ log.Fatal(err)
+ }
+ }()
+ }
}
if argv.Dns {
@@ -63,3 +75,20 @@ func main() {
select {}
}
}
+
+// unixListener binds an HTTP unix socket, clearing any stale socket left by a
+// previous run and relaxing perms to 0660 so a same-group reverse proxy can
+// reach it.
+func unixListener(path string) net.Listener {
+ if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
+ log.Fatalf("could not clear stale socket %s: %v", path, err)
+ }
+ listener, err := net.Listen("unix", path)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := os.Chmod(path, 0o660); err != nil {
+ log.Fatalf("could not chmod socket %s: %v", path, err)
+ }
+ return listener
+}