Now supports socket activation

This commit is contained in:
2022-08-06 19:54:37 +02:00
parent 01c80e1f83
commit 7d12859ef7
7 changed files with 239 additions and 28 deletions

View File

@@ -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

View File

@@ -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
];
};
}));
}

2
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

21
multilistener/LICENSE Normal file
View File

@@ -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.

125
multilistener/listener.go Normal file
View File

@@ -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")
}
}

View File

@@ -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")
}
}