mirror of
https://github.com/v2fly/v2ray-core.git
synced 2024-12-21 17:46:58 -05:00
Merge branch 'master' into v5
This commit is contained in:
commit
e6bea0d112
63
app/dns/config.go
Normal file
63
app/dns/config.go
Normal file
@ -0,0 +1,63 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"v2ray.com/core/common/net"
|
||||
"v2ray.com/core/common/strmatcher"
|
||||
"v2ray.com/core/common/uuid"
|
||||
)
|
||||
|
||||
var typeMap = map[DomainMatchingType]strmatcher.Type{
|
||||
DomainMatchingType_Full: strmatcher.Full,
|
||||
DomainMatchingType_Subdomain: strmatcher.Domain,
|
||||
DomainMatchingType_Keyword: strmatcher.Substr,
|
||||
DomainMatchingType_Regex: strmatcher.Regex,
|
||||
}
|
||||
|
||||
// References:
|
||||
// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml
|
||||
// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
|
||||
var localTLDsAndDotlessDomains = []*NameServer_PriorityDomain{
|
||||
{Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "local"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "localhost"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "lan"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "home.arpa"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "example"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "invalid"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "test"},
|
||||
}
|
||||
|
||||
var localTLDsAndDotlessDomainsRule = &NameServer_OriginalRule{
|
||||
Rule: "geosite:private",
|
||||
Size: uint32(len(localTLDsAndDotlessDomains)),
|
||||
}
|
||||
|
||||
func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) {
|
||||
strMType, f := typeMap[t]
|
||||
if !f {
|
||||
return nil, newError("unknown mapping type", t).AtWarning()
|
||||
}
|
||||
matcher, err := strMType.New(domain)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create str matcher").Base(err)
|
||||
}
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
func toNetIP(addrs []net.Address) ([]net.IP, error) {
|
||||
ips := make([]net.IP, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
if addr.Family().IsIP() {
|
||||
ips = append(ips, addr.IP())
|
||||
} else {
|
||||
return nil, newError("Failed to convert address", addr, "to Net IP.").AtWarning()
|
||||
}
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func generateRandomTag() string {
|
||||
id := uuid.New()
|
||||
return "v2ray.system." + id.String()
|
||||
}
|
243
app/dns/dns.go
243
app/dns/dns.go
@ -2,3 +2,246 @@
|
||||
package dns
|
||||
|
||||
//go:generate go run v2ray.com/core/common/errors/errorgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"v2ray.com/core/app/router"
|
||||
"v2ray.com/core/common"
|
||||
"v2ray.com/core/common/errors"
|
||||
"v2ray.com/core/common/net"
|
||||
"v2ray.com/core/common/session"
|
||||
"v2ray.com/core/common/strmatcher"
|
||||
"v2ray.com/core/features"
|
||||
"v2ray.com/core/features/dns"
|
||||
)
|
||||
|
||||
// DNS is a DNS rely server.
|
||||
type DNS struct {
|
||||
sync.Mutex
|
||||
tag string
|
||||
hosts *StaticHosts
|
||||
clients []*Client
|
||||
|
||||
domainMatcher strmatcher.IndexMatcher
|
||||
matcherInfos []DomainMatcherInfo
|
||||
}
|
||||
|
||||
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher
|
||||
type DomainMatcherInfo struct {
|
||||
clientIdx uint16
|
||||
domainRuleIdx uint16
|
||||
}
|
||||
|
||||
// New creates a new DNS server with given configuration.
|
||||
func New(ctx context.Context, config *Config) (*DNS, error) {
|
||||
var tag string
|
||||
if len(config.Tag) > 0 {
|
||||
tag = config.Tag
|
||||
} else {
|
||||
tag = generateRandomTag()
|
||||
}
|
||||
|
||||
var clientIP net.IP
|
||||
switch len(config.ClientIp) {
|
||||
case 0, net.IPv4len, net.IPv6len:
|
||||
clientIP = net.IP(config.ClientIp)
|
||||
default:
|
||||
return nil, newError("unexpected client IP length ", len(config.ClientIp))
|
||||
}
|
||||
|
||||
hosts, err := NewStaticHosts(config.StaticHosts, config.Hosts)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create hosts").Base(err)
|
||||
}
|
||||
|
||||
clients := []*Client{}
|
||||
domainRuleCount := 0
|
||||
for _, ns := range config.NameServer {
|
||||
domainRuleCount += len(ns.PrioritizedDomain)
|
||||
}
|
||||
// MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1
|
||||
matcherInfos := make([]DomainMatcherInfo, domainRuleCount+1)
|
||||
domainMatcher := &strmatcher.MatcherGroup{}
|
||||
geoipContainer := router.GeoIPMatcherContainer{}
|
||||
|
||||
for _, endpoint := range config.NameServers {
|
||||
features.PrintDeprecatedFeatureWarning("simple DNS server")
|
||||
client, err := NewSimpleClient(ctx, endpoint, clientIP)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create client").Base(err)
|
||||
}
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
for _, ns := range config.NameServer {
|
||||
clientIdx := len(clients)
|
||||
updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int) error {
|
||||
midx := domainMatcher.Add(domainRule)
|
||||
matcherInfos[midx] = DomainMatcherInfo{
|
||||
clientIdx: uint16(clientIdx),
|
||||
domainRuleIdx: uint16(originalRuleIdx),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
myClientIP := clientIP
|
||||
switch len(ns.ClientIp) {
|
||||
case net.IPv4len, net.IPv6len:
|
||||
myClientIP = net.IP(ns.ClientIp)
|
||||
}
|
||||
client, err := NewClient(ctx, ns, myClientIP, geoipContainer, updateDomain)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create client").Base(err)
|
||||
}
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
if len(clients) == 0 {
|
||||
clients = append(clients, NewLocalDNSClient())
|
||||
}
|
||||
|
||||
return &DNS{
|
||||
tag: tag,
|
||||
hosts: hosts,
|
||||
clients: clients,
|
||||
domainMatcher: domainMatcher,
|
||||
matcherInfos: matcherInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Type implements common.HasType.
|
||||
func (*DNS) Type() interface{} {
|
||||
return dns.ClientType()
|
||||
}
|
||||
|
||||
// Start implements common.Runnable.
|
||||
func (s *DNS) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements common.Closable.
|
||||
func (s *DNS) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsOwnLink implements proxy.dns.ownLinkVerifier
|
||||
func (s *DNS) IsOwnLink(ctx context.Context) bool {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
return inbound != nil && inbound.Tag == s.tag
|
||||
}
|
||||
|
||||
// LookupIP implements dns.Client.
|
||||
func (s *DNS) LookupIP(domain string) ([]net.IP, error) {
|
||||
return s.lookupIPInternal(domain, IPOption{
|
||||
IPv4Enable: true,
|
||||
IPv6Enable: true,
|
||||
})
|
||||
}
|
||||
|
||||
// LookupIPv4 implements dns.IPv4Lookup.
|
||||
func (s *DNS) LookupIPv4(domain string) ([]net.IP, error) {
|
||||
return s.lookupIPInternal(domain, IPOption{
|
||||
IPv4Enable: true,
|
||||
IPv6Enable: false,
|
||||
})
|
||||
}
|
||||
|
||||
// LookupIPv6 implements dns.IPv6Lookup.
|
||||
func (s *DNS) LookupIPv6(domain string) ([]net.IP, error) {
|
||||
return s.lookupIPInternal(domain, IPOption{
|
||||
IPv4Enable: false,
|
||||
IPv6Enable: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DNS) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) {
|
||||
if domain == "" {
|
||||
return nil, newError("empty domain name")
|
||||
}
|
||||
|
||||
// Normalize the FQDN form query
|
||||
if domain[len(domain)-1] == '.' {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
// Static host lookup
|
||||
switch addrs := s.hosts.Lookup(domain, option); {
|
||||
case addrs == nil: // Domain not recorded in static host
|
||||
break
|
||||
case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled)
|
||||
return nil, dns.ErrEmptyResponse
|
||||
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement
|
||||
newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog()
|
||||
domain = addrs[0].Domain()
|
||||
default: // Successfully found ip records in static host
|
||||
newError("returning ", len(addrs), " IPs for domain ", domain).WriteToLog()
|
||||
return toNetIP(addrs)
|
||||
}
|
||||
|
||||
// Name servers lookup
|
||||
errs := []error{}
|
||||
ctx := session.ContextWithInbound(context.Background(), &session.Inbound{Tag: s.tag})
|
||||
for _, client := range s.sortClients(domain) {
|
||||
ips, err := client.QueryIP(ctx, domain, option)
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
if err != nil {
|
||||
newError("failed to lookup ip for domain ", domain, " at server ", client.Name()).Base(err).WriteToLog()
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, newError("returning nil for domain ", domain).Base(errors.Combine(errs...))
|
||||
}
|
||||
|
||||
func (s *DNS) sortClients(domain string) []*Client {
|
||||
clients := make([]*Client, 0, len(s.clients))
|
||||
clientUsed := make([]bool, len(s.clients))
|
||||
clientNames := make([]string, 0, len(s.clients))
|
||||
domainRules := []string{}
|
||||
|
||||
// Priority domain matching
|
||||
for _, match := range s.domainMatcher.Match(domain) {
|
||||
info := s.matcherInfos[match]
|
||||
client := s.clients[info.clientIdx]
|
||||
domainRule := client.domains[info.domainRuleIdx]
|
||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
|
||||
if clientUsed[info.clientIdx] {
|
||||
continue
|
||||
}
|
||||
clientUsed[info.clientIdx] = true
|
||||
clients = append(clients, client)
|
||||
clientNames = append(clientNames, client.Name())
|
||||
}
|
||||
|
||||
// Default round-robin query
|
||||
for idx, client := range s.clients {
|
||||
if clientUsed[idx] {
|
||||
continue
|
||||
}
|
||||
clientUsed[idx] = true
|
||||
clients = append(clients, client)
|
||||
clientNames = append(clientNames, client.Name())
|
||||
}
|
||||
|
||||
if len(domainRules) > 0 {
|
||||
newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog()
|
||||
}
|
||||
if len(clientNames) > 0 {
|
||||
newError("domain ", domain, " will use DNS in order: ", clientNames).AtDebug().WriteToLog()
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"v2ray.com/core"
|
||||
"v2ray.com/core/app/dispatcher"
|
||||
. "v2ray.com/core/app/dns"
|
@ -13,25 +13,6 @@ type StaticHosts struct {
|
||||
matchers *strmatcher.MatcherGroup
|
||||
}
|
||||
|
||||
var typeMap = map[DomainMatchingType]strmatcher.Type{
|
||||
DomainMatchingType_Full: strmatcher.Full,
|
||||
DomainMatchingType_Subdomain: strmatcher.Domain,
|
||||
DomainMatchingType_Keyword: strmatcher.Substr,
|
||||
DomainMatchingType_Regex: strmatcher.Regex,
|
||||
}
|
||||
|
||||
func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) {
|
||||
strMType, f := typeMap[t]
|
||||
if !f {
|
||||
return nil, newError("unknown mapping type", t).AtWarning()
|
||||
}
|
||||
matcher, err := strMType.New(domain)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create str matcher").Base(err)
|
||||
}
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
// NewStaticHosts creates a new StaticHosts instance.
|
||||
func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDomain) (*StaticHosts, error) {
|
||||
g := new(strmatcher.MatcherGroup)
|
||||
@ -99,24 +80,35 @@ func filterIP(ips []net.Address, option IPOption) []net.Address {
|
||||
filtered = append(filtered, ip)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// LookupIP returns IP address for the given domain, if exists in this StaticHosts.
|
||||
func (h *StaticHosts) LookupIP(domain string, option IPOption) []net.Address {
|
||||
indices := h.matchers.Match(domain)
|
||||
if len(indices) == 0 {
|
||||
return nil
|
||||
}
|
||||
ips := []net.Address{}
|
||||
for _, id := range indices {
|
||||
func (h *StaticHosts) lookupInternal(domain string) []net.Address {
|
||||
var ips []net.Address
|
||||
for _, id := range h.matchers.Match(domain) {
|
||||
ips = append(ips, h.ips[id]...)
|
||||
}
|
||||
if len(ips) == 1 && ips[0].Family().IsDomain() {
|
||||
return ips
|
||||
}
|
||||
return filterIP(ips, option)
|
||||
return ips
|
||||
}
|
||||
|
||||
func (h *StaticHosts) lookup(domain string, option IPOption, maxDepth int) []net.Address {
|
||||
switch addrs := h.lookupInternal(domain); {
|
||||
case len(addrs) == 0: // Not recorded in static hosts, return nil
|
||||
return nil
|
||||
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain
|
||||
if maxDepth > 0 {
|
||||
unwrapped := h.lookup(addrs[0].Domain(), option, maxDepth-1)
|
||||
if unwrapped != nil {
|
||||
return unwrapped
|
||||
}
|
||||
}
|
||||
return addrs
|
||||
default: // IP record found, return a non-nil IP array
|
||||
return filterIP(addrs, option)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
|
||||
func (h *StaticHosts) Lookup(domain string, option IPOption) []net.Address {
|
||||
return h.lookup(domain, option, 5)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
. "v2ray.com/core/app/dns"
|
||||
"v2ray.com/core/common"
|
||||
"v2ray.com/core/common/net"
|
||||
@ -39,7 +38,7 @@ func TestStaticHosts(t *testing.T) {
|
||||
common.Must(err)
|
||||
|
||||
{
|
||||
ips := hosts.LookupIP("v2ray.com", IPOption{
|
||||
ips := hosts.Lookup("v2ray.com", IPOption{
|
||||
IPv4Enable: true,
|
||||
IPv6Enable: true,
|
||||
})
|
||||
@ -52,7 +51,7 @@ func TestStaticHosts(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
ips := hosts.LookupIP("www.v2ray.cn", IPOption{
|
||||
ips := hosts.Lookup("www.v2ray.cn", IPOption{
|
||||
IPv4Enable: true,
|
||||
IPv6Enable: true,
|
||||
})
|
||||
@ -65,7 +64,7 @@ func TestStaticHosts(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
ips := hosts.LookupIP("baidu.com", IPOption{
|
||||
ips := hosts.Lookup("baidu.com", IPOption{
|
||||
IPv4Enable: false,
|
||||
IPv6Enable: true,
|
||||
})
|
||||
|
@ -2,9 +2,15 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"v2ray.com/core"
|
||||
"v2ray.com/core/app/router"
|
||||
"v2ray.com/core/common/errors"
|
||||
"v2ray.com/core/common/net"
|
||||
"v2ray.com/core/features/dns/localdns"
|
||||
"v2ray.com/core/common/strmatcher"
|
||||
"v2ray.com/core/features/routing"
|
||||
)
|
||||
|
||||
// IPOption is an object for IP query options.
|
||||
@ -13,42 +19,182 @@ type IPOption struct {
|
||||
IPv6Enable bool
|
||||
}
|
||||
|
||||
// Client is the interface for DNS client.
|
||||
type Client interface {
|
||||
// Server is the interface for Name Server.
|
||||
type Server interface {
|
||||
// Name of the Client.
|
||||
Name() string
|
||||
|
||||
// QueryIP sends IP queries to its configured server.
|
||||
QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error)
|
||||
QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error)
|
||||
}
|
||||
|
||||
type LocalNameServer struct {
|
||||
client *localdns.Client
|
||||
// Client is the interface for DNS client.
|
||||
type Client struct {
|
||||
server Server
|
||||
clientIP net.IP
|
||||
domains []string
|
||||
expectIPs []*router.GeoIPMatcher
|
||||
}
|
||||
|
||||
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
|
||||
if option.IPv4Enable && option.IPv6Enable {
|
||||
return s.client.LookupIP(domain)
|
||||
var errExpectedIPNonMatch = errors.New("expectIPs not match")
|
||||
|
||||
// NewServer creates a name server object according to the network destination url.
|
||||
func NewServer(dest net.Destination, dispatcher routing.Dispatcher) (Server, error) {
|
||||
if address := dest.Address; address.Family().IsDomain() {
|
||||
u, err := url.Parse(address.Domain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case u.String() == "localhost":
|
||||
return NewLocalNameServer(), nil
|
||||
case u.Scheme == "https": // DOH Remote mode
|
||||
return NewDoHNameServer(u, dispatcher)
|
||||
case u.Scheme == "https+local": // DOH Local mode
|
||||
return NewDoHLocalNameServer(u), nil
|
||||
}
|
||||
}
|
||||
if dest.Network == net.Network_Unknown {
|
||||
dest.Network = net.Network_UDP
|
||||
}
|
||||
if dest.Network == net.Network_UDP { // UDP classic DNS mode
|
||||
return NewClassicNameServer(dest, dispatcher), nil
|
||||
}
|
||||
return nil, newError("No available name server could be created from ", dest).AtWarning()
|
||||
}
|
||||
|
||||
// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs.
|
||||
func NewClient(ctx context.Context, ns *NameServer, clientIP net.IP, container router.GeoIPMatcherContainer, updateDomainRule func(strmatcher.Matcher, int) error) (*Client, error) {
|
||||
client := &Client{}
|
||||
err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
|
||||
// Create a new server for each client for now
|
||||
server, err := NewServer(ns.Address.AsDestination(), dispatcher)
|
||||
if err != nil {
|
||||
return newError("failed to create nameserver").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
// Priotize local domains with specific TLDs or without any dot to local DNS
|
||||
if _, isLocalDNS := server.(*LocalNameServer); isLocalDNS {
|
||||
ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...)
|
||||
ns.OriginalRules = append(ns.OriginalRules, localTLDsAndDotlessDomainsRule)
|
||||
}
|
||||
|
||||
// Establish domain rules
|
||||
var rules []string
|
||||
ruleCurr := 0
|
||||
ruleIter := 0
|
||||
for _, domain := range ns.PrioritizedDomain {
|
||||
domainRule, err := toStrMatcher(domain.Type, domain.Domain)
|
||||
if err != nil {
|
||||
return newError("failed to create prioritized domain").Base(err).AtWarning()
|
||||
}
|
||||
originalRuleIdx := ruleCurr
|
||||
if ruleCurr < len(ns.OriginalRules) {
|
||||
rule := ns.OriginalRules[ruleCurr]
|
||||
if ruleCurr >= len(rules) {
|
||||
rules = append(rules, rule.Rule)
|
||||
}
|
||||
ruleIter++
|
||||
if ruleIter >= int(rule.Size) {
|
||||
ruleIter = 0
|
||||
ruleCurr++
|
||||
}
|
||||
} else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests)
|
||||
rules = append(rules, domainRule.String())
|
||||
ruleCurr++
|
||||
}
|
||||
err = updateDomainRule(domainRule, originalRuleIdx)
|
||||
if err != nil {
|
||||
return newError("failed to create prioritized domain").Base(err).AtWarning()
|
||||
}
|
||||
}
|
||||
|
||||
// Establish expected IPs
|
||||
var matchers []*router.GeoIPMatcher
|
||||
for _, geoip := range ns.Geoip {
|
||||
matcher, err := container.Add(geoip)
|
||||
if err != nil {
|
||||
return newError("failed to create ip matcher").Base(err).AtWarning()
|
||||
}
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
|
||||
if len(clientIP) > 0 {
|
||||
switch ns.Address.Address.GetAddress().(type) {
|
||||
case *net.IPOrDomain_Domain:
|
||||
newError("DNS: client ", ns.Address.Address.GetDomain(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
|
||||
case *net.IPOrDomain_Ip:
|
||||
newError("DNS: client ", ns.Address.Address.GetIp(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
|
||||
}
|
||||
}
|
||||
|
||||
client.server = server
|
||||
client.clientIP = clientIP
|
||||
client.domains = rules
|
||||
client.expectIPs = matchers
|
||||
return nil
|
||||
})
|
||||
return client, err
|
||||
}
|
||||
|
||||
// NewSimpleClient creates a DNS client with a simple destination.
|
||||
func NewSimpleClient(ctx context.Context, endpoint *net.Endpoint, clientIP net.IP) (*Client, error) {
|
||||
client := &Client{}
|
||||
err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
|
||||
server, err := NewServer(endpoint.AsDestination(), dispatcher)
|
||||
if err != nil {
|
||||
return newError("failed to create nameserver").Base(err).AtWarning()
|
||||
}
|
||||
client.server = server
|
||||
client.clientIP = clientIP
|
||||
return nil
|
||||
})
|
||||
|
||||
if len(clientIP) > 0 {
|
||||
switch endpoint.Address.GetAddress().(type) {
|
||||
case *net.IPOrDomain_Domain:
|
||||
newError("DNS: client ", endpoint.Address.GetDomain(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
|
||||
case *net.IPOrDomain_Ip:
|
||||
newError("DNS: client ", endpoint.Address.GetIp(), " uses clientIP ", clientIP.String()).AtInfo().WriteToLog()
|
||||
}
|
||||
}
|
||||
|
||||
if option.IPv4Enable {
|
||||
return s.client.LookupIPv4(domain)
|
||||
}
|
||||
|
||||
if option.IPv6Enable {
|
||||
return s.client.LookupIPv6(domain)
|
||||
}
|
||||
|
||||
return nil, newError("neither IPv4 nor IPv6 is enabled")
|
||||
return client, err
|
||||
}
|
||||
|
||||
func (s *LocalNameServer) Name() string {
|
||||
return "localhost"
|
||||
// Name returns the server name the client manages.
|
||||
func (c *Client) Name() string {
|
||||
return c.server.Name()
|
||||
}
|
||||
|
||||
func NewLocalNameServer() *LocalNameServer {
|
||||
newError("DNS: created localhost client").AtInfo().WriteToLog()
|
||||
return &LocalNameServer{
|
||||
client: localdns.New(),
|
||||
// QueryIP send DNS query to the name server with the client's IP.
|
||||
func (c *Client) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
|
||||
ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
return c.MatchExpectedIPs(domain, ips)
|
||||
}
|
||||
|
||||
// MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones.
|
||||
func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) ([]net.IP, error) {
|
||||
if len(c.expectIPs) == 0 {
|
||||
return ips, nil
|
||||
}
|
||||
newIps := []net.IP{}
|
||||
for _, ip := range ips {
|
||||
for _, matcher := range c.expectIPs {
|
||||
if matcher.Match(ip) {
|
||||
newIps = append(newIps, ip)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(newIps) == 0 {
|
||||
return nil, errExpectedIPNonMatch
|
||||
}
|
||||
newError("domain ", domain, " expectIPs ", newIps, " matched at server ", c.Name()).AtDebug().WriteToLog()
|
||||
return newIps, nil
|
||||
}
|
||||
|
@ -33,19 +33,15 @@ type DoHNameServer struct {
|
||||
pub *pubsub.Service
|
||||
cleanup *task.Periodic
|
||||
reqID uint32
|
||||
clientIP net.IP
|
||||
httpClient *http.Client
|
||||
dohURL string
|
||||
name string
|
||||
}
|
||||
|
||||
// NewDoHNameServer creates DOH client object for remote resolving
|
||||
func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.IP) (*DoHNameServer, error) {
|
||||
// NewDoHNameServer creates DOH server object for remote resolving.
|
||||
func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher) (*DoHNameServer, error) {
|
||||
newError("DNS: created Remote DOH client for ", url.String()).AtInfo().WriteToLog()
|
||||
if clientIP != nil {
|
||||
newError("DNS: Remote DOH client ", url.String(), " uses clientip ", clientIP.String()).AtInfo().WriteToLog()
|
||||
}
|
||||
s := baseDOHNameServer(url, "DOH", clientIP)
|
||||
s := baseDOHNameServer(url, "DOH")
|
||||
|
||||
// Dispatched connection will be closed (interrupted) after each request
|
||||
// This makes DOH inefficient without a keep-alived connection
|
||||
@ -85,9 +81,9 @@ func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.
|
||||
}
|
||||
|
||||
// NewDoHLocalNameServer creates DOH client object for local resolving
|
||||
func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer {
|
||||
func NewDoHLocalNameServer(url *url.URL) *DoHNameServer {
|
||||
url.Scheme = "https"
|
||||
s := baseDOHNameServer(url, "DOHL", clientIP)
|
||||
s := baseDOHNameServer(url, "DOHL")
|
||||
tr := &http.Transport{
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
@ -108,29 +104,24 @@ func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer {
|
||||
Transport: tr,
|
||||
}
|
||||
newError("DNS: created Local DOH client for ", url.String()).AtInfo().WriteToLog()
|
||||
if clientIP != nil {
|
||||
newError("DNS: Local DOH client ", url.String(), " uses clientip ", clientIP.String()).AtInfo().WriteToLog()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func baseDOHNameServer(url *url.URL, prefix string, clientIP net.IP) *DoHNameServer {
|
||||
func baseDOHNameServer(url *url.URL, prefix string) *DoHNameServer {
|
||||
s := &DoHNameServer{
|
||||
ips: make(map[string]record),
|
||||
clientIP: clientIP,
|
||||
pub: pubsub.NewService(),
|
||||
name: prefix + "//" + url.Host,
|
||||
dohURL: url.String(),
|
||||
ips: make(map[string]record),
|
||||
pub: pubsub.NewService(),
|
||||
name: prefix + "//" + url.Host,
|
||||
dohURL: url.String(),
|
||||
}
|
||||
s.cleanup = &task.Periodic{
|
||||
Interval: time.Minute,
|
||||
Execute: s.Cleanup,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Name returns client name
|
||||
// Name implements Server.
|
||||
func (s *DoHNameServer) Name() string {
|
||||
return s.name
|
||||
}
|
||||
@ -213,10 +204,10 @@ func (s *DoHNameServer) newReqID() uint16 {
|
||||
return uint16(atomic.AddUint32(&s.reqID, 1))
|
||||
}
|
||||
|
||||
func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
|
||||
func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option IPOption) {
|
||||
newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx))
|
||||
|
||||
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP))
|
||||
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
|
||||
|
||||
var deadline time.Time
|
||||
if d, ok := ctx.Deadline(); ok {
|
||||
@ -320,7 +311,7 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
return toNetIP(ips), nil
|
||||
return toNetIP(ips)
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
@ -334,8 +325,8 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.
|
||||
return nil, errRecordNotFound
|
||||
}
|
||||
|
||||
// QueryIP is called from dns.Server->queryIPTimeout
|
||||
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
|
||||
// QueryIP implements Server.
|
||||
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) { // nolint: dupl
|
||||
fqdn := Fqdn(domain)
|
||||
|
||||
ips, err := s.findIPsForDomain(fqdn, option)
|
||||
@ -370,7 +361,7 @@ func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option IPOpt
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
s.sendQuery(ctx, fqdn, option)
|
||||
s.sendQuery(ctx, fqdn, clientIP, option)
|
||||
|
||||
for {
|
||||
ips, err := s.findIPsForDomain(fqdn, option)
|
48
app/dns/nameserver_local.go
Normal file
48
app/dns/nameserver_local.go
Normal file
@ -0,0 +1,48 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"v2ray.com/core/common/net"
|
||||
"v2ray.com/core/features/dns/localdns"
|
||||
)
|
||||
|
||||
// LocalNameServer is an wrapper over local DNS feature.
|
||||
type LocalNameServer struct {
|
||||
client *localdns.Client
|
||||
}
|
||||
|
||||
// QueryIP implements Server.
|
||||
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) {
|
||||
if option.IPv4Enable && option.IPv6Enable {
|
||||
return s.client.LookupIP(domain)
|
||||
}
|
||||
|
||||
if option.IPv4Enable {
|
||||
return s.client.LookupIPv4(domain)
|
||||
}
|
||||
|
||||
if option.IPv6Enable {
|
||||
return s.client.LookupIPv6(domain)
|
||||
}
|
||||
|
||||
return nil, newError("neither IPv4 nor IPv6 is enabled")
|
||||
}
|
||||
|
||||
// Name implements Server.
|
||||
func (s *LocalNameServer) Name() string {
|
||||
return "localhost"
|
||||
}
|
||||
|
||||
// NewLocalNameServer creates localdns server object for directly lookup in system DNS.
|
||||
func NewLocalNameServer() *LocalNameServer {
|
||||
newError("DNS: created localhost client").AtInfo().WriteToLog()
|
||||
return &LocalNameServer{
|
||||
client: localdns.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocalDNSClient creates localdns client object for directly lookup in system DNS.
|
||||
func NewLocalDNSClient() *Client {
|
||||
return &Client{server: NewLocalNameServer()}
|
||||
}
|
@ -7,12 +7,13 @@ import (
|
||||
|
||||
. "v2ray.com/core/app/dns"
|
||||
"v2ray.com/core/common"
|
||||
"v2ray.com/core/common/net"
|
||||
)
|
||||
|
||||
func TestLocalNameServer(t *testing.T) {
|
||||
s := NewLocalNameServer()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
||||
ips, err := s.QueryIP(ctx, "google.com", IPOption{
|
||||
ips, err := s.QueryIP(ctx, "google.com", net.IP{}, IPOption{
|
||||
IPv4Enable: true,
|
||||
IPv6Enable: true,
|
||||
})
|
@ -20,6 +20,7 @@ import (
|
||||
"v2ray.com/core/transport/internet/udp"
|
||||
)
|
||||
|
||||
// ClassicNameServer implemented traditional UDP DNS.
|
||||
type ClassicNameServer struct {
|
||||
sync.RWMutex
|
||||
name string
|
||||
@ -30,10 +31,10 @@ type ClassicNameServer struct {
|
||||
udpServer *udp.Dispatcher
|
||||
cleanup *task.Periodic
|
||||
reqID uint32
|
||||
clientIP net.IP
|
||||
}
|
||||
|
||||
func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, clientIP net.IP) *ClassicNameServer {
|
||||
// NewClassicNameServer creates udp server object for remote resolving.
|
||||
func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher) *ClassicNameServer {
|
||||
// default to 53 if unspecific
|
||||
if address.Port == 0 {
|
||||
address.Port = net.Port(53)
|
||||
@ -43,7 +44,6 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher
|
||||
address: address,
|
||||
ips: make(map[string]record),
|
||||
requests: make(map[uint16]dnsRequest),
|
||||
clientIP: clientIP,
|
||||
pub: pubsub.NewService(),
|
||||
name: strings.ToUpper(address.String()),
|
||||
}
|
||||
@ -53,16 +53,15 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher
|
||||
}
|
||||
s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse)
|
||||
newError("DNS: created UDP client inited for ", address.NetAddr()).AtInfo().WriteToLog()
|
||||
if clientIP != nil {
|
||||
newError("DNS: UDP client ", address.NetAddr(), " uses clientip ", clientIP.String()).AtInfo().WriteToLog()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Name implements Server.
|
||||
func (s *ClassicNameServer) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// Cleanup clears expired items from cache
|
||||
func (s *ClassicNameServer) Cleanup() error {
|
||||
now := time.Now()
|
||||
s.Lock()
|
||||
@ -104,6 +103,7 @@ func (s *ClassicNameServer) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleResponse handles udp response packet from remote DNS server.
|
||||
func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_proto.Packet) {
|
||||
ipRec, err := parseResponse(packet.Payload.Bytes())
|
||||
if err != nil {
|
||||
@ -181,10 +181,10 @@ func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) {
|
||||
s.requests[id] = *req
|
||||
}
|
||||
|
||||
func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, option IPOption) {
|
||||
func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option IPOption) {
|
||||
newError(s.name, " querying DNS for: ", domain).AtDebug().WriteToLog(session.ExportIDToError(ctx))
|
||||
|
||||
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP))
|
||||
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
|
||||
|
||||
for _, req := range reqs {
|
||||
s.addPendingRequest(req)
|
||||
@ -228,7 +228,7 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
return toNetIP(ips), nil
|
||||
return toNetIP(ips)
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
@ -238,7 +238,8 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]
|
||||
return nil, dns_feature.ErrEmptyResponse
|
||||
}
|
||||
|
||||
func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) {
|
||||
// QueryIP implements Server.
|
||||
func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) {
|
||||
fqdn := Fqdn(domain)
|
||||
|
||||
ips, err := s.findIPsForDomain(fqdn, option)
|
||||
@ -273,7 +274,7 @@ func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option I
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
s.sendQuery(ctx, fqdn, option)
|
||||
s.sendQuery(ctx, fqdn, clientIP, option)
|
||||
|
||||
for {
|
||||
ips, err := s.findIPsForDomain(fqdn, option)
|
@ -1,454 +0,0 @@
|
||||
package dns
|
||||
|
||||
//go:generate go run v2ray.com/core/common/errors/errorgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"v2ray.com/core"
|
||||
"v2ray.com/core/app/router"
|
||||
"v2ray.com/core/common"
|
||||
"v2ray.com/core/common/errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Server is a DNS rely server.
|
||||
type Server struct {
|
||||
sync.Mutex
|
||||
hosts *StaticHosts
|
||||
clientIP net.IP
|
||||
clients []Client // clientIdx -> Client
|
||||
ipIndexMap []*MultiGeoIPMatcher // clientIdx -> *MultiGeoIPMatcher
|
||||
domainRules [][]string // clientIdx -> domainRuleIdx -> DomainRule
|
||||
domainMatcher strmatcher.IndexMatcher
|
||||
matcherInfos []DomainMatcherInfo // matcherIdx -> DomainMatcherInfo
|
||||
tag string
|
||||
}
|
||||
|
||||
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher
|
||||
type DomainMatcherInfo struct {
|
||||
clientIdx uint16
|
||||
domainRuleIdx uint16
|
||||
}
|
||||
|
||||
// MultiGeoIPMatcher for match
|
||||
type MultiGeoIPMatcher struct {
|
||||
matchers []*router.GeoIPMatcher
|
||||
}
|
||||
|
||||
var errExpectedIPNonMatch = errors.New("expectIPs not match")
|
||||
|
||||
// Match check ip match
|
||||
func (c *MultiGeoIPMatcher) Match(ip net.IP) bool {
|
||||
for _, matcher := range c.matchers {
|
||||
if matcher.Match(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasMatcher check has matcher
|
||||
func (c *MultiGeoIPMatcher) HasMatcher() bool {
|
||||
return len(c.matchers) > 0
|
||||
}
|
||||
|
||||
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 server.tag == "" {
|
||||
server.tag = generateRandomTag()
|
||||
}
|
||||
if len(config.ClientIp) > 0 {
|
||||
if len(config.ClientIp) != net.IPv4len && len(config.ClientIp) != net.IPv6len {
|
||||
return nil, newError("unexpected IP length", len(config.ClientIp))
|
||||
}
|
||||
server.clientIP = net.IP(config.ClientIp)
|
||||
}
|
||||
|
||||
hosts, err := NewStaticHosts(config.StaticHosts, config.Hosts)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create hosts").Base(err)
|
||||
}
|
||||
server.hosts = hosts
|
||||
|
||||
addNameServer := func(ns *NameServer) int {
|
||||
endpoint := ns.Address
|
||||
address := endpoint.Address.AsAddress()
|
||||
|
||||
var myClientIP net.IP
|
||||
if len(ns.ClientIp) == net.IPv4len || len(ns.ClientIp) == net.IPv6len {
|
||||
myClientIP = net.IP(ns.ClientIp)
|
||||
} else {
|
||||
myClientIP = server.clientIP
|
||||
}
|
||||
|
||||
switch {
|
||||
case address.Family().IsDomain() && address.Domain() == "localhost":
|
||||
server.clients = append(server.clients, NewLocalNameServer())
|
||||
// Priotize local domains with specific TLDs or without any dot to local DNS
|
||||
// References:
|
||||
// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml
|
||||
// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
|
||||
localTLDsAndDotlessDomains := []*NameServer_PriorityDomain{
|
||||
{Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "local"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "localhost"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "lan"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "home.arpa"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "example"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "invalid"},
|
||||
{Type: DomainMatchingType_Subdomain, Domain: "test"},
|
||||
}
|
||||
ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...)
|
||||
|
||||
case address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https+local://"):
|
||||
// URI schemed string treated as domain
|
||||
// DOH Local mode
|
||||
u, err := url.Parse(address.Domain())
|
||||
if err != nil {
|
||||
log.Fatalln(newError("DNS config error").Base(err))
|
||||
}
|
||||
server.clients = append(server.clients, NewDoHLocalNameServer(u, myClientIP))
|
||||
|
||||
case address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https://"):
|
||||
// DOH Remote mode
|
||||
u, err := url.Parse(address.Domain())
|
||||
if err != nil {
|
||||
log.Fatalln(newError("DNS config error").Base(err))
|
||||
}
|
||||
idx := len(server.clients)
|
||||
server.clients = append(server.clients, nil)
|
||||
|
||||
// need the core dispatcher, register DOHClient at callback
|
||||
common.Must(core.RequireFeatures(ctx, func(d routing.Dispatcher) {
|
||||
c, err := NewDoHNameServer(u, d, myClientIP)
|
||||
if err != nil {
|
||||
log.Fatalln(newError("DNS config error").Base(err))
|
||||
}
|
||||
server.clients[idx] = c
|
||||
}))
|
||||
|
||||
default:
|
||||
// UDP classic DNS mode
|
||||
dest := endpoint.AsDestination()
|
||||
if dest.Network == net.Network_Unknown {
|
||||
dest.Network = net.Network_UDP
|
||||
}
|
||||
if dest.Network == net.Network_UDP {
|
||||
idx := len(server.clients)
|
||||
server.clients = append(server.clients, nil)
|
||||
|
||||
common.Must(core.RequireFeatures(ctx, func(d routing.Dispatcher) {
|
||||
server.clients[idx] = NewClassicNameServer(dest, d, myClientIP)
|
||||
}))
|
||||
}
|
||||
}
|
||||
server.ipIndexMap = append(server.ipIndexMap, nil)
|
||||
return len(server.clients) - 1
|
||||
}
|
||||
|
||||
if len(config.NameServers) > 0 {
|
||||
features.PrintDeprecatedFeatureWarning("simple DNS server")
|
||||
for _, destPB := range config.NameServers {
|
||||
addNameServer(&NameServer{Address: destPB})
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.NameServer) > 0 {
|
||||
clientIndices := []int{}
|
||||
domainRuleCount := 0
|
||||
for _, ns := range config.NameServer {
|
||||
idx := addNameServer(ns)
|
||||
clientIndices = append(clientIndices, idx)
|
||||
domainRuleCount += len(ns.PrioritizedDomain)
|
||||
}
|
||||
|
||||
domainRules := make([][]string, len(server.clients))
|
||||
domainMatcher := &strmatcher.MatcherGroup{}
|
||||
matcherInfos := make([]DomainMatcherInfo, domainRuleCount+1) // matcher index starts from 1
|
||||
var geoIPMatcherContainer router.GeoIPMatcherContainer
|
||||
for nidx, ns := range config.NameServer {
|
||||
idx := clientIndices[nidx]
|
||||
|
||||
// Establish domain rule matcher
|
||||
rules := []string{}
|
||||
ruleCurr := 0
|
||||
ruleIter := 0
|
||||
for _, domain := range ns.PrioritizedDomain {
|
||||
matcher, err := toStrMatcher(domain.Type, domain.Domain)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create prioritized domain").Base(err).AtWarning()
|
||||
}
|
||||
midx := domainMatcher.Add(matcher)
|
||||
if midx >= uint32(len(matcherInfos)) { // This rarely happens according to current matcher's implementation
|
||||
newError("expanding domain matcher info array to size ", midx, " when adding ", matcher).AtDebug().WriteToLog()
|
||||
matcherInfos = append(matcherInfos, make([]DomainMatcherInfo, midx-uint32(len(matcherInfos))+1)...)
|
||||
}
|
||||
info := &matcherInfos[midx]
|
||||
info.clientIdx = uint16(idx)
|
||||
if ruleCurr < len(ns.OriginalRules) {
|
||||
info.domainRuleIdx = uint16(ruleCurr)
|
||||
rule := ns.OriginalRules[ruleCurr]
|
||||
if ruleCurr >= len(rules) {
|
||||
rules = append(rules, rule.Rule)
|
||||
}
|
||||
ruleIter++
|
||||
if ruleIter >= int(rule.Size) {
|
||||
ruleIter = 0
|
||||
ruleCurr++
|
||||
}
|
||||
} else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests)
|
||||
info.domainRuleIdx = uint16(len(rules))
|
||||
rules = append(rules, matcher.String())
|
||||
}
|
||||
}
|
||||
domainRules[idx] = rules
|
||||
|
||||
// only add to ipIndexMap if GeoIP is configured
|
||||
if len(ns.Geoip) > 0 {
|
||||
var matchers []*router.GeoIPMatcher
|
||||
for _, geoip := range ns.Geoip {
|
||||
matcher, err := geoIPMatcherContainer.Add(geoip)
|
||||
if err != nil {
|
||||
return nil, newError("failed to create ip matcher").Base(err).AtWarning()
|
||||
}
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
matcher := &MultiGeoIPMatcher{matchers: matchers}
|
||||
server.ipIndexMap[idx] = matcher
|
||||
}
|
||||
}
|
||||
server.domainRules = domainRules
|
||||
server.domainMatcher = domainMatcher
|
||||
server.matcherInfos = matcherInfos
|
||||
}
|
||||
|
||||
if len(server.clients) == 0 {
|
||||
server.clients = append(server.clients, NewLocalNameServer())
|
||||
server.ipIndexMap = append(server.ipIndexMap, nil)
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// Type implements common.HasType.
|
||||
func (*Server) Type() interface{} {
|
||||
return dns.ClientType()
|
||||
}
|
||||
|
||||
// Start implements common.Runnable.
|
||||
func (s *Server) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements common.Closable.
|
||||
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
|
||||
}
|
||||
|
||||
// Match check dns ip match geoip
|
||||
func (s *Server) Match(idx int, client Client, domain string, ips []net.IP) ([]net.IP, error) {
|
||||
var matcher *MultiGeoIPMatcher
|
||||
if idx < len(s.ipIndexMap) {
|
||||
matcher = s.ipIndexMap[idx]
|
||||
}
|
||||
if matcher == nil {
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
if !matcher.HasMatcher() {
|
||||
newError("domain ", domain, " server has no valid matcher: ", client.Name(), " idx:", idx).AtDebug().WriteToLog()
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
newIps := []net.IP{}
|
||||
for _, ip := range ips {
|
||||
if matcher.Match(ip) {
|
||||
newIps = append(newIps, ip)
|
||||
}
|
||||
}
|
||||
if len(newIps) == 0 {
|
||||
return nil, errExpectedIPNonMatch
|
||||
}
|
||||
newError("domain ", domain, " expectIPs ", newIps, " matched at server ", client.Name(), " idx:", idx).AtDebug().WriteToLog()
|
||||
return newIps, nil
|
||||
}
|
||||
|
||||
func (s *Server) queryIPTimeout(idx int, client Client, domain string, option IPOption) ([]net.IP, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*4)
|
||||
if len(s.tag) > 0 {
|
||||
ctx = session.ContextWithInbound(ctx, &session.Inbound{
|
||||
Tag: s.tag,
|
||||
})
|
||||
}
|
||||
ips, err := client.QueryIP(ctx, domain, option)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
|
||||
ips, err = s.Match(idx, client, domain, ips)
|
||||
return ips, err
|
||||
}
|
||||
|
||||
// LookupIP implements dns.Client.
|
||||
func (s *Server) LookupIP(domain string) ([]net.IP, error) {
|
||||
return s.lookupIPInternal(domain, IPOption{
|
||||
IPv4Enable: true,
|
||||
IPv6Enable: true,
|
||||
})
|
||||
}
|
||||
|
||||
// LookupIPv4 implements dns.IPv4Lookup.
|
||||
func (s *Server) LookupIPv4(domain string) ([]net.IP, error) {
|
||||
return s.lookupIPInternal(domain, IPOption{
|
||||
IPv4Enable: true,
|
||||
IPv6Enable: false,
|
||||
})
|
||||
}
|
||||
|
||||
// LookupIPv6 implements dns.IPv6Lookup.
|
||||
func (s *Server) LookupIPv6(domain string) ([]net.IP, error) {
|
||||
return s.lookupIPInternal(domain, IPOption{
|
||||
IPv4Enable: false,
|
||||
IPv6Enable: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) lookupStatic(domain string, option IPOption, depth int32) []net.Address {
|
||||
ips := s.hosts.LookupIP(domain, option)
|
||||
if ips == nil {
|
||||
return nil
|
||||
}
|
||||
if ips[0].Family().IsDomain() && depth < 5 {
|
||||
if newIPs := s.lookupStatic(ips[0].Domain(), option, depth+1); newIPs != nil {
|
||||
return newIPs
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func toNetIP(ips []net.Address) []net.IP {
|
||||
if len(ips) == 0 {
|
||||
return nil
|
||||
}
|
||||
netips := make([]net.IP, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
netips = append(netips, ip.IP())
|
||||
}
|
||||
return netips
|
||||
}
|
||||
|
||||
func (s *Server) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) {
|
||||
if domain == "" {
|
||||
return nil, newError("empty domain name")
|
||||
}
|
||||
|
||||
// normalize the FQDN form query
|
||||
if domain[len(domain)-1] == '.' {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
ips := s.lookupStatic(domain, option, 0)
|
||||
if ips != nil && ips[0].Family().IsIP() {
|
||||
newError("returning ", len(ips), " IPs for domain ", domain).WriteToLog()
|
||||
return toNetIP(ips), nil
|
||||
}
|
||||
|
||||
if ips != nil && ips[0].Family().IsDomain() {
|
||||
newdomain := ips[0].Domain()
|
||||
newError("domain replaced: ", domain, " -> ", newdomain).WriteToLog()
|
||||
domain = newdomain
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
var matchedClient Client
|
||||
if s.domainMatcher != nil {
|
||||
indices := s.domainMatcher.Match(domain)
|
||||
domainRules := []string{}
|
||||
matchingDNS := []string{}
|
||||
for _, idx := range indices {
|
||||
info := s.matcherInfos[idx]
|
||||
rule := s.domainRules[info.clientIdx][info.domainRuleIdx]
|
||||
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", rule, info.clientIdx))
|
||||
matchingDNS = append(matchingDNS, s.clients[info.clientIdx].Name())
|
||||
}
|
||||
if len(domainRules) > 0 {
|
||||
newError("domain ", domain, " matches following rules: ", domainRules).AtDebug().WriteToLog()
|
||||
}
|
||||
if len(matchingDNS) > 0 {
|
||||
newError("domain ", domain, " uses following DNS first: ", matchingDNS).AtDebug().WriteToLog()
|
||||
}
|
||||
for _, idx := range indices {
|
||||
clientIdx := int(s.matcherInfos[idx].clientIdx)
|
||||
matchedClient = s.clients[clientIdx]
|
||||
ips, err := s.queryIPTimeout(clientIdx, matchedClient, domain, option)
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
if err == dns.ErrEmptyResponse {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
newError("failed to lookup ip for domain ", domain, " at server ", matchedClient.Name()).Base(err).WriteToLog()
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx, client := range s.clients {
|
||||
if client == matchedClient {
|
||||
newError("domain ", domain, " at server ", client.Name(), " idx:", idx, " already lookup failed, just ignore").AtDebug().WriteToLog()
|
||||
continue
|
||||
}
|
||||
|
||||
ips, err := s.queryIPTimeout(idx, client, domain, option)
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
newError("failed to lookup ip for domain ", domain, " at server ", client.Name()).Base(err).WriteToLog()
|
||||
lastErr = err
|
||||
}
|
||||
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, newError("returning nil for domain ", domain).Base(lastErr)
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
}
|
@ -36,19 +36,23 @@ func (m *AccessMessage) String() string {
|
||||
builder.WriteString(string(m.Status))
|
||||
builder.WriteByte(' ')
|
||||
builder.WriteString(serial.ToString(m.To))
|
||||
builder.WriteByte(' ')
|
||||
|
||||
if len(m.Detour) > 0 {
|
||||
builder.WriteByte('[')
|
||||
builder.WriteString(" [")
|
||||
builder.WriteString(m.Detour)
|
||||
builder.WriteString("] ")
|
||||
builder.WriteByte(']')
|
||||
}
|
||||
|
||||
if reason := serial.ToString(m.Reason); len(reason) > 0 {
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(reason)
|
||||
}
|
||||
builder.WriteString(serial.ToString(m.Reason))
|
||||
|
||||
if len(m.Email) > 0 {
|
||||
builder.WriteString("email:")
|
||||
builder.WriteString(" email: ")
|
||||
builder.WriteString(m.Email)
|
||||
builder.WriteByte(' ')
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
// ToString serialize an arbitrary value into string.
|
||||
func ToString(v interface{}) string {
|
||||
if v == nil {
|
||||
return " "
|
||||
return ""
|
||||
}
|
||||
|
||||
switch value := v.(type) {
|
||||
|
@ -87,7 +87,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
|
||||
|
||||
geoipList, err := toCidrList(c.ExpectIPs)
|
||||
if err != nil {
|
||||
return nil, newError("invalid ip rule: ", c.ExpectIPs).Base(err)
|
||||
return nil, newError("invalid IP rule: ", c.ExpectIPs).Base(err)
|
||||
}
|
||||
|
||||
var myClientIP []byte
|
||||
@ -153,7 +153,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||
for _, server := range c.Servers {
|
||||
ns, err := server.Build()
|
||||
if err != nil {
|
||||
return nil, newError("failed to build name server").Base(err)
|
||||
return nil, newError("failed to build nameserver").Base(err)
|
||||
}
|
||||
config.NameServer = append(config.NameServer, ns)
|
||||
}
|
||||
@ -170,15 +170,23 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||
var mappings []*dns.Config_HostMapping
|
||||
switch {
|
||||
case strings.HasPrefix(domain, "domain:"):
|
||||
domainName := domain[7:]
|
||||
if len(domainName) == 0 {
|
||||
return nil, newError("empty domain type of rule: ", domain)
|
||||
}
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Subdomain
|
||||
mapping.Domain = domain[7:]
|
||||
mapping.Domain = domainName
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "geosite:"):
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", strings.ToUpper(domain[8:]))
|
||||
listName := domain[8:]
|
||||
if len(listName) == 0 {
|
||||
return nil, newError("empty geosite rule: ", domain)
|
||||
}
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", listName)
|
||||
if err != nil {
|
||||
return nil, newError("invalid geosite settings: ", domain).Base(err)
|
||||
return nil, newError("failed to load geosite: ", listName).Base(err)
|
||||
}
|
||||
for _, d := range domains {
|
||||
mapping := getHostMapping(addr)
|
||||
@ -188,21 +196,33 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||
}
|
||||
|
||||
case strings.HasPrefix(domain, "regexp:"):
|
||||
regexpVal := domain[7:]
|
||||
if len(regexpVal) == 0 {
|
||||
return nil, newError("empty regexp type of rule: ", domain)
|
||||
}
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Regex
|
||||
mapping.Domain = domain[7:]
|
||||
mapping.Domain = regexpVal
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "keyword:"):
|
||||
keywordVal := domain[8:]
|
||||
if len(keywordVal) == 0 {
|
||||
return nil, newError("empty keyword type of rule: ", domain)
|
||||
}
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Keyword
|
||||
mapping.Domain = domain[8:]
|
||||
mapping.Domain = keywordVal
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "full:"):
|
||||
fullVal := domain[5:]
|
||||
if len(fullVal) == 0 {
|
||||
return nil, newError("empty full domain type of rule: ", domain)
|
||||
}
|
||||
mapping := getHostMapping(addr)
|
||||
mapping.Type = dns.DomainMatchingType_Full
|
||||
mapping.Domain = domain[5:]
|
||||
mapping.Domain = fullVal
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
case strings.HasPrefix(domain, "dotless:"):
|
||||
@ -224,10 +244,10 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||
return nil, newError("invalid external resource: ", domain)
|
||||
}
|
||||
filename := kv[0]
|
||||
country := kv[1]
|
||||
domains, err := loadGeositeWithAttr(filename, country)
|
||||
list := kv[1]
|
||||
domains, err := loadGeositeWithAttr(filename, list)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load domains: ", country, " from ", filename).Base(err)
|
||||
return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err)
|
||||
}
|
||||
for _, d := range domains {
|
||||
mapping := getHostMapping(addr)
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"v2ray.com/core/app/router"
|
||||
"v2ray.com/core/common/net"
|
||||
"v2ray.com/core/common/platform/filesystem"
|
||||
@ -52,11 +51,11 @@ func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
|
||||
}
|
||||
|
||||
switch strings.ToLower(ds) {
|
||||
case "alwaysip":
|
||||
case "alwaysip", "always_ip", "always-ip":
|
||||
return router.Config_UseIp
|
||||
case "ipifnonmatch":
|
||||
case "ipifnonmatch", "ip_if_non_match", "ip-if-non-match":
|
||||
return router.Config_IpIfNonMatch
|
||||
case "ipondemand":
|
||||
case "ipondemand", "ip_on_demand", "ip-on-demand":
|
||||
return router.Config_IpOnDemand
|
||||
default:
|
||||
return router.Config_AsIs
|
||||
@ -162,7 +161,7 @@ func loadIP(filename, country string) ([]*router.CIDR, error) {
|
||||
}
|
||||
|
||||
for _, geoip := range geoipList.Entry {
|
||||
if geoip.CountryCode == country {
|
||||
if strings.EqualFold(geoip.CountryCode, country) {
|
||||
return geoip.Cidr, nil
|
||||
}
|
||||
}
|
||||
@ -170,7 +169,7 @@ func loadIP(filename, country string) ([]*router.CIDR, error) {
|
||||
return nil, newError("country not found in ", filename, ": ", country)
|
||||
}
|
||||
|
||||
func loadSite(filename, country string) ([]*router.Domain, error) {
|
||||
func loadSite(filename, list string) ([]*router.Domain, error) {
|
||||
geositeBytes, err := filesystem.ReadAsset(filename)
|
||||
if err != nil {
|
||||
return nil, newError("failed to open file: ", filename).Base(err)
|
||||
@ -181,12 +180,12 @@ func loadSite(filename, country string) ([]*router.Domain, error) {
|
||||
}
|
||||
|
||||
for _, site := range geositeList.Entry {
|
||||
if site.CountryCode == country {
|
||||
if strings.EqualFold(site.CountryCode, list) {
|
||||
return site.Domain, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, newError("list not found in ", filename, ": ", country)
|
||||
return nil, newError("list not found in ", filename, ": ", list)
|
||||
}
|
||||
|
||||
type AttributeMatcher interface {
|
||||
@ -197,7 +196,7 @@ type BooleanMatcher string
|
||||
|
||||
func (m BooleanMatcher) Match(domain *router.Domain) bool {
|
||||
for _, attr := range domain.Attribute {
|
||||
if attr.Key == string(m) {
|
||||
if strings.EqualFold(attr.GetKey(), string(m)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -224,8 +223,11 @@ func (al *AttributeList) IsEmpty() bool {
|
||||
func parseAttrs(attrs []string) *AttributeList {
|
||||
al := new(AttributeList)
|
||||
for _, attr := range attrs {
|
||||
lc := strings.ToLower(attr)
|
||||
al.matcher = append(al.matcher, BooleanMatcher(lc))
|
||||
trimmedAttr := strings.ToLower(strings.TrimSpace(attr))
|
||||
if len(trimmedAttr) == 0 {
|
||||
continue
|
||||
}
|
||||
al.matcher = append(al.matcher, BooleanMatcher(trimmedAttr))
|
||||
}
|
||||
return al
|
||||
}
|
||||
@ -233,38 +235,57 @@ func parseAttrs(attrs []string) *AttributeList {
|
||||
func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) {
|
||||
parts := strings.Split(siteWithAttr, "@")
|
||||
if len(parts) == 0 {
|
||||
return nil, newError("empty site")
|
||||
return nil, newError("empty rule")
|
||||
}
|
||||
country := strings.ToUpper(parts[0])
|
||||
attrs := parseAttrs(parts[1:])
|
||||
domains, err := loadSite(file, country)
|
||||
list := strings.TrimSpace(parts[0])
|
||||
attrVal := parts[1:]
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, newError("empty listname in rule: ", siteWithAttr)
|
||||
}
|
||||
|
||||
domains, err := loadSite(file, list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attrs := parseAttrs(attrVal)
|
||||
if attrs.IsEmpty() {
|
||||
if strings.Contains(siteWithAttr, "@") {
|
||||
newError("empty attribute list: ", siteWithAttr)
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
filteredDomains := make([]*router.Domain, 0, len(domains))
|
||||
hasAttrMatched := false
|
||||
for _, domain := range domains {
|
||||
if attrs.Match(domain) {
|
||||
hasAttrMatched = true
|
||||
filteredDomains = append(filteredDomains, domain)
|
||||
}
|
||||
}
|
||||
if !hasAttrMatched {
|
||||
newError("attribute match no rule: geosite:", siteWithAttr)
|
||||
}
|
||||
|
||||
return filteredDomains, nil
|
||||
}
|
||||
|
||||
func parseDomainRule(domain string) ([]*router.Domain, error) {
|
||||
if strings.HasPrefix(domain, "geosite:") {
|
||||
country := strings.ToUpper(domain[8:])
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", country)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load geosite: ", country).Base(err)
|
||||
list := domain[8:]
|
||||
if len(list) == 0 {
|
||||
return nil, newError("empty listname in rule: ", domain)
|
||||
}
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", list)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load geosite: ", list).Base(err)
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
var isExtDatFile = 0
|
||||
{
|
||||
const prefix = "ext:"
|
||||
@ -276,37 +297,55 @@ func parseDomainRule(domain string) ([]*router.Domain, error) {
|
||||
isExtDatFile = len(prefixQualified)
|
||||
}
|
||||
}
|
||||
|
||||
if isExtDatFile != 0 {
|
||||
kv := strings.Split(domain[isExtDatFile:], ":")
|
||||
if len(kv) != 2 {
|
||||
return nil, newError("invalid external resource: ", domain)
|
||||
}
|
||||
filename := kv[0]
|
||||
country := kv[1]
|
||||
domains, err := loadGeositeWithAttr(filename, country)
|
||||
list := kv[1]
|
||||
domains, err := loadGeositeWithAttr(filename, list)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load external sites: ", country, " from ", filename).Base(err)
|
||||
return nil, newError("failed to load external geosite: ", list, " from ", filename).Base(err)
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
domainRule := new(router.Domain)
|
||||
switch {
|
||||
case strings.HasPrefix(domain, "regexp:"):
|
||||
regexpVal := domain[7:]
|
||||
if len(regexpVal) == 0 {
|
||||
return nil, newError("empty regexp type of rule: ", domain)
|
||||
}
|
||||
domainRule.Type = router.Domain_Regex
|
||||
domainRule.Value = domain[7:]
|
||||
domainRule.Value = regexpVal
|
||||
|
||||
case strings.HasPrefix(domain, "domain:"):
|
||||
domainName := domain[7:]
|
||||
if len(domainName) == 0 {
|
||||
return nil, newError("empty domain type of rule: ", domain)
|
||||
}
|
||||
domainRule.Type = router.Domain_Domain
|
||||
domainRule.Value = domain[7:]
|
||||
domainRule.Value = domainName
|
||||
|
||||
case strings.HasPrefix(domain, "full:"):
|
||||
fullVal := domain[5:]
|
||||
if len(fullVal) == 0 {
|
||||
return nil, newError("empty full domain type of rule: ", domain)
|
||||
}
|
||||
domainRule.Type = router.Domain_Full
|
||||
domainRule.Value = domain[5:]
|
||||
domainRule.Value = fullVal
|
||||
|
||||
case strings.HasPrefix(domain, "keyword:"):
|
||||
keywordVal := domain[8:]
|
||||
if len(keywordVal) == 0 {
|
||||
return nil, newError("empty keyword type of rule: ", domain)
|
||||
}
|
||||
domainRule.Type = router.Domain_Plain
|
||||
domainRule.Value = domain[8:]
|
||||
domainRule.Value = keywordVal
|
||||
|
||||
case strings.HasPrefix(domain, "dotless:"):
|
||||
domainRule.Type = router.Domain_Regex
|
||||
@ -333,17 +372,22 @@ func toCidrList(ips StringList) ([]*router.GeoIP, error) {
|
||||
for _, ip := range ips {
|
||||
if strings.HasPrefix(ip, "geoip:") {
|
||||
country := ip[6:]
|
||||
geoip, err := loadGeoIP(strings.ToUpper(country))
|
||||
if len(country) == 0 {
|
||||
return nil, newError("empty country name in rule")
|
||||
}
|
||||
geoip, err := loadGeoIP(country)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load GeoIP: ", country).Base(err)
|
||||
return nil, newError("failed to load geoip: ", country).Base(err)
|
||||
}
|
||||
|
||||
geoipList = append(geoipList, &router.GeoIP{
|
||||
CountryCode: strings.ToUpper(country),
|
||||
Cidr: geoip,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var isExtDatFile = 0
|
||||
{
|
||||
const prefix = "ext:"
|
||||
@ -355,6 +399,7 @@ func toCidrList(ips StringList) ([]*router.GeoIP, error) {
|
||||
isExtDatFile = len(prefixQualified)
|
||||
}
|
||||
}
|
||||
|
||||
if isExtDatFile != 0 {
|
||||
kv := strings.Split(ip[isExtDatFile:], ":")
|
||||
if len(kv) != 2 {
|
||||
@ -363,9 +408,12 @@ func toCidrList(ips StringList) ([]*router.GeoIP, error) {
|
||||
|
||||
filename := kv[0]
|
||||
country := kv[1]
|
||||
geoip, err := loadIP(filename, strings.ToUpper(country))
|
||||
if len(filename) == 0 || len(country) == 0 {
|
||||
return nil, newError("empty filename or empty country in rule")
|
||||
}
|
||||
geoip, err := loadIP(filename, country)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load IPs: ", country, " from ", filename).Base(err)
|
||||
return nil, newError("failed to load geoip: ", country, " from ", filename).Base(err)
|
||||
}
|
||||
|
||||
geoipList = append(geoipList, &router.GeoIP{
|
||||
@ -506,62 +554,13 @@ func ParseRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
||||
if err != nil {
|
||||
return nil, newError("invalid router rule").Base(err)
|
||||
}
|
||||
if rawRule.Type == "field" {
|
||||
if strings.EqualFold(rawRule.Type, "field") {
|
||||
fieldrule, err := parseFieldRule(msg)
|
||||
if err != nil {
|
||||
return nil, newError("invalid field rule").Base(err)
|
||||
}
|
||||
return fieldrule, nil
|
||||
}
|
||||
if rawRule.Type == "chinaip" {
|
||||
chinaiprule, err := parseChinaIPRule(msg)
|
||||
if err != nil {
|
||||
return nil, newError("invalid chinaip rule").Base(err)
|
||||
}
|
||||
return chinaiprule, nil
|
||||
}
|
||||
if rawRule.Type == "chinasites" {
|
||||
chinasitesrule, err := parseChinaSitesRule(msg)
|
||||
if err != nil {
|
||||
return nil, newError("invalid chinasites rule").Base(err)
|
||||
}
|
||||
return chinasitesrule, nil
|
||||
}
|
||||
|
||||
return nil, newError("unknown router rule type: ", rawRule.Type)
|
||||
}
|
||||
|
||||
func parseChinaIPRule(data []byte) (*router.RoutingRule, error) {
|
||||
rawRule := new(RouterRule)
|
||||
err := json.Unmarshal(data, rawRule)
|
||||
if err != nil {
|
||||
return nil, newError("invalid router rule").Base(err)
|
||||
}
|
||||
chinaIPs, err := loadGeoIP("CN")
|
||||
if err != nil {
|
||||
return nil, newError("failed to load geoip:cn").Base(err)
|
||||
}
|
||||
return &router.RoutingRule{
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: rawRule.OutboundTag,
|
||||
},
|
||||
Cidr: chinaIPs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseChinaSitesRule(data []byte) (*router.RoutingRule, error) {
|
||||
rawRule := new(RouterRule)
|
||||
err := json.Unmarshal(data, rawRule)
|
||||
if err != nil {
|
||||
return nil, newError("invalid router rule").Base(err).AtError()
|
||||
}
|
||||
domains, err := loadGeositeWithAttr("geosite.dat", "CN")
|
||||
if err != nil {
|
||||
return nil, newError("failed to load geosite:cn.").Base(err)
|
||||
}
|
||||
return &router.RoutingRule{
|
||||
TargetTag: &router.RoutingRule_Tag{
|
||||
Tag: rawRule.OutboundTag,
|
||||
},
|
||||
Domain: domains,
|
||||
}, nil
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
|
||||
switch fb.Dest[0] {
|
||||
case '@', '/':
|
||||
fb.Type = "unix"
|
||||
if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && runtime.GOOS == "linux" {
|
||||
if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") {
|
||||
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy
|
||||
copy(fullAddr, fb.Dest[1:])
|
||||
fb.Dest = string(fullAddr)
|
||||
|
@ -93,7 +93,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
|
||||
switch fb.Dest[0] {
|
||||
case '@', '/':
|
||||
fb.Type = "unix"
|
||||
if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && runtime.GOOS == "linux" {
|
||||
if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") {
|
||||
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy
|
||||
copy(fullAddr, fb.Dest[1:])
|
||||
fb.Dest = string(fullAddr)
|
||||
|
@ -39,8 +39,10 @@ var addrParser = protocol.NewAddressParser(
|
||||
)
|
||||
|
||||
type ServerSession struct {
|
||||
config *ServerConfig
|
||||
port net.Port
|
||||
config *ServerConfig
|
||||
address net.Address
|
||||
port net.Port
|
||||
clientAddress net.Address
|
||||
}
|
||||
|
||||
func (s *ServerSession) handshake4(cmd byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) {
|
||||
@ -185,15 +187,20 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri
|
||||
request.Address = addr
|
||||
request.Port = port
|
||||
|
||||
responseAddress := net.AnyIP
|
||||
responsePort := net.Port(1717)
|
||||
responseAddress := s.address
|
||||
responsePort := s.port
|
||||
//nolint:gocritic // Use if else chain for clarity
|
||||
if request.Command == protocol.RequestCommandUDP {
|
||||
addr := s.config.Address.AsAddress()
|
||||
if addr == nil {
|
||||
addr = net.LocalHostIP
|
||||
if s.config.Address != nil {
|
||||
// Use configured IP as remote address in the response to UdpAssociate
|
||||
responseAddress = s.config.Address.AsAddress()
|
||||
} else if s.clientAddress == net.LocalHostIP || s.clientAddress == net.LocalHostIPv6 {
|
||||
// For localhost clients use loopback IP
|
||||
responseAddress = s.clientAddress
|
||||
} else {
|
||||
// For non-localhost clients use inbound listening address
|
||||
responseAddress = s.address
|
||||
}
|
||||
responseAddress = addr
|
||||
responsePort = s.port
|
||||
}
|
||||
if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil {
|
||||
return nil, err
|
||||
|
@ -89,8 +89,10 @@ func (s *Server) processTCP(ctx context.Context, conn internet.Connection, dispa
|
||||
}
|
||||
|
||||
svrSession := &ServerSession{
|
||||
config: s.config,
|
||||
port: inbound.Gateway.Port,
|
||||
config: s.config,
|
||||
address: inbound.Gateway.Address,
|
||||
port: inbound.Gateway.Port,
|
||||
clientAddress: inbound.Source.Address,
|
||||
}
|
||||
|
||||
reader := &buf.BufferedReader{Reader: buf.NewReader(conn)}
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -136,8 +136,8 @@ func TestHTTPConnectionHeader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDomainSocket(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Not supported on windows")
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "android" {
|
||||
t.Skip("Not supported on windows and android")
|
||||
return
|
||||
}
|
||||
tcpServer := tcp.Server{
|
||||
|
@ -1,4 +1,5 @@
|
||||
// +build !windows
|
||||
// +build !android
|
||||
|
||||
package domainsocket_test
|
||||
|
||||
|
@ -54,7 +54,7 @@ func (dl *DefaultListener) Listen(ctx context.Context, addr net.Addr, sockopt *S
|
||||
lc.Control = nil
|
||||
network = addr.Network()
|
||||
address = addr.Name
|
||||
if runtime.GOOS == "linux" && address[0] == '@' {
|
||||
if (runtime.GOOS == "linux" || runtime.GOOS == "android") && address[0] == '@' {
|
||||
// linux abstract unix domain socket is lockfree
|
||||
if len(address) > 1 && address[1] == '@' {
|
||||
// but may need padding to work with haproxy
|
||||
|
Loading…
Reference in New Issue
Block a user