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:E9:D8": "Ring",
|
||||||
"FC:EC:DA": "Ubiquiti",
|
"FC:EC:DA": "Ubiquiti",
|
||||||
"FC:F1:36": "Samsung",
|
"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.DNSProber{},
|
||||||
discovery.NewARPProber(ouiDB),
|
discovery.NewARPProber(ouiDB),
|
||||||
&discovery.MDNSProber{},
|
&discovery.MDNSProber{},
|
||||||
|
&discovery.HTTPProber{},
|
||||||
}
|
}
|
||||||
probeService := discovery.NewProbeService(probers, logger)
|
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 {
|
if v, ok := r.data.(*models.MDNSProbeResult); ok {
|
||||||
response.Probes.MDNS = v
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||||
"github.com/eduard256/Strix/internal/models"
|
"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.
|
// MDNSProber performs mDNS unicast query to detect HomeKit devices.
|
||||||
// It sends a DNS query to ip:5353 for the _hap._tcp.local. service
|
// It sends a DNS query to ip:5353 for the _hap._tcp.local. service
|
||||||
// and parses TXT records to extract device information.
|
// 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{}
|
type MDNSProber struct{}
|
||||||
|
|
||||||
func (p *MDNSProber) Name() string { return "mdns" }
|
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.
|
// Probe queries the device for HomeKit (HAP) mDNS service.
|
||||||
// Returns nil if the device does not advertise HomeKit or is not a camera/doorbell.
|
// 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) {
|
func (p *MDNSProber) Probe(ctx context.Context, ip string) (any, error) {
|
||||||
// Unicast mDNS query directly to the device IP.
|
// Run mdns.Query in a goroutine with 100ms timeout.
|
||||||
// mdns.Query has internal timeouts (~1s), which fits within our 3s budget.
|
// mdns.Query has an internal 1s timeout and doesn't accept context,
|
||||||
entry, err := mdns.Query(ip, mdns.ServiceHAP)
|
// so we wrap it. The background goroutine will clean up on its own
|
||||||
if err != nil || entry == nil {
|
// after the internal timeout expires (~1s, negligible resource cost).
|
||||||
return nil, nil // Not a HomeKit device is not an error
|
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)
|
// Check if it's complete (has IP, port, and TXT records)
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ type ProbeResults struct {
|
|||||||
DNS *DNSProbeResult `json:"dns"`
|
DNS *DNSProbeResult `json:"dns"`
|
||||||
ARP *ARPProbeResult `json:"arp"`
|
ARP *ARPProbeResult `json:"arp"`
|
||||||
MDNS *MDNSProbeResult `json:"mdns"`
|
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.
|
// DNSProbeResult contains reverse DNS lookup result.
|
||||||
|
|||||||
Reference in New Issue
Block a user