package proxy import ( "context" "fmt" "net" "net/http" "net/url" "strconv" "time" ) // Validation step names. const ( StepSyntax = "syntax" StepDNS = "dns" StepTCP = "tcp" StepHTTP = "http" ) // ValidationStep holds the result of a single validation check. type ValidationStep struct { Name string `json:"name"` Passed bool `json:"passed"` Message string `json:"message,omitempty"` Hint string `json:"hint,omitempty"` } // ValidationResult holds the aggregate result of the validation pipeline. type ValidationResult struct { Valid bool `json:"valid"` Steps []ValidationStep `json:"steps"` } // ValidateDestination runs the multi-step validation pipeline against the given // destination host and port. It checks syntax, DNS, TCP reachability, and HTTP health. // The pipeline short-circuits on failure: later steps are skipped if an earlier one fails. func ValidateDestination(ctx context.Context, host string, port int) ValidationResult { result := ValidationResult{Valid: true} // Step 1: Syntax validation. if step, ok := validateSyntax(host, port); !ok { result.Valid = false result.Steps = append(result.Steps, step) return result } else { result.Steps = append(result.Steps, step) } // Step 2: DNS resolution (skip for IP addresses). ip := net.ParseIP(host) if ip == nil { if step, ok := validateDNS(ctx, host); !ok { result.Valid = false result.Steps = append(result.Steps, step) return result } else { result.Steps = append(result.Steps, step) } } else { result.Steps = append(result.Steps, ValidationStep{ Name: StepDNS, Passed: true, Message: "Skipped (IP address provided)", }) } // Step 3: TCP port reachability. if step, ok := validateTCP(ctx, host, port); !ok { result.Valid = false result.Steps = append(result.Steps, step) return result } else { result.Steps = append(result.Steps, step) } // Step 4: HTTP health probe. step := validateHTTP(ctx, host, port) result.Steps = append(result.Steps, step) if !step.Passed { result.Valid = false } return result } // validateSyntax checks that the host and port values are syntactically valid. func validateSyntax(host string, port int) (ValidationStep, bool) { if host == "" { return ValidationStep{ Name: StepSyntax, Passed: false, Message: "Host is empty", Hint: "Provide a valid hostname or IP address.", }, false } if port < 1 || port > 65535 { return ValidationStep{ Name: StepSyntax, Passed: false, Message: fmt.Sprintf("Port %d is out of range (1-65535)", port), Hint: "Provide a valid port number between 1 and 65535.", }, false } // Reject obviously invalid hostnames (but allow IPs). if net.ParseIP(host) == nil { // Basic hostname validation: must not contain spaces or schemes. if _, err := url.Parse("http://" + host); err != nil { return ValidationStep{ Name: StepSyntax, Passed: false, Message: "Invalid hostname: " + err.Error(), Hint: "Provide a valid hostname without scheme (e.g., 'example.com' not 'http://example.com').", }, false } } return ValidationStep{ Name: StepSyntax, Passed: true, Message: fmt.Sprintf("Host %q port %d syntax OK", host, port), }, true } // validateDNS performs a DNS lookup on the given host. func validateDNS(ctx context.Context, host string) (ValidationStep, bool) { resolver := net.DefaultResolver addrs, err := resolver.LookupHost(ctx, host) if err != nil { return ValidationStep{ Name: StepDNS, Passed: false, Message: fmt.Sprintf("DNS resolution failed for %q: %s", host, err.Error()), Hint: diagnosticHint(StepDNS, err), }, false } return ValidationStep{ Name: StepDNS, Passed: true, Message: fmt.Sprintf("Resolved to %v", addrs), }, true } // validateTCP attempts a TCP connection to host:port with a 5-second timeout. func validateTCP(ctx context.Context, host string, port int) (ValidationStep, bool) { addr := net.JoinHostPort(host, strconv.Itoa(port)) dialCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() var d net.Dialer conn, err := d.DialContext(dialCtx, "tcp", addr) if err != nil { return ValidationStep{ Name: StepTCP, Passed: false, Message: fmt.Sprintf("TCP connect to %s failed: %s", addr, err.Error()), Hint: diagnosticHint(StepTCP, err), }, false } conn.Close() return ValidationStep{ Name: StepTCP, Passed: true, Message: fmt.Sprintf("TCP connect to %s succeeded", addr), }, true } // validateHTTP performs a GET request to the destination and checks for a response. // Non-5xx responses are considered passing (the service is responding). func validateHTTP(ctx context.Context, host string, port int) ValidationStep { target := fmt.Sprintf("http://%s:%d/", host, port) httpCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, target, nil) if err != nil { return ValidationStep{ Name: StepHTTP, Passed: false, Message: fmt.Sprintf("Failed to build HTTP request: %s", err.Error()), Hint: diagnosticHint(StepHTTP, err), } } client := &http.Client{ Timeout: 10 * time.Second, // Do not follow redirects — we just want to see if the port responds to HTTP. CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, } resp, err := client.Do(req) if err != nil { return ValidationStep{ Name: StepHTTP, Passed: false, Message: fmt.Sprintf("HTTP probe to %s failed: %s", target, err.Error()), Hint: diagnosticHint(StepHTTP, err), } } resp.Body.Close() if resp.StatusCode >= 500 { return ValidationStep{ Name: StepHTTP, Passed: false, Message: fmt.Sprintf("Service responded with HTTP %d. The service may not be healthy.", resp.StatusCode), Hint: fmt.Sprintf("Service responded with HTTP %d. The service may not be healthy.", resp.StatusCode), } } return ValidationStep{ Name: StepHTTP, Passed: true, Message: fmt.Sprintf("HTTP probe returned %d", resp.StatusCode), } }