浏览代码

Add dns client

世界 3 年之前
父节点
当前提交
8a761d7e3b

+ 22 - 0
adapter/dns.go

@@ -0,0 +1,22 @@
+package adapter
+
+import (
+	"context"
+	"net/netip"
+
+	C "github.com/sagernet/sing-box/constant"
+
+	"golang.org/x/net/dns/dnsmessage"
+)
+
+type DNSClient interface {
+	Exchange(ctx context.Context, transport DNSTransport, message *dnsmessage.Message) (*dnsmessage.Message, error)
+	Lookup(ctx context.Context, transport DNSTransport, domain string, strategy C.DomainStrategy) ([]netip.Addr, error)
+}
+
+type DNSTransport interface {
+	Service
+	Raw() bool
+	Exchange(ctx context.Context, message *dnsmessage.Message) (*dnsmessage.Message, error)
+	Lookup(ctx context.Context, domain string, strategy C.DomainStrategy) ([]netip.Addr, error)
+}

+ 1 - 0
adapter/router.go

@@ -13,6 +13,7 @@ import (
 type Router interface {
 type Router interface {
 	Service
 	Service
 	Outbound(tag string) (Outbound, bool)
 	Outbound(tag string) (Outbound, bool)
+	DefaultOutbound(network string) Outbound
 	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
 	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
 	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 	GeoIPReader() *geoip.Reader
 	GeoIPReader() *geoip.Reader

+ 327 - 0
dns/client.go

@@ -0,0 +1,327 @@
+package dns
+
+import (
+	"context"
+	"net"
+	"net/netip"
+	"time"
+
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/cache"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/task"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+
+	"golang.org/x/net/dns/dnsmessage"
+)
+
+const DefaultTTL = 600
+
+var (
+	ErrNoRawSupport = E.New("no raw query support by current transport")
+	ErrNotCached    = E.New("not cached")
+)
+
+var _ adapter.DNSClient = (*Client)(nil)
+
+type Client struct {
+	cache *cache.LruCache[dnsmessage.Question, dnsmessage.Message]
+}
+
+func NewClient() *Client {
+	return &Client{
+		cache: cache.New[dnsmessage.Question, dnsmessage.Message](),
+	}
+}
+
+func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dnsmessage.Message) (*dnsmessage.Message, error) {
+	if len(message.Questions) == 0 {
+		return nil, E.New("empty query")
+	}
+	question := message.Questions[0]
+	cachedAnswer, cached := c.cache.Load(question)
+	if cached {
+		cachedAnswer.ID = message.ID
+		return &cachedAnswer, nil
+	}
+	if !transport.Raw() {
+		if question.Type == dnsmessage.TypeA || question.Type == dnsmessage.TypeAAAA {
+			return c.exchangeToLookup(ctx, transport, message, question)
+		}
+		return nil, ErrNoRawSupport
+	}
+	response, err := transport.Exchange(ctx, message)
+	if err != nil {
+		return nil, err
+	}
+	c.cache.StoreWithExpire(question, *response, calculateExpire(message))
+	return message, err
+}
+
+func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, strategy C.DomainStrategy) ([]netip.Addr, error) {
+	dnsName, err := dnsmessage.NewName(domain)
+	if err != nil {
+		return nil, wrapError(err)
+	}
+	if transport.Raw() {
+		if strategy == C.DomainStrategyUseIPv4 {
+			return c.lookupToExchange(ctx, transport, dnsName, dnsmessage.TypeA)
+		} else if strategy == C.DomainStrategyUseIPv6 {
+			return c.lookupToExchange(ctx, transport, dnsName, dnsmessage.TypeAAAA)
+		}
+		var response4 []netip.Addr
+		var response6 []netip.Addr
+		err = task.Run(ctx, func() error {
+			response, err := c.lookupToExchange(ctx, transport, dnsName, dnsmessage.TypeA)
+			if err != nil {
+				return err
+			}
+			response4 = response
+			return nil
+		}, func() error {
+			response, err := c.lookupToExchange(ctx, transport, dnsName, dnsmessage.TypeAAAA)
+			if err != nil {
+				return err
+			}
+			response6 = response
+			return nil
+		})
+		if len(response4) == 0 && len(response6) == 0 {
+			return nil, err
+		}
+		return sortAddresses(response4, response6, strategy), nil
+	}
+	if strategy == C.DomainStrategyUseIPv4 {
+		response, err := c.questionCache(dnsmessage.Question{
+			Name:  dnsName,
+			Type:  dnsmessage.TypeA,
+			Class: dnsmessage.ClassINET,
+		})
+		if err != ErrNotCached {
+			return response, err
+		}
+	} else if strategy == C.DomainStrategyUseIPv6 {
+		response, err := c.questionCache(dnsmessage.Question{
+			Name:  dnsName,
+			Type:  dnsmessage.TypeAAAA,
+			Class: dnsmessage.ClassINET,
+		})
+		if err != ErrNotCached {
+			return response, err
+		}
+	} else {
+		response4, _ := c.questionCache(dnsmessage.Question{
+			Name:  dnsName,
+			Type:  dnsmessage.TypeA,
+			Class: dnsmessage.ClassINET,
+		})
+		response6, _ := c.questionCache(dnsmessage.Question{
+			Name:  dnsName,
+			Type:  dnsmessage.TypeAAAA,
+			Class: dnsmessage.ClassINET,
+		})
+		if len(response4) > 0 || len(response6) > 0 {
+			return sortAddresses(response4, response6, strategy), nil
+		}
+	}
+	var rCode dnsmessage.RCode
+	response, err := transport.Lookup(ctx, domain, strategy)
+	if err != nil {
+		err = wrapError(err)
+		if rCodeError, isRCodeError := err.(RCodeError); !isRCodeError {
+			return nil, err
+		} else {
+			rCode = dnsmessage.RCode(rCodeError)
+		}
+	}
+	header := dnsmessage.Header{
+		Response:      true,
+		Authoritative: true,
+		RCode:         rCode,
+	}
+	expire := time.Now().Add(time.Second * time.Duration(DefaultTTL))
+	if strategy != C.DomainStrategyUseIPv6 {
+		question4 := dnsmessage.Question{
+			Name:  dnsName,
+			Type:  dnsmessage.TypeA,
+			Class: dnsmessage.ClassINET,
+		}
+		response4 := common.Filter(response, func(addr netip.Addr) bool {
+			return addr.Is4() || addr.Is4In6()
+		})
+		message4 := dnsmessage.Message{
+			Header:    header,
+			Questions: []dnsmessage.Question{question4},
+		}
+		if len(response4) > 0 {
+			for _, address := range response4 {
+				message4.Answers = append(message4.Answers, dnsmessage.Resource{
+					Header: dnsmessage.ResourceHeader{
+						Name:  question4.Name,
+						Class: question4.Class,
+						TTL:   DefaultTTL,
+					},
+					Body: &dnsmessage.AResource{
+						A: address.As4(),
+					},
+				})
+			}
+		}
+		c.cache.StoreWithExpire(question4, message4, expire)
+	}
+	if strategy != C.DomainStrategyUseIPv4 {
+		question6 := dnsmessage.Question{
+			Name:  dnsName,
+			Type:  dnsmessage.TypeAAAA,
+			Class: dnsmessage.ClassINET,
+		}
+		response6 := common.Filter(response, func(addr netip.Addr) bool {
+			return addr.Is6() && !addr.Is4In6()
+		})
+		message6 := dnsmessage.Message{
+			Header:    header,
+			Questions: []dnsmessage.Question{question6},
+		}
+		if len(response6) > 0 {
+			for _, address := range response6 {
+				message6.Answers = append(message6.Answers, dnsmessage.Resource{
+					Header: dnsmessage.ResourceHeader{
+						Name:  question6.Name,
+						Class: question6.Class,
+						TTL:   DefaultTTL,
+					},
+					Body: &dnsmessage.AAAAResource{
+						AAAA: address.As16(),
+					},
+				})
+			}
+		}
+		c.cache.StoreWithExpire(question6, message6, expire)
+	}
+	return response, err
+}
+
+func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
+	if strategy == C.DomainStrategyPreferIPv6 {
+		return append(response6, response4...)
+	} else {
+		return append(response4, response6...)
+	}
+}
+
+func calculateExpire(message *dnsmessage.Message) time.Time {
+	timeToLive := DefaultTTL
+	for _, answer := range message.Answers {
+		if int(answer.Header.TTL) < timeToLive {
+			timeToLive = int(answer.Header.TTL)
+		}
+	}
+	return time.Now().Add(time.Second * time.Duration(timeToLive))
+}
+
+func (c *Client) exchangeToLookup(ctx context.Context, transport adapter.DNSTransport, message *dnsmessage.Message, question dnsmessage.Question) (*dnsmessage.Message, error) {
+	domain := question.Name.String()
+	var strategy C.DomainStrategy
+	if question.Type == dnsmessage.TypeA {
+		strategy = C.DomainStrategyUseIPv4
+	} else {
+		strategy = C.DomainStrategyUseIPv6
+	}
+	var rCode dnsmessage.RCode
+	result, err := c.Lookup(ctx, transport, domain, strategy)
+	if err != nil {
+		err = wrapError(err)
+		if rCodeError, isRCodeError := err.(RCodeError); !isRCodeError {
+			return nil, err
+		} else {
+			rCode = dnsmessage.RCode(rCodeError)
+		}
+	}
+	response := dnsmessage.Message{
+		Header: dnsmessage.Header{
+			ID:                 message.ID,
+			RCode:              rCode,
+			RecursionAvailable: true,
+			RecursionDesired:   true,
+			Response:           true,
+		},
+		Questions: message.Questions,
+	}
+	for _, address := range result {
+		var resource dnsmessage.Resource
+		resource.Header = dnsmessage.ResourceHeader{
+			Name:  question.Name,
+			Class: question.Class,
+			TTL:   DefaultTTL,
+		}
+		if address.Is4() || address.Is4In6() {
+			resource.Body = &dnsmessage.AResource{
+				A: address.As4(),
+			}
+		} else {
+			resource.Body = &dnsmessage.AAAAResource{
+				AAAA: address.As16(),
+			}
+		}
+	}
+	return &response, nil
+}
+
+func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name dnsmessage.Name, qType dnsmessage.Type) ([]netip.Addr, error) {
+	question := dnsmessage.Question{
+		Name:  name,
+		Type:  qType,
+		Class: dnsmessage.ClassINET,
+	}
+	cachedAddresses, err := c.questionCache(question)
+	if err != ErrNotCached {
+		return cachedAddresses, err
+	}
+	message := dnsmessage.Message{
+		Header: dnsmessage.Header{
+			ID:               0,
+			RecursionDesired: true,
+		},
+		Questions: []dnsmessage.Question{question},
+	}
+	response, err := c.Exchange(ctx, transport, &message)
+	if err != nil {
+		return nil, err
+	}
+	return messageToAddresses(response)
+}
+
+func (c *Client) questionCache(question dnsmessage.Question) ([]netip.Addr, error) {
+	response, cached := c.cache.Load(question)
+	if !cached {
+		return nil, ErrNotCached
+	}
+	return messageToAddresses(&response)
+}
+
+func messageToAddresses(response *dnsmessage.Message) ([]netip.Addr, error) {
+	if response.RCode != dnsmessage.RCodeSuccess {
+		return nil, RCodeError(response.RCode)
+	}
+	addresses := make([]netip.Addr, 0, len(response.Answers))
+	for _, answer := range response.Answers {
+		switch resource := answer.Body.(type) {
+		case *dnsmessage.AResource:
+			addresses = append(addresses, netip.AddrFrom4(resource.A))
+		case *dnsmessage.AAAAResource:
+			addresses = append(addresses, netip.AddrFrom16(resource.AAAA))
+		}
+	}
+	return addresses, nil
+}
+
+func wrapError(err error) error {
+	if dnsErr, isDNSError := err.(*net.DNSError); isDNSError {
+		if dnsErr.IsNotFound {
+			return RCodeNameError
+		}
+	}
+	return err
+}

+ 33 - 0
dns/rcode.go

@@ -0,0 +1,33 @@
+package dns
+
+import F "github.com/sagernet/sing/common/format"
+
+const (
+	RCodeSuccess        RCodeError = 0 // NoError
+	RCodeFormatError    RCodeError = 1 // FormErr
+	RCodeServerFailure  RCodeError = 2 // ServFail
+	RCodeNameError      RCodeError = 3 // NXDomain
+	RCodeNotImplemented RCodeError = 4 // NotImp
+	RCodeRefused        RCodeError = 5 // Refused
+)
+
+type RCodeError uint16
+
+func (e RCodeError) Error() string {
+	switch e {
+	case RCodeSuccess:
+		return "success"
+	case RCodeFormatError:
+		return "format error"
+	case RCodeServerFailure:
+		return "server failure"
+	case RCodeNameError:
+		return "name error"
+	case RCodeNotImplemented:
+		return "not implemented"
+	case RCodeRefused:
+		return "refused"
+	default:
+		return F.ToString("unknown error: ", uint16(e))
+	}
+}

+ 32 - 9
dns/transport.go

@@ -2,17 +2,40 @@ package dns
 
 
 import (
 import (
 	"context"
 	"context"
-	"net/netip"
+	"net/url"
 
 
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
 
 
-	"golang.org/x/net/dns/dnsmessage"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
 )
 )
 
 
-type Transport interface {
-	adapter.Service
-	Raw() bool
-	Exchange(ctx context.Context, message *dnsmessage.Message) (*dnsmessage.Message, error)
-	Lookup(ctx context.Context, domain string, strategy C.DomainStrategy) ([]netip.Addr, error)
+func NewTransport(ctx context.Context, dialer N.Dialer, logger log.Logger, address string) (adapter.DNSTransport, error) {
+	if address == "local" {
+		return NewLocalTransport(), nil
+	}
+	serverURL, err := url.Parse(address)
+	if err != nil {
+		return nil, err
+	}
+	host := serverURL.Hostname()
+	port := serverURL.Port()
+	if port == "" {
+		port = "53"
+	}
+	destination := M.ParseSocksaddrHostPortStr(host, port)
+	switch serverURL.Scheme {
+	case "", "udp":
+		return NewUDPTransport(ctx, dialer, logger, destination), nil
+	case "tcp":
+		return NewTCPTransport(ctx, dialer, logger, destination), nil
+	case "tls":
+		return NewTLSTransport(ctx, dialer, logger, destination), nil
+	case "https":
+		return NewHTTPSTransport(dialer, serverURL.String()), nil
+	default:
+		return nil, E.New("unknown dns scheme: " + serverURL.Scheme)
+	}
 }
 }

+ 46 - 0
dns/transport_base.go

@@ -0,0 +1,46 @@
+package dns
+
+import (
+	"context"
+	"net/netip"
+	"os"
+	"sync"
+
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+)
+
+type myTransportAdapter struct {
+	ctx         context.Context
+	dialer      N.Dialer
+	logger      log.Logger
+	destination M.Socksaddr
+	done        chan struct{}
+	access      sync.RWMutex
+	connection  *dnsConnection
+}
+
+func (t *myTransportAdapter) Start() error {
+	return nil
+}
+
+func (t *myTransportAdapter) Close() error {
+	select {
+	case <-t.done:
+		return os.ErrClosed
+	default:
+	}
+	close(t.done)
+	return nil
+}
+
+func (t *myTransportAdapter) Raw() bool {
+	return true
+}
+
+func (t *myTransportAdapter) Lookup(ctx context.Context, domain string, strategy C.DomainStrategy) ([]netip.Addr, error) {
+	return nil, os.ErrInvalid
+}

+ 2 - 1
dns/transport_https.go

@@ -13,6 +13,7 @@ import (
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 
 
+	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 
 
 	"golang.org/x/net/dns/dnsmessage"
 	"golang.org/x/net/dns/dnsmessage"
@@ -20,7 +21,7 @@ import (
 
 
 const dnsMimeType = "application/dns-message"
 const dnsMimeType = "application/dns-message"
 
 
-var _ Transport = (*HTTPSTransport)(nil)
+var _ adapter.DNSTransport = (*HTTPSTransport)(nil)
 
 
 type HTTPSTransport struct {
 type HTTPSTransport struct {
 	destination string
 	destination string

+ 4 - 3
dns/transport_local.go

@@ -9,21 +9,22 @@ import (
 
 
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
 
 
+	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 
 
 	"golang.org/x/net/dns/dnsmessage"
 	"golang.org/x/net/dns/dnsmessage"
 )
 )
 
 
-var LocalTransportConstructor func() Transport
+var LocalTransportConstructor func() adapter.DNSTransport
 
 
-func NewLocalTransport() Transport {
+func NewLocalTransport() adapter.DNSTransport {
 	if LocalTransportConstructor != nil {
 	if LocalTransportConstructor != nil {
 		return LocalTransportConstructor()
 		return LocalTransportConstructor()
 	}
 	}
 	return &LocalTransport{}
 	return &LocalTransport{}
 }
 }
 
 
-var _ Transport = (*LocalTransport)(nil)
+var _ adapter.DNSTransport = (*LocalTransport)(nil)
 
 
 type LocalTransport struct {
 type LocalTransport struct {
 	resolver net.Resolver
 	resolver net.Resolver

+ 10 - 37
dns/transport_tcp.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"context"
 	"encoding/binary"
 	"encoding/binary"
 	"net"
 	"net"
-	"net/netip"
 	"os"
 	"os"
 	"sync"
 	"sync"
 
 
@@ -15,52 +14,30 @@ import (
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/task"
 	"github.com/sagernet/sing/common/task"
 
 
-	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 
 
 	"golang.org/x/net/dns/dnsmessage"
 	"golang.org/x/net/dns/dnsmessage"
 )
 )
 
 
-var _ Transport = (*TCPTransport)(nil)
+var _ adapter.DNSTransport = (*TCPTransport)(nil)
 
 
 type TCPTransport struct {
 type TCPTransport struct {
-	ctx         context.Context
-	dialer      N.Dialer
-	logger      log.Logger
-	destination M.Socksaddr
-	done        chan struct{}
-	access      sync.RWMutex
-	connection  *dnsConnection
+	myTransportAdapter
 }
 }
 
 
 func NewTCPTransport(ctx context.Context, dialer N.Dialer, logger log.Logger, destination M.Socksaddr) *TCPTransport {
 func NewTCPTransport(ctx context.Context, dialer N.Dialer, logger log.Logger, destination M.Socksaddr) *TCPTransport {
 	return &TCPTransport{
 	return &TCPTransport{
-		ctx:         ctx,
-		dialer:      dialer,
-		logger:      logger,
-		destination: destination,
-		done:        make(chan struct{}),
+		myTransportAdapter{
+			ctx:         ctx,
+			dialer:      dialer,
+			logger:      logger,
+			destination: destination,
+			done:        make(chan struct{}),
+		},
 	}
 	}
 }
 }
 
 
-func (t *TCPTransport) Start() error {
-	return nil
-}
-
-func (t *TCPTransport) Close() error {
-	select {
-	case <-t.done:
-		return os.ErrClosed
-	default:
-	}
-	close(t.done)
-	return nil
-}
-
-func (t *TCPTransport) Raw() bool {
-	return true
-}
-
 func (t *TCPTransport) offer() (*dnsConnection, error) {
 func (t *TCPTransport) offer() (*dnsConnection, error) {
 	t.access.RLock()
 	t.access.RLock()
 	connection := t.connection
 	connection := t.connection
@@ -207,7 +184,3 @@ func (t *TCPTransport) Exchange(ctx context.Context, message *dnsmessage.Message
 		return nil, ctx.Err()
 		return nil, ctx.Err()
 	}
 	}
 }
 }
-
-func (t *TCPTransport) Lookup(ctx context.Context, domain string, strategy C.DomainStrategy) ([]netip.Addr, error) {
-	return nil, os.ErrInvalid
-}

+ 10 - 38
dns/transport_tls.go

@@ -4,9 +4,7 @@ import (
 	"context"
 	"context"
 	"crypto/tls"
 	"crypto/tls"
 	"encoding/binary"
 	"encoding/binary"
-	"net/netip"
 	"os"
 	"os"
-	"sync"
 
 
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	"github.com/sagernet/sing/common/buf"
@@ -15,52 +13,30 @@ import (
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/task"
 	"github.com/sagernet/sing/common/task"
 
 
-	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 
 
 	"golang.org/x/net/dns/dnsmessage"
 	"golang.org/x/net/dns/dnsmessage"
 )
 )
 
 
-var _ Transport = (*TLSTransport)(nil)
+var _ adapter.DNSTransport = (*TLSTransport)(nil)
 
 
 type TLSTransport struct {
 type TLSTransport struct {
-	ctx         context.Context
-	dialer      N.Dialer
-	logger      log.Logger
-	destination M.Socksaddr
-	done        chan struct{}
-	access      sync.RWMutex
-	connection  *dnsConnection
+	myTransportAdapter
 }
 }
 
 
 func NewTLSTransport(ctx context.Context, dialer N.Dialer, logger log.Logger, destination M.Socksaddr) *TLSTransport {
 func NewTLSTransport(ctx context.Context, dialer N.Dialer, logger log.Logger, destination M.Socksaddr) *TLSTransport {
 	return &TLSTransport{
 	return &TLSTransport{
-		ctx:         ctx,
-		dialer:      dialer,
-		logger:      logger,
-		destination: destination,
-		done:        make(chan struct{}),
+		myTransportAdapter{
+			ctx:         ctx,
+			dialer:      dialer,
+			logger:      logger,
+			destination: destination,
+			done:        make(chan struct{}),
+		},
 	}
 	}
 }
 }
 
 
-func (t *TLSTransport) Start() error {
-	return nil
-}
-
-func (t *TLSTransport) Close() error {
-	select {
-	case <-t.done:
-		return os.ErrClosed
-	default:
-	}
-	close(t.done)
-	return nil
-}
-
-func (t *TLSTransport) Raw() bool {
-	return true
-}
-
 func (t *TLSTransport) offer(ctx context.Context) (*dnsConnection, error) {
 func (t *TLSTransport) offer(ctx context.Context) (*dnsConnection, error) {
 	t.access.RLock()
 	t.access.RLock()
 	connection := t.connection
 	connection := t.connection
@@ -207,7 +183,3 @@ func (t *TLSTransport) Exchange(ctx context.Context, message *dnsmessage.Message
 		return nil, ctx.Err()
 		return nil, ctx.Err()
 	}
 	}
 }
 }
-
-func (t *TLSTransport) Lookup(ctx context.Context, domain string, strategy C.DomainStrategy) ([]netip.Addr, error) {
-	return nil, os.ErrInvalid
-}

+ 10 - 38
dns/transport_udp.go

@@ -2,9 +2,7 @@ package dns
 
 
 import (
 import (
 	"context"
 	"context"
-	"net/netip"
 	"os"
 	"os"
-	"sync"
 
 
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	"github.com/sagernet/sing/common/buf"
@@ -12,52 +10,30 @@ import (
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/task"
 	"github.com/sagernet/sing/common/task"
 
 
-	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 
 
 	"golang.org/x/net/dns/dnsmessage"
 	"golang.org/x/net/dns/dnsmessage"
 )
 )
 
 
-var _ Transport = (*UDPTransport)(nil)
+var _ adapter.DNSTransport = (*UDPTransport)(nil)
 
 
 type UDPTransport struct {
 type UDPTransport struct {
-	ctx         context.Context
-	dialer      N.Dialer
-	logger      log.Logger
-	destination M.Socksaddr
-	done        chan struct{}
-	access      sync.RWMutex
-	connection  *dnsConnection
+	myTransportAdapter
 }
 }
 
 
 func NewUDPTransport(ctx context.Context, dialer N.Dialer, logger log.Logger, destination M.Socksaddr) *UDPTransport {
 func NewUDPTransport(ctx context.Context, dialer N.Dialer, logger log.Logger, destination M.Socksaddr) *UDPTransport {
 	return &UDPTransport{
 	return &UDPTransport{
-		ctx:         ctx,
-		dialer:      dialer,
-		logger:      logger,
-		destination: destination,
-		done:        make(chan struct{}),
+		myTransportAdapter{
+			ctx:         ctx,
+			dialer:      dialer,
+			logger:      logger,
+			destination: destination,
+			done:        make(chan struct{}),
+		},
 	}
 	}
 }
 }
 
 
-func (t *UDPTransport) Start() error {
-	return nil
-}
-
-func (t *UDPTransport) Close() error {
-	select {
-	case <-t.done:
-		return os.ErrClosed
-	default:
-	}
-	close(t.done)
-	return nil
-}
-
-func (t *UDPTransport) Raw() bool {
-	return true
-}
-
 func (t *UDPTransport) offer() (*dnsConnection, error) {
 func (t *UDPTransport) offer() (*dnsConnection, error) {
 	t.access.RLock()
 	t.access.RLock()
 	connection := t.connection
 	connection := t.connection
@@ -184,7 +160,3 @@ func (t *UDPTransport) Exchange(ctx context.Context, message *dnsmessage.Message
 		return nil, ctx.Err()
 		return nil, ctx.Err()
 	}
 	}
 }
 }
-
-func (t *UDPTransport) Lookup(ctx context.Context, domain string, strategy C.DomainStrategy) ([]netip.Addr, error) {
-	return nil, os.ErrInvalid
-}

+ 1 - 1
go.mod

@@ -7,7 +7,7 @@ require (
 	github.com/goccy/go-json v0.9.8
 	github.com/goccy/go-json v0.9.8
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/oschwald/maxminddb-golang v1.9.0
 	github.com/oschwald/maxminddb-golang v1.9.0
-	github.com/sagernet/sing v0.0.0-20220706103716-44ec149b1efc
+	github.com/sagernet/sing v0.0.0-20220706131532-6d16497f03a6
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
 	github.com/sirupsen/logrus v1.8.1
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/cobra v1.5.0

+ 2 - 2
go.sum

@@ -23,8 +23,8 @@ github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagernet/sing v0.0.0-20220706103716-44ec149b1efc h1:TpmuXk61HoJHOY6ScS3t2Bz41HTbuPnffsf6QdnQoSg=
-github.com/sagernet/sing v0.0.0-20220706103716-44ec149b1efc/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing v0.0.0-20220706131532-6d16497f03a6 h1:NKDjOKPHP4JOrYomj2Q/tvKDWLmCNLHNQSPZLE5o3I4=
+github.com/sagernet/sing v0.0.0-20220706131532-6d16497f03a6/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=

+ 1 - 0
option/config.go

@@ -12,6 +12,7 @@ type _Options struct {
 	Log       *LogOption    `json:"log,omitempty"`
 	Log       *LogOption    `json:"log,omitempty"`
 	Inbounds  []Inbound     `json:"inbounds,omitempty"`
 	Inbounds  []Inbound     `json:"inbounds,omitempty"`
 	Outbounds []Outbound    `json:"outbounds,omitempty"`
 	Outbounds []Outbound    `json:"outbounds,omitempty"`
+	DNS       *DNSOptions   `json:"dns,omitempty"`
 	Route     *RouteOptions `json:"route,omitempty"`
 	Route     *RouteOptions `json:"route,omitempty"`
 }
 }
 
 

+ 12 - 0
option/dns.go

@@ -0,0 +1,12 @@
+package option
+
+type DNSOptions struct {
+	Servers []DNSServerOptions `json:"servers,omitempty"`
+}
+
+type DNSServerOptions struct {
+	Tag             string `json:"tag,omitempty"`
+	Address         string `json:"address"`
+	Detour          string `json:"detour,omitempty"`
+	AddressResolver string `json:"address_resolver,omitempty"`
+}

+ 1 - 3
option/route.go

@@ -101,9 +101,7 @@ type DefaultRule struct {
 	IPCIDR        Listable[string] `json:"ip_cidr,omitempty"`
 	IPCIDR        Listable[string] `json:"ip_cidr,omitempty"`
 	SourcePort    Listable[uint16] `json:"source_port,omitempty"`
 	SourcePort    Listable[uint16] `json:"source_port,omitempty"`
 	Port          Listable[uint16] `json:"port,omitempty"`
 	Port          Listable[uint16] `json:"port,omitempty"`
-	// ProcessName   Listable[string] `json:"process_name,omitempty"`
-	// ProcessPath   Listable[string] `json:"process_path,omitempty"`
-	Outbound string `json:"outbound,omitempty"`
+	Outbound      string           `json:"outbound,omitempty"`
 }
 }
 
 
 func (r DefaultRule) IsValid() bool {
 func (r DefaultRule) IsValid() bool {

+ 44 - 0
option/types.go

@@ -90,3 +90,47 @@ func (l *Listable[T]) UnmarshalJSON(content []byte) error {
 	*l = []T{singleItem}
 	*l = []T{singleItem}
 	return nil
 	return nil
 }
 }
+
+type DomainStrategy C.DomainStrategy
+
+func (s DomainStrategy) MarshalJSON() ([]byte, error) {
+	var value string
+	switch C.DomainStrategy(s) {
+	case C.DomainStrategyAsIS:
+		value = "AsIS"
+	case C.DomainStrategyPreferIPv4:
+		value = "PreferIPv4"
+	case C.DomainStrategyPreferIPv6:
+		value = "PreferIPv6"
+	case C.DomainStrategyUseIPv4:
+		value = "UseIPv4"
+	case C.DomainStrategyUseIPv6:
+		value = "UseIPv6"
+	default:
+		return nil, E.New("unknown domain strategy: ", s)
+	}
+	return json.Marshal(value)
+}
+
+func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error {
+	var value string
+	err := json.Unmarshal(bytes, &value)
+	if err != nil {
+		return err
+	}
+	switch value {
+	case "AsIS":
+		*s = DomainStrategy(C.DomainStrategyAsIS)
+	case "PreferIPv4":
+		*s = DomainStrategy(C.DomainStrategyPreferIPv4)
+	case "PreferIPv6":
+		*s = DomainStrategy(C.DomainStrategyPreferIPv6)
+	case "UseIPv4":
+		*s = DomainStrategy(C.DomainStrategyUseIPv4)
+	case "UseIPv6":
+		*s = DomainStrategy(C.DomainStrategyUseIPv6)
+	default:
+		return E.New("unknown domain strategy: ", value)
+	}
+	return nil
+}

+ 1 - 1
outbound/dialer/default.go

@@ -20,7 +20,7 @@ type defaultDialer struct {
 	net.ListenConfig
 	net.ListenConfig
 }
 }
 
 
-func newDefault(options option.DialerOptions) N.Dialer {
+func NewDefault(options option.DialerOptions) N.Dialer {
 	var dialer net.Dialer
 	var dialer net.Dialer
 	var listener net.ListenConfig
 	var listener net.ListenConfig
 	if options.BindInterface != "" {
 	if options.BindInterface != "" {

+ 10 - 6
outbound/dialer/detour.go

@@ -10,27 +10,31 @@ import (
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/option"
 )
 )
 
 
 type detourDialer struct {
 type detourDialer struct {
 	router   adapter.Router
 	router   adapter.Router
-	options  option.DialerOptions
+	detour   string
 	dialer   N.Dialer
 	dialer   N.Dialer
 	initOnce sync.Once
 	initOnce sync.Once
 	initErr  error
 	initErr  error
 }
 }
 
 
-func newDetour(router adapter.Router, options option.DialerOptions) N.Dialer {
-	return &detourDialer{router: router, options: options}
+func NewDetour(router adapter.Router, detour string) N.Dialer {
+	return &detourDialer{router: router, detour: detour}
+}
+
+func (d *detourDialer) Start() error {
+	_, err := d.Dialer()
+	return err
 }
 }
 
 
 func (d *detourDialer) Dialer() (N.Dialer, error) {
 func (d *detourDialer) Dialer() (N.Dialer, error) {
 	d.initOnce.Do(func() {
 	d.initOnce.Do(func() {
 		var loaded bool
 		var loaded bool
-		d.dialer, loaded = d.router.Outbound(d.options.Detour)
+		d.dialer, loaded = d.router.Outbound(d.detour)
 		if !loaded {
 		if !loaded {
-			d.initErr = E.New("outbound detour not found: ", d.options.Detour)
+			d.initErr = E.New("outbound detour not found: ", d.detour)
 		}
 		}
 	})
 	})
 	return d.dialer, d.initErr
 	return d.dialer, d.initErr

+ 3 - 3
outbound/dialer/dialer.go

@@ -11,12 +11,12 @@ import (
 func New(router adapter.Router, options option.DialerOptions) N.Dialer {
 func New(router adapter.Router, options option.DialerOptions) N.Dialer {
 	var dialer N.Dialer
 	var dialer N.Dialer
 	if options.Detour == "" {
 	if options.Detour == "" {
-		dialer = newDefault(options)
+		dialer = NewDefault(options)
 	} else {
 	} else {
-		dialer = newDetour(router, options)
+		dialer = NewDetour(router, options.Detour)
 	}
 	}
 	if options.OverrideOptions.IsValid() {
 	if options.OverrideOptions.IsValid() {
-		dialer = newOverride(dialer, common.PtrValueOrDefault(options.OverrideOptions))
+		dialer = NewOverride(dialer, common.PtrValueOrDefault(options.OverrideOptions))
 	}
 	}
 	return dialer
 	return dialer
 }
 }

+ 1 - 1
outbound/dialer/override.go

@@ -22,7 +22,7 @@ type overrideDialer struct {
 	uotEnabled bool
 	uotEnabled bool
 }
 }
 
 
-func newOverride(upstream N.Dialer, options option.OverrideStreamOptions) N.Dialer {
+func NewOverride(upstream N.Dialer, options option.OverrideStreamOptions) N.Dialer {
 	return &overrideDialer{
 	return &overrideDialer{
 		upstream,
 		upstream,
 		options.TLS,
 		options.TLS,

+ 1 - 2
outbound/dialer/protect.go

@@ -5,7 +5,6 @@ package dialer
 import (
 import (
 	"syscall"
 	"syscall"
 
 
-	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/control"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 )
@@ -42,6 +41,6 @@ func ProtectPath(protectPath string) control.Func {
 		err := conn.Control(func(fd uintptr) {
 		err := conn.Control(func(fd uintptr) {
 			innerErr = sendAncillaryFileDescriptors(protectPath, []int{int(fd)})
 			innerErr = sendAncillaryFileDescriptors(protectPath, []int{int(fd)})
 		})
 		})
-		return common.AnyError(innerErr, err)
+		return E.Errors(innerErr, err)
 	}
 	}
 }
 }

+ 8 - 0
route/router.go

@@ -191,6 +191,14 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 	return outbound, loaded
 	return outbound, loaded
 }
 }
 
 
+func (r *Router) DefaultOutbound(network string) adapter.Outbound {
+	if network == C.NetworkTCP {
+		return r.defaultOutboundForConnection
+	} else {
+		return r.defaultOutboundForPacketConnection
+	}
+}
+
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if metadata.SniffEnabled {
 	if metadata.SniffEnabled {
 		_buffer := buf.StackNew()
 		_buffer := buf.StackNew()