From ffaa0a39643c5198d6d90a48c7cccf8f03371a74 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 24 Sep 2024 02:36:04 +0000 Subject: [PATCH 01/28] chore: define more DNS scheme --- component/dns/upstream.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/component/dns/upstream.go b/component/dns/upstream.go index bd8682f7d..71545187e 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -30,6 +30,10 @@ const ( UpstreamScheme_UDP UpstreamScheme = "udp" UpstreamScheme_TCP_UDP UpstreamScheme = "tcp+udp" upstreamScheme_TCP_UDP_Alias UpstreamScheme = "udp+tcp" + upstreamScheme_TLS UpstreamScheme = "tls" + upstreamScheme_QUIC UpstreamScheme = "quic" + upstreamScheme_HTTPS UpstreamScheme = "https" + upstreamScheme_HTTP3 UpstreamScheme = "http3" ) func (s UpstreamScheme) ContainsTcp() bool { @@ -53,6 +57,16 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por if __port == "" { __port = "53" } + case upstreamScheme_HTTPS, upstreamScheme_HTTP3: + __port = raw.Port() + if __port == "" { + __port = "443" + } + case upstreamScheme_QUIC, upstreamScheme_TLS: + __port = raw.Port() + if __port == "" { + __port = "853" + } default: return "", "", 0, fmt.Errorf("unexpected scheme: %v", raw.Scheme) } @@ -115,9 +129,9 @@ func (u *Upstream) SupportedNetworks() (ipversions []consts.IpVersionStr, l4prot } } switch u.Scheme { - case UpstreamScheme_TCP: + case UpstreamScheme_TCP, upstreamScheme_HTTPS, upstreamScheme_TLS: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_TCP} - case UpstreamScheme_UDP: + case UpstreamScheme_UDP, upstreamScheme_QUIC, upstreamScheme_HTTP3: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_UDP} case UpstreamScheme_TCP_UDP: // UDP first. From de0850d5f31a6a16b97d97e0fe554e188f247c32 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 24 Sep 2024 02:36:16 +0000 Subject: [PATCH 02/28] feat: support DoT --- control/dns_control.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/control/dns_control.go b/control/dns_control.go index ac653e885..879b625e8 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -7,6 +7,7 @@ package control import ( "context" + "crypto/tls" "encoding/binary" "fmt" "io" @@ -33,6 +34,18 @@ import ( "github.com/sirupsen/logrus" ) +type NetConnWrapper struct { + netproxy.Conn +} + +func (c NetConnWrapper) LocalAddr() net.Addr { + return nil +} + +func (c NetConnWrapper) RemoteAddr() net.Addr { + return nil +} + const ( MaxDnsLookupDepth = 3 minFirefoxCacheTtl = 120 @@ -634,6 +647,13 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte // We can block here because we are in a coroutine. conn, err = dialArgument.bestDialer.DialContext(ctxDial, common.MagicNetwork("tcp", dialArgument.mark, dialArgument.mptcp), dialArgument.bestTarget.String()) + if upstream.Scheme == "tls" { + tlsConn := tls.Client(NetConnWrapper{Conn: conn}, &tls.Config{ + InsecureSkipVerify: false, + ServerName: upstream.Hostname, + }) + conn = tlsConn + } if err != nil { return fmt.Errorf("failed to dial proxy to tcp: %w", err) } From fd8ca888ce9c956aecd01e2d872ada6901f9a2fd Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 24 Sep 2024 02:54:26 +0000 Subject: [PATCH 03/28] feat: support DoH --- control/dns_control.go | 96 +++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 29 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 879b625e8..07eeb1884 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -13,7 +13,9 @@ import ( "io" "math" "net" + "net/http" "net/netip" + "net/url" "strconv" "strings" "sync" @@ -664,38 +666,74 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte }() _ = conn.SetDeadline(time.Now().Add(4900 * time.Millisecond)) - // We should write two byte length in the front of TCP DNS request. - bReq := pool.Get(2 + len(data)) - defer pool.Put(bReq) - binary.BigEndian.PutUint16(bReq, uint16(len(data))) - copy(bReq[2:], data) - _, err = conn.Write(bReq) - if err != nil { - return fmt.Errorf("failed to write DNS req: %w", err) - } + if upstream.Scheme == "tcp" || upstream.Scheme == "tls" { + + // We should write two byte length in the front of TCP DNS request. + bReq := pool.Get(2 + len(data)) + defer pool.Put(bReq) + binary.BigEndian.PutUint16(bReq, uint16(len(data))) + copy(bReq[2:], data) + _, err = conn.Write(bReq) + if err != nil { + return fmt.Errorf("failed to write DNS req: %w", err) + } - // Read two byte length. - if _, err = io.ReadFull(conn, bReq[:2]); err != nil { - return fmt.Errorf("failed to read DNS resp payload length: %w", err) - } - respLen := int(binary.BigEndian.Uint16(bReq)) - // Try to reuse the buf. - var buf []byte - if len(bReq) < respLen { - buf = pool.Get(respLen) - defer pool.Put(buf) + // Read two byte length. + if _, err = io.ReadFull(conn, bReq[:2]); err != nil { + return fmt.Errorf("failed to read DNS resp payload length: %w", err) + } + respLen := int(binary.BigEndian.Uint16(bReq)) + // Try to reuse the buf. + var buf []byte + if len(bReq) < respLen { + buf = pool.Get(respLen) + defer pool.Put(buf) + } else { + buf = bReq + } + var n int + if n, err = io.ReadFull(conn, buf[:respLen]); err != nil { + return fmt.Errorf("failed to read DNS resp payload: %w", err) + } + var msg dnsmessage.Msg + if err = msg.Unpack(buf[:n]); err != nil { + return err + } + respMsg = &msg } else { - buf = bReq - } - var n int - if n, err = io.ReadFull(conn, buf[:respLen]); err != nil { - return fmt.Errorf("failed to read DNS resp payload: %w", err) - } - var msg dnsmessage.Msg - if err = msg.Unpack(buf[:n]); err != nil { - return err + httpTransport := http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return NetConnWrapper{Conn: conn}, nil + }, + } + client := http.Client{ + Transport: &httpTransport, + } + serverURL := url.URL{ + Scheme: "https", + Host: dialArgument.bestTarget.String(), + Path: "/dns-query", + } + req, err := http.NewRequest(http.MethodPost, serverURL.String(), strings.NewReader(string(data))) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/dns-message") + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + buf, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + var msg dnsmessage.Msg + if err = msg.Unpack(buf); err != nil { + return err + } + respMsg = &msg } - respMsg = &msg default: return fmt.Errorf("unexpected l4proto: %v", dialArgument.l4proto) } From f0ef835ea83a38c62d93a99ca39b09ab622e26e4 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 24 Sep 2024 15:08:43 +0000 Subject: [PATCH 04/28] feat: support DoH3 --- control/dns_control.go | 180 ++++++++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 73 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 07eeb1884..e026f8612 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -31,23 +31,13 @@ import ( "github.com/daeuniverse/outbound/netproxy" "github.com/daeuniverse/outbound/pkg/fastrand" "github.com/daeuniverse/outbound/pool" + "github.com/daeuniverse/quic-go" + "github.com/daeuniverse/quic-go/http3" dnsmessage "github.com/miekg/dns" "github.com/mohae/deepcopy" "github.com/sirupsen/logrus" ) -type NetConnWrapper struct { - netproxy.Conn -} - -func (c NetConnWrapper) LocalAddr() net.Addr { - return nil -} - -func (c NetConnWrapper) RemoteAddr() net.Addr { - return nil -} - const ( MaxDnsLookupDepth = 3 minFirefoxCacheTtl = 120 @@ -601,58 +591,90 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte _ = conn.SetDeadline(time.Now().Add(timeout)) dnsReqCtx, cancelDnsReqCtx := context.WithTimeout(context.TODO(), timeout) defer cancelDnsReqCtx() - go func() { - // Send DNS request every seconds. - for { - _, err = conn.Write(data) - if err != nil { - if c.log.IsLevelEnabled(logrus.DebugLevel) { - c.log.WithFields(logrus.Fields{ - "to": dialArgument.bestTarget.String(), - "pid": req.routingResult.Pid, - "pname": ProcessName2String(req.routingResult.Pname[:]), - "mac": Mac2String(req.routingResult.Mac[:]), - "from": req.realSrc.String(), - "network": networkType.String(), - "err": err.Error(), - }).Debugln("Failed to write UDP(DNS) packet request.") + if upstream.Scheme == "udp" { + go func() { + // Send DNS request every seconds. + for { + _, err = conn.Write(data) + if err != nil { + if c.log.IsLevelEnabled(logrus.DebugLevel) { + c.log.WithFields(logrus.Fields{ + "to": dialArgument.bestTarget.String(), + "pid": req.routingResult.Pid, + "pname": ProcessName2String(req.routingResult.Pname[:]), + "mac": Mac2String(req.routingResult.Mac[:]), + "from": req.realSrc.String(), + "network": networkType.String(), + "err": err.Error(), + }).Debugln("Failed to write UDP(DNS) packet request.") + } + return + } + select { + case <-dnsReqCtx.Done(): + return + case <-time.After(1 * time.Second): } - return } - select { - case <-dnsReqCtx.Done(): - return - case <-time.After(1 * time.Second): + }() + + // We can block here because we are in a coroutine. + respBuf := pool.GetFullCap(consts.EthernetMtu) + defer pool.Put(respBuf) + // Wait for response. + n, err := conn.Read(respBuf) + if err != nil { + if c.timeoutExceedCallback != nil { + c.timeoutExceedCallback(dialArgument, err) } + return fmt.Errorf("failed to read from: %v (dialer: %v): %w", dialArgument.bestTarget, dialArgument.bestDialer.Property().Name, err) } - }() + var msg dnsmessage.Msg + if err = msg.Unpack(respBuf[:n]); err != nil { + return err + } + respMsg = &msg + cancelDnsReqCtx() + } else if upstream.Scheme == "http3" { + roundTripper := &http3.RoundTripper{ + TLSClientConfig: &tls.Config{ + ServerName: upstream.Hostname, + NextProtos: []string{"h3"}, + InsecureSkipVerify: false, + }, + QuicConfig: &quic.Config{}, + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + udpAddr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) + pkt := conn.(netproxy.PacketConn) + fakePkt := &netproxy.FakeNetPacketConn{ + PacketConn: pkt, + LAddr: net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.MustParseAddr("::1"), 0)), + RAddr: udpAddr, + } + c, e := quic.DialEarly(ctx, fakePkt, udpAddr, tlsCfg, cfg) + return c, e + }, + } + defer roundTripper.Close() - // We can block here because we are in a coroutine. - respBuf := pool.GetFullCap(consts.EthernetMtu) - defer pool.Put(respBuf) - // Wait for response. - n, err := conn.Read(respBuf) - if err != nil { - if c.timeoutExceedCallback != nil { - c.timeoutExceedCallback(dialArgument, err) + client := &http.Client{ + Transport: roundTripper, } - return fmt.Errorf("failed to read from: %v (dialer: %v): %w", dialArgument.bestTarget, dialArgument.bestDialer.Property().Name, err) - } - var msg dnsmessage.Msg - if err = msg.Unpack(respBuf[:n]); err != nil { - return err + msg, err := httpDNS(client, dialArgument.bestTarget.String(), data) + if err != nil { + return err + } + respMsg = msg } - respMsg = &msg - cancelDnsReqCtx() case consts.L4ProtoStr_TCP: // We can block here because we are in a coroutine. conn, err = dialArgument.bestDialer.DialContext(ctxDial, common.MagicNetwork("tcp", dialArgument.mark, dialArgument.mptcp), dialArgument.bestTarget.String()) if upstream.Scheme == "tls" { - tlsConn := tls.Client(NetConnWrapper{Conn: conn}, &tls.Config{ + tlsConn := tls.Client(&netproxy.FakeNetConn{Conn: conn}, &tls.Config{ InsecureSkipVerify: false, - ServerName: upstream.Hostname, + ServerName: upstream.Hostname, }) conn = tlsConn } @@ -700,39 +722,21 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return err } respMsg = &msg - } else { + } else if upstream.Scheme == "https" { + httpTransport := http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return NetConnWrapper{Conn: conn}, nil + return &netproxy.FakeNetConn{Conn: conn}, nil }, } client := http.Client{ Transport: &httpTransport, } - serverURL := url.URL{ - Scheme: "https", - Host: dialArgument.bestTarget.String(), - Path: "/dns-query", - } - req, err := http.NewRequest(http.MethodPost, serverURL.String(), strings.NewReader(string(data))) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/dns-message") - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - buf, err := io.ReadAll(resp.Body) + msg, err := httpDNS(&client, dialArgument.bestTarget.String(), data) if err != nil { return err } - var msg dnsmessage.Msg - if err = msg.Unpack(buf); err != nil { - return err - } - respMsg = &msg + respMsg = msg } default: return fmt.Errorf("unexpected l4proto: %v", dialArgument.l4proto) @@ -824,3 +828,33 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte } return nil } + +func httpDNS(client *http.Client, target string, data []byte) (respMsg *dnsmessage.Msg, err error) { + serverURL := url.URL{ + Scheme: "https", + Host: target, + Path: "/dns-query", + } + + req, err := http.NewRequest(http.MethodPost, serverURL.String(), strings.NewReader(string(data))) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/dns-message") + req.Header.Set("Accept", "application/dns-message") + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + buf, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var msg dnsmessage.Msg + if err = msg.Unpack(buf); err != nil { + return nil, err + } + respMsg = &msg + return respMsg, nil +} From 9261ec129c5250d788e8a876d35f311ba63bd11b Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 24 Sep 2024 17:16:26 +0000 Subject: [PATCH 05/28] chore: change DoH3 scheme to h3 --- component/dns/upstream.go | 2 +- control/dns_control.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/component/dns/upstream.go b/component/dns/upstream.go index 71545187e..af5edcb73 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -33,7 +33,7 @@ const ( upstreamScheme_TLS UpstreamScheme = "tls" upstreamScheme_QUIC UpstreamScheme = "quic" upstreamScheme_HTTPS UpstreamScheme = "https" - upstreamScheme_HTTP3 UpstreamScheme = "http3" + upstreamScheme_HTTP3 UpstreamScheme = "h3" ) func (s UpstreamScheme) ContainsTcp() bool { diff --git a/control/dns_control.go b/control/dns_control.go index e026f8612..54d6a4ad9 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -635,7 +635,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte } respMsg = &msg cancelDnsReqCtx() - } else if upstream.Scheme == "http3" { + } else if upstream.Scheme == "h3" { roundTripper := &http3.RoundTripper{ TLSClientConfig: &tls.Config{ ServerName: upstream.Hostname, From 0bef80f2df61c387563ee3ef1a4d45a324cca23f Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 24 Sep 2024 17:42:23 +0000 Subject: [PATCH 06/28] chore: DoH3 scheme support both h3 and http3 --- component/dns/upstream.go | 17 +++++++++-------- control/dns_control.go | 14 +++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/component/dns/upstream.go b/component/dns/upstream.go index af5edcb73..1e600a06f 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -30,10 +30,11 @@ const ( UpstreamScheme_UDP UpstreamScheme = "udp" UpstreamScheme_TCP_UDP UpstreamScheme = "tcp+udp" upstreamScheme_TCP_UDP_Alias UpstreamScheme = "udp+tcp" - upstreamScheme_TLS UpstreamScheme = "tls" - upstreamScheme_QUIC UpstreamScheme = "quic" - upstreamScheme_HTTPS UpstreamScheme = "https" - upstreamScheme_HTTP3 UpstreamScheme = "h3" + UpstreamScheme_TLS UpstreamScheme = "tls" + UpstreamScheme_QUIC UpstreamScheme = "quic" + UpstreamScheme_HTTPS UpstreamScheme = "https" + UpstreamScheme_HTTP3 UpstreamScheme = "http3" + UpstreamScheme_H3 UpstreamScheme = "h3" ) func (s UpstreamScheme) ContainsTcp() bool { @@ -57,12 +58,12 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por if __port == "" { __port = "53" } - case upstreamScheme_HTTPS, upstreamScheme_HTTP3: + case UpstreamScheme_HTTPS, UpstreamScheme_HTTP3, UpstreamScheme_H3: __port = raw.Port() if __port == "" { __port = "443" } - case upstreamScheme_QUIC, upstreamScheme_TLS: + case UpstreamScheme_QUIC, UpstreamScheme_TLS: __port = raw.Port() if __port == "" { __port = "853" @@ -129,9 +130,9 @@ func (u *Upstream) SupportedNetworks() (ipversions []consts.IpVersionStr, l4prot } } switch u.Scheme { - case UpstreamScheme_TCP, upstreamScheme_HTTPS, upstreamScheme_TLS: + case UpstreamScheme_TCP, UpstreamScheme_HTTPS, UpstreamScheme_TLS: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_TCP} - case UpstreamScheme_UDP, upstreamScheme_QUIC, upstreamScheme_HTTP3: + case UpstreamScheme_UDP, UpstreamScheme_QUIC, UpstreamScheme_HTTP3, UpstreamScheme_H3: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_UDP} case UpstreamScheme_TCP_UDP: // UDP first. diff --git a/control/dns_control.go b/control/dns_control.go index 54d6a4ad9..4b30b140f 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -591,7 +591,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte _ = conn.SetDeadline(time.Now().Add(timeout)) dnsReqCtx, cancelDnsReqCtx := context.WithTimeout(context.TODO(), timeout) defer cancelDnsReqCtx() - if upstream.Scheme == "udp" { + if upstream.Scheme == dns.UpstreamScheme_UDP { go func() { // Send DNS request every seconds. for { @@ -635,7 +635,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte } respMsg = &msg cancelDnsReqCtx() - } else if upstream.Scheme == "h3" { + } else if upstream.Scheme == dns.UpstreamScheme_H3 || upstream.Scheme == dns.UpstreamScheme_HTTP3 { roundTripper := &http3.RoundTripper{ TLSClientConfig: &tls.Config{ ServerName: upstream.Hostname, @@ -648,8 +648,8 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte pkt := conn.(netproxy.PacketConn) fakePkt := &netproxy.FakeNetPacketConn{ PacketConn: pkt, - LAddr: net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.MustParseAddr("::1"), 0)), - RAddr: udpAddr, + LAddr: net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.MustParseAddr("::1"), 0)), + RAddr: udpAddr, } c, e := quic.DialEarly(ctx, fakePkt, udpAddr, tlsCfg, cfg) return c, e @@ -671,7 +671,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte // We can block here because we are in a coroutine. conn, err = dialArgument.bestDialer.DialContext(ctxDial, common.MagicNetwork("tcp", dialArgument.mark, dialArgument.mptcp), dialArgument.bestTarget.String()) - if upstream.Scheme == "tls" { + if upstream.Scheme == dns.UpstreamScheme_TLS { tlsConn := tls.Client(&netproxy.FakeNetConn{Conn: conn}, &tls.Config{ InsecureSkipVerify: false, ServerName: upstream.Hostname, @@ -688,7 +688,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte }() _ = conn.SetDeadline(time.Now().Add(4900 * time.Millisecond)) - if upstream.Scheme == "tcp" || upstream.Scheme == "tls" { + if upstream.Scheme == dns.UpstreamScheme_TCP || upstream.Scheme == dns.UpstreamScheme_TLS { // We should write two byte length in the front of TCP DNS request. bReq := pool.Get(2 + len(data)) @@ -722,7 +722,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return err } respMsg = &msg - } else if upstream.Scheme == "https" { + } else if upstream.Scheme == dns.UpstreamScheme_HTTPS { httpTransport := http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { From 1643f71e7e8e2a8a1afb57b29cd11d934e0d0d9d Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 25 Sep 2024 05:11:04 +0000 Subject: [PATCH 07/28] fix: tcp+udp scheme broken and change control flow to switch when send DNS req --- control/dns_control.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 4b30b140f..f2cba5a27 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -591,7 +591,8 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte _ = conn.SetDeadline(time.Now().Add(timeout)) dnsReqCtx, cancelDnsReqCtx := context.WithTimeout(context.TODO(), timeout) defer cancelDnsReqCtx() - if upstream.Scheme == dns.UpstreamScheme_UDP { + switch upstream.Scheme { + case dns.UpstreamScheme_UDP, dns.UpstreamScheme_TCP_UDP: go func() { // Send DNS request every seconds. for { @@ -635,7 +636,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte } respMsg = &msg cancelDnsReqCtx() - } else if upstream.Scheme == dns.UpstreamScheme_H3 || upstream.Scheme == dns.UpstreamScheme_HTTP3 { + case dns.UpstreamScheme_H3, dns.UpstreamScheme_HTTP3: roundTripper := &http3.RoundTripper{ TLSClientConfig: &tls.Config{ ServerName: upstream.Hostname, @@ -688,8 +689,8 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte }() _ = conn.SetDeadline(time.Now().Add(4900 * time.Millisecond)) - if upstream.Scheme == dns.UpstreamScheme_TCP || upstream.Scheme == dns.UpstreamScheme_TLS { - + switch upstream.Scheme { + case dns.UpstreamScheme_TCP, dns.UpstreamScheme_TLS, dns.UpstreamScheme_TCP_UDP: // We should write two byte length in the front of TCP DNS request. bReq := pool.Get(2 + len(data)) defer pool.Put(bReq) @@ -722,7 +723,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return err } respMsg = &msg - } else if upstream.Scheme == dns.UpstreamScheme_HTTPS { + case dns.UpstreamScheme_HTTPS: httpTransport := http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { From 4fbd640925bb657f53d8bbd49b03987fb898936b Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 25 Sep 2024 05:19:31 +0000 Subject: [PATCH 08/28] chore: make DNS scheme http3 as a h3 alias --- component/dns/upstream.go | 9 ++++++--- control/dns_control.go | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/component/dns/upstream.go b/component/dns/upstream.go index 1e600a06f..e2ab73218 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -33,7 +33,7 @@ const ( UpstreamScheme_TLS UpstreamScheme = "tls" UpstreamScheme_QUIC UpstreamScheme = "quic" UpstreamScheme_HTTPS UpstreamScheme = "https" - UpstreamScheme_HTTP3 UpstreamScheme = "http3" + upstreamScheme_H3_Alias UpstreamScheme = "http3" UpstreamScheme_H3 UpstreamScheme = "h3" ) @@ -53,12 +53,15 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por case upstreamScheme_TCP_UDP_Alias: scheme = UpstreamScheme_TCP_UDP fallthrough + case upstreamScheme_H3_Alias: + scheme = UpstreamScheme_H3 + fallthrough case UpstreamScheme_TCP, UpstreamScheme_UDP, UpstreamScheme_TCP_UDP: __port = raw.Port() if __port == "" { __port = "53" } - case UpstreamScheme_HTTPS, UpstreamScheme_HTTP3, UpstreamScheme_H3: + case UpstreamScheme_HTTPS, UpstreamScheme_H3: __port = raw.Port() if __port == "" { __port = "443" @@ -132,7 +135,7 @@ func (u *Upstream) SupportedNetworks() (ipversions []consts.IpVersionStr, l4prot switch u.Scheme { case UpstreamScheme_TCP, UpstreamScheme_HTTPS, UpstreamScheme_TLS: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_TCP} - case UpstreamScheme_UDP, UpstreamScheme_QUIC, UpstreamScheme_HTTP3, UpstreamScheme_H3: + case UpstreamScheme_UDP, UpstreamScheme_QUIC, UpstreamScheme_H3: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_UDP} case UpstreamScheme_TCP_UDP: // UDP first. diff --git a/control/dns_control.go b/control/dns_control.go index f2cba5a27..ebcd5441b 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -636,7 +636,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte } respMsg = &msg cancelDnsReqCtx() - case dns.UpstreamScheme_H3, dns.UpstreamScheme_HTTP3: + case dns.UpstreamScheme_H3: roundTripper := &http3.RoundTripper{ TLSClientConfig: &tls.Config{ ServerName: upstream.Hostname, From 83507252af9a9e7d5f82f960c53d4b473fecfb22 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 25 Sep 2024 05:25:38 +0000 Subject: [PATCH 09/28] fix: DoH3 fake addr issue https://github.com/daeuniverse/dae/pull/649#discussion_r1773788026 --- control/dns_control.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/control/dns_control.go b/control/dns_control.go index ebcd5441b..63cf451b9 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -31,6 +31,7 @@ import ( "github.com/daeuniverse/outbound/netproxy" "github.com/daeuniverse/outbound/pkg/fastrand" "github.com/daeuniverse/outbound/pool" + tc "github.com/daeuniverse/outbound/protocol/tuic/common" "github.com/daeuniverse/quic-go" "github.com/daeuniverse/quic-go/http3" dnsmessage "github.com/miekg/dns" @@ -649,7 +650,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte pkt := conn.(netproxy.PacketConn) fakePkt := &netproxy.FakeNetPacketConn{ PacketConn: pkt, - LAddr: net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.MustParseAddr("::1"), 0)), + LAddr: net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), RAddr: udpAddr, } c, e := quic.DialEarly(ctx, fakePkt, udpAddr, tlsCfg, cfg) From 31c24df1f076e9c40e6eb415b55c25a4f99a89f5 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 25 Sep 2024 05:48:23 +0000 Subject: [PATCH 10/28] fix: udp+tcp alias to h3 --- component/dns/upstream.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/component/dns/upstream.go b/component/dns/upstream.go index e2ab73218..4df98f74a 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -53,14 +53,14 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por case upstreamScheme_TCP_UDP_Alias: scheme = UpstreamScheme_TCP_UDP fallthrough - case upstreamScheme_H3_Alias: - scheme = UpstreamScheme_H3 - fallthrough case UpstreamScheme_TCP, UpstreamScheme_UDP, UpstreamScheme_TCP_UDP: __port = raw.Port() if __port == "" { __port = "53" } + case upstreamScheme_H3_Alias: + scheme = UpstreamScheme_H3 + fallthrough case UpstreamScheme_HTTPS, UpstreamScheme_H3: __port = raw.Port() if __port == "" { From 3a01eee1752766b770624f2b90ea92f463f15502 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 25 Sep 2024 05:55:47 +0000 Subject: [PATCH 11/28] chore: remove DNS quic scheme since it not be implemented yet --- component/dns/upstream.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/component/dns/upstream.go b/component/dns/upstream.go index 4df98f74a..1ac06fb23 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -31,7 +31,6 @@ const ( UpstreamScheme_TCP_UDP UpstreamScheme = "tcp+udp" upstreamScheme_TCP_UDP_Alias UpstreamScheme = "udp+tcp" UpstreamScheme_TLS UpstreamScheme = "tls" - UpstreamScheme_QUIC UpstreamScheme = "quic" UpstreamScheme_HTTPS UpstreamScheme = "https" upstreamScheme_H3_Alias UpstreamScheme = "http3" UpstreamScheme_H3 UpstreamScheme = "h3" @@ -66,7 +65,7 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por if __port == "" { __port = "443" } - case UpstreamScheme_QUIC, UpstreamScheme_TLS: + case UpstreamScheme_TLS: __port = raw.Port() if __port == "" { __port = "853" @@ -135,7 +134,7 @@ func (u *Upstream) SupportedNetworks() (ipversions []consts.IpVersionStr, l4prot switch u.Scheme { case UpstreamScheme_TCP, UpstreamScheme_HTTPS, UpstreamScheme_TLS: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_TCP} - case UpstreamScheme_UDP, UpstreamScheme_QUIC, UpstreamScheme_H3: + case UpstreamScheme_UDP, UpstreamScheme_H3: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_UDP} case UpstreamScheme_TCP_UDP: // UDP first. From bd5e41df10a4ce2b29336833b992523f27a8ee03 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 25 Sep 2024 15:48:13 +0000 Subject: [PATCH 12/28] Revert "chore: remove DNS quic scheme since it not be implemented yet" This reverts commit c6c3643e5ea0f01e630a1c7c59fd2093dbae6902. --- component/dns/upstream.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/component/dns/upstream.go b/component/dns/upstream.go index 1ac06fb23..4df98f74a 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -31,6 +31,7 @@ const ( UpstreamScheme_TCP_UDP UpstreamScheme = "tcp+udp" upstreamScheme_TCP_UDP_Alias UpstreamScheme = "udp+tcp" UpstreamScheme_TLS UpstreamScheme = "tls" + UpstreamScheme_QUIC UpstreamScheme = "quic" UpstreamScheme_HTTPS UpstreamScheme = "https" upstreamScheme_H3_Alias UpstreamScheme = "http3" UpstreamScheme_H3 UpstreamScheme = "h3" @@ -65,7 +66,7 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por if __port == "" { __port = "443" } - case UpstreamScheme_TLS: + case UpstreamScheme_QUIC, UpstreamScheme_TLS: __port = raw.Port() if __port == "" { __port = "853" @@ -134,7 +135,7 @@ func (u *Upstream) SupportedNetworks() (ipversions []consts.IpVersionStr, l4prot switch u.Scheme { case UpstreamScheme_TCP, UpstreamScheme_HTTPS, UpstreamScheme_TLS: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_TCP} - case UpstreamScheme_UDP, UpstreamScheme_H3: + case UpstreamScheme_UDP, UpstreamScheme_QUIC, UpstreamScheme_H3: l4protos = []consts.L4ProtoStr{consts.L4ProtoStr_UDP} case UpstreamScheme_TCP_UDP: // UDP first. From 3aaa67aeee1dd5454e04c43238db1d84c57d30bf Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 25 Sep 2024 17:11:58 +0000 Subject: [PATCH 13/28] feat: support DoQ --- control/dns_control.go | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/control/dns_control.go b/control/dns_control.go index 63cf451b9..e05b270ba 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -667,6 +667,66 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return err } respMsg = msg + case dns.UpstreamScheme_QUIC: + udpAddr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) + pkt := conn.(netproxy.PacketConn) + fakePkt := &netproxy.FakeNetPacketConn{ + PacketConn: pkt, + LAddr: net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), + RAddr: udpAddr, + } + tlsCfg := &tls.Config{ + NextProtos: []string{"doq"}, + InsecureSkipVerify: false, + ServerName: upstream.Hostname, + } + addr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) + qc, err := quic.DialEarly(ctxDial, fakePkt, addr, tlsCfg, nil) + if err != nil { + return err + } + defer qc.CloseWithError(0, "") + + stream, err := qc.OpenStreamSync(ctxDial) + if err != nil { + return err + } + defer func() { + _ = stream.Close() + }() + + // We should write two byte length in the front of QUIC DNS request. + bReq := pool.Get(2 + len(data)) + defer pool.Put(bReq) + binary.BigEndian.PutUint16(bReq, uint16(len(data))) + copy(bReq[2:], data) + _, err = stream.Write(bReq) + if err != nil { + return fmt.Errorf("failed to write DNS req: %w", err) + } + + // Read two byte length. + if _, err = io.ReadFull(stream, bReq[:2]); err != nil { + return fmt.Errorf("failed to read DNS resp payload length: %w", err) + } + respLen := int(binary.BigEndian.Uint16(bReq)) + // Try to reuse the buf. + var buf []byte + if len(bReq) < respLen { + buf = pool.Get(respLen) + defer pool.Put(buf) + } else { + buf = bReq + } + var n int + if n, err = io.ReadFull(stream, buf[:respLen]); err != nil { + return fmt.Errorf("failed to read DNS resp payload: %w", err) + } + var msg dnsmessage.Msg + if err = msg.Unpack(buf[:n]); err != nil { + return err + } + respMsg = &msg } case consts.L4ProtoStr_TCP: From 203506d14a8c23d0a641bc3565c42e782e8ab6fc Mon Sep 17 00:00:00 2001 From: EkkoG Date: Thu, 26 Sep 2024 04:57:14 +0000 Subject: [PATCH 14/28] refactor: add streamDNS func for DoT, tcp, DoQ --- control/dns_control.go | 103 ++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index e05b270ba..2d4d71944 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -695,38 +695,11 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte _ = stream.Close() }() - // We should write two byte length in the front of QUIC DNS request. - bReq := pool.Get(2 + len(data)) - defer pool.Put(bReq) - binary.BigEndian.PutUint16(bReq, uint16(len(data))) - copy(bReq[2:], data) - _, err = stream.Write(bReq) + msg, err := streamDNS(stream, data) if err != nil { - return fmt.Errorf("failed to write DNS req: %w", err) - } - - // Read two byte length. - if _, err = io.ReadFull(stream, bReq[:2]); err != nil { - return fmt.Errorf("failed to read DNS resp payload length: %w", err) - } - respLen := int(binary.BigEndian.Uint16(bReq)) - // Try to reuse the buf. - var buf []byte - if len(bReq) < respLen { - buf = pool.Get(respLen) - defer pool.Put(buf) - } else { - buf = bReq - } - var n int - if n, err = io.ReadFull(stream, buf[:respLen]); err != nil { - return fmt.Errorf("failed to read DNS resp payload: %w", err) - } - var msg dnsmessage.Msg - if err = msg.Unpack(buf[:n]); err != nil { return err } - respMsg = &msg + respMsg = msg } case consts.L4ProtoStr_TCP: @@ -752,38 +725,11 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte _ = conn.SetDeadline(time.Now().Add(4900 * time.Millisecond)) switch upstream.Scheme { case dns.UpstreamScheme_TCP, dns.UpstreamScheme_TLS, dns.UpstreamScheme_TCP_UDP: - // We should write two byte length in the front of TCP DNS request. - bReq := pool.Get(2 + len(data)) - defer pool.Put(bReq) - binary.BigEndian.PutUint16(bReq, uint16(len(data))) - copy(bReq[2:], data) - _, err = conn.Write(bReq) + msg, err := streamDNS(conn, data) if err != nil { - return fmt.Errorf("failed to write DNS req: %w", err) - } - - // Read two byte length. - if _, err = io.ReadFull(conn, bReq[:2]); err != nil { - return fmt.Errorf("failed to read DNS resp payload length: %w", err) - } - respLen := int(binary.BigEndian.Uint16(bReq)) - // Try to reuse the buf. - var buf []byte - if len(bReq) < respLen { - buf = pool.Get(respLen) - defer pool.Put(buf) - } else { - buf = bReq - } - var n int - if n, err = io.ReadFull(conn, buf[:respLen]); err != nil { - return fmt.Errorf("failed to read DNS resp payload: %w", err) - } - var msg dnsmessage.Msg - if err = msg.Unpack(buf[:n]); err != nil { return err } - respMsg = &msg + respMsg = msg case dns.UpstreamScheme_HTTPS: httpTransport := http.Transport{ @@ -920,3 +866,44 @@ func httpDNS(client *http.Client, target string, data []byte) (respMsg *dnsmessa respMsg = &msg return respMsg, nil } + +type stream interface { + io.Reader + io.Writer +} + +func streamDNS(stream stream, data []byte) (respMsg *dnsmessage.Msg, err error) { + // We should write two byte length in the front of QUIC DNS request. + bReq := pool.Get(2 + len(data)) + defer pool.Put(bReq) + binary.BigEndian.PutUint16(bReq, uint16(len(data))) + copy(bReq[2:], data) + _, err = stream.Write(bReq) + if err != nil { + return nil, fmt.Errorf("failed to write DNS req: %w", err) + } + + // Read two byte length. + if _, err = io.ReadFull(stream, bReq[:2]); err != nil { + return nil, fmt.Errorf("failed to read DNS resp payload length: %w", err) + } + respLen := int(binary.BigEndian.Uint16(bReq)) + // Try to reuse the buf. + var buf []byte + if len(bReq) < respLen { + buf = pool.Get(respLen) + defer pool.Put(buf) + } else { + buf = bReq + } + var n int + if n, err = io.ReadFull(stream, buf[:respLen]); err != nil { + return nil, fmt.Errorf("failed to read DNS resp payload: %w", err) + } + var msg dnsmessage.Msg + if err = msg.Unpack(buf[:n]); err != nil { + return nil, err + } + return &msg, nil +} + From b483825cc86c0569f94884e488092e21993bc682 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Thu, 26 Sep 2024 05:23:35 +0000 Subject: [PATCH 15/28] fix: DoQ msg id issue msg id should set to 0 when transport over QUIC. https://github.com/natesales/q/blob/1cb2639caf69bd0a9b46494a3c689130df8fb24a/transport/quic.go#L97 https://datatracker.ietf.org/doc/html/rfc9250#section-4.2.1 --- control/dns_control.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/control/dns_control.go b/control/dns_control.go index 2d4d71944..5d41742b7 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -695,6 +695,12 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte _ = stream.Close() }() + // According https://datatracker.ietf.org/doc/html/rfc9250#section-4.2.1 + // msg id should set to 0 when transport over QUIC. + // thanks https://github.com/natesales/q/blob/1cb2639caf69bd0a9b46494a3c689130df8fb24a/transport/quic.go#L97 + binary.BigEndian.PutUint16(data[0:2], 0) + + msg, err := streamDNS(stream, data) if err != nil { return err From c89819ea57fa0ee9a49063ff39d34dd83a635086 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Thu, 26 Sep 2024 06:19:28 +0000 Subject: [PATCH 16/28] chore: remove stream interface, use io.ReadWriter instead --- control/dns_control.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 5d41742b7..60a6882cb 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -873,12 +873,7 @@ func httpDNS(client *http.Client, target string, data []byte) (respMsg *dnsmessa return respMsg, nil } -type stream interface { - io.Reader - io.Writer -} - -func streamDNS(stream stream, data []byte) (respMsg *dnsmessage.Msg, err error) { +func streamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg, err error) { // We should write two byte length in the front of QUIC DNS request. bReq := pool.Get(2 + len(data)) defer pool.Put(bReq) From 850bf649e8fce75bdb2459987a8fe7883e24cf54 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Thu, 26 Sep 2024 10:04:20 +0000 Subject: [PATCH 17/28] Fix: set DoH req's SNI and HTTP host to avoid certificate verify fail and CF DNS 403 https://github.com/daeuniverse/dae/pull/649#issuecomment-2376509545 --- control/dns_control.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 60a6882cb..2b9455956 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -662,7 +662,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := &http.Client{ Transport: roundTripper, } - msg, err := httpDNS(client, dialArgument.bestTarget.String(), data) + msg, err := httpDNS(client, dialArgument.bestTarget.String(), upstream.Hostname, data) if err != nil { return err } @@ -739,6 +739,10 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte case dns.UpstreamScheme_HTTPS: httpTransport := http.Transport{ + TLSClientConfig: &tls.Config{ + ServerName: upstream.Hostname, + InsecureSkipVerify: false, + }, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return &netproxy.FakeNetConn{Conn: conn}, nil }, @@ -746,7 +750,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := http.Client{ Transport: &httpTransport, } - msg, err := httpDNS(&client, dialArgument.bestTarget.String(), data) + msg, err := httpDNS(&client, dialArgument.bestTarget.String(), upstream.Hostname, data) if err != nil { return err } @@ -843,7 +847,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return nil } -func httpDNS(client *http.Client, target string, data []byte) (respMsg *dnsmessage.Msg, err error) { +func httpDNS(client *http.Client, target string, host string, data []byte) (respMsg *dnsmessage.Msg, err error) { serverURL := url.URL{ Scheme: "https", Host: target, @@ -856,6 +860,7 @@ func httpDNS(client *http.Client, target string, data []byte) (respMsg *dnsmessa } req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Accept", "application/dns-message") + req.Host = host resp, err := client.Do(req) if err != nil { return nil, err From 78af4433ac452ced2b43cc664240bde1997ebb19 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Thu, 26 Sep 2024 13:24:47 +0000 Subject: [PATCH 18/28] refactor: more clear send request function name --- control/dns_control.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 2b9455956..5db20fee9 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -662,7 +662,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := &http.Client{ Transport: roundTripper, } - msg, err := httpDNS(client, dialArgument.bestTarget.String(), upstream.Hostname, data) + msg, err := sendHttpDNS(client, dialArgument.bestTarget.String(), upstream.Hostname, data) if err != nil { return err } @@ -701,7 +701,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte binary.BigEndian.PutUint16(data[0:2], 0) - msg, err := streamDNS(stream, data) + msg, err := sendStreamDNS(stream, data) if err != nil { return err } @@ -731,7 +731,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte _ = conn.SetDeadline(time.Now().Add(4900 * time.Millisecond)) switch upstream.Scheme { case dns.UpstreamScheme_TCP, dns.UpstreamScheme_TLS, dns.UpstreamScheme_TCP_UDP: - msg, err := streamDNS(conn, data) + msg, err := sendStreamDNS(conn, data) if err != nil { return err } @@ -750,7 +750,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := http.Client{ Transport: &httpTransport, } - msg, err := httpDNS(&client, dialArgument.bestTarget.String(), upstream.Hostname, data) + msg, err := sendHttpDNS(&client, dialArgument.bestTarget.String(), upstream.Hostname, data) if err != nil { return err } @@ -847,7 +847,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return nil } -func httpDNS(client *http.Client, target string, host string, data []byte) (respMsg *dnsmessage.Msg, err error) { +func sendHttpDNS(client *http.Client, target string, host string, data []byte) (respMsg *dnsmessage.Msg, err error) { serverURL := url.URL{ Scheme: "https", Host: target, @@ -878,8 +878,8 @@ func httpDNS(client *http.Client, target string, host string, data []byte) (resp return respMsg, nil } -func streamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg, err error) { - // We should write two byte length in the front of QUIC DNS request. +func sendStreamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg, err error) { + // We should write two byte length in the front of stream DNS request. bReq := pool.Get(2 + len(data)) defer pool.Put(bReq) binary.BigEndian.PutUint16(bReq, uint16(len(data))) From 103f60b94605dc755dcdf2e4bc784a539e6322d1 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Fri, 27 Sep 2024 07:04:56 +0000 Subject: [PATCH 19/28] refactor: avoid convert []byte to string when send http request --- control/dns_control.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/control/dns_control.go b/control/dns_control.go index 5db20fee9..eb89bddb3 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -6,6 +6,7 @@ package control import ( + "bytes" "context" "crypto/tls" "encoding/binary" @@ -854,7 +855,7 @@ func sendHttpDNS(client *http.Client, target string, host string, data []byte) ( Path: "/dns-query", } - req, err := http.NewRequest(http.MethodPost, serverURL.String(), strings.NewReader(string(data))) + req, err := http.NewRequest(http.MethodPost, serverURL.String(), bytes.NewReader(data)) if err != nil { return nil, err } From db8929896386a0fba73b458e3caa34039a4671a7 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Fri, 27 Sep 2024 13:50:33 +0000 Subject: [PATCH 20/28] feat: support custom DoH/DoH3 url path --- component/dns/dns.go | 2 +- component/dns/upstream.go | 19 +++++++++++++------ control/dns_control.go | 12 +++++------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/component/dns/dns.go b/component/dns/dns.go index b6917e3ce..96ca5ce34 100644 --- a/component/dns/dns.go +++ b/component/dns/dns.go @@ -128,7 +128,7 @@ func New(dns *config.Dns, opt *NewOption) (s *Dns, err error) { func (s *Dns) CheckUpstreamsFormat() error { for _, upstream := range s.upstream { - _, _, _, err := ParseRawUpstream(upstream.Raw) + _, _, _, _, err := ParseRawUpstream(upstream.Raw) if err != nil { return err } diff --git a/component/dns/upstream.go b/component/dns/upstream.go index 4df98f74a..0adee2fe1 100644 --- a/component/dns/upstream.go +++ b/component/dns/upstream.go @@ -47,8 +47,9 @@ func (s UpstreamScheme) ContainsTcp() bool { } } -func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, port uint16, err error) { +func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, port uint16, path string, err error) { var __port string + var __path string switch scheme = UpstreamScheme(raw.Scheme); scheme { case upstreamScheme_TCP_UDP_Alias: scheme = UpstreamScheme_TCP_UDP @@ -66,32 +67,37 @@ func ParseRawUpstream(raw *url.URL) (scheme UpstreamScheme, hostname string, por if __port == "" { __port = "443" } + __path = raw.Path + if __path == "" { + __path = "/dns-query" + } case UpstreamScheme_QUIC, UpstreamScheme_TLS: __port = raw.Port() if __port == "" { __port = "853" } default: - return "", "", 0, fmt.Errorf("unexpected scheme: %v", raw.Scheme) + return "", "", 0, "", fmt.Errorf("unexpected scheme: %v", raw.Scheme) } _port, err := strconv.ParseUint(__port, 10, 16) if err != nil { - return "", "", 0, fmt.Errorf("failed to parse dns_upstream port: %v", err) + return "", "", 0, "", fmt.Errorf("failed to parse dns_upstream port: %v", err) } port = uint16(_port) hostname = raw.Hostname() - return scheme, hostname, port, nil + return scheme, hostname, port, __path, nil } type Upstream struct { Scheme UpstreamScheme Hostname string Port uint16 + Path string *netutils.Ip46 } func NewUpstream(ctx context.Context, upstream *url.URL, resolverNetwork string) (up *Upstream, err error) { - scheme, hostname, port, err := ParseRawUpstream(upstream) + scheme, hostname, port, path, err := ParseRawUpstream(upstream) if err != nil { return nil, fmt.Errorf("%w: %v", ErrFormat, err) } @@ -118,6 +124,7 @@ func NewUpstream(ctx context.Context, upstream *url.URL, resolverNetwork string) Scheme: scheme, Hostname: hostname, Port: port, + Path: path, Ip46: ip46, }, nil } @@ -145,7 +152,7 @@ func (u *Upstream) SupportedNetworks() (ipversions []consts.IpVersionStr, l4prot } func (u *Upstream) String() string { - return string(u.Scheme) + "://" + net.JoinHostPort(u.Hostname, strconv.Itoa(int(u.Port))) + return string(u.Scheme) + "://" + net.JoinHostPort(u.Hostname, strconv.Itoa(int(u.Port))) + u.Path } type UpstreamResolver struct { diff --git a/control/dns_control.go b/control/dns_control.go index eb89bddb3..4c03bbd12 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -663,7 +663,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := &http.Client{ Transport: roundTripper, } - msg, err := sendHttpDNS(client, dialArgument.bestTarget.String(), upstream.Hostname, data) + msg, err := sendHttpDNS(client, dialArgument.bestTarget.String(), upstream.Hostname, upstream.Path, data) if err != nil { return err } @@ -700,7 +700,6 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte // msg id should set to 0 when transport over QUIC. // thanks https://github.com/natesales/q/blob/1cb2639caf69bd0a9b46494a3c689130df8fb24a/transport/quic.go#L97 binary.BigEndian.PutUint16(data[0:2], 0) - msg, err := sendStreamDNS(stream, data) if err != nil { @@ -751,7 +750,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := http.Client{ Transport: &httpTransport, } - msg, err := sendHttpDNS(&client, dialArgument.bestTarget.String(), upstream.Hostname, data) + msg, err := sendHttpDNS(&client, dialArgument.bestTarget.String(), upstream.Hostname, upstream.Path, data) if err != nil { return err } @@ -848,11 +847,11 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return nil } -func sendHttpDNS(client *http.Client, target string, host string, data []byte) (respMsg *dnsmessage.Msg, err error) { +func sendHttpDNS(client *http.Client, target string, host string, path string, data []byte) (respMsg *dnsmessage.Msg, err error) { serverURL := url.URL{ Scheme: "https", Host: target, - Path: "/dns-query", + Path: path, } req, err := http.NewRequest(http.MethodPost, serverURL.String(), bytes.NewReader(data)) @@ -909,8 +908,7 @@ func sendStreamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg, } var msg dnsmessage.Msg if err = msg.Unpack(buf[:n]); err != nil { - return nil, err + return nil, err } return &msg, nil } - From 4c5a4620b68453ebd94e926e8304302562fd4d5f Mon Sep 17 00:00:00 2001 From: EkkoG Date: Fri, 27 Sep 2024 16:08:16 +0000 Subject: [PATCH 21/28] feat: disable redirect when use HTTP DNS https://github.com/daeuniverse/dae/pull/649#issuecomment-2379577896 --- control/dns_control.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 4c03bbd12..db9f1b522 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -6,9 +6,9 @@ package control import ( - "bytes" "context" "crypto/tls" + "encoding/base64" "encoding/binary" "fmt" "io" @@ -663,7 +663,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := &http.Client{ Transport: roundTripper, } - msg, err := sendHttpDNS(client, dialArgument.bestTarget.String(), upstream.Hostname, upstream.Path, data) + msg, err := sendHttpDNS(client, dialArgument.bestTarget.String(), upstream, data) if err != nil { return err } @@ -750,7 +750,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte client := http.Client{ Transport: &httpTransport, } - msg, err := sendHttpDNS(&client, dialArgument.bestTarget.String(), upstream.Hostname, upstream.Path, data) + msg, err := sendHttpDNS(&client, dialArgument.bestTarget.String(), upstream, data) if err != nil { return err } @@ -847,20 +847,26 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte return nil } -func sendHttpDNS(client *http.Client, target string, host string, path string, data []byte) (respMsg *dnsmessage.Msg, err error) { +func sendHttpDNS(client *http.Client, target string, upstream *dns.Upstream, data []byte) (respMsg *dnsmessage.Msg, err error) { + // disable redirect https://github.com/daeuniverse/dae/pull/649#issuecomment-2379577896 + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return fmt.Errorf("do not use a server that will redirect, upstream: %v", upstream.String()) + } serverURL := url.URL{ Scheme: "https", Host: target, - Path: path, + Path: upstream.Path, } + q := serverURL.Query() + q.Set("dns", base64.RawURLEncoding.EncodeToString(data)) + serverURL.RawQuery = q.Encode() - req, err := http.NewRequest(http.MethodPost, serverURL.String(), bytes.NewReader(data)) + req, err := http.NewRequest(http.MethodGet, serverURL.String(), nil) if err != nil { return nil, err } - req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Accept", "application/dns-message") - req.Host = host + req.Host = upstream.Hostname resp, err := client.Do(req) if err != nil { return nil, err @@ -874,8 +880,7 @@ func sendHttpDNS(client *http.Client, target string, host string, path string, d if err = msg.Unpack(buf); err != nil { return nil, err } - respMsg = &msg - return respMsg, nil + return &msg, nil } func sendStreamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg, err error) { From d9ad712fb8209c7783f17cffe6ce6a4fb013bd3b Mon Sep 17 00:00:00 2001 From: EkkoG Date: Fri, 27 Sep 2024 17:05:36 +0000 Subject: [PATCH 22/28] fix: error when DNS request over proxy https://github.com/daeuniverse/dae/pull/649/commits/a1ac88c8ee879ed056580fc547612aa176bc6b90 --- control/dns_control.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index db9f1b522..b4c202dac 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -648,12 +648,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte QuicConfig: &quic.Config{}, Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { udpAddr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) - pkt := conn.(netproxy.PacketConn) - fakePkt := &netproxy.FakeNetPacketConn{ - PacketConn: pkt, - LAddr: net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), - RAddr: udpAddr, - } + fakePkt := netproxy.NewFakeNetPacketConn(conn.(netproxy.PacketConn), net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), udpAddr) c, e := quic.DialEarly(ctx, fakePkt, udpAddr, tlsCfg, cfg) return c, e }, @@ -670,12 +665,7 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte respMsg = msg case dns.UpstreamScheme_QUIC: udpAddr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) - pkt := conn.(netproxy.PacketConn) - fakePkt := &netproxy.FakeNetPacketConn{ - PacketConn: pkt, - LAddr: net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), - RAddr: udpAddr, - } + fakePkt := netproxy.NewFakeNetPacketConn(conn.(netproxy.PacketConn), net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), udpAddr) tlsCfg := &tls.Config{ NextProtos: []string{"doq"}, InsecureSkipVerify: false, @@ -866,7 +856,7 @@ func sendHttpDNS(client *http.Client, target string, upstream *dns.Upstream, dat return nil, err } req.Header.Set("Accept", "application/dns-message") - req.Host = upstream.Hostname + req.Host = upstream.Hostname resp, err := client.Do(req) if err != nil { return nil, err From bfa975d6428e250bb0a4962ce2b84f23818e0926 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 8 Oct 2024 13:38:01 +0000 Subject: [PATCH 23/28] feat(dns): set id to 0 when DoH and DoH3 for cache friendly --- control/dns_control.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/control/dns_control.go b/control/dns_control.go index b4c202dac..655ac1992 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -848,6 +848,9 @@ func sendHttpDNS(client *http.Client, target string, upstream *dns.Upstream, dat Path: upstream.Path, } q := serverURL.Query() + // According https://datatracker.ietf.org/doc/html/rfc8484#section-4 + // msg id should set to 0 when transport over HTTPS for cache friendly. + binary.BigEndian.PutUint16(data[0:2], 0) q.Set("dns", base64.RawURLEncoding.EncodeToString(data)) serverURL.RawQuery = q.Encode() From a0ccbc35b2d318b9179a220852feaab1d898c5c7 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 8 Oct 2024 17:33:02 +0000 Subject: [PATCH 24/28] feat(dns): reuse connection for DoH/DoH3/DoQ --- control/dns.go | 428 +++++++++++++++++++++++++++++++++++++++++ control/dns_control.go | 277 ++------------------------ 2 files changed, 442 insertions(+), 263 deletions(-) create mode 100644 control/dns.go diff --git a/control/dns.go b/control/dns.go new file mode 100644 index 000000000..24fc0e218 --- /dev/null +++ b/control/dns.go @@ -0,0 +1,428 @@ +package control + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/daeuniverse/dae/common" + "github.com/daeuniverse/dae/common/consts" + "github.com/daeuniverse/dae/component/dns" + "github.com/daeuniverse/outbound/netproxy" + "github.com/daeuniverse/outbound/pool" + tc "github.com/daeuniverse/outbound/protocol/tuic/common" + "github.com/daeuniverse/quic-go" + "github.com/daeuniverse/quic-go/http3" + dnsmessage "github.com/miekg/dns" +) + +type DnsForwarder interface { + ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) + Close() error +} + +var forwarderCache = make(map[string]DnsForwarder) + +func newDnsForwarder(upstream *dns.Upstream, dialArgument dialArgument) (DnsForwarder, error) { + if forwarder, ok := forwarderCache[upstream.String()]; ok { + return forwarder, nil + } + forwarder, err := func() (DnsForwarder, error) { + switch dialArgument.l4proto { + case consts.L4ProtoStr_TCP: + switch upstream.Scheme { + case dns.UpstreamScheme_TCP, dns.UpstreamScheme_TCP_UDP: + return &DoTCP{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument}, nil + case dns.UpstreamScheme_TLS: + return &DoTLS{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument}, nil + case dns.UpstreamScheme_HTTPS: + return &DoH{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument, http3: false}, nil + default: + return nil, fmt.Errorf("unexpected scheme: %v", upstream.Scheme) + } + case consts.L4ProtoStr_UDP: + switch upstream.Scheme { + case dns.UpstreamScheme_UDP, dns.UpstreamScheme_TCP_UDP: + return &DoUDP{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument}, nil + case dns.UpstreamScheme_QUIC: + return &DoQ{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument}, nil + case dns.UpstreamScheme_H3: + return &DoH{Upstream: *upstream, Dialer: dialArgument.bestDialer, dialArgument: dialArgument, http3: true}, nil + default: + return nil, fmt.Errorf("unexpected scheme: %v", upstream.Scheme) + } + default: + return nil, fmt.Errorf("unexpected l4proto: %v", dialArgument.l4proto) + } + }() + if err != nil { + return nil, err + } + forwarderCache[upstream.String()] = forwarder + return forwarder, nil +} + +type DoH struct { + dns.Upstream + netproxy.Dialer + dialArgument dialArgument + http3 bool + client *http.Client +} + +func (d *DoH) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) { + if d.client == nil { + var roundTripper http.RoundTripper + if d.http3 { + roundTripper = d.getHttp3RoundTripper() + } else { + roundTripper = d.getHttpRoundTripper() + } + + d.client = &http.Client{ + Transport: roundTripper, + } + } + return sendHttpDNS(d.client, d.dialArgument.bestTarget.String(), &d.Upstream, data) +} + +func (d *DoH) getHttpRoundTripper() *http.Transport { + httpTransport := http.Transport{ + TLSClientConfig: &tls.Config{ + ServerName: d.Upstream.Hostname, + InsecureSkipVerify: false, + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := d.dialArgument.bestDialer.DialContext( + ctx, + common.MagicNetwork("tcp", d.dialArgument.mark, d.dialArgument.mptcp), + d.dialArgument.bestTarget.String(), + ) + if err != nil { + return nil, err + } + return &netproxy.FakeNetConn{Conn: conn}, nil + }, + } + + return &httpTransport +} + +func (d *DoH) getHttp3RoundTripper() *http3.RoundTripper { + roundTripper := &http3.RoundTripper{ + TLSClientConfig: &tls.Config{ + ServerName: d.Upstream.Hostname, + NextProtos: []string{"h3"}, + InsecureSkipVerify: false, + }, + QuicConfig: &quic.Config{}, + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + udpAddr := net.UDPAddrFromAddrPort(d.dialArgument.bestTarget) + conn, err := d.dialArgument.bestDialer.DialContext( + ctx, + common.MagicNetwork("udp", d.dialArgument.mark, d.dialArgument.mptcp), + d.dialArgument.bestTarget.String(), + ) + if err != nil { + return nil, err + } + fakePkt := netproxy.NewFakeNetPacketConn(conn.(netproxy.PacketConn), net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), udpAddr) + c, e := quic.DialEarly(ctx, fakePkt, udpAddr, tlsCfg, cfg) + return c, e + }, + } + return roundTripper +} + +func (d *DoH) Close() error { + return nil +} + +type DoQ struct { + dns.Upstream + netproxy.Dialer + dialArgument dialArgument + connection quic.EarlyConnection +} + +func (d *DoQ) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) { + if d.connection == nil { + qc, err := d.createConnection(ctx) + if err != nil { + return nil, err + } + d.connection = qc + } + + stream, err := d.connection.OpenStreamSync(ctx) + if err != nil { + qc, err := d.createConnection(ctx) + if err != nil { + return nil, err + } + d.connection = qc + stream, err = d.connection.OpenStreamSync(ctx) + if err != nil { + return nil, err + } + } + defer func() { + _ = stream.Close() + }() + + // According https://datatracker.ietf.org/doc/html/rfc9250#section-4.2.1 + // msg id should set to 0 when transport over QUIC. + // thanks https://github.com/natesales/q/blob/1cb2639caf69bd0a9b46494a3c689130df8fb24a/transport/quic.go#L97 + binary.BigEndian.PutUint16(data[0:2], 0) + + msg, err := sendStreamDNS(stream, data) + if err != nil { + return nil, err + } + return msg, nil +} +func (d *DoQ) createConnection(ctx context.Context) (quic.EarlyConnection, error) { + + udpAddr := net.UDPAddrFromAddrPort(d.dialArgument.bestTarget) + conn, err := d.dialArgument.bestDialer.DialContext( + ctx, + common.MagicNetwork("udp", d.dialArgument.mark, d.dialArgument.mptcp), + d.dialArgument.bestTarget.String(), + ) + if err != nil { + return nil, err + } + + fakePkt := netproxy.NewFakeNetPacketConn(conn.(netproxy.PacketConn), net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), udpAddr) + tlsCfg := &tls.Config{ + NextProtos: []string{"doq"}, + InsecureSkipVerify: false, + ServerName: d.Upstream.Hostname, + } + addr := net.UDPAddrFromAddrPort(d.dialArgument.bestTarget) + qc, err := quic.DialEarly(ctx, fakePkt, addr, tlsCfg, nil) + if err != nil { + return nil, err + } + return qc, nil + +} + +func (d *DoQ) Close() error { + return nil +} + +type DoTLS struct { + dns.Upstream + netproxy.Dialer + dialArgument dialArgument + conn netproxy.Conn +} + +func (d *DoTLS) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) { + conn, err := d.dialArgument.bestDialer.DialContext( + ctx, + common.MagicNetwork("tcp", d.dialArgument.mark, d.dialArgument.mptcp), + d.dialArgument.bestTarget.String(), + ) + if err != nil { + return nil, err + } + + tlsConn := tls.Client(&netproxy.FakeNetConn{Conn: conn}, &tls.Config{ + InsecureSkipVerify: false, + ServerName: d.Upstream.Hostname, + }) + if err = tlsConn.Handshake(); err != nil { + return nil, err + } + d.conn = tlsConn + + return sendStreamDNS(tlsConn, data) +} + +func (d *DoTLS) Close() error { + if d.conn != nil { + return d.conn.Close() + } + return nil +} + +type DoTCP struct { + dns.Upstream + netproxy.Dialer + dialArgument dialArgument + conn netproxy.Conn +} + +func (d *DoTCP) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) { + conn, err := d.dialArgument.bestDialer.DialContext( + ctx, + common.MagicNetwork("tcp", d.dialArgument.mark, d.dialArgument.mptcp), + d.dialArgument.bestTarget.String(), + ) + if err != nil { + return nil, err + } + + d.conn = conn + return sendStreamDNS(conn, data) +} + +func (d *DoTCP) Close() error { + if d.conn != nil { + return d.conn.Close() + } + return nil +} + +type DoUDP struct { + dns.Upstream + netproxy.Dialer + dialArgument dialArgument + conn netproxy.Conn +} + +func (d *DoUDP) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) { + conn, err := d.dialArgument.bestDialer.DialContext( + ctx, + common.MagicNetwork("udp", d.dialArgument.mark, d.dialArgument.mptcp), + d.dialArgument.bestTarget.String(), + ) + if err != nil { + return nil, err + } + + timeout := 5 * time.Second + _ = conn.SetDeadline(time.Now().Add(timeout)) + dnsReqCtx, cancelDnsReqCtx := context.WithTimeout(context.TODO(), timeout) + defer cancelDnsReqCtx() + + go func() { + // Send DNS request every seconds. + for { + _, err = conn.Write(data) + // if err != nil { + // if c.log.IsLevelEnabled(logrus.DebugLevel) { + // c.log.WithFields(logrus.Fields{ + // "to": dialArgument.bestTarget.String(), + // "pid": req.routingResult.Pid, + // "pname": ProcessName2String(req.routingResult.Pname[:]), + // "mac": Mac2String(req.routingResult.Mac[:]), + // "from": req.realSrc.String(), + // "network": networkType.String(), + // "err": err.Error(), + // }).Debugln("Failed to write UDP(DNS) packet request.") + // } + // return + // } + select { + case <-dnsReqCtx.Done(): + return + case <-time.After(1 * time.Second): + } + } + }() + + // We can block here because we are in a coroutine. + respBuf := pool.GetFullCap(consts.EthernetMtu) + defer pool.Put(respBuf) + // Wait for response. + n, err := conn.Read(respBuf) + if err != nil { + return nil, err + } + var msg dnsmessage.Msg + if err = msg.Unpack(respBuf[:n]); err != nil { + return nil, err + } + return &msg, nil +} + +func (d *DoUDP) Close() error { + if d.conn != nil { + return d.conn.Close() + } + return nil +} + +func sendHttpDNS(client *http.Client, target string, upstream *dns.Upstream, data []byte) (respMsg *dnsmessage.Msg, err error) { + // disable redirect https://github.com/daeuniverse/dae/pull/649#issuecomment-2379577896 + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return fmt.Errorf("do not use a server that will redirect, upstream: %v", upstream.String()) + } + serverURL := url.URL{ + Scheme: "https", + Host: target, + Path: upstream.Path, + } + q := serverURL.Query() + // According https://datatracker.ietf.org/doc/html/rfc8484#section-4 + // msg id should set to 0 when transport over HTTPS for cache friendly. + binary.BigEndian.PutUint16(data[0:2], 0) + q.Set("dns", base64.RawURLEncoding.EncodeToString(data)) + serverURL.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, serverURL.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/dns-message") + req.Host = upstream.Hostname + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + buf, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var msg dnsmessage.Msg + if err = msg.Unpack(buf); err != nil { + return nil, err + } + return &msg, nil +} + +func sendStreamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg, err error) { + // We should write two byte length in the front of stream DNS request. + bReq := pool.Get(2 + len(data)) + defer pool.Put(bReq) + binary.BigEndian.PutUint16(bReq, uint16(len(data))) + copy(bReq[2:], data) + _, err = stream.Write(bReq) + if err != nil { + return nil, fmt.Errorf("failed to write DNS req: %w", err) + } + + // Read two byte length. + if _, err = io.ReadFull(stream, bReq[:2]); err != nil { + return nil, fmt.Errorf("failed to read DNS resp payload length: %w", err) + } + respLen := int(binary.BigEndian.Uint16(bReq)) + // Try to reuse the buf. + var buf []byte + if len(bReq) < respLen { + buf = pool.Get(respLen) + defer pool.Put(buf) + } else { + buf = bReq + } + var n int + if n, err = io.ReadFull(stream, buf[:respLen]); err != nil { + return nil, fmt.Errorf("failed to read DNS resp payload: %w", err) + } + var msg dnsmessage.Msg + if err = msg.Unpack(buf[:n]); err != nil { + return nil, err + } + return &msg, nil +} diff --git a/control/dns_control.go b/control/dns_control.go index 655ac1992..84373d7cc 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -7,34 +7,22 @@ package control import ( "context" - "crypto/tls" - "encoding/base64" - "encoding/binary" "fmt" - "io" "math" "net" "net/http" "net/netip" - "net/url" "strconv" "strings" "sync" "time" - "github.com/daeuniverse/dae/common" - "github.com/daeuniverse/dae/common/consts" "github.com/daeuniverse/dae/common/netutils" "github.com/daeuniverse/dae/component/dns" "github.com/daeuniverse/dae/component/outbound" "github.com/daeuniverse/dae/component/outbound/dialer" - "github.com/daeuniverse/outbound/netproxy" "github.com/daeuniverse/outbound/pkg/fastrand" - "github.com/daeuniverse/outbound/pool" - tc "github.com/daeuniverse/outbound/protocol/tuic/common" - "github.com/daeuniverse/quic-go" - "github.com/daeuniverse/quic-go/http3" dnsmessage "github.com/miekg/dns" "github.com/mohae/deepcopy" "github.com/sirupsen/logrus" @@ -521,6 +509,8 @@ func (c *DnsController) sendReject_(dnsMessage *dnsmessage.Msg, req *udpRequest) return nil } +var clientCache = make(map[string]*http.Client) + func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte, id uint16, upstream *dns.Upstream, needResp bool) (err error) { if invokingDepth >= MaxDnsLookupDepth { return fmt.Errorf("too deep DNS lookup invoking (depth: %v); there may be infinite loop in your DNS response routing", MaxDnsLookupDepth) @@ -565,193 +555,28 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte // the next recursive call. However, a connection cannot be closed twice. // We should set a connClosed flag to avoid it. var connClosed bool - var conn netproxy.Conn ctxDial, cancel := context.WithTimeout(context.TODO(), consts.DefaultDialTimeout) defer cancel() - switch dialArgument.l4proto { - case consts.L4ProtoStr_UDP: - // Get udp endpoint. - - // TODO: connection pool. - conn, err = dialArgument.bestDialer.DialContext( - ctxDial, - common.MagicNetwork("udp", dialArgument.mark, dialArgument.mptcp), - dialArgument.bestTarget.String(), - ) - if err != nil { - return fmt.Errorf("failed to dial '%v': %w", dialArgument.bestTarget, err) - } - defer func() { - if !connClosed { - conn.Close() - } - }() - - timeout := 5 * time.Second - _ = conn.SetDeadline(time.Now().Add(timeout)) - dnsReqCtx, cancelDnsReqCtx := context.WithTimeout(context.TODO(), timeout) - defer cancelDnsReqCtx() - switch upstream.Scheme { - case dns.UpstreamScheme_UDP, dns.UpstreamScheme_TCP_UDP: - go func() { - // Send DNS request every seconds. - for { - _, err = conn.Write(data) - if err != nil { - if c.log.IsLevelEnabled(logrus.DebugLevel) { - c.log.WithFields(logrus.Fields{ - "to": dialArgument.bestTarget.String(), - "pid": req.routingResult.Pid, - "pname": ProcessName2String(req.routingResult.Pname[:]), - "mac": Mac2String(req.routingResult.Mac[:]), - "from": req.realSrc.String(), - "network": networkType.String(), - "err": err.Error(), - }).Debugln("Failed to write UDP(DNS) packet request.") - } - return - } - select { - case <-dnsReqCtx.Done(): - return - case <-time.After(1 * time.Second): - } - } - }() - - // We can block here because we are in a coroutine. - respBuf := pool.GetFullCap(consts.EthernetMtu) - defer pool.Put(respBuf) - // Wait for response. - n, err := conn.Read(respBuf) - if err != nil { - if c.timeoutExceedCallback != nil { - c.timeoutExceedCallback(dialArgument, err) - } - return fmt.Errorf("failed to read from: %v (dialer: %v): %w", dialArgument.bestTarget, dialArgument.bestDialer.Property().Name, err) - } - var msg dnsmessage.Msg - if err = msg.Unpack(respBuf[:n]); err != nil { - return err - } - respMsg = &msg - cancelDnsReqCtx() - case dns.UpstreamScheme_H3: - roundTripper := &http3.RoundTripper{ - TLSClientConfig: &tls.Config{ - ServerName: upstream.Hostname, - NextProtos: []string{"h3"}, - InsecureSkipVerify: false, - }, - QuicConfig: &quic.Config{}, - Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { - udpAddr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) - fakePkt := netproxy.NewFakeNetPacketConn(conn.(netproxy.PacketConn), net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), udpAddr) - c, e := quic.DialEarly(ctx, fakePkt, udpAddr, tlsCfg, cfg) - return c, e - }, - } - defer roundTripper.Close() - - client := &http.Client{ - Transport: roundTripper, - } - msg, err := sendHttpDNS(client, dialArgument.bestTarget.String(), upstream, data) - if err != nil { - return err - } - respMsg = msg - case dns.UpstreamScheme_QUIC: - udpAddr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) - fakePkt := netproxy.NewFakeNetPacketConn(conn.(netproxy.PacketConn), net.UDPAddrFromAddrPort(tc.GetUniqueFakeAddrPort()), udpAddr) - tlsCfg := &tls.Config{ - NextProtos: []string{"doq"}, - InsecureSkipVerify: false, - ServerName: upstream.Hostname, - } - addr := net.UDPAddrFromAddrPort(dialArgument.bestTarget) - qc, err := quic.DialEarly(ctxDial, fakePkt, addr, tlsCfg, nil) - if err != nil { - return err - } - defer qc.CloseWithError(0, "") - - stream, err := qc.OpenStreamSync(ctxDial) - if err != nil { - return err - } - defer func() { - _ = stream.Close() - }() - - // According https://datatracker.ietf.org/doc/html/rfc9250#section-4.2.1 - // msg id should set to 0 when transport over QUIC. - // thanks https://github.com/natesales/q/blob/1cb2639caf69bd0a9b46494a3c689130df8fb24a/transport/quic.go#L97 - binary.BigEndian.PutUint16(data[0:2], 0) - - msg, err := sendStreamDNS(stream, data) - if err != nil { - return err - } - respMsg = msg + forwarder, err := newDnsForwarder(upstream, *dialArgument) + defer func() { + if !connClosed { + forwarder.Close() } + }() - case consts.L4ProtoStr_TCP: - // We can block here because we are in a coroutine. + if err != nil { + return err + } - conn, err = dialArgument.bestDialer.DialContext(ctxDial, common.MagicNetwork("tcp", dialArgument.mark, dialArgument.mptcp), dialArgument.bestTarget.String()) - if upstream.Scheme == dns.UpstreamScheme_TLS { - tlsConn := tls.Client(&netproxy.FakeNetConn{Conn: conn}, &tls.Config{ - InsecureSkipVerify: false, - ServerName: upstream.Hostname, - }) - conn = tlsConn - } - if err != nil { - return fmt.Errorf("failed to dial proxy to tcp: %w", err) - } - defer func() { - if !connClosed { - conn.Close() - } - }() - - _ = conn.SetDeadline(time.Now().Add(4900 * time.Millisecond)) - switch upstream.Scheme { - case dns.UpstreamScheme_TCP, dns.UpstreamScheme_TLS, dns.UpstreamScheme_TCP_UDP: - msg, err := sendStreamDNS(conn, data) - if err != nil { - return err - } - respMsg = msg - case dns.UpstreamScheme_HTTPS: - - httpTransport := http.Transport{ - TLSClientConfig: &tls.Config{ - ServerName: upstream.Hostname, - InsecureSkipVerify: false, - }, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return &netproxy.FakeNetConn{Conn: conn}, nil - }, - } - client := http.Client{ - Transport: &httpTransport, - } - msg, err := sendHttpDNS(&client, dialArgument.bestTarget.String(), upstream, data) - if err != nil { - return err - } - respMsg = msg - } - default: - return fmt.Errorf("unexpected l4proto: %v", dialArgument.l4proto) + respMsg, err = forwarder.ForwardDNS(ctxDial, data) + if err != nil { + return err } // Close conn before the recursive call. - conn.Close() + forwarder.Close() connClosed = true // Route response. @@ -836,77 +661,3 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte } return nil } - -func sendHttpDNS(client *http.Client, target string, upstream *dns.Upstream, data []byte) (respMsg *dnsmessage.Msg, err error) { - // disable redirect https://github.com/daeuniverse/dae/pull/649#issuecomment-2379577896 - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return fmt.Errorf("do not use a server that will redirect, upstream: %v", upstream.String()) - } - serverURL := url.URL{ - Scheme: "https", - Host: target, - Path: upstream.Path, - } - q := serverURL.Query() - // According https://datatracker.ietf.org/doc/html/rfc8484#section-4 - // msg id should set to 0 when transport over HTTPS for cache friendly. - binary.BigEndian.PutUint16(data[0:2], 0) - q.Set("dns", base64.RawURLEncoding.EncodeToString(data)) - serverURL.RawQuery = q.Encode() - - req, err := http.NewRequest(http.MethodGet, serverURL.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", "application/dns-message") - req.Host = upstream.Hostname - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - buf, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var msg dnsmessage.Msg - if err = msg.Unpack(buf); err != nil { - return nil, err - } - return &msg, nil -} - -func sendStreamDNS(stream io.ReadWriter, data []byte) (respMsg *dnsmessage.Msg, err error) { - // We should write two byte length in the front of stream DNS request. - bReq := pool.Get(2 + len(data)) - defer pool.Put(bReq) - binary.BigEndian.PutUint16(bReq, uint16(len(data))) - copy(bReq[2:], data) - _, err = stream.Write(bReq) - if err != nil { - return nil, fmt.Errorf("failed to write DNS req: %w", err) - } - - // Read two byte length. - if _, err = io.ReadFull(stream, bReq[:2]); err != nil { - return nil, fmt.Errorf("failed to read DNS resp payload length: %w", err) - } - respLen := int(binary.BigEndian.Uint16(bReq)) - // Try to reuse the buf. - var buf []byte - if len(bReq) < respLen { - buf = pool.Get(respLen) - defer pool.Put(buf) - } else { - buf = bReq - } - var n int - if n, err = io.ReadFull(stream, buf[:respLen]); err != nil { - return nil, fmt.Errorf("failed to read DNS resp payload: %w", err) - } - var msg dnsmessage.Msg - if err = msg.Unpack(buf[:n]); err != nil { - return nil, err - } - return &msg, nil -} From e01ce625b3efb1564dbacaeeb3ad4b443f9c5cfe Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 8 Oct 2024 17:35:04 +0000 Subject: [PATCH 25/28] chore: remove unused code --- control/dns_control.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/control/dns_control.go b/control/dns_control.go index 84373d7cc..bf8f8729f 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -10,7 +10,6 @@ import ( "fmt" "math" "net" - "net/http" "net/netip" "strconv" "strings" @@ -509,8 +508,6 @@ func (c *DnsController) sendReject_(dnsMessage *dnsmessage.Msg, req *udpRequest) return nil } -var clientCache = make(map[string]*http.Client) - func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte, id uint16, upstream *dns.Upstream, needResp bool) (err error) { if invokingDepth >= MaxDnsLookupDepth { return fmt.Errorf("too deep DNS lookup invoking (depth: %v); there may be infinite loop in your DNS response routing", MaxDnsLookupDepth) From 60d45a3571af6e38c96f1f441a5ab2f4756a7c21 Mon Sep 17 00:00:00 2001 From: EkkoG Date: Tue, 8 Oct 2024 17:40:49 +0000 Subject: [PATCH 26/28] chore(dns): add comment for create new QUIC connection --- control/dns.go | 1 + 1 file changed, 1 insertion(+) diff --git a/control/dns.go b/control/dns.go index 24fc0e218..f0cc384be 100644 --- a/control/dns.go +++ b/control/dns.go @@ -163,6 +163,7 @@ func (d *DoQ) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, err stream, err := d.connection.OpenStreamSync(ctx) if err != nil { + // If failed to open stream, we should try to create a new connection. qc, err := d.createConnection(ctx) if err != nil { return nil, err From 54185ee6ddecec368f16188f822dd8f50fe51c1c Mon Sep 17 00:00:00 2001 From: EkkoG Date: Wed, 9 Oct 2024 15:45:18 +0000 Subject: [PATCH 27/28] fix(doh): problems with not recreating a new one when the client is not valid --- control/dns.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/control/dns.go b/control/dns.go index f0cc384be..b83cc73d7 100644 --- a/control/dns.go +++ b/control/dns.go @@ -79,18 +79,32 @@ type DoH struct { func (d *DoH) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, error) { if d.client == nil { - var roundTripper http.RoundTripper - if d.http3 { - roundTripper = d.getHttp3RoundTripper() - } else { - roundTripper = d.getHttpRoundTripper() + d.client = d.getClient() + } + msg, err := sendHttpDNS(d.client, d.dialArgument.bestTarget.String(), &d.Upstream, data) + if err != nil { + // If failed to send DNS request, we should try to create a new client. + d.client = d.getClient() + msg, err = sendHttpDNS(d.client, d.dialArgument.bestTarget.String(), &d.Upstream, data) + if err != nil { + return nil, err } + return msg, nil + } + return msg, nil +} - d.client = &http.Client{ - Transport: roundTripper, - } +func (d *DoH) getClient() *http.Client { + var roundTripper http.RoundTripper + if d.http3 { + roundTripper = d.getHttp3RoundTripper() + } else { + roundTripper = d.getHttpRoundTripper() + } + + return &http.Client{ + Transport: roundTripper, } - return sendHttpDNS(d.client, d.dialArgument.bestTarget.String(), &d.Upstream, data) } func (d *DoH) getHttpRoundTripper() *http.Transport { From d9ecac65ec0c0fac498dda562873032a619bce0d Mon Sep 17 00:00:00 2001 From: EkkoG Date: Sun, 13 Oct 2024 13:49:05 +0000 Subject: [PATCH 28/28] fix(dns): concurrent map writes --- control/dns.go | 6 ------ control/dns_control.go | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/control/dns.go b/control/dns.go index b83cc73d7..1a3878036 100644 --- a/control/dns.go +++ b/control/dns.go @@ -28,12 +28,7 @@ type DnsForwarder interface { Close() error } -var forwarderCache = make(map[string]DnsForwarder) - func newDnsForwarder(upstream *dns.Upstream, dialArgument dialArgument) (DnsForwarder, error) { - if forwarder, ok := forwarderCache[upstream.String()]; ok { - return forwarder, nil - } forwarder, err := func() (DnsForwarder, error) { switch dialArgument.l4proto { case consts.L4ProtoStr_TCP: @@ -65,7 +60,6 @@ func newDnsForwarder(upstream *dns.Upstream, dialArgument dialArgument) (DnsForw if err != nil { return nil, err } - forwarderCache[upstream.String()] = forwarder return forwarder, nil } diff --git a/control/dns_control.go b/control/dns_control.go index bf8f8729f..543581473 100644 --- a/control/dns_control.go +++ b/control/dns_control.go @@ -78,6 +78,8 @@ type DnsController struct { // mutex protects the dnsCache. dnsCacheMu sync.Mutex dnsCache map[string]*DnsCache + dnsForwarderCacheMu sync.Mutex + dnsForwarderCache map[string]DnsForwarder } func parseIpVersionPreference(prefer int) (uint16, error) { @@ -114,6 +116,8 @@ func NewDnsController(routing *dns.Dns, option *DnsControllerOption) (c *DnsCont fixedDomainTtl: option.FixedDomainTtl, dnsCacheMu: sync.Mutex{}, dnsCache: make(map[string]*DnsCache), + dnsForwarderCacheMu: sync.Mutex{}, + dnsForwarderCache: make(map[string]DnsForwarder), }, nil } @@ -556,7 +560,19 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte ctxDial, cancel := context.WithTimeout(context.TODO(), consts.DefaultDialTimeout) defer cancel() - forwarder, err := newDnsForwarder(upstream, *dialArgument) + // get forwarder from cache + c.dnsForwarderCacheMu.Lock() + forwarder, ok := c.dnsForwarderCache[upstreamName] + if !ok { + forwarder, err = newDnsForwarder(upstream, *dialArgument) + if err != nil { + c.dnsForwarderCacheMu.Unlock() + return err + } + c.dnsForwarderCache[upstreamName] = forwarder + } + c.dnsForwarderCacheMu.Unlock() + defer func() { if !connClosed { forwarder.Close()