diff --git a/app/dns/server.go b/app/dns/server.go index 494af755..22ecd955 100644 --- a/app/dns/server.go +++ b/app/dns/server.go @@ -14,6 +14,7 @@ import ( "v2ray.com/core/common/net" "v2ray.com/core/common/session" "v2ray.com/core/common/strmatcher" + "v2ray.com/core/common/uuid" "v2ray.com/core/features" "v2ray.com/core/features/dns" "v2ray.com/core/features/routing" @@ -30,12 +31,20 @@ type Server struct { tag string } +func generateRandomTag() string { + id := uuid.New() + return "v2ray.system." + id.String() +} + // New creates a new DNS server with given configuration. func New(ctx context.Context, config *Config) (*Server, error) { server := &Server{ clients: make([]Client, 0, len(config.NameServers)+len(config.NameServer)), tag: config.Tag, } + if len(server.tag) == 0 { + server.tag = generateRandomTag() + } if len(config.ClientIp) > 0 { if len(config.ClientIp) != 4 && len(config.ClientIp) != 16 { return nil, newError("unexpected IP length", len(config.ClientIp)) @@ -121,6 +130,11 @@ func (s *Server) Close() error { return nil } +func (s *Server) IsOwnLink(ctx context.Context) bool { + inbound := session.InboundFromContext(ctx) + return inbound != nil && inbound.Tag == s.tag +} + func (s *Server) queryIPTimeout(client Client, domain string, option IPOption) ([]net.IP, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*4) if len(s.tag) > 0 { diff --git a/app/dns/server_test.go b/app/dns/server_test.go index 112cbcb3..90e2aa2d 100644 --- a/app/dns/server_test.go +++ b/app/dns/server_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/miekg/dns" "v2ray.com/core" "v2ray.com/core/app/dispatcher" @@ -18,8 +19,6 @@ import ( feature_dns "v2ray.com/core/features/dns" "v2ray.com/core/proxy/freedom" "v2ray.com/core/testing/servers/udp" - - "github.com/miekg/dns" ) type staticHandler struct { diff --git a/common/protocol/dns/errors.generated.go b/common/protocol/dns/errors.generated.go new file mode 100644 index 00000000..ba70372f --- /dev/null +++ b/common/protocol/dns/errors.generated.go @@ -0,0 +1,9 @@ +package dns + +import "v2ray.com/core/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/protocol/dns/io.go b/common/protocol/dns/io.go index 6b236112..e1a75932 100644 --- a/common/protocol/dns/io.go +++ b/common/protocol/dns/io.go @@ -1,11 +1,13 @@ package dns import ( + "encoding/binary" "sync" "golang.org/x/net/dns/dnsmessage" "v2ray.com/core/common" "v2ray.com/core/common/buf" + "v2ray.com/core/common/serial" ) func PackMessage(msg *dnsmessage.Message) (*buf.Buffer, error) { @@ -75,3 +77,67 @@ func (r *UDPReader) Close() error { return common.Close(r.Reader) } + +type TCPReader struct { + reader *buf.BufferedReader +} + +func NewTCPReader(reader buf.Reader) *TCPReader { + return &TCPReader{ + reader: &buf.BufferedReader{ + Reader: reader, + }, + } +} + +func (r *TCPReader) ReadMessage() (*buf.Buffer, error) { + size, err := serial.ReadUint16(r.reader) + if err != nil { + return nil, err + } + if size > buf.Size { + return nil, newError("message size too large: ", size) + } + b := buf.New() + if _, err := b.ReadFullFrom(r.reader, int32(size)); err != nil { + return nil, err + } + return b, nil +} + +func (r *TCPReader) Interrupt() { + common.Interrupt(r.reader) +} + +func (r *TCPReader) Close() error { + return common.Close(r.reader) +} + +type MessageWriter interface { + WriteMessage(msg *buf.Buffer) error +} + +type UDPWriter struct { + buf.Writer +} + +func (w *UDPWriter) WriteMessage(b *buf.Buffer) error { + return w.WriteMultiBuffer(buf.MultiBuffer{b}) +} + +type TCPWriter struct { + buf.Writer +} + +func (w *TCPWriter) WriteMessage(b *buf.Buffer) error { + if b.IsEmpty() { + return nil + } + + mb := make(buf.MultiBuffer, 0, 2) + + size := buf.New() + binary.BigEndian.PutUint16(size.Extend(2), uint16(b.Len())) + mb = append(mb, size, b) + return w.WriteMultiBuffer(mb) +} diff --git a/proxy/dns/config.pb.go b/proxy/dns/config.pb.go new file mode 100644 index 00000000..d7b5086b --- /dev/null +++ b/proxy/dns/config.pb.go @@ -0,0 +1,69 @@ +package dns + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Config struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Config) Reset() { *m = Config{} } +func (m *Config) String() string { return proto.CompactTextString(m) } +func (*Config) ProtoMessage() {} +func (*Config) Descriptor() ([]byte, []int) { + return fileDescriptor_c49bb2d51e576d57, []int{0} +} + +func (m *Config) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Config.Unmarshal(m, b) +} +func (m *Config) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Config.Marshal(b, m, deterministic) +} +func (m *Config) XXX_Merge(src proto.Message) { + xxx_messageInfo_Config.Merge(m, src) +} +func (m *Config) XXX_Size() int { + return xxx_messageInfo_Config.Size(m) +} +func (m *Config) XXX_DiscardUnknown() { + xxx_messageInfo_Config.DiscardUnknown(m) +} + +var xxx_messageInfo_Config proto.InternalMessageInfo + +func init() { + proto.RegisterType((*Config)(nil), "v2ray.core.proxy.dns.Config") +} + +func init() { + proto.RegisterFile("v2ray.com/core/proxy/dns/config.proto", fileDescriptor_c49bb2d51e576d57) +} + +var fileDescriptor_c49bb2d51e576d57 = []byte{ + // 123 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x2d, 0x33, 0x2a, 0x4a, + 0xac, 0xd4, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0x2f, 0x4a, 0xd5, 0x2f, 0x28, 0xca, 0xaf, 0xa8, + 0xd4, 0x4f, 0xc9, 0x2b, 0xd6, 0x4f, 0xce, 0xcf, 0x4b, 0xcb, 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, + 0xc9, 0x17, 0x12, 0x81, 0x29, 0x2b, 0x4a, 0xd5, 0x03, 0x2b, 0xd1, 0x4b, 0xc9, 0x2b, 0x56, 0xe2, + 0xe0, 0x62, 0x73, 0x06, 0xab, 0x72, 0xb2, 0xe0, 0x92, 0x48, 0xce, 0xcf, 0xd5, 0xc3, 0xa6, 0x2a, + 0x80, 0x31, 0x8a, 0x39, 0x25, 0xaf, 0x78, 0x15, 0x93, 0x48, 0x98, 0x51, 0x50, 0x62, 0xa5, 0x9e, + 0x33, 0x48, 0x36, 0x00, 0x2c, 0xeb, 0x92, 0x57, 0x9c, 0xc4, 0x06, 0xb6, 0xc0, 0x18, 0x10, 0x00, + 0x00, 0xff, 0xff, 0xee, 0x22, 0xde, 0xc9, 0x89, 0x00, 0x00, 0x00, +} diff --git a/proxy/dns/config.proto b/proxy/dns/config.proto new file mode 100644 index 00000000..2aa74774 --- /dev/null +++ b/proxy/dns/config.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package v2ray.core.proxy.dns; +option csharp_namespace = "V2Ray.Core.Proxy.Dns"; +option go_package = "dns"; +option java_package = "com.v2ray.core.proxy.dns"; +option java_multiple_files = true; + +message Config { +} diff --git a/proxy/dns/dns.go b/proxy/dns/dns.go new file mode 100644 index 00000000..6081fe7f --- /dev/null +++ b/proxy/dns/dns.go @@ -0,0 +1,304 @@ +// +build !confonly + +package dns + +import ( + "context" + "io" + "sync" + + "golang.org/x/net/dns/dnsmessage" + + "v2ray.com/core" + "v2ray.com/core/common" + "v2ray.com/core/common/buf" + "v2ray.com/core/common/net" + dns_proto "v2ray.com/core/common/protocol/dns" + "v2ray.com/core/common/session" + "v2ray.com/core/common/task" + "v2ray.com/core/features/dns" + "v2ray.com/core/transport" + "v2ray.com/core/transport/internet" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + h := new(Handler) + if err := core.RequireFeatures(ctx, func(dnsClient dns.Client) error { + return h.Init(config.(*Config), dnsClient) + }); err != nil { + return nil, err + } + return h, nil + })) +} + +type ownLinkVerifier interface { + IsOwnLink(ctx context.Context) bool +} + +type Handler struct { + ipv4Lookup dns.IPv4Lookup + ipv6Lookup dns.IPv6Lookup + ownLinkVerifier ownLinkVerifier +} + +func (h *Handler) Init(config *Config, dnsClient dns.Client) error { + ipv4lookup, ok := dnsClient.(dns.IPv4Lookup) + if !ok { + return newError("dns.Client doesn't implement IPv4Lookup") + } + h.ipv4Lookup = ipv4lookup + + ipv6lookup, ok := dnsClient.(dns.IPv6Lookup) + if !ok { + return newError("dns.Client doesn't implement IPv6Lookup") + } + h.ipv6Lookup = ipv6lookup + + if v, ok := dnsClient.(ownLinkVerifier); ok { + h.ownLinkVerifier = v + } + return nil +} + +func (h *Handler) isOwnLink(ctx context.Context) bool { + return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx) +} + +func parseIPQuery(b []byte) (r bool, domain string, id uint16, qType dnsmessage.Type) { + var parser dnsmessage.Parser + header, err := parser.Start(b) + if err != nil { + newError("parser start").Base(err).WriteToLog() + return + } + + id = header.ID + q, err := parser.Question() + if err != nil { + newError("question").Base(err).WriteToLog() + return + } + qType = q.Type + if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA { + return + } + + domain = q.Name.String() + r = true + return +} + +// Process implements proxy.Outbound. +func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("invalid outbound") + } + + dest := outbound.Target + + conn := &outboundConn{ + dialer: func() (internet.Connection, error) { + return d.Dial(ctx, dest) + }, + connReady: make(chan struct{}, 1), + } + + var reader dns_proto.MessageReader + var writer dns_proto.MessageWriter + if dest.Network == net.Network_TCP { + reader = dns_proto.NewTCPReader(link.Reader) + writer = &dns_proto.TCPWriter{ + Writer: link.Writer, + } + } else { + reader = &dns_proto.UDPReader{ + Reader: link.Reader, + } + writer = &dns_proto.UDPWriter{ + Writer: link.Writer, + } + } + + var connReader dns_proto.MessageReader + var connWriter dns_proto.MessageWriter + if dest.Network == net.Network_TCP { + connReader = dns_proto.NewTCPReader(buf.NewReader(conn)) + connWriter = &dns_proto.TCPWriter{ + Writer: buf.NewWriter(conn), + } + } else { + connReader = &dns_proto.UDPReader{ + Reader: &buf.PacketReader{Reader: conn}, + } + connWriter = &dns_proto.UDPWriter{ + Writer: buf.NewWriter(conn), + } + } + + request := func() error { + defer conn.Close() + + for { + b, err := reader.ReadMessage() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + if !h.isOwnLink(ctx) { + isIPQuery, domain, id, qType := parseIPQuery(b.Bytes()) + if isIPQuery { + go h.handleIPQuery(id, qType, domain, writer) + continue + } + } + + if err := connWriter.WriteMessage(b); err != nil { + return err + } + } + } + + response := func() error { + for { + b, err := connReader.ReadMessage() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + if err := writer.WriteMessage(b); err != nil { + return err + } + } + } + + if err := task.Run(ctx, request, response); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter) { + var ips []net.IP + var err error + + switch qType { + case dnsmessage.TypeA: + ips, err = h.ipv4Lookup.LookupIPv4(domain) + case dnsmessage.TypeAAAA: + ips, err = h.ipv6Lookup.LookupIPv6(domain) + } + + if err != nil { + newError("ip query").Base(err).WriteToLog() + return + } + + if len(ips) == 0 { + return + } + + b := buf.New() + rawBytes := b.Extend(buf.Size) + builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ + ID: id, + RCode: dnsmessage.RCodeSuccess, + }) + builder.StartAnswers() + + rHeader := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: 600} + for _, ip := range ips { + if len(ip) == net.IPv4len { + var r dnsmessage.AResource + copy(r.A[:], ip) + builder.AResource(rHeader, r) + } else { + var r dnsmessage.AAAAResource + copy(r.AAAA[:], ip) + builder.AAAAResource(rHeader, r) + } + } + msgBytes, err := builder.Finish() + if err != nil { + newError("pack message").Base(err).WriteToLog() + b.Release() + return + } + b.Resize(0, int32(len(msgBytes))) + + if err := writer.WriteMessage(b); err != nil { + newError("write IP answer").Base(err).WriteToLog() + } +} + +type outboundConn struct { + access sync.Mutex + dialer func() (internet.Connection, error) + + conn net.Conn + connReady chan struct{} +} + +func (c *outboundConn) dial() error { + conn, err := c.dialer() + if err != nil { + return err + } + c.conn = conn + c.connReady <- struct{}{} + return nil +} + +func (c *outboundConn) Write(b []byte) (int, error) { + c.access.Lock() + + if c.conn == nil { + if err := c.dial(); err != nil { + c.access.Unlock() + newError("failed to dial outbound connection").Base(err).AtWarning().WriteToLog() + return len(b), nil + } + } + + c.access.Unlock() + + return c.conn.Write(b) +} + +func (c *outboundConn) Read(b []byte) (int, error) { + var conn net.Conn + c.access.Lock() + conn = c.conn + c.access.Unlock() + + if conn == nil { + _, open := <-c.connReady + if !open { + return 0, io.EOF + } + conn = c.conn + } + + return conn.Read(b) +} + +func (c *outboundConn) Close() error { + c.access.Lock() + close(c.connReady) + if c.conn != nil { + c.conn.Close() + } + c.access.Unlock() + return nil +} diff --git a/proxy/dns/dns_test.go b/proxy/dns/dns_test.go new file mode 100644 index 00000000..a84fa4d6 --- /dev/null +++ b/proxy/dns/dns_test.go @@ -0,0 +1,238 @@ +package dns_test + +import ( + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/miekg/dns" + + "v2ray.com/core" + "v2ray.com/core/app/dispatcher" + dnsapp "v2ray.com/core/app/dns" + "v2ray.com/core/app/policy" + "v2ray.com/core/app/proxyman" + _ "v2ray.com/core/app/proxyman/inbound" + _ "v2ray.com/core/app/proxyman/outbound" + "v2ray.com/core/common" + "v2ray.com/core/common/net" + "v2ray.com/core/common/serial" + dns_proxy "v2ray.com/core/proxy/dns" + "v2ray.com/core/proxy/dokodemo" + "v2ray.com/core/testing/servers/tcp" + "v2ray.com/core/testing/servers/udp" +) + +type staticHandler struct { +} + +func (*staticHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + ans := new(dns.Msg) + ans.Id = r.Id + + var clientIP net.IP + + opt := r.IsEdns0() + if opt != nil { + for _, o := range opt.Option { + if o.Option() == dns.EDNS0SUBNET { + subnet := o.(*dns.EDNS0_SUBNET) + clientIP = subnet.Address + } + } + } + + for _, q := range r.Question { + if q.Name == "google.com." && q.Qtype == dns.TypeA { + if clientIP == nil { + rr, _ := dns.NewRR("google.com. IN A 8.8.8.8") + ans.Answer = append(ans.Answer, rr) + } else { + rr, _ := dns.NewRR("google.com. IN A 8.8.4.4") + ans.Answer = append(ans.Answer, rr) + } + } else if q.Name == "facebook.com." && q.Qtype == dns.TypeA { + rr, _ := dns.NewRR("facebook.com. IN A 9.9.9.9") + ans.Answer = append(ans.Answer, rr) + } else if q.Name == "ipv6.google.com." && q.Qtype == dns.TypeA { + rr, err := dns.NewRR("ipv6.google.com. IN A 8.8.8.7") + common.Must(err) + ans.Answer = append(ans.Answer, rr) + } else if q.Name == "ipv6.google.com." && q.Qtype == dns.TypeAAAA { + rr, err := dns.NewRR("ipv6.google.com. IN AAAA 2001:4860:4860::8888") + common.Must(err) + ans.Answer = append(ans.Answer, rr) + } + } + w.WriteMsg(ans) +} + +func TestUDPDNSTunnel(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := udp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.Config{ + NameServers: []*net.Endpoint{ + { + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_UDP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{"google.com.", dns.TypeA, dns.ClassINET} + + c := new(dns.Client) + in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort))) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} + +func TestTCPDNSTunnel(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := tcp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.Config{ + NameServer: []*dnsapp.NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_TCP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{"google.com.", dns.TypeA, dns.ClassINET} + + c := &dns.Client{ + Net: "tcp", + } + in, _, err := c.Exchange(m1, "127.0.0.1:"+serverPort.String()) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} diff --git a/proxy/dns/errors.generated.go b/proxy/dns/errors.generated.go new file mode 100644 index 00000000..ba70372f --- /dev/null +++ b/proxy/dns/errors.generated.go @@ -0,0 +1,9 @@ +package dns + +import "v2ray.com/core/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +}