From 7d12859ef769a9085d732f910cc31fe538dacd5e Mon Sep 17 00:00:00 2001 From: TQ Hirsch Date: Sat, 6 Aug 2022 19:54:37 +0200 Subject: [PATCH] Now supports socket activation --- cmd/qddns-server/main.go | 14 +++- flake.nix | 49 +++++++------ go.mod | 2 +- go.sum | 2 + multilistener/LICENSE | 21 ++++++ multilistener/listener.go | 125 +++++++++++++++++++++++++++++++++ multilistener/listener_test.go | 54 ++++++++++++++ 7 files changed, 239 insertions(+), 28 deletions(-) create mode 100644 multilistener/LICENSE create mode 100644 multilistener/listener.go create mode 100644 multilistener/listener_test.go diff --git a/cmd/qddns-server/main.go b/cmd/qddns-server/main.go index d4c90bf..4cc2ac3 100644 --- a/cmd/qddns-server/main.go +++ b/cmd/qddns-server/main.go @@ -3,10 +3,11 @@ package main import ( "flag" "fmt" + "github.com/coreos/go-systemd/activation" "github.com/gin-gonic/gin" "github.com/thequux/qddns/common" "github.com/thequux/qddns/db" - _ "go.uber.org/zap" + "github.com/thequux/qddns/multilistener" "net" "net/http" "os" @@ -96,7 +97,16 @@ func main() { r.POST("/update/:domain", Update) var err error - if _, err = net.ResolveTCPAddr("tcp", *listen); err == nil { + if listeners, err := activation.Listeners(); err == nil && len(listeners) > 0 { + // Socket activation + var listener net.Listener + if len(listeners) > 1 { + listener, _ = multilistener.New(listeners...) + } else { + listener = listeners[0] + } + err = r.RunListener(listener) + } else if _, err = net.ResolveTCPAddr("tcp", *listen); err == nil { err = r.Run(*listen) } else { // Probably a UNIX address diff --git a/flake.nix b/flake.nix index 328aede..b1d0e07 100644 --- a/flake.nix +++ b/flake.nix @@ -1,29 +1,28 @@ { inputs.nixpkgs.url = "nixpkgs/nixos-22.05"; inputs.flake-utils.url = "github:numtide/flake-utils"; - outputs = {self, nixpkgs, flake-utils, ...}: flake-utils.lib.eachDefaultSystem (system: - let pkgs = nixpkgs.legacyPackages.${system}; - in rec { - defaultPackage = pkgs.buildGoModule { - pname = "qddns"; - version = "1.0"; - src = ./.; - vendorSha256 = "sha256-JsN+NFYOQskocFKcjklQmXQTEKgnoZd6R/v/335X2CI="; - - preferLocalBuild = true; - }; - apps.qddns-server = { - type = "app"; - program = "${defaultPackage}/bin/qddns-server"; - }; - apps.qddns-client = { - type = "app"; - program = "${defaultPackage}/bin/qddns-client"; - }; - devShells.default = pkgs.mkShell { - nativeBuildInputs = [ - pkgs.go - ]; - }; - }); + outputs = {self, nixpkgs, flake-utils, ...}: + { + nixosModules.qddns = import ./nix/qddns-module.nix self; + } // + (flake-utils.lib.eachDefaultSystem (system: + let pkgs = nixpkgs.legacyPackages.${system}; + in rec { + defaultPackage = pkgs.buildGoModule { + pname = "qddns"; + version = "1.0"; + src = ./.; + vendorSha256 = "sha256-J/r7K5gDVNmd+hGH7tFBOMPcuPzo0PWAqvyX19EnkLc="; + + preferLocalBuild = true; + }; + apps.qddns-admin = {type = "app"; program = "${defaultPackage}/bin/qddns-admin";}; + apps.qddns-client = {type = "app"; program = "${defaultPackage}/bin/qddns-client";}; + apps.qddns-server = {type = "app"; program = "${defaultPackage}/bin/qddns-server";}; + devShells.default = pkgs.mkShell { + nativeBuildInputs = [ + pkgs.go + ]; + }; + })); } diff --git a/go.mod b/go.mod index 834bf2f..38ca2a9 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/alecthomas/kong v0.6.1 github.com/gin-gonic/gin v1.8.1 github.com/jackc/pgx/v4 v4.16.1 - go.uber.org/zap v1.13.0 ) require ( + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect diff --git a/go.sum b/go.sum index 6d586d0..eb12fc9 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/multilistener/LICENSE b/multilistener/LICENSE new file mode 100644 index 0000000..bbe296a --- /dev/null +++ b/multilistener/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Daniel Garcia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/multilistener/listener.go b/multilistener/listener.go new file mode 100644 index 0000000..cf7e4d3 --- /dev/null +++ b/multilistener/listener.go @@ -0,0 +1,125 @@ +package multilistener + +import ( + "fmt" + "net" +) + +// Listener implements a net.Listener interface but multiplexes connections +// from multiple listeners. +type Listener struct { + listeners []net.Listener + closing chan struct{} + conns chan acceptResults +} + +type acceptResults struct { + conn net.Conn + err error +} + +var _ net.Listener = &Listener{} + +// New creates an instance of a Listener using the given listeners. You must +// pass at least one listener. The new listener object listens for new connection +// on all the given listeners. +func New(listeners ...net.Listener) (*Listener, error) { + + if len(listeners) == 0 { + return nil, fmt.Errorf("multilistener requires at least 1 listener") + } + + n := &Listener{ + listeners: listeners, + closing: make(chan struct{}), + conns: make(chan acceptResults), + } + for i := range n.listeners { + go n.loop(n.listeners[i]) + } + return n, nil + +} + +// Addr returns the address of the first listener the multi-listener is using. +// The address of other listeners are not available. +func (l *Listener) Addr() net.Addr { + return l.listeners[0].Addr() +} + +// Close will close the multi-listener by iterating over its listeners and calling +// Close() on each one. If an error is encountered, it is returned. If multiple +// errors are encountered they are returned in a MutiError. Close will also shut down +// the background goroutines that are calling Accept() on the underlying listeners. +// +// Calling Close() more than once will cause it to panic. +func (l *Listener) Close() error { + close(l.closing) + var errors []error + for i := range l.listeners { + err := l.listeners[i].Close() + if err != nil { + errors = append(errors, err) + } + } + switch len(errors) { + case 0: + return nil + case 1: + return errors[0] + } + return &MultiError{Errors: errors} +} + +// MultiError is a wrapper around a slice of errors that implements the error interface. +type MultiError struct { + Errors []error +} + +// Error concats the Error() messages of the underlying errors +func (m *MultiError) Error() string { + if len(m.Errors) == 0 { + return "" + } + s := "errors: " + for _, e := range m.Errors { + s += e.Error() + ", " + } + return s +} + +// loop continually accepts connections from the given listener. It forwards the result +// of the .Accept() method to a channel on the listener. When a user of the Listener object +// calls Accept(), it receives a value from that channel. Closing the listener will cause +// this loop to exit. +func (l *Listener) loop(listener net.Listener) { + for { + conn, err := listener.Accept() + r := acceptResults{ + conn: conn, + err: err, + } + select { + case l.conns <- r: + case <-l.closing: + if r.err == nil { + r.conn.Close() + } + return + } + } +} + +// Accept will wait for a result from calling Accept from the underlying listeners. +// It will return an error if the multi-listener is closed. +func (l *Listener) Accept() (net.Conn, error) { + select { + case acceptResult, ok := <-l.conns: + if ok { + return acceptResult.conn, acceptResult.err + } + return nil, fmt.Errorf("closed conn channel") + case <-l.closing: + return nil, fmt.Errorf("listener is closed") + } +} diff --git a/multilistener/listener_test.go b/multilistener/listener_test.go new file mode 100644 index 0000000..175f3f0 --- /dev/null +++ b/multilistener/listener_test.go @@ -0,0 +1,54 @@ +package multilistener + +import ( + "net" + "testing" +) + +func TestNew(t *testing.T) { + + _, err := New() + if err == nil { + t.Fatalf("expected error when creating listener with no underlying listeners") + } + l1, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("could not create listener") + } + l2, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("could not create listener") + } + + ml, err := New(l1, l2) + if err != nil { + t.Fatalf("expected ok, got %s", err) + } + + c1, err := net.Dial(l1.Addr().Network(), l1.Addr().String()) + if err != nil { + t.Fatalf("expected to dial to l1: %s", err) + } + if n, err := c1.Write([]byte("a")); n != 1 || err != nil { + t.Fatalf("expected n = 1, err = nil, got %d, %s", n, err) + } + c1_ml, err := ml.Accept() + if err != nil { + t.Fatalf("expected to connect to c1: %s", err) + } + buf := make([]byte, 100) + if n, err := c1_ml.Read(buf); n != 1 || err != nil { + t.Fatalf("expected 1 byte got %d, %s", n, err) + } + if buf[0] != 'a' { + t.Fatalf("expected a, got %c", buf[0]) + } + + if err := ml.Close(); err != nil { + t.Fatalf("expected no error closing: %s", err) + } + + if _, err := ml.Accept(); err == nil { + t.Fatalf("expected error after closing") + } +}