diff --git a/bin/checksums.txt b/bin/checksums.txt index ec18fe9..031f323 100644 --- a/bin/checksums.txt +++ b/bin/checksums.txt @@ -1,14 +1,14 @@ # DarkFlare Binary Checksums -# Generated: Tue Dec 3 19:19:42 UTC 2024 +# Generated: Wed Dec 4 15:13:34 UTC 2024 -d74d77f413a7607cb742678bb2cc35ea009ef9e9bedeb8f6461a4f5222c52a33 checksums.txt -444278a858153540bc7b8ba97435d6d73e93e5fb93cc767373606463b1410c16 darkflare-client-darwin-amd64 -3d2eb54b97b4aa09949d94985e1dcc6fff363b22be8362d26b45e2f258403f98 darkflare-client-darwin-arm64 -46de95f637486958231679b1ea53f5bb724c9b731d323c9cf0528c5cd2dc41d9 darkflare-client-linux-amd64 -866618bb5ca641a8fa78823fa2dc4d235086c47d8ae7a2210613fd9b94bd4681 darkflare-client-linux-arm64 -4dc79e82b47ce82ca894431756c0f9b5620fcc0d228bdbefbd106b7c354c17cd darkflare-client-windows-amd64.exe -0df41165f843f3e53c9de5d9c5ec94660696d20cc6b6da3f28fe9ddaee696e4f darkflare-server-darwin-amd64 -05d6156bd38ac90784ff4b862cbc83d9c77b047237f0d731b10faf78b728878c darkflare-server-darwin-arm64 -88a2c60f99d0372d7e71593f724fbedef1a5cdd4c53eb215e87f969f6d6788ae darkflare-server-linux-amd64 -404ed6254b31f39ee42a6906d9b1a6d2af8050e2f81df14316ac95e9db3f9c70 darkflare-server-linux-arm64 -9178c5f3f06182ef150237b8aa97802679b2def2edde271c909c81271aca7a6b darkflare-server-windows-amd64.exe +dcb32ff048378506046d364cd2c226ba0f8d137a49a924670a9f7c434a8d3049 checksums.txt +192bf6b64d0849325882c2f8e626fffc7bd3414c47fccdc14b582bae2f161c36 darkflare-client-darwin-amd64 +4362838083f35051ff6dae482b754b875c8413e9c3ac73363257cd752c8e3751 darkflare-client-darwin-arm64 +a500646c956354f087c74f2553d3ace97d3c45fcc1b7523fe6a8020b73bc6546 darkflare-client-linux-amd64 +ac31b5ad270c58232e97d9075e5e2edd92f3d1f3fac794d6ffa22cc2ea98cd74 darkflare-client-linux-arm64 +c963e061ff460230a76ceed0caa34575e8eea716a1845be37d2aa25f913c6547 darkflare-client-windows-amd64.exe +066cec69f0535357477f375e930a4177ec113b8ca9fe4a9d93a9e8ea5871a989 darkflare-server-darwin-amd64 +e2e6289a82224326f83f09ffffc0d81c1687a59faae53ec76bee202efa21c432 darkflare-server-darwin-arm64 +6b2dfc3eff05d72acdd1987c07083aaf95fad39fd21058efd114d8d41a16fde3 darkflare-server-linux-amd64 +000e4aeff6dadf39c2ce764b75570589562bf43ace691278da115a9e37e7c52b darkflare-server-linux-arm64 +efdca3b075f561dfeac9f0713d8f47a5e95017b2541c083f8bedf2250dd50fd0 darkflare-server-windows-amd64.exe diff --git a/bin/darkflare-client-darwin-amd64 b/bin/darkflare-client-darwin-amd64 index 930e0ce..7fd41bd 100755 Binary files a/bin/darkflare-client-darwin-amd64 and b/bin/darkflare-client-darwin-amd64 differ diff --git a/bin/darkflare-client-darwin-arm64 b/bin/darkflare-client-darwin-arm64 index 0348396..df22831 100755 Binary files a/bin/darkflare-client-darwin-arm64 and b/bin/darkflare-client-darwin-arm64 differ diff --git a/bin/darkflare-client-linux-amd64 b/bin/darkflare-client-linux-amd64 index 353b9aa..916b445 100755 Binary files a/bin/darkflare-client-linux-amd64 and b/bin/darkflare-client-linux-amd64 differ diff --git a/bin/darkflare-client-linux-arm64 b/bin/darkflare-client-linux-arm64 index 6ff2e41..1299a8c 100755 Binary files a/bin/darkflare-client-linux-arm64 and b/bin/darkflare-client-linux-arm64 differ diff --git a/bin/darkflare-client-windows-amd64.exe b/bin/darkflare-client-windows-amd64.exe index ee715ea..8289cf9 100755 Binary files a/bin/darkflare-client-windows-amd64.exe and b/bin/darkflare-client-windows-amd64.exe differ diff --git a/bin/darkflare-server-darwin-amd64 b/bin/darkflare-server-darwin-amd64 index 83dc3ef..5a0cc07 100755 Binary files a/bin/darkflare-server-darwin-amd64 and b/bin/darkflare-server-darwin-amd64 differ diff --git a/bin/darkflare-server-darwin-arm64 b/bin/darkflare-server-darwin-arm64 index 1e6d609..d59b71e 100755 Binary files a/bin/darkflare-server-darwin-arm64 and b/bin/darkflare-server-darwin-arm64 differ diff --git a/bin/darkflare-server-linux-amd64 b/bin/darkflare-server-linux-amd64 index 3cc5dc7..194711b 100755 Binary files a/bin/darkflare-server-linux-amd64 and b/bin/darkflare-server-linux-amd64 differ diff --git a/bin/darkflare-server-linux-arm64 b/bin/darkflare-server-linux-arm64 index e7b9e3c..51b9c6d 100755 Binary files a/bin/darkflare-server-linux-arm64 and b/bin/darkflare-server-linux-arm64 differ diff --git a/bin/darkflare-server-windows-amd64.exe b/bin/darkflare-server-windows-amd64.exe index b8b128b..8507d10 100755 Binary files a/bin/darkflare-server-windows-amd64.exe and b/bin/darkflare-server-windows-amd64.exe differ diff --git a/client/main.go b/client/main.go index b9a6cab..1441de2 100644 --- a/client/main.go +++ b/client/main.go @@ -21,6 +21,8 @@ import ( "sync" "time" + "crypto/x509" + "golang.org/x/time/rate" ) @@ -91,33 +93,46 @@ func NewClient(cloudflareHost string, destPort int, scheme string, destAddr stri batchSize: 64 * 1024, // 64KB batch size } + // Load system root CAs + rootCAs, err := x509.SystemCertPool() + if err != nil { + log.Printf("Warning: failed to load system cert pool: %v", err) + rootCAs = x509.NewCertPool() + if rootCAs == nil { + log.Fatal("Failed to create cert pool") + } + } + transport := &http.Transport{ TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, MinVersion: tls.VersionTLS12, - MaxVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{ tls.X25519, tls.CurveP256, + tls.CurveP384, }, CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, }, - PreferServerCipherSuites: true, + PreferServerCipherSuites: false, SessionTicketsDisabled: false, InsecureSkipVerify: false, - Renegotiation: tls.RenegotiateNever, + NextProtos: []string{"http/1.1"}, // Force HTTP/1.1 }, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - DisableCompression: true, - ForceAttemptHTTP2: !client.isDirectMode(), - MaxIdleConnsPerHost: 100, - MaxConnsPerHost: 100, - WriteBufferSize: 64 * 1024, - ReadBufferSize: 64 * 1024, - ResponseHeaderTimeout: 30 * time.Second, - ExpectContinueTimeout: 1 * time.Second, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + DisableCompression: true, + ForceAttemptHTTP2: false, // Disable HTTP/2 + MaxIdleConnsPerHost: 100, + MaxConnsPerHost: 100, + WriteBufferSize: 64 * 1024, + ReadBufferSize: 64 * 1024, } client.httpClient = &http.Client{ diff --git a/server/main.go b/server/main.go index 1bc6c1b..d8c9705 100644 --- a/server/main.go +++ b/server/main.go @@ -17,7 +17,6 @@ import ( "strconv" "strings" "sync" - "sync/atomic" "time" ) @@ -26,21 +25,16 @@ type Session struct { lastActive time.Time buffer []byte mu sync.Mutex - bytesUp int64 - bytesDown int64 - startTime time.Time - sourceIP string } type Server struct { - sessions sync.Map - sessionMutex sync.Mutex - destHost string - destPort string - debug bool - appCommand string - isAppMode bool - allowDirect bool + sessions sync.Map + destHost string + destPort string + debug bool + appCommand string + isAppMode bool + allowDirect bool } func NewServer(destHost, destPort string, appCommand string, debug bool, allowDirect bool) *Server { @@ -53,11 +47,8 @@ func NewServer(destHost, destPort string, appCommand string, debug bool, allowDi allowDirect: allowDirect, } - if s.debug { - log.Printf("Server configuration:") - log.Printf(" Allow Direct: %v", allowDirect) - log.Printf(" Debug Mode: %v", debug) - log.Printf(" App Mode: %v", s.isAppMode) + if s.isAppMode && s.debug { + log.Printf("Starting in application mode with command: %s", appCommand) } go s.cleanupSessions() @@ -155,43 +146,78 @@ func (s *Server) handleApplication(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { - // Get client IP from various possible sources - clientIP := r.Header.Get("Cf-Connecting-Ip") + if s.isAppMode { + s.handleApplication(w, r) + return + } + + // Add basic connection logging + clientIP := r.Header.Get("X-Forwarded-For") if clientIP == "" { - // Try X-Real-IP - clientIP = r.Header.Get("X-Real-IP") - if clientIP == "" { - // Try X-Forwarded-For - clientIP = r.Header.Get("X-Forwarded-For") - if clientIP == "" { - // Finally, use RemoteAddr - clientIP, _, _ = net.SplitHostPort(r.RemoteAddr) - } + clientIP = r.Header.Get("Cf-Connecting-Ip") + } + if clientIP == "" { + clientIP = r.RemoteAddr + } + + // Get session ID early + sessionID := r.Header.Get("X-For") + if sessionID == "" { + sessionID = r.Header.Get("Cf-Ray") + if sessionID == "" { + sessionID = r.Header.Get("Cf-Connecting-Ip") } } + // Get and decode destination early + encodedDest := r.Header.Get("X-Requested-With") + if encodedDest == "" { + log.Printf("Redirect: %s → https://github.com/doxx/darkflare", clientIP) + http.Redirect(w, r, "https://github.com/doxx/darkflare", http.StatusFound) + return + } + + // Check for connection termination + if r.Header.Get("X-Connection-Close") == "true" { + sessionDisplay := "no-session" + if sessionID != "" { + sessionDisplay = sessionID[:8] + } + log.Printf("Disconnect: %s [%s]", clientIP, sessionDisplay) + if sessionInterface, exists := s.sessions.LoadAndDelete(sessionID); exists { + session := sessionInterface.(*Session) + session.conn.Close() + } + return + } + + destBytes, err := base64.StdEncoding.DecodeString(encodedDest) + if err != nil { + http.Error(w, "Invalid destination encoding", http.StatusBadRequest) + return + } + destination := string(destBytes) + + // Always log basic connection info + sessionDisplay := "no-session" + if sessionID != "" { + sessionDisplay = sessionID[:8] // First 8 chars of session ID + } + log.Printf("Connection: %s [%s] → %s", clientIP, sessionDisplay, destination) + + // Debug logging only when enabled if s.debug { - log.Printf("Request: %s %s from %s", - r.Method, - r.URL.Path, - clientIP, - ) log.Printf("Headers: %+v", r.Header) + // ... rest of debug logging ... } // Verify Cloudflare connection - if clientIP == "" && !s.allowDirect { + cfConnecting := r.Header.Get("Cf-Connecting-Ip") + if cfConnecting == "" && !s.allowDirect { http.Error(w, "Direct access not allowed", http.StatusForbidden) return } - // Check if the request is using TLS - if r.TLS == nil { - log.Printf("[%s] Non-TLS connection attempt from %s", time.Now().Format(time.RFC3339), clientIP) - http.Error(w, "TLS required", http.StatusUpgradeRequired) - return - } - // Set Apache-like headers w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)") w.Header().Set("X-Powered-By", "PHP/7.4.33") @@ -205,29 +231,55 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { w.Header().Set("Expires", "0") w.Header().Set("Content-Type", "application/octet-stream") - // Get the encoded destination from headers - encodedDest := r.Header.Get("X-Requested-With") - if encodedDest == "" { + // Validate the destination format and DNS resolution + host, port, err := net.SplitHostPort(destination) + if err != nil { if s.debug { - log.Printf("[DEBUG] Missing X-Requested-With header, redirecting to project page") + log.Printf("[DEBUG] Invalid destination format %s: %v", destination, err) } - http.Redirect(w, r, "https://github.com/doxx/darkflare", http.StatusTemporaryRedirect) + http.Error(w, fmt.Sprintf("Invalid destination format: %v", err), http.StatusBadRequest) return } - // Decode the destination - destBytes, err := base64.StdEncoding.DecodeString(encodedDest) - if err != nil { + // Additional host validation + if host == "" { if s.debug { - log.Printf("[DEBUG] Failed to decode X-Requested-With: %v", err) + log.Printf("[DEBUG] Empty host in destination: %s", destination) } - http.Error(w, "Invalid destination encoding", http.StatusBadRequest) + http.Error(w, "Empty host not allowed", http.StatusBadRequest) return } - destination := string(destBytes) - if s.debug { - log.Printf("[DEBUG] Decoded destination: %s", destination) + // Validate port + portNum, err := strconv.Atoi(port) + if err != nil || portNum < 1 || portNum > 65535 { + if s.debug { + log.Printf("[DEBUG] Invalid port %s in destination: %v", port, err) + } + http.Error(w, fmt.Sprintf("Invalid port number: %s", port), http.StatusBadRequest) + return + } + + // DNS resolution check + if ip := net.ParseIP(host); ip == nil { + ips, err := net.LookupHost(host) + if err != nil { + if s.debug { + log.Printf("[DEBUG] DNS resolution failed for %s: %v", host, err) + } + http.Error(w, fmt.Sprintf("DNS resolution failed: %v", err), http.StatusBadRequest) + return + } + if len(ips) == 0 { + if s.debug { + log.Printf("[DEBUG] No IP addresses found for host: %s", host) + } + http.Error(w, "No IP addresses found for host", http.StatusBadRequest) + return + } + if s.debug { + log.Printf("[DEBUG] Resolved %s to %v", host, ips) + } } // Validate the destination @@ -240,21 +292,12 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { } // Use the decoded destination for the connection - host, port, err := net.SplitHostPort(destination) - if err != nil { - if s.debug { - log.Printf("[DEBUG] Failed to split host:port: %v", err) - } - http.Error(w, "Invalid destination format", http.StatusBadRequest) - return - } - if s.debug { log.Printf("[DEBUG] Connecting to %s:%s", host, port) } // Try to get session ID from various possible headers - sessionID := r.Header.Get("X-For") + sessionID = r.Header.Get("X-For") if sessionID == "" { // Try Cloudflare-specific headers sessionID = r.Header.Get("Cf-Ray") @@ -272,9 +315,6 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { return } - userAgent := r.Header.Get("User-Agent") - xForwardedFor := r.Header.Get("X-Forwarded-For") - var session *Session sessionInterface, exists := s.sessions.Load(sessionID) if !exists { @@ -283,68 +323,21 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + session = &Session{ conn: conn, lastActive: time.Now(), buffer: make([]byte, 0), - startTime: time.Now(), - sourceIP: clientIP, } s.sessions.Store(sessionID, session) - log.Printf("[%s] New session: ID=%s, Source=%s, Dest=%s, XFF=%s, UA=%s", - time.Now().Format(time.RFC3339), - sessionID[:8], - clientIP, - destination, - xForwardedFor, - userAgent, - ) - // Start statistics goroutine for this session - go s.trackSessionStats(sessionID, session) } else { session = sessionInterface.(*Session) - if session.conn == nil { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", host, port)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - session.conn = conn - } } session.mu.Lock() defer session.mu.Unlock() session.lastActive = time.Now() - if r.Header.Get("X-Connection-Close") == "true" { - session.conn.Close() - session.conn = nil - s.sessions.Delete(sessionID) - - // Calculate final statistics - duration := time.Since(session.startTime).Seconds() - upBytes := atomic.LoadInt64(&session.bytesUp) - downBytes := atomic.LoadInt64(&session.bytesDown) - upKbps := float64(upBytes*8) / (1024 * duration) - downKbps := float64(downBytes*8) / (1024 * duration) - - log.Printf("[%s] Session closed: ID=%s, Source=%s, Dest=%s, XFF=%s, UA=%s, Duration=%.1fs, Up=%d bytes (%.2f kbps), Down=%d bytes (%.2f kbps)", - time.Now().Format(time.RFC3339), - sessionID[:8], - session.sourceIP, - destination, - xForwardedFor, - userAgent, - duration, - upBytes, - upKbps, - downBytes, - downKbps, - ) - return - } - if r.Method == http.MethodPost { data, err := io.ReadAll(r.Body) if err != nil { @@ -369,17 +362,16 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - atomic.AddInt64(&session.bytesUp, int64(len(data))) } return } // For GET requests, read any available data - buffer := make([]byte, 128*1024) // 128KB buffer - readData := make([]byte, 0, 256*1024) // 256KB initial capacity + buffer := make([]byte, 32*1024) // 32KB buffer + readData := make([]byte, 0, 64*1024) // 64KB initial capacity for { - session.conn.SetReadDeadline(time.Now().Add(250 * time.Millisecond)) // Increased from 10ms to 250ms + session.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) // Increased from 10ms to 100ms n, err := session.conn.Read(buffer) if err != nil { if err != io.EOF && !err.(net.Error).Timeout() { @@ -394,7 +386,7 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { if n > 0 { readData = append(readData, buffer[:n]...) } - if n < len(buffer) || len(readData) >= 256*1024 { // Added size limit check + if n < len(buffer) || len(readData) >= 64*1024 { // Added size limit check break } } @@ -411,7 +403,6 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { ) } w.Write([]byte(encoded)) - atomic.AddInt64(&session.bytesDown, int64(len(readData))) } else if s.debug { log.Printf("Response: No data to send for session %s path %s", sessionID[:8], @@ -420,38 +411,6 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) trackSessionStats(sessionID string, session *Session) { - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - // Check if session still exists - if _, exists := s.sessions.Load(sessionID); !exists { - return - } - - upBytes := atomic.LoadInt64(&session.bytesUp) - downBytes := atomic.LoadInt64(&session.bytesDown) - duration := time.Since(session.startTime).Seconds() - - // Calculate rates - upKbps := float64(upBytes*8) / (1024 * duration) - downKbps := float64(downBytes*8) / (1024 * duration) - - log.Printf("Stats: ID=%s, Source=%s, Up=%d bytes (%.2f kbps), Down=%d bytes (%.2f kbps)", - sessionID, - session.sourceIP, - upBytes, - upKbps, - downBytes, - downKbps, - ) - } - } -} - func main() { var origin string var certFile string @@ -541,9 +500,6 @@ func main() { log.Fatalf("Failed to load certificate and key: %v", err) } - // Create a TLS session cache - tlsSessionCache := tls.NewLRUClientSessionCache(1000) // Cache up to 1000 sessions - server := &http.Server{ Addr: fmt.Sprintf("%s:%s", originHost, originPort), Handler: http.HandlerFunc(server.handleRequest), @@ -551,21 +507,10 @@ func main() { Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, - // Disable HTTP/2 - NextProtos: []string{"http/1.1"}, - // Enable session tickets for session resumption - SessionTicketsDisabled: false, - // Use client session cache - ClientSessionCache: tlsSessionCache, - // Prefer server cipher suites - PreferServerCipherSuites: true, - // Let server choose cipher suites - ClientAuth: func() tls.ClientAuthType { - if server.allowDirect { - return tls.NoClientCert - } - return tls.RequestClientCert - }(), + // Allow any cipher suites + CipherSuites: nil, + // Don't verify client certs + ClientAuth: tls.NoClientCert, // Handle SNI GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { if debug { @@ -582,6 +527,7 @@ func main() { log.Printf(" Supported Ciphers: %v", hello.CipherSuites) log.Printf(" Supported Curves: %v", hello.SupportedCurves) log.Printf(" Supported Points: %v", hello.SupportedPoints) + log.Printf(" ALPN Protocols: %v", hello.SupportedProtos) } return nil, nil }, @@ -596,6 +542,8 @@ func main() { } return nil }, + // Enable HTTP/2 support + NextProtos: []string{"h2", "http/1.1"}, }, ErrorLog: log.New(os.Stderr, "[HTTPS] ", log.LstdFlags), ConnState: func(conn net.Conn, state http.ConnState) { @@ -604,10 +552,6 @@ func main() { state, conn.RemoteAddr().String()) } }, - // Add timeouts to prevent hanging connections - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 120 * time.Second, } log.Printf("Starting HTTPS server on %s:%s", originHost, originPort) @@ -631,54 +575,65 @@ func main() { } func isLocalIP(ip string) bool { - if ip == "0.0.0.0" || ip == "127.0.0.1" || ip == "::1" { - return true - } - ipAddr := net.ParseIP(ip) if ipAddr == nil { return false } - // Check if IP is assigned to any local interface interfaces, err := net.Interfaces() if err != nil { + log.Printf("Error getting network interfaces: %v", err) return false } for _, iface := range interfaces { addrs, err := iface.Addrs() if err != nil { + log.Printf("Error getting addresses for interface %s: %v", iface.Name, err) continue } + for _, addr := range addrs { + var localIP net.IP switch v := addr.(type) { case *net.IPNet: - if v.IP.String() == ip { - return true - } + localIP = v.IP case *net.IPAddr: - if v.IP.String() == ip { - return true - } + localIP = v.IP + } + + if localIP.Equal(ipAddr) { + return true } } } - // Also allow loopback and private IPs - return ipAddr.IsLoopback() || ipAddr.IsPrivate() + return false } func isValidDestination(dest string) bool { - _, portStr, err := net.SplitHostPort(dest) + host, portStr, err := net.SplitHostPort(dest) if err != nil { return false } + // Validate port port, err := strconv.Atoi(portStr) if err != nil || port < 1 || port > 65535 { return false } - return true + // Validate host + if host == "" { + return false + } + + // Check if it's an IP address + if ip := net.ParseIP(host); ip != nil { + return true + } + + // Try DNS resolution + ips, err := net.LookupHost(host) + return err == nil && len(ips) > 0 }