Add HTTP prober, optimize mDNS timeout, add Trassir/ZOSI to OUI
- Add HTTPProber: parallel HEAD+GET on ports 80/8080, extracts Server header - Reduce mDNS timeout from 1s to 100ms using context wrapper around mdns.Query (HomeKit devices respond in 2-10ms, no need to wait 1s) - Add Trassir (F0:23:B9) and ZOSI (00:05:FE) to camera OUI database - Probe response time improved from ~1s to ~110ms for reachable devices
This commit is contained in:
@@ -2401,5 +2401,7 @@
|
||||
"FC:E9:D8": "Ring",
|
||||
"FC:EC:DA": "Ubiquiti",
|
||||
"FC:F1:36": "Samsung",
|
||||
"FC:F1:52": "Sony"
|
||||
"FC:F1:52": "Sony",
|
||||
"F0:23:B9": "Trassir",
|
||||
"00:05:FE": "ZOSI"
|
||||
}
|
||||
@@ -89,6 +89,7 @@ func NewServer(
|
||||
&discovery.DNSProber{},
|
||||
discovery.NewARPProber(ouiDB),
|
||||
&discovery.MDNSProber{},
|
||||
&discovery.HTTPProber{},
|
||||
}
|
||||
probeService := discovery.NewProbeService(probers, logger)
|
||||
|
||||
|
||||
@@ -146,6 +146,10 @@ func (s *ProbeService) Probe(ctx context.Context, ip string) *models.ProbeRespon
|
||||
if v, ok := r.data.(*models.MDNSProbeResult); ok {
|
||||
response.Probes.MDNS = v
|
||||
}
|
||||
case "http":
|
||||
if v, ok := r.data.(*models.HTTPProbeResult); ok {
|
||||
response.Probes.HTTP = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/eduard256/Strix/internal/models"
|
||||
)
|
||||
|
||||
// HTTPProber identifies the device by checking HTTP server headers.
|
||||
// It sends HEAD and GET requests in parallel to port 80 (some devices
|
||||
// like XMEye/JAWS don't respond to HEAD), and returns whichever
|
||||
// responds first.
|
||||
type HTTPProber struct{}
|
||||
|
||||
func (p *HTTPProber) Name() string { return "http" }
|
||||
|
||||
// Probe sends parallel HEAD+GET to port 80 and extracts Server header.
|
||||
// Returns nil if no HTTP server is found.
|
||||
func (p *HTTPProber) Probe(ctx context.Context, ip string) (any, error) {
|
||||
ports := []int{80, 8080}
|
||||
|
||||
client := &http.Client{
|
||||
// Don't follow redirects -- we want the original response headers
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
type result struct {
|
||||
resp *http.Response
|
||||
port int
|
||||
err error
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
url := fmt.Sprintf("http://%s:%d/", ip, port)
|
||||
ch := make(chan result, 2)
|
||||
|
||||
// HEAD and GET in parallel -- take whichever responds first
|
||||
for _, method := range []string{"HEAD", "GET"} {
|
||||
go func(method string) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
if err != nil {
|
||||
ch <- result{err: err}
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", "Strix/1.0")
|
||||
resp, err := client.Do(req)
|
||||
ch <- result{resp: resp, port: port, err: err}
|
||||
}(method)
|
||||
}
|
||||
|
||||
// Wait for first success
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case r := <-ch:
|
||||
if r.err != nil {
|
||||
continue
|
||||
}
|
||||
if r.resp.Body != nil {
|
||||
r.resp.Body.Close()
|
||||
}
|
||||
|
||||
server := r.resp.Header.Get("Server")
|
||||
if server == "" && r.resp.StatusCode == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
return &models.HTTPProbeResult{
|
||||
Port: r.port,
|
||||
StatusCode: r.resp.StatusCode,
|
||||
Server: server,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -2,15 +2,27 @@ package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
"github.com/eduard256/Strix/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
// mdnsTimeout is the maximum time to wait for mDNS response.
|
||||
// HomeKit devices respond in 2-10ms. If no response in 100ms,
|
||||
// the device is definitely not a HomeKit camera.
|
||||
// The underlying mdns.Query has a 1s internal timeout, but we
|
||||
// cut it short with this context-based wrapper.
|
||||
mdnsTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// MDNSProber performs mDNS unicast query to detect HomeKit devices.
|
||||
// It sends a DNS query to ip:5353 for the _hap._tcp.local. service
|
||||
// and parses TXT records to extract device information.
|
||||
// Uses a 100ms timeout wrapper around go2rtc's mdns.Query to avoid
|
||||
// waiting the full 1s on non-HomeKit devices.
|
||||
type MDNSProber struct{}
|
||||
|
||||
func (p *MDNSProber) Name() string { return "mdns" }
|
||||
@@ -18,11 +30,37 @@ func (p *MDNSProber) Name() string { return "mdns" }
|
||||
// Probe queries the device for HomeKit (HAP) mDNS service.
|
||||
// Returns nil if the device does not advertise HomeKit or is not a camera/doorbell.
|
||||
func (p *MDNSProber) Probe(ctx context.Context, ip string) (any, error) {
|
||||
// Unicast mDNS query directly to the device IP.
|
||||
// mdns.Query has internal timeouts (~1s), which fits within our 3s budget.
|
||||
entry, err := mdns.Query(ip, mdns.ServiceHAP)
|
||||
if err != nil || entry == nil {
|
||||
return nil, nil // Not a HomeKit device is not an error
|
||||
// Run mdns.Query in a goroutine with 100ms timeout.
|
||||
// mdns.Query has an internal 1s timeout and doesn't accept context,
|
||||
// so we wrap it. The background goroutine will clean up on its own
|
||||
// after the internal timeout expires (~1s, negligible resource cost).
|
||||
type queryResult struct {
|
||||
entry *mdns.ServiceEntry
|
||||
err error
|
||||
}
|
||||
|
||||
ch := make(chan queryResult, 1)
|
||||
go func() {
|
||||
entry, err := mdns.Query(ip, mdns.ServiceHAP)
|
||||
ch <- queryResult{entry, err}
|
||||
}()
|
||||
|
||||
// Wait for result or timeout
|
||||
timer := time.NewTimer(mdnsTimeout)
|
||||
defer timer.Stop()
|
||||
|
||||
var entry *mdns.ServiceEntry
|
||||
|
||||
select {
|
||||
case r := <-ch:
|
||||
if r.err != nil || r.entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
entry = r.entry
|
||||
case <-timer.C:
|
||||
return nil, nil // No response within 100ms -- not a HomeKit device
|
||||
case <-ctx.Done():
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if it's complete (has IP, port, and TXT records)
|
||||
|
||||
@@ -20,6 +20,14 @@ type ProbeResults struct {
|
||||
DNS *DNSProbeResult `json:"dns"`
|
||||
ARP *ARPProbeResult `json:"arp"`
|
||||
MDNS *MDNSProbeResult `json:"mdns"`
|
||||
HTTP *HTTPProbeResult `json:"http"`
|
||||
}
|
||||
|
||||
// HTTPProbeResult contains HTTP server identification from port 80.
|
||||
type HTTPProbeResult struct {
|
||||
Port int `json:"port"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Server string `json:"server"`
|
||||
}
|
||||
|
||||
// DNSProbeResult contains reverse DNS lookup result.
|
||||
|
||||
Reference in New Issue
Block a user