From 74fef8838e33a7099f5f8da8bc2ca31cbecf5059 Mon Sep 17 00:00:00 2001 From: wenyifan Date: Sat, 4 Jul 2026 13:01:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Ednst=E9=9A=A7=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dialer/dns/conn.go | 54 ++++++ dialer/dns/dialer.go | 153 +++++++++++++++++ dialer/dns/metadata.go | 48 ++++++ internal/util/dns/conn.go | 284 ++++++++++++++++++++++++++++++++ internal/util/dns/conn_test.go | 61 +++++++ listener/dns/listener.go | 36 ++++ listener/dns/metadata.go | 10 ++ listener/dns/tunnel.go | 205 +++++++++++++++++++++++ listener/dns/tunnel_metadata.go | 55 +++++++ 9 files changed, 906 insertions(+) create mode 100644 dialer/dns/conn.go create mode 100644 dialer/dns/dialer.go create mode 100644 dialer/dns/metadata.go create mode 100644 internal/util/dns/conn.go create mode 100644 internal/util/dns/conn_test.go create mode 100644 listener/dns/tunnel.go create mode 100644 listener/dns/tunnel_metadata.go diff --git a/dialer/dns/conn.go b/dialer/dns/conn.go new file mode 100644 index 00000000..15ffd046 --- /dev/null +++ b/dialer/dns/conn.go @@ -0,0 +1,54 @@ +package dns + +import ( + "context" + "net" + "time" + + "github.com/quic-go/quic-go" +) + +type quicSession struct { + session *quic.Conn +} + +func (session *quicSession) GetConn() (*quicConn, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + stream, err := session.session.OpenStreamSync(ctx) + if err != nil { + return nil, err + } + return &quicConn{ + Stream: stream, + laddr: session.session.LocalAddr(), + raddr: session.session.RemoteAddr(), + }, nil +} + +func (session *quicSession) IsClosed() bool { + select { + case <-session.session.Context().Done(): + return true + default: + return false + } +} + +func (session *quicSession) Close() error { + return session.session.CloseWithError(quic.ApplicationErrorCode(0), "closed") +} + +type quicConn struct { + *quic.Stream + laddr net.Addr + raddr net.Addr +} + +func (c *quicConn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *quicConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/dialer/dns/dialer.go b/dialer/dns/dialer.go new file mode 100644 index 00000000..fea076dc --- /dev/null +++ b/dialer/dns/dialer.go @@ -0,0 +1,153 @@ +package dns + +import ( + "context" + "crypto/tls" + "errors" + "net" + "sync" + + "github.com/go-gost/core/dialer" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + dns_util "github.com/go-gost/x/internal/util/dns" + "github.com/go-gost/x/registry" + "github.com/quic-go/quic-go" +) + +func init() { + registry.DialerRegistry().Register("dns", NewDialer) + registry.DialerRegistry().Register("dns-tunnel", NewDialer) + registry.DialerRegistry().Register("dnst", NewDialer) +} + +type dnsDialer struct { + sessions map[string]*quicSession + sessionMutex sync.Mutex + logger logger.Logger + md metadata + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &dnsDialer{ + sessions: make(map[string]*quicSession), + logger: options.Logger, + options: options, + } +} + +func (d *dnsDialer) Init(md md.Metadata) (err error) { + if err = d.parseMetadata(md); err != nil { + return + } + + return nil +} + +func (d *dnsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) { + if _, _, err := net.SplitHostPort(addr); err != nil { + addr = net.JoinHostPort(addr, "0") + } + + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + + d.sessionMutex.Lock() + + session, ok := d.sessions[addr] + if session != nil && session.IsClosed() { + delete(d.sessions, addr) + ok = false + } + if !ok { + options := &dialer.DialOptions{} + for _, opt := range opts { + opt(options) + } + + c, err := options.Dialer.Dial(ctx, "udp", "") + if err != nil { + d.sessionMutex.Unlock() + return nil, err + } + pc, ok := c.(net.PacketConn) + if !ok { + c.Close() + d.sessionMutex.Unlock() + return nil, errors.New("dns: wrong connection type") + } + + pc = dns_util.ClientConn( + pc, + dns_util.DomainOption(d.md.domain), + dns_util.UDPSizeOption(d.md.udpSize), + ) + + session, err = d.initSession(ctx, udpAddr, pc) + if err != nil { + d.logger.Error(err) + pc.Close() + d.sessionMutex.Unlock() + return nil, err + } + + d.sessions[addr] = session + } + d.sessionMutex.Unlock() + + conn, err = session.GetConn() + if err != nil { + d.sessionMutex.Lock() + if d.sessions[addr] == session { + delete(d.sessions, addr) + } + session.Close() + d.sessionMutex.Unlock() + return nil, err + } + + return +} + +func (d *dnsDialer) initSession(ctx context.Context, addr net.Addr, conn net.PacketConn) (*quicSession, error) { + quicConfig := &quic.Config{ + KeepAlivePeriod: d.md.keepAlivePeriod, + HandshakeIdleTimeout: d.md.handshakeTimeout, + MaxIdleTimeout: d.md.maxIdleTimeout, + Versions: []quic.Version{ + quic.Version1, + quic.Version2, + }, + MaxIncomingStreams: int64(d.md.maxStreams), + EnableDatagrams: d.md.enableDatagram, + } + + tlsCfg := d.options.TLSConfig + if tlsCfg == nil { + tlsCfg = &tls.Config{} + } else { + tlsCfg = tlsCfg.Clone() + } + if len(tlsCfg.NextProtos) == 0 { + tlsCfg.NextProtos = []string{"h3", "quic/v1"} + } + + session, err := quic.DialEarly(ctx, conn, addr, tlsCfg, quicConfig) + if err != nil { + return nil, err + } + return &quicSession{session: session}, nil +} + +// Multiplex implements dialer.Multiplexer interface. +func (d *dnsDialer) Multiplex() bool { + return true +} diff --git a/dialer/dns/metadata.go b/dialer/dns/metadata.go new file mode 100644 index 00000000..c4df397e --- /dev/null +++ b/dialer/dns/metadata.go @@ -0,0 +1,48 @@ +package dns + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/x/metadata/util" +) + +type metadata struct { + keepAlivePeriod time.Duration + maxIdleTimeout time.Duration + handshakeTimeout time.Duration + maxStreams int + enableDatagram bool + + domain string + udpSize int +} + +func (d *dnsDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepAlive" + keepAlivePeriod = "ttl" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + maxStreams = "maxStreams" + + domain = "domain" + udpSize = "udpSize" + ) + + if md == nil || !md.IsExists(keepAlive) || mdutil.GetBool(md, keepAlive) { + d.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod) + if d.md.keepAlivePeriod <= 0 { + d.md.keepAlivePeriod = 10 * time.Second + } + } + d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) + d.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout) + d.md.maxStreams = mdutil.GetInt(md, maxStreams) + d.md.enableDatagram = mdutil.GetBool(md, "quic.enableDatagram", "enableDatagram") + + d.md.domain = mdutil.GetString(md, domain, "dns.domain") + d.md.udpSize = mdutil.GetInt(md, udpSize, "dns.udpSize") + + return +} diff --git a/internal/util/dns/conn.go b/internal/util/dns/conn.go new file mode 100644 index 00000000..ebb2037f --- /dev/null +++ b/internal/util/dns/conn.go @@ -0,0 +1,284 @@ +package dns + +import ( + "errors" + "net" + "sync" + "sync/atomic" + + "github.com/go-gost/core/common/bufpool" + mdns "github.com/miekg/dns" +) + +const ( + defaultDomain = "gost.local." + defaultUDPSize = 4096 + readBufferSize = 65535 + + edns0PayloadOption uint16 = mdns.EDNS0LOCALSTART +) + +var ( + ErrInvalidPacket = errors.New("dns: invalid packet") + ErrShortBuffer = errors.New("dns: short buffer") +) + +type options struct { + domain string + udpSize uint16 +} + +// Option configures a DNS packet wrapper. +type Option func(*options) + +// DomainOption sets the DNS question name used by wrapped packets. +func DomainOption(domain string) Option { + return func(o *options) { + if domain != "" { + o.domain = mdns.Fqdn(domain) + } + } +} + +// UDPSizeOption sets the EDNS0 UDP payload size advertised in wrapped packets. +func UDPSizeOption(size int) Option { + return func(o *options) { + if size > 0 && size <= readBufferSize { + o.udpSize = uint16(size) + } + } +} + +func parseOptions(opts ...Option) options { + o := options{ + domain: defaultDomain, + udpSize: defaultUDPSize, + } + for _, opt := range opts { + opt(&o) + } + return o +} + +type clientConn struct { + net.PacketConn + id uint32 + options options +} + +// ClientConn wraps outbound payloads as DNS queries and unwraps DNS responses. +func ClientConn(conn net.PacketConn, opts ...Option) net.PacketConn { + return &clientConn{ + PacketConn: conn, + options: parseOptions(opts...), + } +} + +func (c *clientConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + buf := bufpool.Get(readBufferSize) + defer bufpool.Put(buf) + + for { + n, addr, err = c.PacketConn.ReadFrom(buf) + if err != nil { + return + } + + m := mdns.Msg{} + if err = m.Unpack(buf[:n]); err != nil { + return 0, addr, err + } + if !m.Response { + continue + } + + payload, ok := payloadFromMessage(&m) + if !ok { + continue + } + if len(b) < len(payload) { + return 0, addr, ErrShortBuffer + } + n = copy(b, payload) + return + } +} + +func (c *clientConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + id := uint16(atomic.AddUint32(&c.id, 1)) + wb, err := packQuery(id, c.options.domain, c.options.udpSize, b) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(wb, addr) + if err != nil { + return 0, err + } + return len(b), nil +} + +type queryState struct { + id uint16 + question []mdns.Question +} + +type serverConn struct { + net.PacketConn + mu sync.RWMutex + queries map[string]queryState + options options +} + +// ServerConn unwraps DNS queries as inbound payloads and wraps outbound +// payloads as DNS responses. +func ServerConn(conn net.PacketConn, opts ...Option) net.PacketConn { + return &serverConn{ + PacketConn: conn, + queries: make(map[string]queryState), + options: parseOptions(opts...), + } +} + +func (c *serverConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + buf := bufpool.Get(readBufferSize) + defer bufpool.Put(buf) + + for { + n, addr, err = c.PacketConn.ReadFrom(buf) + if err != nil { + return + } + + m := mdns.Msg{} + if err = m.Unpack(buf[:n]); err != nil { + return 0, addr, err + } + if m.Response || !validQuestion(&m, c.options.domain) { + continue + } + + payload, ok := payloadFromMessage(&m) + if !ok { + continue + } + if len(b) < len(payload) { + return 0, addr, ErrShortBuffer + } + + c.storeQuery(addr, &m) + n = copy(b, payload) + return + } +} + +func (c *serverConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + state := c.loadQuery(addr) + wb, err := packResponse(state.id, state.question, c.options.domain, c.options.udpSize, b) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(wb, addr) + if err != nil { + return 0, err + } + return len(b), nil +} + +func (c *serverConn) storeQuery(addr net.Addr, m *mdns.Msg) { + if addr == nil { + return + } + + state := queryState{ + id: m.Id, + question: append([]mdns.Question(nil), m.Question...), + } + + c.mu.Lock() + c.queries[addr.String()] = state + c.mu.Unlock() +} + +func (c *serverConn) loadQuery(addr net.Addr) queryState { + if addr == nil { + return queryState{} + } + + c.mu.RLock() + state := c.queries[addr.String()] + c.mu.RUnlock() + return state +} + +func packQuery(id uint16, domain string, udpSize uint16, payload []byte) ([]byte, error) { + m := mdns.Msg{} + m.SetQuestion(domain, mdns.TypeNULL) + m.Id = id + m.RecursionDesired = false + addPayloadOpt(&m, udpSize, payload) + return m.Pack() +} + +func packResponse(id uint16, question []mdns.Question, domain string, udpSize uint16, payload []byte) ([]byte, error) { + m := mdns.Msg{} + m.Id = id + m.Response = true + m.Authoritative = true + m.Rcode = mdns.RcodeSuccess + if len(question) > 0 { + m.Question = append([]mdns.Question(nil), question...) + } else { + m.SetQuestion(domain, mdns.TypeNULL) + m.Id = id + m.Response = true + m.Authoritative = true + } + addPayloadOpt(&m, udpSize, payload) + return m.Pack() +} + +func addPayloadOpt(m *mdns.Msg, udpSize uint16, payload []byte) { + opt := &mdns.OPT{ + Hdr: mdns.RR_Header{ + Name: ".", + Rrtype: mdns.TypeOPT, + Class: udpSize, + }, + Option: []mdns.EDNS0{ + &mdns.EDNS0_LOCAL{ + Code: edns0PayloadOption, + Data: append([]byte(nil), payload...), + }, + }, + } + m.Extra = append(m.Extra, opt) +} + +func payloadFromMessage(m *mdns.Msg) ([]byte, bool) { + for _, rr := range m.Extra { + opt, ok := rr.(*mdns.OPT) + if !ok { + continue + } + for _, option := range opt.Option { + local, ok := option.(*mdns.EDNS0_LOCAL) + if !ok || local.Code != edns0PayloadOption { + continue + } + return append([]byte(nil), local.Data...), true + } + } + return nil, false +} + +func validQuestion(m *mdns.Msg, domain string) bool { + if len(m.Question) == 0 { + return false + } + for _, q := range m.Question { + if q.Qtype == mdns.TypeNULL && mdns.CanonicalName(q.Name) == mdns.CanonicalName(domain) { + return true + } + } + return false +} diff --git a/internal/util/dns/conn_test.go b/internal/util/dns/conn_test.go new file mode 100644 index 00000000..44120749 --- /dev/null +++ b/internal/util/dns/conn_test.go @@ -0,0 +1,61 @@ +package dns + +import ( + "bytes" + "net" + "testing" + "time" +) + +func TestPacketConnRoundTrip(t *testing.T) { + serverPC, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer serverPC.Close() + + clientPC, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer clientPC.Close() + + server := ServerConn(serverPC, DomainOption("tunnel.example.")) + client := ClientConn(clientPC, DomainOption("tunnel.example.")) + _ = server.SetDeadline(time.Now().Add(3 * time.Second)) + _ = client.SetDeadline(time.Now().Add(3 * time.Second)) + + requestPayload := []byte("hello over dns") + if n, err := client.WriteTo(requestPayload, server.LocalAddr()); err != nil { + t.Fatal(err) + } else if n != len(requestPayload) { + t.Fatalf("client WriteTo wrote %d bytes, want %d", n, len(requestPayload)) + } + + buf := make([]byte, 1500) + n, clientAddr, err := server.ReadFrom(buf) + if err != nil { + t.Fatal(err) + } + if got := buf[:n]; !bytes.Equal(got, requestPayload) { + t.Fatalf("server ReadFrom got %q, want %q", got, requestPayload) + } + + responsePayload := []byte("world over dns") + if n, err := server.WriteTo(responsePayload, clientAddr); err != nil { + t.Fatal(err) + } else if n != len(responsePayload) { + t.Fatalf("server WriteTo wrote %d bytes, want %d", n, len(responsePayload)) + } + + n, serverAddr, err := client.ReadFrom(buf) + if err != nil { + t.Fatal(err) + } + if serverAddr.String() != server.LocalAddr().String() { + t.Fatalf("response addr = %v, want %v", serverAddr, server.LocalAddr()) + } + if got := buf[:n]; !bytes.Equal(got, responsePayload) { + t.Fatalf("client ReadFrom got %q, want %q", got, responsePayload) + } +} diff --git a/listener/dns/listener.go b/listener/dns/listener.go index 5ac48857..d6904150 100644 --- a/listener/dns/listener.go +++ b/listener/dns/listener.go @@ -27,11 +27,14 @@ import ( func init() { registry.ListenerRegistry().Register("dns", NewListener) + registry.ListenerRegistry().Register("dns-tunnel", NewTunnelListener) + registry.ListenerRegistry().Register("dnst", NewTunnelListener) } type dnsListener struct { addr net.Addr server Server + tunnel listener.Listener cqueue chan net.Conn errChan chan error logger logger.Logger @@ -55,6 +58,29 @@ func (l *dnsListener) Init(md md.Metadata) (err error) { return } + if isTunnelMode(l.md.mode) { + tunnel := NewTunnelListener( + listener.AddrOption(l.options.Addr), + listener.RouterOption(l.options.Router), + listener.AutherOption(l.options.Auther), + listener.AuthOption(l.options.Auth), + listener.TLSConfigOption(l.options.TLSConfig), + listener.AdmissionOption(l.options.Admission), + listener.TrafficLimiterOption(l.options.TrafficLimiter), + listener.ConnLimiterOption(l.options.ConnLimiter), + listener.ServiceOption(l.options.Service), + listener.ProxyProtocolOption(l.options.ProxyProtocol), + listener.StatsOption(l.options.Stats), + listener.NetnsOption(l.options.Netns), + listener.LoggerOption(l.options.Logger), + ) + if err = tunnel.Init(md); err != nil { + return + } + l.tunnel = tunnel + return nil + } + switch strings.ToLower(l.md.mode) { case "tcp": l.addr, err = net.ResolveTCPAddr("tcp", l.options.Addr) @@ -230,6 +256,10 @@ func (l *dnsListener) Init(md md.Metadata) (err error) { } func (l *dnsListener) Accept() (conn net.Conn, err error) { + if l.tunnel != nil { + return l.tunnel.Accept() + } + var ok bool select { case conn = <-l.cqueue: @@ -254,10 +284,16 @@ func (l *dnsListener) Accept() (conn net.Conn, err error) { } func (l *dnsListener) Close() error { + if l.tunnel != nil { + return l.tunnel.Close() + } return l.server.Shutdown() } func (l *dnsListener) Addr() net.Addr { + if l.tunnel != nil { + return l.tunnel.Addr() + } return l.addr } diff --git a/listener/dns/metadata.go b/listener/dns/metadata.go index 47d9051f..d6d10293 100644 --- a/listener/dns/metadata.go +++ b/listener/dns/metadata.go @@ -1,6 +1,7 @@ package dns import ( + "strings" "time" mdata "github.com/go-gost/core/metadata" @@ -20,6 +21,15 @@ type metadata struct { mptcp bool } +func isTunnelMode(mode string) bool { + switch strings.ToLower(mode) { + case "tunnel", "quic", "packet": + return true + default: + return false + } +} + func (l *dnsListener) parseMetadata(md mdata.Metadata) (err error) { const ( backlog = "backlog" diff --git a/listener/dns/tunnel.go b/listener/dns/tunnel.go new file mode 100644 index 00000000..41b7e70f --- /dev/null +++ b/listener/dns/tunnel.go @@ -0,0 +1,205 @@ +package dns + +import ( + "context" + "crypto/tls" + "net" + "strings" + + "github.com/go-gost/core/limiter" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + admission "github.com/go-gost/x/admission/wrapper" + xnet "github.com/go-gost/x/internal/net" + dns_util "github.com/go-gost/x/internal/util/dns" + traffic_limiter "github.com/go-gost/x/limiter/traffic" + limiter_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" + metrics "github.com/go-gost/x/metrics/wrapper" + stats "github.com/go-gost/x/observer/stats/wrapper" + "github.com/quic-go/quic-go" +) + +type dnsTunnelListener struct { + ln quic.EarlyListener + cqueue chan net.Conn + errChan chan error + logger logger.Logger + md tunnelMetadata + options listener.Options +} + +func NewTunnelListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &dnsTunnelListener{ + logger: options.Logger, + options: options, + } +} + +func (l *dnsTunnelListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + addr := l.options.Addr + if _, _, err := net.SplitHostPort(addr); err != nil { + addr = net.JoinHostPort(strings.Trim(addr, "[]"), "0") + } + + network := "udp" + if xnet.IsIPv4(addr) { + network = "udp4" + } + laddr, err := net.ResolveUDPAddr(network, addr) + if err != nil { + return + } + + lc := net.ListenConfig{} + var conn net.PacketConn + conn, err = lc.ListenPacket(context.Background(), network, laddr.String()) + if err != nil { + return + } + + conn = dns_util.ServerConn( + conn, + dns_util.DomainOption(l.md.domain), + dns_util.UDPSizeOption(l.md.udpSize), + ) + conn = metrics.WrapPacketConn(l.options.Service, conn) + conn = stats.WrapPacketConn(conn, l.options.Stats) + conn = admission.WrapPacketConn(l.options.Admission, conn) + conn = limiter_wrapper.WrapPacketConn( + conn, + l.options.TrafficLimiter, + traffic_limiter.ServiceLimitKey, + limiter.ScopeOption(limiter.ScopeService), + limiter.ServiceOption(l.options.Service), + limiter.NetworkOption(conn.LocalAddr().Network()), + ) + + config := &quic.Config{ + KeepAlivePeriod: l.md.keepAlivePeriod, + HandshakeIdleTimeout: l.md.handshakeTimeout, + MaxIdleTimeout: l.md.maxIdleTimeout, + Versions: []quic.Version{ + quic.Version1, + quic.Version2, + }, + MaxIncomingStreams: int64(l.md.maxStreams), + EnableDatagrams: l.md.enableDatagram, + } + + tlsCfg := l.options.TLSConfig + if tlsCfg == nil { + tlsCfg = &tls.Config{} + } else { + tlsCfg = tlsCfg.Clone() + } + if len(tlsCfg.NextProtos) == 0 { + tlsCfg.NextProtos = []string{"h3", "quic/v1"} + } + + ln, err := quic.ListenEarly(conn, tlsCfg, config) + if err != nil { + _ = conn.Close() + return + } + + l.ln = *ln + l.cqueue = make(chan net.Conn, l.md.backlog) + l.errChan = make(chan error, 1) + + go l.listenLoop() + + return +} + +func (l *dnsTunnelListener) Accept() (conn net.Conn, err error) { + var ok bool + select { + case conn = <-l.cqueue: + conn = limiter_wrapper.WrapConn( + conn, + l.options.TrafficLimiter, + conn.RemoteAddr().String(), + limiter.ScopeOption(limiter.ScopeConn), + limiter.ServiceOption(l.options.Service), + limiter.NetworkOption(conn.LocalAddr().Network()), + limiter.SrcOption(conn.RemoteAddr().String()), + ) + case err, ok = <-l.errChan: + if !ok { + err = listener.ErrClosed + } + } + return +} + +func (l *dnsTunnelListener) Close() error { + return l.ln.Close() +} + +func (l *dnsTunnelListener) Addr() net.Addr { + return l.ln.Addr() +} + +func (l *dnsTunnelListener) listenLoop() { + for { + ctx := context.Background() + session, err := l.ln.Accept(ctx) + if err != nil { + l.logger.Error("accept: ", err) + l.errChan <- err + close(l.errChan) + return + } + l.logger.Infof("new client session: %v", session.RemoteAddr()) + go l.mux(ctx, session) + } +} + +func (l *dnsTunnelListener) mux(ctx context.Context, session *quic.Conn) { + defer session.CloseWithError(0, "closed") + + for { + stream, err := session.AcceptStream(ctx) + if err != nil { + l.logger.Error("accept stream: ", err) + return + } + + conn := &dnsTunnelConn{ + Stream: stream, + laddr: session.LocalAddr(), + raddr: session.RemoteAddr(), + } + select { + case l.cqueue <- conn: + case <-stream.Context().Done(): + stream.Close() + default: + stream.Close() + l.logger.Warnf("connection queue is full, client %s discarded", session.RemoteAddr()) + } + } +} + +type dnsTunnelConn struct { + *quic.Stream + laddr net.Addr + raddr net.Addr +} + +func (c *dnsTunnelConn) LocalAddr() net.Addr { + return c.laddr +} + +func (c *dnsTunnelConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/listener/dns/tunnel_metadata.go b/listener/dns/tunnel_metadata.go new file mode 100644 index 00000000..a18fd1e0 --- /dev/null +++ b/listener/dns/tunnel_metadata.go @@ -0,0 +1,55 @@ +package dns + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/x/metadata/util" +) + +type tunnelMetadata struct { + keepAlivePeriod time.Duration + handshakeTimeout time.Duration + maxIdleTimeout time.Duration + maxStreams int + enableDatagram bool + + backlog int + domain string + udpSize int +} + +func (l *dnsTunnelListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + keepAlive = "keepAlive" + keepAlivePeriod = "ttl" + handshakeTimeout = "handshakeTimeout" + maxIdleTimeout = "maxIdleTimeout" + maxStreams = "maxStreams" + + backlog = "backlog" + domain = "domain" + udpSize = "udpSize" + ) + + l.md.backlog = mdutil.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + if mdutil.GetBool(md, keepAlive) { + l.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod) + if l.md.keepAlivePeriod <= 0 { + l.md.keepAlivePeriod = 10 * time.Second + } + } + l.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) + l.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout) + l.md.maxStreams = mdutil.GetInt(md, maxStreams) + l.md.enableDatagram = mdutil.GetBool(md, "quic.enableDatagram", "enableDatagram") + + l.md.domain = mdutil.GetString(md, domain, "dns.domain") + l.md.udpSize = mdutil.GetInt(md, udpSize, "dns.udpSize") + + return +}