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