diff --git a/app/cmd/server.go b/app/cmd/server.go index f8b0be6..386c4f2 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -368,6 +368,7 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { var uOb outbounds.PluggableOutbound // "unified" outbound // ACL + hasACL := false if c.ACL.File != "" && len(c.ACL.Inline) > 0 { return configError{Field: "acl", Err: errors.New("cannot set both acl.file and acl.inline")} } @@ -377,12 +378,14 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { DownloadErrFunc: geoipDownloadErrFunc, } if c.ACL.File != "" { + hasACL = true acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader.Load) if err != nil { return configError{Field: "acl.file", Err: err} } uOb = acl } else if len(c.ACL.Inline) > 0 { + hasACL = true acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader.Load) if err != nil { return configError{Field: "acl.inline", Err: err} @@ -396,7 +399,12 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { // Resolver switch strings.ToLower(c.Resolver.Type) { case "", "system": - // Do nothing. DirectOutbound will use system resolver by default. + if hasACL { + // If the user uses ACL, we must put a resolver in front of it, + // for IP rules to work on domain requests. + uOb = outbounds.NewSystemResolver(uOb) + } + // Otherwise we can just rely on outbound handling on its own. case "tcp": if c.Resolver.TCP.Addr == "" { return configError{Field: "resolver.tcp.addr", Err: errors.New("empty resolver address")} diff --git a/extras/outbounds/dns_system.go b/extras/outbounds/dns_system.go new file mode 100644 index 0000000..8f9a429 --- /dev/null +++ b/extras/outbounds/dns_system.go @@ -0,0 +1,41 @@ +package outbounds + +import ( + "net" +) + +// systemResolver is a PluggableOutbound DNS resolver that resolves hostnames +// using the default system DNS server. +// Outbounds typically don't require a resolver, as they can do DNS resolution +// themselves. However, when using ACL, it's necessary to place a resolver in +// front of it in the pipeline (for IP rules to work on domain requests). +type systemResolver struct { + Next PluggableOutbound +} + +func NewSystemResolver(next PluggableOutbound) PluggableOutbound { + return &systemResolver{ + Next: next, + } +} + +func (r *systemResolver) resolve(reqAddr *AddrEx) { + ips, err := net.LookupIP(reqAddr.Host) + if err != nil { + reqAddr.ResolveInfo = &ResolveInfo{Err: err} + return + } + info := &ResolveInfo{} + info.IPv4, info.IPv6 = splitIPv4IPv6(ips) + reqAddr.ResolveInfo = info +} + +func (r *systemResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { + r.resolve(reqAddr) + return r.Next.TCP(reqAddr) +} + +func (r *systemResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { + r.resolve(reqAddr) + return r.Next.UDP(reqAddr) +}