package upstream // Package upstream provides DNS resolution and availability testing for nginx upstream targets, // with special support for dynamic service discovery and nginx-style SRV record resolution. // // # SRV Record Resolution (nginx.org compliant) // // This package implements nginx's SRV record resolution rules as documented at nginx.org. // The service=name parameter enables resolving of DNS SRV records and sets the service name. // // Rules for SRV record construction: // // 1. If the service name does not contain a dot ("."), then the RFC-compliant name is constructed // and the TCP protocol is added to the service prefix. // Example: "backend.example.com service=http resolve" -> "_http._tcp.backend.example.com" // // 2. If the service name contains one or more dots, then the name is constructed by joining // the service prefix and the server name. // Example: "backend.example.com service=_http._tcp resolve" -> "_http._tcp.backend.example.com" // Example: "example.com service=server1.backend resolve" -> "server1.backend.example.com" // // # Dynamic DNS Integration // // The resolver supports various DNS interfaces for service discovery: // - DNS servers (e.g., Consul DNS on 127.0.0.1:8600, CoreDNS, etc.) // - Service registration and health checking // - SRV record-based load balancing with proper priority handling // // # Usage Examples // // // Create resolver with DNS server // resolver := NewDynamicResolver("127.0.0.1:8600") // // // Resolve nginx-style service URL // addresses, err := resolver.ResolveService("backend.example.com service=http resolve") // // This queries "_http._tcp.backend.example.com" SRV records // // // Resolve with dotted service name // addresses, err := resolver.ResolveService("example.com service=server1.backend resolve") // // This queries "server1.backend.example.com" SRV records import ( "context" "fmt" "net" "strings" "time" ) // DynamicResolver handles DNS resolution through dynamic DNS servers type DynamicResolver struct { resolver string // e.g., "127.0.0.1:8600" } // NewDynamicResolver creates a new dynamic resolver func NewDynamicResolver(resolver string) *DynamicResolver { return &DynamicResolver{ resolver: resolver, } } // ServiceInfo contains parsed service information from nginx config type ServiceInfo struct { Hostname string // e.g., "backend.example.com" or "service.consul" ServiceName string // e.g., "http", "_http._tcp", "server1.backend" } // ResolveService resolves a nginx service to actual IP addresses and ports func (dr *DynamicResolver) ResolveService(serviceURL string) ([]string, error) { // Parse service URL to extract hostname and service name serviceInfo, err := dr.parseServiceURL(serviceURL) if err != nil { return nil, fmt.Errorf("failed to parse service URL %s: %v", serviceURL, err) } // Create a custom resolver that uses the DNS server dialer := &net.Dialer{ Timeout: 5 * time.Second, } resolver := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { return dialer.DialContext(ctx, network, dr.resolver) }, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Construct SRV query domain according to nginx rules srvDomain := dr.constructSRVDomain(serviceInfo) // Query for service SRV records _, srvRecords, err := resolver.LookupSRV(ctx, "", "", srvDomain) if err != nil { // Fallback to A record lookup if SRV fails ips, err := resolver.LookupIPAddr(ctx, srvDomain) if err != nil { return nil, fmt.Errorf("failed to resolve service %s: %v", srvDomain, err) } // Return IP addresses with default port (80) var addresses []string for _, ip := range ips { addresses = append(addresses, fmt.Sprintf("%s:80", ip.IP.String())) } return addresses, nil } // Convert SRV records to address:port format var addresses []string for _, srv := range srvRecords { // Resolve the target hostname to IP ips, err := resolver.LookupIPAddr(ctx, srv.Target) if err != nil { continue // Skip this record if resolution fails } for _, ip := range ips { addresses = append(addresses, fmt.Sprintf("%s:%d", ip.IP.String(), srv.Port)) } } if len(addresses) == 0 { return nil, fmt.Errorf("no addresses found for service %s", srvDomain) } return addresses, nil } // parseServiceURL parses nginx service URL and extracts hostname and service name func (dr *DynamicResolver) parseServiceURL(serviceURL string) (*ServiceInfo, error) { serviceURL = strings.TrimSpace(serviceURL) if serviceURL == "" { return nil, fmt.Errorf("empty service URL") } // Parse nginx format: "hostname service=servicename resolve" parts := strings.Fields(serviceURL) if len(parts) < 3 { return nil, fmt.Errorf("invalid service URL format: %s", serviceURL) } hostname := parts[0] var serviceName string // Find service=name parameter for _, part := range parts[1:] { if strings.HasPrefix(part, "service=") { serviceName = strings.TrimPrefix(part, "service=") serviceName = strings.TrimSpace(serviceName) break } } if serviceName == "" { return nil, fmt.Errorf("service parameter not found in: %s", serviceURL) } return &ServiceInfo{ Hostname: hostname, ServiceName: serviceName, }, nil } // constructSRVDomain constructs SRV query domain according to nginx.org rules func (dr *DynamicResolver) constructSRVDomain(serviceInfo *ServiceInfo) string { // According to nginx.org documentation: // 1. If service name does not contain a dot ("."), then RFC-compliant name is constructed // and TCP protocol is added to the service prefix. // Example: service=http -> _http._tcp.hostname // 2. If service name contains one or more dots, then the name is constructed by joining // the service prefix and the server name. // Example: service=_http._tcp -> _http._tcp.hostname // Example: service=server1.backend -> server1.backend.hostname if !strings.Contains(serviceInfo.ServiceName, ".") { // Case 1: No dots - construct RFC-compliant name with TCP protocol return fmt.Sprintf("_%s._tcp.%s", serviceInfo.ServiceName, serviceInfo.Hostname) } else { // Case 2: Contains dots - join service prefix and hostname return fmt.Sprintf("%s.%s", serviceInfo.ServiceName, serviceInfo.Hostname) } } // extractServiceName extracts the service name from service URL // Deprecated: Use parseServiceURL instead for proper nginx-style parsing func (dr *DynamicResolver) extractServiceName(serviceURL string) string { serviceInfo, err := dr.parseServiceURL(serviceURL) if err != nil { // Fallback to old parsing logic for backward compatibility serviceURL = strings.TrimSpace(serviceURL) // Handle empty input if serviceURL == "" { return "" } // Parse "service.consul service=redacted-net resolve" format if strings.Contains(serviceURL, "service=") { parts := strings.Fields(serviceURL) for _, part := range parts { if strings.HasPrefix(part, "service=") { serviceName := strings.TrimPrefix(part, "service=") // Handle edge cases like "service=" or "service= " serviceName = strings.TrimSpace(serviceName) if serviceName == "" { return "" } return serviceName } } } // Fallback: try to extract from hostname format like "my-service.service.consul" if strings.Contains(serviceURL, ".service.consul") { parts := strings.Split(serviceURL, ".") if len(parts) > 0 { serviceName := strings.TrimSpace(parts[0]) if serviceName == "" { return "" } return serviceName } } return "" } return serviceInfo.ServiceName } // TestDynamicTargets performs availability test specifically for dynamic DNS targets func TestDynamicTargets(dynamicTargets []ProxyTarget) map[string]*Status { result := make(map[string]*Status) // Group dynamic targets by resolver dynamicTargetsByResolver := make(map[string][]ProxyTarget) for _, target := range dynamicTargets { if target.Resolver != "" { dynamicTargetsByResolver[target.Resolver] = append(dynamicTargetsByResolver[target.Resolver], target) } else { // No resolver specified, mark as offline key := formatSocketAddress(target.Host, target.Port) result[key] = &Status{ Online: false, Latency: 0, } } } // Test each resolver group for resolver, targets := range dynamicTargetsByResolver { dynamicResolver := NewDynamicResolver(resolver) for _, target := range targets { key := formatSocketAddress(target.Host, target.Port) // Try to resolve the service addresses, err := dynamicResolver.ResolveService(target.ServiceURL) if err != nil { // If resolution fails, mark as offline result[key] = &Status{ Online: false, Latency: 0, } continue } // Test the first resolved address as representative if len(addresses) > 0 { addressResults := AvailabilityTest(addresses[:1]) if status, exists := addressResults[addresses[0]]; exists { result[key] = status } else { result[key] = &Status{ Online: false, Latency: 0, } } } else { result[key] = &Status{ Online: false, Latency: 0, } } } } return result } // EnhancedAvailabilityTest performs availability test with dynamic DNS resolution support // Deprecated: Use TestDynamicTargets for dynamic targets and AvailabilityTest for regular targets func EnhancedAvailabilityTest(targets []ProxyTarget) map[string]*Status { result := make(map[string]*Status) // Group targets by type dynamicTargets := make([]ProxyTarget, 0) regularTargets := make([]string, 0) for _, target := range targets { if target.IsConsul && target.Resolver != "" { dynamicTargets = append(dynamicTargets, target) } else { // Regular target - use properly formatted socket address for traditional AvailabilityTest key := formatSocketAddress(target.Host, target.Port) regularTargets = append(regularTargets, key) } } // Use traditional AvailabilityTest for regular targets (more efficient) if len(regularTargets) > 0 { regularResults := AvailabilityTest(regularTargets) // Merge results for k, v := range regularResults { result[k] = v } } // Test dynamic targets with DNS resolution if len(dynamicTargets) > 0 { dynamicResults := TestDynamicTargets(dynamicTargets) for k, v := range dynamicResults { result[k] = v } } return result }