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:
eduard256
2026-03-16 14:46:58 +00:00
parent 4d6c2fd878
commit 3fec89be7f
6 changed files with 146 additions and 6 deletions
+3 -1
View File
@@ -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"
}
+1
View File
@@ -89,6 +89,7 @@ func NewServer(
&discovery.DNSProber{},
discovery.NewARPProber(ouiDB),
&discovery.MDNSProber{},
&discovery.HTTPProber{},
}
probeService := discovery.NewProbeService(probers, logger)
+4
View File
@@ -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
}
}
}
+87
View File
@@ -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
}
+43 -5
View File
@@ -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)
+8
View File
@@ -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.