From df0762ce550c33e1cfd423fef95020c41ca770da Mon Sep 17 00:00:00 2001
From: Irine Sistiana <49315432+IrineSistiana@users.noreply.github.com>
Date: Fri, 22 Sep 2023 10:39:07 +0800
Subject: [PATCH 4/9] server: add doq server
---
pkg/server/doq.go | 121 +++++++++++++++++++++++
plugin/enabled_plugins.go | 20 ++--
plugin/server/quic_server/quic_server.go | 120 ++++++++++++++++++++++
3 files changed, 248 insertions(+), 13 deletions(-)
create mode 100644 pkg/server/doq.go
create mode 100644 plugin/server/quic_server/quic_server.go
diff --git a/pkg/server/doq.go b/pkg/server/doq.go
new file mode 100644
index 0000000..8fb5f81
--- /dev/null
+++ b/pkg/server/doq.go
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020-2022, IrineSistiana
+ *
+ * This file is part of mosdns.
+ *
+ * mosdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * mosdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package server
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/netip"
+ "time"
+
+ "github.com/IrineSistiana/mosdns/v5/pkg/dnsutils"
+ "github.com/IrineSistiana/mosdns/v5/pkg/pool"
+ "github.com/quic-go/quic-go"
+ "go.uber.org/zap"
+)
+
+const (
+ defaultQuicIdleTimeout = time.Second * 30
+ streamReadTimeout = time.Second * 1
+ quicFirstReadTimeout = time.Millisecond * 500
+)
+
+type DoQServerOpts struct {
+ Logger *zap.Logger
+ IdleTimeout time.Duration
+}
+
+// ServeDoQ starts a server at l. It returns if l had an Accept() error.
+// It always returns a non-nil error.
+func ServeDoQ(l *quic.Listener, h Handler, opts DoQServerOpts) error {
+ logger := opts.Logger
+ if logger == nil {
+ logger = nopLogger
+ }
+ idleTimeout := opts.IdleTimeout
+ if idleTimeout <= 0 {
+ idleTimeout = defaultQuicIdleTimeout
+ }
+
+ listenerCtx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ for {
+ c, err := l.Accept(listenerCtx)
+ if err != nil {
+ return fmt.Errorf("unexpected listener err: %w", err)
+ }
+
+ // handle connection
+ connCtx, cancelConn := context.WithCancel(listenerCtx)
+ go func() {
+ defer c.CloseWithError(0, "")
+ defer cancelConn()
+
+ var clientAddr netip.Addr
+ ta, ok := c.RemoteAddr().(*net.UDPAddr)
+ if ok {
+ clientAddr = ta.AddrPort().Addr()
+ }
+
+ firstRead := true
+ for {
+ var streamAcceptTimeout time.Duration
+ if firstRead {
+ firstRead = false
+ streamAcceptTimeout = quicFirstReadTimeout
+ } else {
+ streamAcceptTimeout = idleTimeout
+ }
+ streamAcceptCtx, cancelStreamAccept := context.WithTimeout(connCtx, streamAcceptTimeout)
+ stream, err := c.AcceptStream(streamAcceptCtx)
+ cancelStreamAccept()
+ if err != nil {
+ return
+ }
+
+ // Handle stream.
+ // For doq, one stream, one query.
+ go func() {
+ defer stream.Close()
+
+ // Avoid fragmentation attack.
+ stream.SetReadDeadline(time.Now().Add(streamReadTimeout))
+ req, _, err := dnsutils.ReadMsgFromTCP(stream)
+ if err != nil {
+ return
+ }
+ queryMeta := QueryMeta{
+ ClientAddr: clientAddr,
+ ServerName: c.ConnectionState().TLS.ServerName,
+ }
+
+ resp := h.Handle(connCtx, req, queryMeta, pool.PackTCPBuffer)
+ if resp == nil {
+ return
+ }
+ if _, err := stream.Write(*resp); err != nil {
+ logger.Warn("failed to write response", zap.Stringer("client", c.RemoteAddr()), zap.Error(err))
+ }
+ }()
+ }
+ }()
+ }
+}
diff --git a/plugin/enabled_plugins.go b/plugin/enabled_plugins.go
index 199587c..0f7531b 100644
--- a/plugin/enabled_plugins.go
+++ b/plugin/enabled_plugins.go
@@ -21,12 +21,11 @@ package plugin
// data providers
import (
+ // data provider
_ "github.com/IrineSistiana/mosdns/v5/plugin/data_provider/domain_set"
_ "github.com/IrineSistiana/mosdns/v5/plugin/data_provider/ip_set"
-)
-// matches
-import (
+ // matcher
_ "github.com/IrineSistiana/mosdns/v5/plugin/matcher/client_ip"
_ "github.com/IrineSistiana/mosdns/v5/plugin/matcher/cname"
_ "github.com/IrineSistiana/mosdns/v5/plugin/matcher/env"
@@ -39,10 +38,8 @@ import (
_ "github.com/IrineSistiana/mosdns/v5/plugin/matcher/random"
_ "github.com/IrineSistiana/mosdns/v5/plugin/matcher/rcode"
_ "github.com/IrineSistiana/mosdns/v5/plugin/matcher/resp_ip"
-)
-// executables
-import (
+ // executable
_ "github.com/IrineSistiana/mosdns/v5/plugin/executable/arbitrary"
_ "github.com/IrineSistiana/mosdns/v5/plugin/executable/black_hole"
_ "github.com/IrineSistiana/mosdns/v5/plugin/executable/cache"
@@ -62,16 +59,13 @@ import (
_ "github.com/IrineSistiana/mosdns/v5/plugin/executable/sequence/fallback"
_ "github.com/IrineSistiana/mosdns/v5/plugin/executable/sleep"
_ "github.com/IrineSistiana/mosdns/v5/plugin/executable/ttl"
-)
-// other
-import (
- _ "github.com/IrineSistiana/mosdns/v5/plugin/mark" // executable and matcher
-)
+ // executable and matcher
+ _ "github.com/IrineSistiana/mosdns/v5/plugin/mark"
-// servers
-import (
+ // server
_ "github.com/IrineSistiana/mosdns/v5/plugin/server/http_server"
+ _ "github.com/IrineSistiana/mosdns/v5/plugin/server/quic_server"
_ "github.com/IrineSistiana/mosdns/v5/plugin/server/tcp_server"
_ "github.com/IrineSistiana/mosdns/v5/plugin/server/udp_server"
)
diff --git a/plugin/server/quic_server/quic_server.go b/plugin/server/quic_server/quic_server.go
new file mode 100644
index 0000000..8a5a4c1
--- /dev/null
+++ b/plugin/server/quic_server/quic_server.go
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2020-2022, IrineSistiana
+ *
+ * This file is part of mosdns.
+ *
+ * mosdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * mosdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package quic_server
+
+import (
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "net"
+ "time"
+
+ "github.com/IrineSistiana/mosdns/v5/coremain"
+ "github.com/IrineSistiana/mosdns/v5/pkg/server"
+ "github.com/IrineSistiana/mosdns/v5/pkg/utils"
+ "github.com/IrineSistiana/mosdns/v5/plugin/server/server_utils"
+ "github.com/quic-go/quic-go"
+)
+
+const PluginType = "quic_server"
+
+func init() {
+ coremain.RegNewPluginFunc(PluginType, Init, func() any { return new(Args) })
+}
+
+type Args struct {
+ Entry string `yaml:"entry"`
+ Listen string `yaml:"listen"`
+ Cert string `yaml:"cert"`
+ Key string `yaml:"key"`
+ IdleTimeout int `yaml:"idle_timeout"`
+}
+
+func (a *Args) init() {
+ utils.SetDefaultNum(&a.IdleTimeout, 30)
+}
+
+type QuicServer struct {
+ args *Args
+
+ l *quic.Listener
+}
+
+func (s *QuicServer) Close() error {
+ return s.l.Close()
+}
+
+func Init(bp *coremain.BP, args any) (any, error) {
+ return StartServer(bp, args.(*Args))
+}
+
+func StartServer(bp *coremain.BP, args *Args) (*QuicServer, error) {
+ dh, err := server_utils.NewHandler(bp, args.Entry)
+ if err != nil {
+ return nil, fmt.Errorf("failed to init dns handler, %w", err)
+ }
+
+ // Init tls
+ if len(args.Key) == 0 || len(args.Cert) == 0 {
+ return nil, errors.New("quic server requires a tls certificate")
+ }
+ tlsConfig := new(tls.Config)
+ if err := server.LoadCert(tlsConfig, args.Cert, args.Key); err != nil {
+ return nil, fmt.Errorf("failed to read tls cert, %w", err)
+ }
+ tlsConfig.NextProtos = []string{"doq"}
+
+ uc, err := net.ListenPacket("udp", args.Listen)
+ if err != nil {
+ return nil, fmt.Errorf("failed to listen socket, %w", err)
+ }
+
+ idleTimeout := time.Duration(args.IdleTimeout) * time.Second
+
+ quicConfig := &quic.Config{
+ MaxIdleTimeout: idleTimeout,
+ InitialStreamReceiveWindow: 4 * 1024,
+ MaxStreamReceiveWindow: 4 * 1024,
+ InitialConnectionReceiveWindow: 8 * 1024,
+ MaxConnectionReceiveWindow: 16 * 1024,
+ Allow0RTT: false,
+ }
+
+ qt := &quic.Transport{
+ Conn: uc,
+ }
+
+ quicListener, err := qt.Listen(tlsConfig, quicConfig)
+ if err != nil {
+ qt.Close()
+ return nil, fmt.Errorf("failed to listen quic, %w", err)
+ }
+
+ go func() {
+ defer quicListener.Close()
+ serverOpts := server.DoQServerOpts{Logger: bp.L(), IdleTimeout: idleTimeout}
+ err := server.ServeDoQ(quicListener, dh, serverOpts)
+ bp.M().GetSafeClose().SendCloseSignal(err)
+ }()
+ return &QuicServer{
+ args: args,
+ l: quicListener,
+ }, nil
+}
--
2.34.8