Переглянути джерело

feat(dns): add optimistic caching (#5237)

Meow 3 тижнів тому
батько
коміт
cd51f57535

+ 28 - 12
app/dns/cache_controller.go

@@ -25,23 +25,29 @@ const (
 )
 
 type CacheController struct {
+	name            string
+	disableCache    bool
+	serveStale      bool
+	serveExpiredTTL int32
+
+	ips      map[string]*record
+	dirtyips map[string]*record
+
 	sync.RWMutex
-	ips           map[string]*record
-	dirtyips      map[string]*record
 	pub           *pubsub.Service
 	cacheCleanup  *task.Periodic
-	name          string
-	disableCache  bool
 	highWatermark int
 	requestGroup  singleflight.Group
 }
 
-func NewCacheController(name string, disableCache bool) *CacheController {
+func NewCacheController(name string, disableCache bool, serveStale bool, serveExpiredTTL uint32) *CacheController {
 	c := &CacheController{
-		name:         name,
-		disableCache: disableCache,
-		ips:          make(map[string]*record),
-		pub:          pubsub.NewService(),
+		name:            name,
+		disableCache:    disableCache,
+		serveStale:      serveStale,
+		serveExpiredTTL: -int32(serveExpiredTTL),
+		ips:             make(map[string]*record),
+		pub:             pubsub.NewService(),
 	}
 
 	c.cacheCleanup = &task.Periodic{
@@ -78,6 +84,10 @@ func (c *CacheController) collectExpiredKeys() ([]string, error) {
 	}
 
 	now := time.Now()
+	if c.serveStale && c.serveExpiredTTL != 0 {
+		now = now.Add(time.Duration(c.serveExpiredTTL) * time.Second)
+	}
+
 	expiredKeys := make([]string, 0, len(c.ips)/4) // pre-allocate
 
 	for domain, rec := range c.ips {
@@ -105,6 +115,10 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
 	}
 
 	now := time.Now()
+	if c.serveStale && c.serveExpiredTTL != 0 {
+		now = now.Add(time.Duration(c.serveExpiredTTL) * time.Second)
+	}
+
 	for _, domain := range expiredKeys {
 		rec := c.ips[domain]
 		if rec == nil {
@@ -280,15 +294,17 @@ func (c *CacheController) updateRecord(req *dnsRequest, rep *IPRecord) {
 	c.Unlock()
 
 	if pubRecord != nil {
-		_, _ /*ttl*/, err := pubRecord.getIPs()
-		if /*ttl >= 0 &&*/ !go_errors.Is(err, errRecordNotFound) {
+		_, ttl, err := pubRecord.getIPs()
+		if ttl > 0 && !go_errors.Is(err, errRecordNotFound) {
 			c.pub.Publish(req.domain+pubSuffix, pubRecord)
 		}
 	}
 
 	errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", rep.IP, ", rtt: ", rtt, ", lock: ", lockWait)
 
-	common.Must(c.cacheCleanup.Start())
+	if !c.serveStale || c.serveExpiredTTL != 0 {
+		common.Must(c.cacheCleanup.Start())
+	}
 }
 
 func (c *CacheController) findRecords(domain string) *record {

+ 112 - 68
app/dns/config.pb.go

@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.35.1
-// 	protoc        v5.28.2
+// 	protoc        v5.29.4
 // source: app/dns/config.proto
 
 package dns
@@ -142,6 +142,8 @@ type NameServer struct {
 	Tag               string                       `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"`
 	TimeoutMs         uint64                       `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"`
 	DisableCache      bool                         `protobuf:"varint,11,opt,name=disableCache,proto3" json:"disableCache,omitempty"`
+	ServeStale        bool                         `protobuf:"varint,15,opt,name=serveStale,proto3" json:"serveStale,omitempty"`
+	ServeExpiredTTL   *uint32                      `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"`
 	FinalQuery        bool                         `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"`
 	UnexpectedGeoip   []*router.GeoIP              `protobuf:"bytes,13,rep,name=unexpected_geoip,json=unexpectedGeoip,proto3" json:"unexpected_geoip,omitempty"`
 	ActUnprior        bool                         `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"`
@@ -254,6 +256,20 @@ func (x *NameServer) GetDisableCache() bool {
 	return false
 }
 
+func (x *NameServer) GetServeStale() bool {
+	if x != nil {
+		return x.ServeStale
+	}
+	return false
+}
+
+func (x *NameServer) GetServeExpiredTTL() uint32 {
+	if x != nil && x.ServeExpiredTTL != nil {
+		return *x.ServeExpiredTTL
+	}
+	return 0
+}
+
 func (x *NameServer) GetFinalQuery() bool {
 	if x != nil {
 		return x.FinalQuery
@@ -291,6 +307,8 @@ type Config struct {
 	Tag string `protobuf:"bytes,6,opt,name=tag,proto3" json:"tag,omitempty"`
 	// DisableCache disables DNS cache
 	DisableCache           bool          `protobuf:"varint,8,opt,name=disableCache,proto3" json:"disableCache,omitempty"`
+	ServeStale             bool          `protobuf:"varint,12,opt,name=serveStale,proto3" json:"serveStale,omitempty"`
+	ServeExpiredTTL        uint32        `protobuf:"varint,13,opt,name=serveExpiredTTL,proto3" json:"serveExpiredTTL,omitempty"`
 	QueryStrategy          QueryStrategy `protobuf:"varint,9,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"`
 	DisableFallback        bool          `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"`
 	DisableFallbackIfMatch bool          `protobuf:"varint,11,opt,name=disableFallbackIfMatch,proto3" json:"disableFallbackIfMatch,omitempty"`
@@ -361,6 +379,20 @@ func (x *Config) GetDisableCache() bool {
 	return false
 }
 
+func (x *Config) GetServeStale() bool {
+	if x != nil {
+		return x.ServeStale
+	}
+	return false
+}
+
+func (x *Config) GetServeExpiredTTL() uint32 {
+	if x != nil {
+		return x.ServeExpiredTTL
+	}
+	return 0
+}
+
 func (x *Config) GetQueryStrategy() QueryStrategy {
 	if x != nil {
 		return x.QueryStrategy
@@ -567,7 +599,7 @@ var file_app_dns_config_proto_rawDesc = []byte{
 	0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x1c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74,
 	0x2f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
 	0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63,
-	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb6, 0x06, 0x0a, 0x0a,
+	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x99, 0x07, 0x0a, 0x0a,
 	0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64,
 	0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72,
 	0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e,
@@ -601,72 +633,83 @@ var file_app_dns_config_proto_rawDesc = []byte{
 	0x75, 0x74, 0x4d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65,
 	0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
 	0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73,
-	0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6e,
-	0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66,
-	0x69, 0x6e, 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x10, 0x75, 0x6e, 0x65,
-	0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0d, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72,
-	0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x0f, 0x75, 0x6e, 0x65,
-	0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x47, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x1e, 0x0a, 0x0a,
-	0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x1a, 0x5e, 0x0a, 0x0e,
-	0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34,
-	0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78,
-	0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61,
-	0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04,
-	0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c,
-	0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04,
-	0x72, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65,
-	0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04,
-	0x73, 0x69, 0x7a, 0x65, 0x22, 0x9c, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
-	0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05,
-	0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
-	0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0a,
-	0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c,
-	0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63,
-	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69,
-	0x63, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e,
-	0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52,
-	0x0b, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03,
-	0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x22,
-	0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x08,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63,
-	0x68, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61,
-	0x74, 0x65, 0x67, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61,
-	0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53,
-	0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74,
-	0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
-	0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52,
-	0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b,
-	0x12, 0x36, 0x0a, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62,
-	0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63,
-	0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73,
-	0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
-	0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63,
-	0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16,
-	0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
-	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03,
-	0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65,
-	0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
-	0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08,
-	0x07, 0x10, 0x08, 0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74,
-	0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c,
-	0x6c, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
-	0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12,
-	0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0d, 0x51, 0x75,
-	0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55,
-	0x53, 0x45, 0x5f, 0x49, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49,
-	0x50, 0x34, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10,
-	0x02, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x10, 0x03, 0x42, 0x46,
-	0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64,
-	0x6e, 0x73, 0x50, 0x01, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f,
-	0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41,
-	0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72,
+	0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73,
+	0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x12, 0x2d, 0x0a, 0x0f, 0x73, 0x65, 0x72,
+	0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x10, 0x20, 0x01,
+	0x28, 0x0d, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72,
+	0x65, 0x64, 0x54, 0x54, 0x4c, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6e, 0x61,
+	0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69,
+	0x6e, 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x10, 0x75, 0x6e, 0x65, 0x78,
+	0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0d, 0x20, 0x03,
+	0x28, 0x0b, 0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f,
+	0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x0f, 0x75, 0x6e, 0x65, 0x78,
+	0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x47, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x1e, 0x0a, 0x0a, 0x61,
+	0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x1a, 0x5e, 0x0a, 0x0e, 0x50,
+	0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a,
+	0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72,
+	0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69,
+	0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
+	0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c, 0x4f,
+	0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72,
+	0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12,
+	0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73,
+	0x69, 0x7a, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70,
+	0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x22, 0xe6, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66,
+	0x69, 0x67, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65,
+	0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
+	0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65,
+	0x72, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a,
+	0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74,
+	0x61, 0x74, 0x69, 0x63, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e,
+	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69,
+	0x6e, 0x67, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12,
+	0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61,
+	0x67, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68,
+	0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
+	0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74,
+	0x61, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65,
+	0x53, 0x74, 0x61, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78,
+	0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f,
+	0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x12,
+	0x42, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
+	0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
+	0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61,
+	0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74,
+	0x65, 0x67, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61,
+	0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69,
+	0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x36, 0x0a,
+	0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b,
+	0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x64,
+	0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66,
+	0x4d, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61,
+	0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64,
+	0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e,
+	0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64,
+	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d,
+	0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52,
+	0x02, 0x69, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64,
+	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f,
+	0x78, 0x69, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08,
+	0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69,
+	0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00,
+	0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12,
+	0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05,
+	0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79,
+	0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f,
+	0x49, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10,
+	0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x02, 0x12, 0x0b,
+	0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63,
+	0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50,
+	0x01, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74,
+	0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70,
+	0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e,
+	0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -718,6 +761,7 @@ func file_app_dns_config_proto_init() {
 	if File_app_dns_config_proto != nil {
 		return
 	}
+	file_app_dns_config_proto_msgTypes[0].OneofWrappers = []any{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{

+ 4 - 0
app/dns/config.proto

@@ -32,6 +32,8 @@ message NameServer {
   string tag = 9;
   uint64 timeoutMs = 10;
   bool disableCache = 11;
+  bool serveStale = 15;
+  optional uint32 serveExpiredTTL = 16;
   bool finalQuery = 12;
   repeated xray.app.router.GeoIP unexpected_geoip = 13;
   bool actUnprior = 14;
@@ -80,6 +82,8 @@ message Config {
 
   // DisableCache disables DNS cache
   bool disableCache = 8;
+  bool serveStale = 12;
+  uint32 serveExpiredTTL = 13;
 
   QueryStrategy query_strategy = 9;
 

+ 6 - 1
app/dns/dns.go

@@ -121,6 +121,11 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
 		}
 
 		disableCache := config.DisableCache || ns.DisableCache
+		serveStale := config.ServeStale || ns.ServeStale
+		serveExpiredTTL := config.ServeExpiredTTL
+		if ns.ServeExpiredTTL != nil {
+			serveExpiredTTL = *ns.ServeExpiredTTL
+		}
 
 		var tag = defaultTag
 		if len(ns.Tag) > 0 {
@@ -131,7 +136,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
 			return nil, errors.New("no QueryStrategy available for ", ns.Address)
 		}
 
-		client, err := NewClient(ctx, ns, myClientIP, disableCache, tag, clientIPOption, &matcherInfos, updateDomain)
+		client, err := NewClient(ctx, ns, myClientIP, disableCache, serveStale, serveExpiredTTL, tag, clientIPOption, &matcherInfos, updateDomain)
 		if err != nil {
 			return nil, errors.New("failed to create client").Base(err)
 		}

+ 5 - 8
app/dns/dnscommon.go

@@ -3,6 +3,7 @@ package dns
 import (
 	"context"
 	"encoding/binary"
+	"math"
 	"strings"
 	"time"
 
@@ -13,6 +14,7 @@ import (
 	"github.com/xtls/xray-core/common/session"
 	"github.com/xtls/xray-core/core"
 	dns_feature "github.com/xtls/xray-core/features/dns"
+
 	"golang.org/x/net/dns/dnsmessage"
 )
 
@@ -39,19 +41,14 @@ type IPRecord struct {
 	RawHeader *dnsmessage.Header
 }
 
-func (r *IPRecord) getIPs() ([]net.IP, uint32, error) {
+func (r *IPRecord) getIPs() ([]net.IP, int32, error) {
 	if r == nil {
 		return nil, 0, errRecordNotFound
 	}
+
 	untilExpire := time.Until(r.Expire).Seconds()
-	if untilExpire <= 0 {
-		return nil, 0, errRecordNotFound
-	}
+	ttl := int32(math.Ceil(untilExpire))
 
-	ttl := uint32(untilExpire) + 1
-	if ttl == 1 {
-		r.Expire = time.Now().Add(time.Second) // To ensure that two consecutive requests get the same result
-	}
 	if r.RCode != dnsmessage.RCodeSuccess {
 		return nil, ttl, dns_feature.RCodeError(r.RCode)
 	}

+ 11 - 11
app/dns/nameserver.go

@@ -41,7 +41,7 @@ type Client struct {
 }
 
 // NewServer creates a name server object according to the network destination url.
-func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, disableCache bool, clientIP net.IP) (Server, error) {
+func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (Server, error) {
 	if address := dest.Address; address.Family().IsDomain() {
 		u, err := url.Parse(address.Domain())
 		if err != nil {
@@ -51,19 +51,19 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
 		case strings.EqualFold(u.String(), "localhost"):
 			return NewLocalNameServer(), nil
 		case strings.EqualFold(u.Scheme, "https"): // DNS-over-HTTPS Remote mode
-			return NewDoHNameServer(u, dispatcher, false, disableCache, clientIP), nil
+			return NewDoHNameServer(u, dispatcher, false, disableCache, serveStale, serveExpiredTTL, clientIP), nil
 		case strings.EqualFold(u.Scheme, "h2c"): // DNS-over-HTTPS h2c Remote mode
-			return NewDoHNameServer(u, dispatcher, true, disableCache, clientIP), nil
+			return NewDoHNameServer(u, dispatcher, true, disableCache, serveStale, serveExpiredTTL, clientIP), nil
 		case strings.EqualFold(u.Scheme, "https+local"): // DNS-over-HTTPS Local mode
-			return NewDoHNameServer(u, nil, false, disableCache, clientIP), nil
+			return NewDoHNameServer(u, nil, false, disableCache, serveStale, serveExpiredTTL, clientIP), nil
 		case strings.EqualFold(u.Scheme, "h2c+local"): // DNS-over-HTTPS h2c Local mode
-			return NewDoHNameServer(u, nil, true, disableCache, clientIP), nil
+			return NewDoHNameServer(u, nil, true, disableCache, serveStale, serveExpiredTTL, clientIP), nil
 		case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode
-			return NewQUICNameServer(u, disableCache, clientIP)
+			return NewQUICNameServer(u, disableCache, serveStale, serveExpiredTTL, clientIP)
 		case strings.EqualFold(u.Scheme, "tcp"): // DNS-over-TCP Remote mode
-			return NewTCPNameServer(u, dispatcher, disableCache, clientIP)
+			return NewTCPNameServer(u, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
 		case strings.EqualFold(u.Scheme, "tcp+local"): // DNS-over-TCP Local mode
-			return NewTCPLocalNameServer(u, disableCache, clientIP)
+			return NewTCPLocalNameServer(u, disableCache, serveStale, serveExpiredTTL, clientIP)
 		case strings.EqualFold(u.String(), "fakedns"):
 			var fd dns.FakeDNSEngine
 			err = core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) {
@@ -79,7 +79,7 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
 		dest.Network = net.Network_UDP
 	}
 	if dest.Network == net.Network_UDP { // UDP classic DNS mode
-		return NewClassicNameServer(dest, dispatcher, disableCache, clientIP), nil
+		return NewClassicNameServer(dest, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP), nil
 	}
 	return nil, errors.New("No available name server could be created from ", dest).AtWarning()
 }
@@ -89,7 +89,7 @@ func NewClient(
 	ctx context.Context,
 	ns *NameServer,
 	clientIP net.IP,
-	disableCache bool,
+	disableCache bool, serveStale bool, serveExpiredTTL uint32,
 	tag string,
 	ipOption dns.IPOption,
 	matcherInfos *[]*DomainMatcherInfo,
@@ -99,7 +99,7 @@ func NewClient(
 
 	err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
 		// Create a new server for each client for now
-		server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, clientIP)
+		server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
 		if err != nil {
 			return errors.New("failed to create nameserver").Base(err).AtWarning()
 		}

+ 31 - 7
app/dns/nameserver_cached.go

@@ -27,9 +27,17 @@ func queryIP(ctx context.Context, s CachedNameserver, domain string, option dns.
 		if rec := cache.findRecords(fqdn); rec != nil {
 			ips, ttl, err := merge(option, rec.A, rec.AAAA)
 			if !go_errors.Is(err, errRecordNotFound) {
-				// errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips)
-				log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
-				return ips, ttl, err
+				if ttl > 0 {
+					// errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips)
+					log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
+					return ips, uint32(ttl), err
+				}
+				if cache.serveStale && (cache.serveExpiredTTL == 0 || cache.serveExpiredTTL < ttl) {
+					// errors.LogDebugInner(ctx, err, cache.name, " cache OPTIMISTE ", fqdn, " -> ", ips)
+					log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheOptimiste, Elapsed: 0, Error: err})
+					go pull(ctx, s, fqdn, option)
+					return ips, 1, err
+				}
 			}
 		}
 	} else {
@@ -39,8 +47,15 @@ func queryIP(ctx context.Context, s CachedNameserver, domain string, option dns.
 	return fetch(ctx, s, fqdn, option)
 }
 
+func pull(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) {
+	nctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 8*time.Second)
+	defer cancel()
+
+	fetch(nctx, s, fqdn, option)
+}
+
 func fetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) ([]net.IP, uint32, error) {
-	key := fqdn + "f"
+	key := fqdn
 	switch {
 	case option.IPv4Enable && option.IPv6Enable:
 		key = key + "46"
@@ -99,13 +114,22 @@ func doFetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IP
 	}
 
 	ips, ttl, err := merge(option, rec4, rec6, errs...)
+	var rTTL uint32
+	if ttl > 0 {
+		rTTL = uint32(ttl)
+	} else if ttl == 0 && go_errors.Is(err, errRecordNotFound) {
+		rTTL = 0
+	} else { // edge case: where a fast rep's ttl expires during the rtt of a slower, parallel query
+		rTTL = 1
+	}
+
 	log.Record(&log.DNSLog{Server: s.getCacheController().name, Domain: fqdn, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
-	return result{ips, ttl, err}
+	return result{ips, rTTL, err}
 }
 
-func merge(option dns.IPOption, rec4 *IPRecord, rec6 *IPRecord, errs ...error) ([]net.IP, uint32, error) {
+func merge(option dns.IPOption, rec4 *IPRecord, rec6 *IPRecord, errs ...error) ([]net.IP, int32, error) {
 	var allIPs []net.IP
-	var rTTL uint32 = dns.DefaultTTL
+	var rTTL int32 = dns.DefaultTTL
 
 	mergeReq := option.IPv4Enable && option.IPv6Enable
 

+ 14 - 6
app/dns/nameserver_doh.go

@@ -37,7 +37,7 @@ type DoHNameServer struct {
 }
 
 // NewDoHNameServer creates DOH/DOHL client object for remote/local resolving.
-func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, disableCache bool, clientIP net.IP) *DoHNameServer {
+func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) *DoHNameServer {
 	url.Scheme = "https"
 	mode := "DOH"
 	if dispatcher == nil {
@@ -45,7 +45,7 @@ func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, dis
 	}
 	errors.LogInfo(context.Background(), "DNS: created ", mode, " client for ", url.String(), ", with h2c ", h2c)
 	s := &DoHNameServer{
-		cacheController: NewCacheController(mode+"//"+url.Host, disableCache),
+		cacheController: NewCacheController(mode+"//"+url.Host, disableCache, serveStale, serveExpiredTTL),
 		dohURL:          url.String(),
 		clientIP:        clientIP,
 	}
@@ -131,7 +131,9 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
 
 	if s.Name()+"." == "DOH//"+fqdn {
 		errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead.")
-		noResponseErrCh <- errors.New("tries to resolve itself!", s.Name())
+		if noResponseErrCh != nil {
+			noResponseErrCh <- errors.New("tries to resolve itself!", s.Name())
+		}
 		return
 	}
 
@@ -172,19 +174,25 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
 			b, err := dns.PackMessage(r.msg)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to pack dns query for ", fqdn)
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes())
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to retrieve response for ", fqdn)
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			rec, err := parseResponse(resp)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to handle DOH response for ", fqdn)
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			s.cacheController.updateRecord(r, rec)

+ 4 - 4
app/dns/nameserver_doh_test.go

@@ -17,7 +17,7 @@ func TestDOHNameServer(t *testing.T) {
 	url, err := url.Parse("https+local://1.1.1.1/dns-query")
 	common.Must(err)
 
-	s := NewDoHNameServer(url, nil, false, false, net.IP(nil))
+	s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil))
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
 		IPv4Enable: true,
@@ -34,7 +34,7 @@ func TestDOHNameServerWithCache(t *testing.T) {
 	url, err := url.Parse("https+local://1.1.1.1/dns-query")
 	common.Must(err)
 
-	s := NewDoHNameServer(url, nil, false, false, net.IP(nil))
+	s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil))
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
 		IPv4Enable: true,
@@ -62,7 +62,7 @@ func TestDOHNameServerWithIPv4Override(t *testing.T) {
 	url, err := url.Parse("https+local://1.1.1.1/dns-query")
 	common.Must(err)
 
-	s := NewDoHNameServer(url, nil, false, false, net.IP(nil))
+	s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil))
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
 		IPv4Enable: true,
@@ -85,7 +85,7 @@ func TestDOHNameServerWithIPv6Override(t *testing.T) {
 	url, err := url.Parse("https+local://1.1.1.1/dns-query")
 	common.Must(err)
 
-	s := NewDoHNameServer(url, nil, false, false, net.IP(nil))
+	s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil))
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
 		IPv4Enable: false,

+ 29 - 11
app/dns/nameserver_quic.go

@@ -36,7 +36,7 @@ type QUICNameServer struct {
 }
 
 // NewQUICNameServer creates DNS-over-QUIC client object for local resolving
-func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICNameServer, error) {
+func NewQUICNameServer(url *url.URL, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*QUICNameServer, error) {
 	errors.LogInfo(context.Background(), "DNS: created Local DNS-over-QUIC client for ", url.String())
 
 	var err error
@@ -50,7 +50,7 @@ func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICN
 	dest := net.UDPDestination(net.ParseAddress(url.Hostname()), port)
 
 	s := &QUICNameServer{
-		cacheController: NewCacheController(url.String(), disableCache),
+		cacheController: NewCacheController(url.String(), disableCache, serveStale, serveExpiredTTL),
 		destination:     &dest,
 		clientIP:        clientIP,
 	}
@@ -106,7 +106,9 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e
 			b, err := dns.PackMessage(r.msg)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to pack dns query")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 
@@ -114,13 +116,17 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e
 			err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len()))
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "binary write failed")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			_, err = dnsReqBuf.Write(b.Bytes())
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "buffer write failed")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			b.Release()
@@ -128,14 +134,18 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e
 			conn, err := s.openStream(dnsCtx)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to open quic connection")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 
 			_, err = conn.Write(dnsReqBuf.Bytes())
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to send query")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 
@@ -146,28 +156,36 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e
 			n, err := respBuf.ReadFullFrom(conn, 2)
 			if err != nil && n == 0 {
 				errors.LogErrorInner(ctx, err, "failed to read response length")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			var length uint16
 			err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to parse response length")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			respBuf.Clear()
 			n, err = respBuf.ReadFullFrom(conn, int32(length))
 			if err != nil && n == 0 {
 				errors.LogErrorInner(ctx, err, "failed to read response length")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 
 			rec, err := parseResponse(respBuf.Bytes())
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to handle response")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			s.cacheController.updateRecord(r, rec)

+ 3 - 3
app/dns/nameserver_quic_test.go

@@ -16,7 +16,7 @@ import (
 func TestQUICNameServer(t *testing.T) {
 	url, err := url.Parse("quic://dns.adguard-dns.com")
 	common.Must(err)
-	s, err := NewQUICNameServer(url, false, net.IP(nil))
+	s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil))
 	common.Must(err)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{
@@ -43,7 +43,7 @@ func TestQUICNameServer(t *testing.T) {
 func TestQUICNameServerWithIPv4Override(t *testing.T) {
 	url, err := url.Parse("quic://dns.adguard-dns.com")
 	common.Must(err)
-	s, err := NewQUICNameServer(url, false, net.IP(nil))
+	s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil))
 	common.Must(err)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{
@@ -66,7 +66,7 @@ func TestQUICNameServerWithIPv4Override(t *testing.T) {
 func TestQUICNameServerWithIPv6Override(t *testing.T) {
 	url, err := url.Parse("quic://dns.adguard-dns.com")
 	common.Must(err)
-	s, err := NewQUICNameServer(url, false, net.IP(nil))
+	s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil))
 	common.Must(err)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{

+ 33 - 15
app/dns/nameserver_tcp.go

@@ -32,10 +32,10 @@ type TCPNameServer struct {
 func NewTCPNameServer(
 	url *url.URL,
 	dispatcher routing.Dispatcher,
-	disableCache bool,
+	disableCache bool, serveStale bool, serveExpiredTTL uint32,
 	clientIP net.IP,
 ) (*TCPNameServer, error) {
-	s, err := baseTCPNameServer(url, "TCP", disableCache, clientIP)
+	s, err := baseTCPNameServer(url, "TCP", disableCache, serveStale, serveExpiredTTL, clientIP)
 	if err != nil {
 		return nil, err
 	}
@@ -56,8 +56,8 @@ func NewTCPNameServer(
 }
 
 // NewTCPLocalNameServer creates DNS over TCP client object for local resolving
-func NewTCPLocalNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*TCPNameServer, error) {
-	s, err := baseTCPNameServer(url, "TCPL", disableCache, clientIP)
+func NewTCPLocalNameServer(url *url.URL, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*TCPNameServer, error) {
+	s, err := baseTCPNameServer(url, "TCPL", disableCache, serveStale, serveExpiredTTL, clientIP)
 	if err != nil {
 		return nil, err
 	}
@@ -69,7 +69,7 @@ func NewTCPLocalNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*T
 	return s, nil
 }
 
-func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, clientIP net.IP) (*TCPNameServer, error) {
+func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*TCPNameServer, error) {
 	port := net.Port(53)
 	if url.Port() != "" {
 		var err error
@@ -80,7 +80,7 @@ func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, clientIP
 	dest := net.TCPDestination(net.ParseAddress(url.Hostname()), port)
 
 	s := &TCPNameServer{
-		cacheController: NewCacheController(prefix+"//"+dest.NetAddr(), disableCache),
+		cacheController: NewCacheController(prefix+"//"+dest.NetAddr(), disableCache, serveStale, serveExpiredTTL),
 		destination:     &dest,
 		clientIP:        clientIP,
 	}
@@ -135,14 +135,18 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
 			b, err := dns.PackMessage(r.msg)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to pack dns query")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 
 			conn, err := s.dial(dnsCtx)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to dial namesever")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			defer conn.Close()
@@ -150,13 +154,17 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
 			err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len()))
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "binary write failed")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			_, err = dnsReqBuf.Write(b.Bytes())
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "buffer write failed")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			b.Release()
@@ -164,7 +172,9 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
 			_, err = conn.Write(dnsReqBuf.Bytes())
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to send query")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			dnsReqBuf.Release()
@@ -174,28 +184,36 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
 			n, err := respBuf.ReadFullFrom(conn, 2)
 			if err != nil && n == 0 {
 				errors.LogErrorInner(ctx, err, "failed to read response length")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			var length uint16
 			err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length)
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to parse response length")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 			respBuf.Clear()
 			n, err = respBuf.ReadFullFrom(conn, int32(length))
 			if err != nil && n == 0 {
 				errors.LogErrorInner(ctx, err, "failed to read response length")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 
 			rec, err := parseResponse(respBuf.Bytes())
 			if err != nil {
 				errors.LogErrorInner(ctx, err, "failed to parse DNS over TCP response")
-				noResponseErrCh <- err
+				if noResponseErrCh != nil {
+					noResponseErrCh <- err
+				}
 				return
 			}
 

+ 4 - 4
app/dns/nameserver_tcp_test.go

@@ -16,7 +16,7 @@ import (
 func TestTCPLocalNameServer(t *testing.T) {
 	url, err := url.Parse("tcp+local://8.8.8.8")
 	common.Must(err)
-	s, err := NewTCPLocalNameServer(url, false, net.IP(nil))
+	s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil))
 	common.Must(err)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
@@ -33,7 +33,7 @@ func TestTCPLocalNameServer(t *testing.T) {
 func TestTCPLocalNameServerWithCache(t *testing.T) {
 	url, err := url.Parse("tcp+local://8.8.8.8")
 	common.Must(err)
-	s, err := NewTCPLocalNameServer(url, false, net.IP(nil))
+	s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil))
 	common.Must(err)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
@@ -61,7 +61,7 @@ func TestTCPLocalNameServerWithCache(t *testing.T) {
 func TestTCPLocalNameServerWithIPv4Override(t *testing.T) {
 	url, err := url.Parse("tcp+local://8.8.8.8")
 	common.Must(err)
-	s, err := NewTCPLocalNameServer(url, false, net.IP(nil))
+	s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil))
 	common.Must(err)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{
@@ -85,7 +85,7 @@ func TestTCPLocalNameServerWithIPv4Override(t *testing.T) {
 func TestTCPLocalNameServerWithIPv6Override(t *testing.T) {
 	url, err := url.Parse("tcp+local://8.8.8.8")
 	common.Must(err)
-	s, err := NewTCPLocalNameServer(url, false, net.IP(nil))
+	s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil))
 	common.Must(err)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 	ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{

+ 2 - 2
app/dns/nameserver_udp.go

@@ -37,14 +37,14 @@ type udpDnsRequest struct {
 }
 
 // NewClassicNameServer creates udp server object for remote resolving.
-func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, disableCache bool, clientIP net.IP) *ClassicNameServer {
+func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) *ClassicNameServer {
 	// default to 53 if unspecific
 	if address.Port == 0 {
 		address.Port = net.Port(53)
 	}
 
 	s := &ClassicNameServer{
-		cacheController: NewCacheController(strings.ToUpper(address.String()), disableCache),
+		cacheController: NewCacheController(strings.ToUpper(address.String()), disableCache, serveStale, serveExpiredTTL),
 		address:         &address,
 		requests:        make(map[uint16]*udpDnsRequest),
 		clientIP:        clientIP,

+ 3 - 2
common/log/dns.go

@@ -43,8 +43,9 @@ func (l *DNSLog) String() string {
 type dnsStatus string
 
 var (
-	DNSQueried  = dnsStatus("got answer:")
-	DNSCacheHit = dnsStatus("cache HIT:")
+	DNSQueried        = dnsStatus("got answer:")
+	DNSCacheHit       = dnsStatus("cache HIT:")
+	DNSCacheOptimiste = dnsStatus("cache OPTIMISTE:")
 )
 
 func joinNetIP(ips []net.IP) string {

+ 38 - 26
infra/conf/dns.go

@@ -16,19 +16,21 @@ import (
 )
 
 type NameServerConfig struct {
-	Address       *Address   `json:"address"`
-	ClientIP      *Address   `json:"clientIp"`
-	Port          uint16     `json:"port"`
-	SkipFallback  bool       `json:"skipFallback"`
-	Domains       []string   `json:"domains"`
-	ExpectedIPs   StringList `json:"expectedIPs"`
-	ExpectIPs     StringList `json:"expectIPs"`
-	QueryStrategy string     `json:"queryStrategy"`
-	Tag           string     `json:"tag"`
-	TimeoutMs     uint64     `json:"timeoutMs"`
-	DisableCache  bool       `json:"disableCache"`
-	FinalQuery    bool       `json:"finalQuery"`
-	UnexpectedIPs StringList `json:"unexpectedIPs"`
+	Address         *Address   `json:"address"`
+	ClientIP        *Address   `json:"clientIp"`
+	Port            uint16     `json:"port"`
+	SkipFallback    bool       `json:"skipFallback"`
+	Domains         []string   `json:"domains"`
+	ExpectedIPs     StringList `json:"expectedIPs"`
+	ExpectIPs       StringList `json:"expectIPs"`
+	QueryStrategy   string     `json:"queryStrategy"`
+	Tag             string     `json:"tag"`
+	TimeoutMs       uint64     `json:"timeoutMs"`
+	DisableCache    bool       `json:"disableCache"`
+	ServeStale      bool       `json:"serveStale"`
+	ServeExpiredTTL *uint32    `json:"serveExpiredTTL"`
+	FinalQuery      bool       `json:"finalQuery"`
+	UnexpectedIPs   StringList `json:"unexpectedIPs"`
 }
 
 // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
@@ -40,19 +42,21 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 	}
 
 	var advanced struct {
-		Address       *Address   `json:"address"`
-		ClientIP      *Address   `json:"clientIp"`
-		Port          uint16     `json:"port"`
-		SkipFallback  bool       `json:"skipFallback"`
-		Domains       []string   `json:"domains"`
-		ExpectedIPs   StringList `json:"expectedIPs"`
-		ExpectIPs     StringList `json:"expectIPs"`
-		QueryStrategy string     `json:"queryStrategy"`
-		Tag           string     `json:"tag"`
-		TimeoutMs     uint64     `json:"timeoutMs"`
-		DisableCache  bool       `json:"disableCache"`
-		FinalQuery    bool       `json:"finalQuery"`
-		UnexpectedIPs StringList `json:"unexpectedIPs"`
+		Address         *Address   `json:"address"`
+		ClientIP        *Address   `json:"clientIp"`
+		Port            uint16     `json:"port"`
+		SkipFallback    bool       `json:"skipFallback"`
+		Domains         []string   `json:"domains"`
+		ExpectedIPs     StringList `json:"expectedIPs"`
+		ExpectIPs       StringList `json:"expectIPs"`
+		QueryStrategy   string     `json:"queryStrategy"`
+		Tag             string     `json:"tag"`
+		TimeoutMs       uint64     `json:"timeoutMs"`
+		DisableCache    bool       `json:"disableCache"`
+		ServeStale      bool       `json:"serveStale"`
+		ServeExpiredTTL *uint32    `json:"serveExpiredTTL"`
+		FinalQuery      bool       `json:"finalQuery"`
+		UnexpectedIPs   StringList `json:"unexpectedIPs"`
 	}
 	if err := json.Unmarshal(data, &advanced); err == nil {
 		c.Address = advanced.Address
@@ -66,6 +70,8 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 		c.Tag = advanced.Tag
 		c.TimeoutMs = advanced.TimeoutMs
 		c.DisableCache = advanced.DisableCache
+		c.ServeStale = advanced.ServeStale
+		c.ServeExpiredTTL = advanced.ServeExpiredTTL
 		c.FinalQuery = advanced.FinalQuery
 		c.UnexpectedIPs = advanced.UnexpectedIPs
 		return nil
@@ -173,6 +179,8 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
 		Tag:               c.Tag,
 		TimeoutMs:         c.TimeoutMs,
 		DisableCache:      c.DisableCache,
+		ServeStale:        c.ServeStale,
+		ServeExpiredTTL:   c.ServeExpiredTTL,
 		FinalQuery:        c.FinalQuery,
 		UnexpectedGeoip:   unexpectedGeoipList,
 		ActUnprior:        actUnprior,
@@ -194,6 +202,8 @@ type DNSConfig struct {
 	Tag                    string              `json:"tag"`
 	QueryStrategy          string              `json:"queryStrategy"`
 	DisableCache           bool                `json:"disableCache"`
+	ServeStale             bool                `json:"serveStale"`
+	ServeExpiredTTL        uint32              `json:"serveExpiredTTL"`
 	DisableFallback        bool                `json:"disableFallback"`
 	DisableFallbackIfMatch bool                `json:"disableFallbackIfMatch"`
 	UseSystemHosts         bool                `json:"useSystemHosts"`
@@ -391,6 +401,8 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
 	config := &dns.Config{
 		Tag:                    c.Tag,
 		DisableCache:           c.DisableCache,
+		ServeStale:             c.ServeStale,
+		ServeExpiredTTL:        c.ServeExpiredTTL,
 		DisableFallback:        c.DisableFallback,
 		DisableFallbackIfMatch: c.DisableFallbackIfMatch,
 		QueryStrategy:          resolveQueryStrategy(c.QueryStrategy),

+ 10 - 2
infra/conf/dns_test.go

@@ -20,7 +20,7 @@ func TestDNSConfigParsing(t *testing.T) {
 			return config.Build()
 		}
 	}
-
+	expectedServeExpiredTTL := uint32(172800)
 	runMultiTestCase(t, []TestCase{
 		{
 			Input: `{
@@ -28,7 +28,9 @@ func TestDNSConfigParsing(t *testing.T) {
 					"address": "8.8.8.8",
 					"port": 5353,
 					"skipFallback": true,
-					"domains": ["domain:example.com"]
+					"domains": ["domain:example.com"],
+					"serveStale": true,
+					"serveExpiredTTL": 172800
 				}],
 				"hosts": {
 					"domain:example.com": "google.com",
@@ -40,6 +42,8 @@ func TestDNSConfigParsing(t *testing.T) {
 				"clientIp": "10.0.0.1",
 				"queryStrategy": "UseIPv4",
 				"disableCache": true,
+				"serveStale": false,
+				"serveExpiredTTL": 86400,
 				"disableFallback": true
 			}`,
 			Parser: parserCreator(),
@@ -68,6 +72,8 @@ func TestDNSConfigParsing(t *testing.T) {
 								Size: 1,
 							},
 						},
+						ServeStale:      true,
+						ServeExpiredTTL: &expectedServeExpiredTTL,
 					},
 				},
 				StaticHosts: []*dns.Config_HostMapping{
@@ -100,6 +106,8 @@ func TestDNSConfigParsing(t *testing.T) {
 				ClientIp:        []byte{10, 0, 0, 1},
 				QueryStrategy:   dns.QueryStrategy_USE_IP4,
 				DisableCache:    true,
+				ServeStale:      false,
+				ServeExpiredTTL: 86400,
 				DisableFallback: true,
 			},
 		},