diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5e3f7c6..14dec28 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,4 +32,4 @@ jobs: platforms: ${{ env.PLATFORMS }} push: true tags: | - ghcr.io/${{ github.repository_owner }}/yggmail:${{ github.ref_name == 'main' && 'latest' || github.ref_name }} + ghcr.io/${{ github.repository_owner }}/yggmail:${{ github.ref_name }} diff --git a/Dockerfile b/Dockerfile index 57b2327..59b03c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/golang:alpine3.18 as builder +FROM docker.io/golang:alpine as builder COPY . /src WORKDIR /src @@ -6,7 +6,7 @@ WORKDIR /src RUN apk add --no-cache --update go gcc g++ RUN go build -o /src/yggmail ./cmd/yggmail -FROM docker.io/alpine:3.18 +FROM docker.io/alpine LABEL org.opencontainers.image.source=https://github.com/neilalexander/yggmail LABEL org.opencontainers.image.description=Yggmail diff --git a/cmd/yggmail/main.go b/cmd/yggmail/main.go index e9d1e94..fac50ba 100644 --- a/cmd/yggmail/main.go +++ b/cmd/yggmail/main.go @@ -207,7 +207,7 @@ func main() { overlayServer.MaxRecipients = 50 overlayServer.AuthDisabled = true - if err := overlayServer.Serve(transport); err != nil { + if err := overlayServer.Serve(transport.Listener()); err != nil { log.Fatal(err) } }() diff --git a/go.mod b/go.mod index 2423061..6d9ac41 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/neilalexander/yggmail go 1.20 require ( + github.com/Arceliar/ironwood v0.0.0-20231104025256-ec84c695fc44 github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap-idle v0.0.0-20210907174914-db2568431445 github.com/emersion/go-message v0.17.0 @@ -10,16 +11,15 @@ require ( github.com/emersion/go-smtp v0.15.0 github.com/fatih/color v1.15.0 github.com/gologme/log v1.3.0 - github.com/mattn/go-sqlite3 v1.14.18 - github.com/yggdrasil-network/yggdrasil-go v0.5.4 - github.com/yggdrasil-network/yggquic v0.0.0-20231209220136-b412fc6f0d7e + github.com/mattn/go-sqlite3 v1.14.17 + github.com/quic-go/quic-go v0.40.0 + github.com/yggdrasil-network/yggdrasil-go v0.5.2 go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.14.0 golang.org/x/term v0.13.0 ) require ( - github.com/Arceliar/ironwood v0.0.0-20231127131626-465b82dfb5bd // indirect github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/bits-and-blooms/bloom/v3 v3.6.0 // indirect @@ -31,7 +31,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/onsi/ginkgo/v2 v2.13.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect - github.com/quic-go/quic-go v0.40.0 // indirect github.com/stretchr/testify v1.7.0 // indirect go.uber.org/mock v0.3.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect diff --git a/go.sum b/go.sum index 9115dfa..984cb2f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Arceliar/ironwood v0.0.0-20231127131626-465b82dfb5bd h1:458tnmZ4zM2gbLtefdYbaxyAJevDNEWu6tLKEqbK4wg= -github.com/Arceliar/ironwood v0.0.0-20231127131626-465b82dfb5bd/go.mod h1:5x7fWW0mshe9WQ1lvSMmmHBYC3BeHH9gpwW5tz7cbfw= +github.com/Arceliar/ironwood v0.0.0-20231104025256-ec84c695fc44 h1:u328GAZGtL0W4oFWQs4YWHZT215LL6Lw9aYJxx76UVs= +github.com/Arceliar/ironwood v0.0.0-20231104025256-ec84c695fc44/go.mod h1:5x7fWW0mshe9WQ1lvSMmmHBYC3BeHH9gpwW5tz7cbfw= github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM= github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q= github.com/bits-and-blooms/bitset v1.3.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= @@ -49,8 +49,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= -github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= @@ -67,10 +67,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -github.com/yggdrasil-network/yggdrasil-go v0.5.4 h1:A7ZFmxkkbZhtqJgQXBVDw5sHsi25aUawLlJCCHnNsAs= -github.com/yggdrasil-network/yggdrasil-go v0.5.4/go.mod h1:TLmU4X0nfzCY9t5xABtFQ6GLoOtCae8xVatC9JwjD5I= -github.com/yggdrasil-network/yggquic v0.0.0-20231209220136-b412fc6f0d7e h1:Ncp4P0jFiIJ+6o06a4PABcvWeJDdbUQSjMVaJVSOu2I= -github.com/yggdrasil-network/yggquic v0.0.0-20231209220136-b412fc6f0d7e/go.mod h1:XFL2wkUXrqdl4AYiJghA+wW9xAm4ISvUtNrHdWPXjsE= +github.com/yggdrasil-network/yggdrasil-go v0.5.2 h1:OEt5xi5iQDhK4yGjp0Bq9B0uZyQz741WIlorE8oVW1c= +github.com/yggdrasil-network/yggdrasil-go v0.5.2/go.mod h1:oATGHx91oFqq3h3RKFU9qADFcO27TCf3CbQqrG2wzvU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= diff --git a/internal/smtpsender/sender.go b/internal/smtpsender/sender.go index ed960c5..bdf0e5b 100644 --- a/internal/smtpsender/sender.go +++ b/internal/smtpsender/sender.go @@ -123,7 +123,7 @@ func (q *Queue) run() { q.queues.Log.Println("Sending mail from", ref.From, "to", q.destination) if err := func() error { - conn, err := q.queues.Transport.Dial("yggdrasil", q.destination) + conn, err := q.queues.Transport.Dial(q.destination) if err != nil { return fmt.Errorf("q.queues.Transport.Dial: %w", err) } diff --git a/internal/transport/transport.go b/internal/transport/transport.go index f193c2b..461903e 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -13,6 +13,6 @@ import ( ) type Transport interface { - Dial(network, host string) (net.Conn, error) - net.Listener + Dial(host string) (net.Conn, error) + Listener() net.Listener } diff --git a/internal/transport/yggdrasil.go b/internal/transport/yggdrasil.go index cae1c56..7a50bca 100644 --- a/internal/transport/yggdrasil.go +++ b/internal/transport/yggdrasil.go @@ -9,21 +9,48 @@ package transport import ( + "context" "crypto/ed25519" + "crypto/tls" "encoding/hex" "fmt" "log" + "net" "regexp" + "sync" + "time" + iwt "github.com/Arceliar/ironwood/types" "github.com/fatih/color" gologme "github.com/gologme/log" + "github.com/quic-go/quic-go" "github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/core" "github.com/yggdrasil-network/yggdrasil-go/src/multicast" - "github.com/yggdrasil-network/yggquic" ) -func NewYggdrasilTransport(log *log.Logger, sk ed25519.PrivateKey, pk ed25519.PublicKey, peers []string, mcast bool) (*yggquic.YggdrasilTransport, error) { +type YggdrasilTransport struct { + listener *quic.Listener + yggdrasil net.PacketConn + transport *quic.Transport + tlsConfig *tls.Config + quicConfig *quic.Config + incoming chan *yggdrasilSession + sessions sync.Map // string -> quic.Connection + dials sync.Map // string -> *yggdrasilDial +} + +type yggdrasilSession struct { + quic.Connection + quic.Stream +} + +type yggdrasilDial struct { + context.Context + context.CancelFunc +} + +func NewYggdrasilTransport(log *log.Logger, sk ed25519.PrivateKey, pk ed25519.PublicKey, peers []string, mcast bool) (*YggdrasilTransport, error) { yellow := color.New(color.FgYellow).SprintfFunc() glog := gologme.New(log.Writer(), fmt.Sprintf("[ %s ] ", yellow("Yggdrasil")), gologme.LstdFlags|gologme.Lmsgprefix) glog.EnableLevel("warn") @@ -69,5 +96,140 @@ func NewYggdrasilTransport(log *log.Logger, sk ed25519.PrivateKey, pk ed25519.Pu } } - return yggquic.New(ygg, *cfg.Certificate, nil) + tr := &YggdrasilTransport{ + tlsConfig: &tls.Config{ + ServerName: hex.EncodeToString(ygg.PublicKey()), + Certificates: []tls.Certificate{ + *cfg.Certificate, + }, + InsecureSkipVerify: true, + }, + quicConfig: &quic.Config{ + HandshakeIdleTimeout: time.Second * 5, + MaxIdleTimeout: time.Second * 60, + }, + transport: &quic.Transport{ + Conn: ygg, + }, + yggdrasil: ygg, + incoming: make(chan *yggdrasilSession, 1), + } + + if tr.listener, err = tr.transport.Listen(tr.tlsConfig, tr.quicConfig); err != nil { + return nil, fmt.Errorf("quic.Listen: %w", err) + } + + go tr.connectionAcceptLoop() + return tr, nil +} + +func (t *YggdrasilTransport) connectionAcceptLoop() { + for { + qc, err := t.listener.Accept(context.TODO()) + if err != nil { + return + } + + host := qc.RemoteAddr().String() + if eqc, ok := t.sessions.LoadAndDelete(host); ok { + eqc := eqc.(quic.Connection) + _ = eqc.CloseWithError(0, "Connection replaced") + } + t.sessions.Store(host, qc) + if dial, ok := t.dials.LoadAndDelete(host); ok { + dial := dial.(*yggdrasilDial) + dial.CancelFunc() + } + + go t.streamAcceptLoop(qc) + } +} + +func (t *YggdrasilTransport) streamAcceptLoop(qc quic.Connection) { + host := qc.RemoteAddr().String() + + defer qc.CloseWithError(0, "Timed out") // nolint:errcheck + defer t.sessions.Delete(host) + + for { + qs, err := qc.AcceptStream(context.Background()) + if err != nil { + break + } + t.incoming <- &yggdrasilSession{qc, qs} + } +} + +func (t *YggdrasilTransport) Dial(host string) (net.Conn, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + var retry bool +retry: + qc, ok := t.sessions.Load(host) + if !ok { + if dial, ok := t.dials.Load(host); ok { + <-dial.(*yggdrasilDial).Done() + } + if qc, ok = t.sessions.Load(host); !ok { + dialctx, dialcancel := context.WithCancel(ctx) + defer dialcancel() + + t.dials.Store(host, &yggdrasilDial{dialctx, dialcancel}) + defer t.dials.Delete(host) + + addr := make(iwt.Addr, ed25519.PublicKeySize) + k, err := hex.DecodeString(host) + if err != nil { + return nil, err + } + copy(addr, k) + + if qc, err = t.transport.Dial(dialctx, addr, t.tlsConfig, t.quicConfig); err != nil { + return nil, err + } + + qc := qc.(quic.Connection) + t.sessions.Store(host, qc) + go t.streamAcceptLoop(qc) + } + } + if qc == nil { + return nil, net.ErrClosed + } else { + qc := qc.(quic.Connection) + qs, err := qc.OpenStreamSync(ctx) + if err != nil { + if !retry { + retry = true + goto retry + } + return nil, err + } + // For some reason this is needed to kick the stream + _, err = qs.Write([]byte(" ")) + return &yggdrasilSession{qc, qs}, err + } +} + +func (t *YggdrasilTransport) Listener() net.Listener { + return &yggdrasilListener{t} +} + +type yggdrasilListener struct { + *YggdrasilTransport +} + +func (t *yggdrasilListener) Accept() (net.Conn, error) { + return <-t.incoming, nil +} + +func (t *yggdrasilListener) Addr() net.Addr { + return t.listener.Addr() +} + +func (t *yggdrasilListener) Close() error { + if err := t.listener.Close(); err != nil { + return err + } + return t.yggdrasil.Close() }