feat(netutil): add local and private host detection helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 16:17:20 +08:00
parent 0eed208bbf
commit 13fdf464ae
2 changed files with 65 additions and 0 deletions
@@ -0,0 +1,32 @@
package netutil
import (
"net"
"strings"
)
func IsLocalOrPrivateHost(host string) bool {
host = strings.ToLower(strings.TrimSpace(host))
if host == "" {
return false
}
if host == "localhost" || strings.HasSuffix(host, ".localhost") || host == "host.docker.internal" {
return true
}
ip := net.ParseIP(host)
return ip != nil && IsLocalOrPrivateIP(ip)
}
func IsLocalOrPrivateIP(ip net.IP) bool {
if ip == nil {
return false
}
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
ip4 := ip.To4()
if ip4 == nil {
return false
}
return ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127
}
@@ -0,0 +1,33 @@
package netutil
import "testing"
func TestIsLocalOrPrivateHost(t *testing.T) {
tests := []struct {
name string
host string
want bool
}{
{name: "localhost", host: "localhost", want: true},
{name: "localhost subdomain", host: "assets.localhost", want: true},
{name: "docker host", host: "host.docker.internal", want: true},
{name: "loopback ipv4", host: "127.0.0.1", want: true},
{name: "loopback ipv6", host: "::1", want: true},
{name: "private 10", host: "10.0.0.5", want: true},
{name: "private 172", host: "172.16.2.3", want: true},
{name: "private 192", host: "192.168.1.10", want: true},
{name: "link local", host: "169.254.10.20", want: true},
{name: "cgnat", host: "100.64.10.20", want: true},
{name: "unspecified", host: "0.0.0.0", want: true},
{name: "public ip", host: "8.8.8.8", want: false},
{name: "public hostname", host: "example.com", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsLocalOrPrivateHost(tt.host); got != tt.want {
t.Fatalf("IsLocalOrPrivateHost(%q) = %t, want %t", tt.host, got, tt.want)
}
})
}
}