diff --git a/app/cmd/server.go b/app/cmd/server.go index e5b097b..a684679 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -19,6 +19,7 @@ import ( "github.com/apernet/hysteria/core/server" "github.com/apernet/hysteria/extras/auth" "github.com/apernet/hysteria/extras/obfs" + "github.com/apernet/hysteria/extras/outbounds" ) var serverCmd = &cobra.Command{ @@ -41,6 +42,7 @@ type serverConfig struct { DisableUDP bool `mapstructure:"disableUDP"` UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"` Auth serverConfigAuth `mapstructure:"auth"` + Resolver serverConfigResolver `mapstructure:"resolver"` Masquerade serverConfigMasquerade `mapstructure:"masquerade"` } @@ -89,6 +91,22 @@ type serverConfigAuth struct { Password string `mapstructure:"password"` } +type serverConfigResolverTCP struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` +} + +type serverConfigResolverUDP struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` +} + +type serverConfigResolver struct { + Type string `mapstructure:"type"` + TCP serverConfigResolverTCP `mapstructure:"tcp"` + UDP serverConfigResolverUDP `mapstructure:"udp"` +} + type serverConfigMasqueradeFile struct { Dir string `mapstructure:"dir"` } @@ -214,6 +232,36 @@ func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error { return nil } +func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { + // Resolver, ACL, actual outbound are all implemented through the Outbound interface. + // Depending on the config, we build a chain like this: + // Resolver(ACL(Outbounds...)) + + // Outbounds + ob := outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto) + + // Resolver + switch strings.ToLower(c.Resolver.Type) { + case "", "system": + // Do nothing. DirectOutbound will use system resolver by default. + case "tcp": + if c.Resolver.TCP.Addr == "" { + return configError{Field: "resolver.tcp.addr", Err: errors.New("empty resolver address")} + } + ob = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, ob) + case "udp": + if c.Resolver.UDP.Addr == "" { + return configError{Field: "resolver.udp.addr", Err: errors.New("empty resolver address")} + } + ob = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, ob) + default: + return configError{Field: "resolver.type", Err: errors.New("unsupported resolver type")} + } + + hyConfig.Outbound = &outbounds.PluggableOutboundAdapter{PluggableOutbound: ob} + return nil +} + func (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error { var err error if c.Bandwidth.Up != "" { @@ -308,6 +356,7 @@ func (c *serverConfig) Config() (*server.Config, error) { c.fillConn, c.fillTLSConfig, c.fillQUICConfig, + c.fillOutboundConfig, c.fillBandwidthConfig, c.fillDisableUDP, c.fillUDPIdleTimeout, @@ -320,6 +369,7 @@ func (c *serverConfig) Config() (*server.Config, error) { return nil, err } } + return hyConfig, nil } diff --git a/app/cmd/server_test.go b/app/cmd/server_test.go index 614bdfd..3c3c1c6 100644 --- a/app/cmd/server_test.go +++ b/app/cmd/server_test.go @@ -61,6 +61,17 @@ func TestServerConfig(t *testing.T) { Type: "password", Password: "goofy_ahh_password", }, + Resolver: serverConfigResolver{ + Type: "udp", + TCP: serverConfigResolverTCP{ + Addr: "123.123.123.123:5353", + Timeout: 4 * time.Second, + }, + UDP: serverConfigResolverUDP{ + Addr: "4.6.8.0:53", + Timeout: 2 * time.Second, + }, + }, Masquerade: serverConfigMasquerade{ Type: "proxy", File: serverConfigMasqueradeFile{ diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index 48744f5..c44f16c 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -41,6 +41,15 @@ auth: type: password password: goofy_ahh_password +resolver: + type: udp + tcp: + addr: 123.123.123.123:5353 + timeout: 4s + udp: + addr: 4.6.8.0:53 + timeout: 2s + masquerade: type: proxy file: diff --git a/app/server.example.yaml b/app/server.example.yaml index 90256dc..e06b51d 100644 --- a/app/server.example.yaml +++ b/app/server.example.yaml @@ -40,6 +40,12 @@ auth: type: password password: some_password +# resolver: +# type: udp +# udp: +# addr: 8.8.4.4 +# timeout: 2s + masquerade: type: proxy proxy: diff --git a/extras/outbounds/dns_standard.go b/extras/outbounds/dns_standard.go index 24748a1..caad77d 100644 --- a/extras/outbounds/dns_standard.go +++ b/extras/outbounds/dns_standard.go @@ -8,7 +8,8 @@ import ( ) const ( - standardResolverRetryTimes = 2 + standardResolverDefaultTimeout = 2 * time.Second + standardResolverRetryTimes = 2 ) // standardResolver is a PluggableOutbound DNS resolver that resolves hostnames @@ -22,9 +23,9 @@ type standardResolver struct { func NewStandardResolverUDP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound { return &standardResolver{ - Addr: addr, + Addr: addDefaultPort(addr), Client: &dns.Client{ - Timeout: timeout, + Timeout: timeoutOrDefault(timeout), }, Next: next, } @@ -32,15 +33,30 @@ func NewStandardResolverUDP(addr string, timeout time.Duration, next PluggableOu func NewStandardResolverTCP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound { return &standardResolver{ - Addr: addr, + Addr: addDefaultPort(addr), Client: &dns.Client{ Net: "tcp", - Timeout: timeout, + Timeout: timeoutOrDefault(timeout), }, Next: next, } } +// addDefaultPort adds the default DNS port (53) to the address if not present. +func addDefaultPort(addr string) string { + if _, _, err := net.SplitHostPort(addr); err != nil { + return net.JoinHostPort(addr, "53") + } + return addr +} + +func timeoutOrDefault(timeout time.Duration) time.Duration { + if timeout == 0 { + return standardResolverDefaultTimeout + } + return timeout +} + // skipCNAMEChain skips the CNAME chain and returns the last CNAME target. // Sometimes the DNS server returns a CNAME chain like this, in one packet: // domain1.com. CNAME domain2.com.