feat: scan host LAN web devices via proxy

This commit is contained in:
Yoilun
2026-05-15 01:17:44 +08:00
parent f8a6d9803d
commit 1114ee00c1
7 changed files with 421 additions and 22 deletions

View File

@@ -39,7 +39,7 @@ func TestWebDeviceForwardPort(t *testing.T) {
}
}
func TestScanBuildsDirectURLAndAllowList(t *testing.T) {
func TestScanBuildsProxyURLAndAllowList(t *testing.T) {
t.Parallel()
svc := NewService()
@@ -68,8 +68,11 @@ func TestScanBuildsDirectURLAndAllowList(t *testing.T) {
if !svc.IsAllowed("192.168.1.124") {
t.Fatal("expected IP to be allowed after scan")
}
if result.Devices[0].DirectURL != "http://10.8.0.14:31124/" {
t.Fatalf("DirectURL = %q", result.Devices[0].DirectURL)
if result.Devices[0].ProxyURL != "/proxy/web/192.168.1.124/" {
t.Fatalf("ProxyURL = %q", result.Devices[0].ProxyURL)
}
if result.Devices[0].DirectURL != "" {
t.Fatalf("DirectURL = %q, want empty", result.Devices[0].DirectURL)
}
}
@@ -77,6 +80,7 @@ func TestScanIncludesPrivateRequestHostSubnet(t *testing.T) {
t.Parallel()
svc := NewService()
svc.hostLANGetter = nil
svc.interfaceGetter = func() ([]InterfaceInfo, error) {
return []InterfaceInfo{{
Name: "eth0",
@@ -113,6 +117,75 @@ func TestScanIncludesPrivateRequestHostSubnet(t *testing.T) {
}
}
func TestScanPrefersHostLANInterfaces(t *testing.T) {
t.Parallel()
svc := NewService()
svc.interfaceGetter = func() ([]InterfaceInfo, error) {
return []InterfaceInfo{{
Name: "eth0",
IP: "172.20.0.4",
Netmask: "255.255.0.0",
}}, nil
}
svc.hostLANGetter = func() ([]InterfaceInfo, error) {
return []InterfaceInfo{{
Name: "eno1",
IP: "192.168.0.117",
Netmask: "255.255.255.0",
}}, nil
}
svc.tcpScanner = func(ip, netmask string, port int, excludeIPs map[string]bool) ([]TCPDevice, error) {
if ip != "192.168.0.117" {
t.Fatalf("scan ip = %q, want host LAN ip", ip)
}
if netmask != "255.255.255.0" {
t.Fatalf("netmask = %q, want 255.255.255.0", netmask)
}
if !excludeIPs["172.20.0.4"] || !excludeIPs["192.168.0.117"] {
t.Fatalf("excludeIPs = %#v, want container and host IPs", excludeIPs)
}
return []TCPDevice{{IP: "192.168.0.108", Port: 80}}, nil
}
svc.newForwarder = func(ip string, port int, listenAddress, targetAddress string) (*webDeviceForwarder, error) {
return &webDeviceForwarder{ip: ip, port: port, targetAddress: targetAddress}, nil
}
req := httptest.NewRequest(http.MethodGet, "http://10.8.0.18:13000/api/web-devices/scan", nil)
result, err := svc.Scan(req)
if err != nil {
t.Fatalf("Scan() error = %v", err)
}
if result.Count != 1 {
t.Fatalf("result.Count = %d, want 1", result.Count)
}
if result.Devices[0].Interface != "eno1" {
t.Fatalf("Interface = %q, want eno1", result.Devices[0].Interface)
}
}
func TestParseHostLANInterfacesFiltersVirtualInterfaces(t *testing.T) {
t.Parallel()
output := `2: eno1 inet 192.168.0.117/24 brd 192.168.0.255 scope global eno1
3: docker0 inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
4: tun0 inet 10.8.0.18/24 scope global tun0
5: wlan0 inet 192.168.5.10/23 brd 192.168.5.255 scope global wlan0
`
interfaces := parseHostLANInterfaces(output)
if len(interfaces) != 2 {
t.Fatalf("len(interfaces) = %d, want 2: %#v", len(interfaces), interfaces)
}
if interfaces[0].Name != "eno1" || interfaces[0].IP != "192.168.0.117" || interfaces[0].Netmask != "255.255.255.0" {
t.Fatalf("interfaces[0] = %#v", interfaces[0])
}
if interfaces[1].Name != "wlan0" || interfaces[1].IP != "192.168.5.10" || interfaces[1].Netmask != "255.255.254.0" {
t.Fatalf("interfaces[1] = %#v", interfaces[1])
}
}
func TestCalculateIPRangeCapsLargeSubnetToLocal24(t *testing.T) {
t.Parallel()