feat: scan host LAN web devices via proxy
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package webdevice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -59,6 +62,7 @@ type Service struct {
|
||||
allowed map[string]time.Time
|
||||
forwarders map[string]*webDeviceForwarder
|
||||
interfaceGetter InterfaceGetter
|
||||
hostLANGetter InterfaceGetter
|
||||
tcpScanner TCPScanner
|
||||
newForwarder ForwarderFactory
|
||||
proxyTarget ProxyTargetResolver
|
||||
@@ -70,6 +74,7 @@ func NewService() *Service {
|
||||
allowed: make(map[string]time.Time),
|
||||
forwarders: make(map[string]*webDeviceForwarder),
|
||||
interfaceGetter: defaultInterfaceGetter,
|
||||
hostLANGetter: defaultHostLANInterfaceGetter,
|
||||
tcpScanner: scanTCP,
|
||||
newForwarder: newWebDeviceForwarder,
|
||||
proxyTarget: defaultProxyTarget,
|
||||
@@ -82,14 +87,15 @@ func (s *Service) Scan(r *http.Request) (*ScanResult, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scheme, host := requestBase(r)
|
||||
interfaces = appendRequestHostInterface(interfaces, host)
|
||||
_, host := requestBase(r)
|
||||
scanInterfaces, scanWarnings := s.scanInterfaces(interfaces, host)
|
||||
|
||||
if len(interfaces) == 0 {
|
||||
if len(scanInterfaces) == 0 {
|
||||
return &ScanResult{
|
||||
Interfaces: []InterfaceInfo{},
|
||||
Devices: []DeviceInfo{},
|
||||
Message: "未找到有效的网卡",
|
||||
Errors: scanWarnings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -97,14 +103,17 @@ func (s *Service) Scan(r *http.Request) (*ScanResult, error) {
|
||||
for _, iface := range interfaces {
|
||||
excludeIPs[iface.IP] = true
|
||||
}
|
||||
|
||||
result := &ScanResult{
|
||||
Interfaces: interfaces,
|
||||
Devices: []DeviceInfo{},
|
||||
Errors: []string{},
|
||||
for _, iface := range scanInterfaces {
|
||||
excludeIPs[iface.IP] = true
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
result := &ScanResult{
|
||||
Interfaces: scanInterfaces,
|
||||
Devices: []DeviceInfo{},
|
||||
Errors: scanWarnings,
|
||||
}
|
||||
|
||||
for _, iface := range scanInterfaces {
|
||||
devices, scanErr := s.tcpScanner(iface.IP, iface.Netmask, 80, excludeIPs)
|
||||
if scanErr != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", iface.Name, scanErr))
|
||||
@@ -117,10 +126,6 @@ func (s *Service) Scan(r *http.Request) (*ScanResult, error) {
|
||||
}
|
||||
|
||||
s.allowIP(device.IP)
|
||||
forwardPort, forwardErr := s.EnsureForwarder(device.IP)
|
||||
if forwardErr != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s: 启动网页直连转发失败: %v", device.IP, forwardErr))
|
||||
}
|
||||
|
||||
deviceInfo := DeviceInfo{
|
||||
IP: device.IP,
|
||||
@@ -129,10 +134,6 @@ func (s *Service) Scan(r *http.Request) (*ScanResult, error) {
|
||||
TargetURL: fmt.Sprintf("http://%s/", device.IP),
|
||||
ProxyURL: fmt.Sprintf("/proxy/web/%s/", device.IP),
|
||||
}
|
||||
if forwardErr == nil {
|
||||
deviceInfo.ForwardPort = forwardPort
|
||||
deviceInfo.DirectURL = buildDirectURL(scheme, host, forwardPort)
|
||||
}
|
||||
result.Devices = append(result.Devices, deviceInfo)
|
||||
}
|
||||
}
|
||||
@@ -144,6 +145,21 @@ func (s *Service) Scan(r *http.Request) (*ScanResult, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) scanInterfaces(containerInterfaces []InterfaceInfo, requestHost string) ([]InterfaceInfo, []string) {
|
||||
if s.hostLANGetter != nil {
|
||||
hostInterfaces, err := s.hostLANGetter()
|
||||
if err == nil && len(hostInterfaces) > 0 {
|
||||
return hostInterfaces, nil
|
||||
}
|
||||
if err != nil {
|
||||
return appendRequestHostInterface(containerInterfaces, requestHost), []string{
|
||||
"宿主机局域网探测失败,已回退到请求地址网段: " + err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
return appendRequestHostInterface(containerInterfaces, requestHost), nil
|
||||
}
|
||||
|
||||
func appendRequestHostInterface(interfaces []InterfaceInfo, host string) []InterfaceInfo {
|
||||
if !IsPrivateIPv4Literal(host) {
|
||||
return interfaces
|
||||
@@ -183,6 +199,12 @@ func (s *Service) SetInterfaceGetter(getter InterfaceGetter) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetHostLANGetter(getter InterfaceGetter) {
|
||||
if getter != nil {
|
||||
s.hostLANGetter = getter
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetTCPScanner(scanner TCPScanner) {
|
||||
if scanner != nil {
|
||||
s.tcpScanner = scanner
|
||||
@@ -403,6 +425,109 @@ func defaultInterfaceGetter() ([]InterfaceInfo, error) {
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
func defaultHostLANInterfaceGetter() ([]InterfaceInfo, error) {
|
||||
image := strings.TrimSpace(os.Getenv("MANAGED_PORTAL_HOST_SCAN_IMAGE"))
|
||||
if image == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--network",
|
||||
"host",
|
||||
"--entrypoint",
|
||||
"/sbin/ip",
|
||||
image,
|
||||
"-o",
|
||||
"-4",
|
||||
"addr",
|
||||
"show",
|
||||
"scope",
|
||||
"global",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("读取宿主机网卡超时")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取宿主机网卡失败: %w: %s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return parseHostLANInterfaces(string(output)), nil
|
||||
}
|
||||
|
||||
func parseHostLANInterfaces(output string) []InterfaceInfo {
|
||||
var interfaces []InterfaceInfo
|
||||
seen := make(map[string]bool)
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(fields[1], ":")
|
||||
if ignoredInterfaceName(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
inetIndex := -1
|
||||
for index, field := range fields {
|
||||
if field == "inet" {
|
||||
inetIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if inetIndex == -1 || inetIndex+1 >= len(fields) {
|
||||
continue
|
||||
}
|
||||
|
||||
ip, ipNet, err := net.ParseCIDR(fields[inetIndex+1])
|
||||
if err != nil || ip.To4() == nil || ipNet == nil {
|
||||
continue
|
||||
}
|
||||
ipString := ip.String()
|
||||
if !IsPrivateIPv4Literal(ipString) || seen[ipString] {
|
||||
continue
|
||||
}
|
||||
ones, bits := ipNet.Mask.Size()
|
||||
if bits != 32 {
|
||||
continue
|
||||
}
|
||||
mask := net.IP(net.CIDRMask(ones, bits)).String()
|
||||
interfaces = append(interfaces, InterfaceInfo{
|
||||
Name: name,
|
||||
IP: ipString,
|
||||
Netmask: mask,
|
||||
})
|
||||
seen[ipString] = true
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func ignoredInterfaceName(name string) bool {
|
||||
lower := strings.ToLower(name)
|
||||
if lower == "lo" ||
|
||||
strings.Contains(lower, "docker") ||
|
||||
strings.Contains(lower, "veth") ||
|
||||
strings.Contains(lower, "br-") ||
|
||||
strings.Contains(lower, "tun") ||
|
||||
strings.Contains(lower, "tap") ||
|
||||
strings.Contains(lower, "wg") ||
|
||||
strings.Contains(lower, "tailscale") ||
|
||||
strings.Contains(lower, "utun") ||
|
||||
strings.Contains(lower, "ppp") ||
|
||||
strings.Contains(lower, "zerotier") ||
|
||||
strings.HasPrefix(lower, "zt") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scanTCP(ip string, netmask string, port int, excludeIPs map[string]bool) ([]TCPDevice, error) {
|
||||
ipRange, err := calculateIPRange(ip, netmask)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user