Remove pcap-based packet sniffing receivers

This commit is contained in:
Ian Gulliver
2026-01-27 14:33:21 -08:00
parent ce98165946
commit ac59d7975c
6 changed files with 17 additions and 341 deletions

View File

@@ -1,120 +0,0 @@
package artnet
import (
"net"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
// PcapReceiver listens for ArtNet packets using packet capture
type PcapReceiver struct {
handle *pcap.Handle
handler PacketHandler
done chan struct{}
}
// NewPcapReceiver creates a new ArtNet receiver using packet capture
// This requires root/admin privileges but avoids port conflicts
func NewPcapReceiver(iface string, handler PacketHandler) (*PcapReceiver, error) {
// Open device for capturing
handle, err := pcap.OpenLive(iface, 1600, true, pcap.BlockForever)
if err != nil {
return nil, err
}
// Filter for UDP port 6454 (ArtNet) - captures both directions
if err := handle.SetBPFFilter("udp port 6454"); err != nil {
handle.Close()
return nil, err
}
return &PcapReceiver{
handle: handle,
handler: handler,
done: make(chan struct{}),
}, nil
}
// Start begins receiving packets
func (r *PcapReceiver) Start() {
go r.receiveLoop()
}
// Stop stops the receiver
func (r *PcapReceiver) Stop() {
close(r.done)
r.handle.Close()
}
func (r *PcapReceiver) receiveLoop() {
packetSource := gopacket.NewPacketSource(r.handle, r.handle.LinkType())
for {
select {
case <-r.done:
return
case packet, ok := <-packetSource.Packets():
if !ok {
return
}
r.handlePacket(packet)
}
}
}
func (r *PcapReceiver) handlePacket(packet gopacket.Packet) {
// Extract UDP layer
udpLayer := packet.Layer(layers.LayerTypeUDP)
if udpLayer == nil {
return
}
udp, _ := udpLayer.(*layers.UDP)
if udp == nil {
return
}
// Extract IP layer for source address
var srcIP, dstIP [4]byte
if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil {
ip, _ := ipLayer.(*layers.IPv4)
if ip != nil {
copy(srcIP[:], ip.SrcIP.To4())
copy(dstIP[:], ip.DstIP.To4())
}
}
// Get payload
data := udp.Payload
if len(data) < 12 {
return
}
// Parse the ArtNet packet
opCode, pkt, err := ParsePacket(data)
if err != nil {
return
}
src := &net.UDPAddr{
IP: net.IP(srcIP[:]),
Port: int(udp.SrcPort),
}
switch opCode {
case OpDmx:
if dmx, ok := pkt.(*DMXPacket); ok {
r.handler.HandleDMX(src, dmx)
}
case OpPoll:
if poll, ok := pkt.(*PollPacket); ok {
r.handler.HandlePoll(src, poll)
}
case OpPollReply:
if reply, ok := pkt.(*PollReplyPacket); ok {
r.handler.HandlePollReply(src, reply)
}
}
}

View File

@@ -3,9 +3,7 @@
#
# Flags:
# --artnet-listen=:6454 ArtNet listen address (empty to disable)
# --artnet-pcap=any Use pcap for ArtNet (requires root, avoids port conflicts)
# --artnet-broadcast=auto Broadcast addresses (comma-separated, or 'auto')
# --sacn-pcap=any Use pcap for sACN (requires root, avoids port conflicts)
# Target addresses for ArtNet output universes
# Each output universe needs a target IP (broadcast or unicast)

1
go.mod
View File

@@ -4,7 +4,6 @@ go 1.25.4
require (
github.com/BurntSushi/toml v1.6.0
github.com/google/gopacket v1.1.19
golang.org/x/net v0.48.0
)

14
go.sum
View File

@@ -1,20 +1,6 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

41
main.go
View File

@@ -20,9 +20,7 @@ import (
type App struct {
cfg *config.Config
artReceiver *artnet.Receiver
artPcapReceiver *artnet.PcapReceiver
sacnReceiver *sacn.Receiver
sacnPcapReceiver *sacn.PcapReceiver
artSender *artnet.Sender
sacnSender *sacn.Sender
discovery *artnet.Discovery
@@ -35,9 +33,7 @@ type App struct {
func main() {
configPath := flag.String("config", "config.toml", "path to config file")
artnetListen := flag.String("artnet-listen", ":6454", "artnet listen address (empty to disable)")
artnetPcap := flag.String("artnet-pcap", "", "use pcap for artnet on interface (e.g. en0, any)")
artnetBroadcast := flag.String("artnet-broadcast", "auto", "artnet broadcast addresses (comma-separated, or 'auto')")
sacnPcap := flag.String("sacn-pcap", "", "use pcap for sacn on interface (e.g. en0, any)")
debug := flag.Bool("debug", false, "log incoming/outgoing dmx packets")
flag.Parse()
@@ -131,20 +127,7 @@ func main() {
}
// Create ArtNet receiver if enabled
if *artnetPcap != "" {
// Use pcap-based receiver (requires root, avoids port conflicts)
iface := *artnetPcap
if iface == "auto" {
iface = "any"
}
pcapReceiver, err := artnet.NewPcapReceiver(iface, app)
if err != nil {
log.Fatalf("artnet pcap error: %v", err)
}
app.artPcapReceiver = pcapReceiver
pcapReceiver.Start()
log.Printf("[artnet] pcap listening iface=%s", iface)
} else if *artnetListen != "" {
if *artnetListen != "" {
addr, err := parseListenAddr(*artnetListen)
if err != nil {
log.Fatalf("artnet listen error: %v", err)
@@ -161,21 +144,6 @@ func main() {
// Create sACN receiver if needed
sacnUniverses := cfg.SACNSourceUniverses()
if len(sacnUniverses) > 0 {
if *sacnPcap != "" {
// Use pcap-based receiver (requires root, avoids port conflicts)
iface := *sacnPcap
if iface == "auto" {
iface = sacn.DefaultInterface()
}
pcapReceiver, err := sacn.NewPcapReceiver(iface, sacnUniverses, app.HandleSACN)
if err != nil {
log.Fatalf("sacn pcap error: %v", err)
}
app.sacnPcapReceiver = pcapReceiver
pcapReceiver.Start()
log.Printf("[sacn] pcap listening iface=%s universes=%v", iface, sacnUniverses)
} else {
// Use standard UDP receiver
sacnReceiver, err := sacn.NewReceiver(sacnUniverses, app.HandleSACN)
if err != nil {
log.Fatalf("sacn receiver error: %v", err)
@@ -184,7 +152,6 @@ func main() {
sacnReceiver.Start()
log.Printf("[sacn] listening universes=%v", sacnUniverses)
}
}
// Start discovery
discovery.Start()
@@ -198,15 +165,9 @@ func main() {
if app.artReceiver != nil {
app.artReceiver.Stop()
}
if app.artPcapReceiver != nil {
app.artPcapReceiver.Stop()
}
if app.sacnReceiver != nil {
app.sacnReceiver.Stop()
}
if app.sacnPcapReceiver != nil {
app.sacnPcapReceiver.Stop()
}
discovery.Stop()
}

View File

@@ -1,148 +0,0 @@
package sacn
import (
"encoding/binary"
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
// PcapReceiver listens for sACN packets using packet capture
type PcapReceiver struct {
handle *pcap.Handle
universes map[uint16]bool
handler DMXHandler
done chan struct{}
}
// NewPcapReceiver creates a new sACN receiver using packet capture
// This requires root/admin privileges but avoids port conflicts
func NewPcapReceiver(iface string, universes []uint16, handler DMXHandler) (*PcapReceiver, error) {
// Open device for capturing
handle, err := pcap.OpenLive(iface, 1600, true, pcap.BlockForever)
if err != nil {
return nil, fmt.Errorf("pcap open: %w", err)
}
// Filter for UDP port 5568 (sACN) - captures both directions
if err := handle.SetBPFFilter("udp port 5568"); err != nil {
handle.Close()
return nil, fmt.Errorf("pcap filter: %w", err)
}
universeMap := make(map[uint16]bool)
for _, u := range universes {
universeMap[u] = true
}
return &PcapReceiver{
handle: handle,
universes: universeMap,
handler: handler,
done: make(chan struct{}),
}, nil
}
// Start begins receiving packets
func (r *PcapReceiver) Start() {
go r.receiveLoop()
}
// Stop stops the receiver
func (r *PcapReceiver) Stop() {
close(r.done)
r.handle.Close()
}
func (r *PcapReceiver) receiveLoop() {
packetSource := gopacket.NewPacketSource(r.handle, r.handle.LinkType())
for {
select {
case <-r.done:
return
case packet, ok := <-packetSource.Packets():
if !ok {
return
}
r.handlePacket(packet)
}
}
}
func (r *PcapReceiver) handlePacket(packet gopacket.Packet) {
// Extract UDP layer
udpLayer := packet.Layer(layers.LayerTypeUDP)
if udpLayer == nil {
return
}
udp, _ := udpLayer.(*layers.UDP)
if udp == nil {
return
}
// Get payload
data := udp.Payload
if len(data) < 126 {
return
}
// Check ACN packet identifier
if data[4] != 0x41 || data[5] != 0x53 || data[6] != 0x43 {
return
}
// Check root vector (E1.31 data)
rootVector := binary.BigEndian.Uint32(data[18:22])
if rootVector != VectorRootE131Data {
return
}
// Check framing vector (DMP data)
framingVector := binary.BigEndian.Uint32(data[40:44])
if framingVector != VectorE131DataPacket {
return
}
// Get universe
universe := binary.BigEndian.Uint16(data[113:115])
// Check if we care about this universe
if !r.universes[universe] {
return
}
// Check DMP vector
if data[117] != VectorDMPSetProperty {
return
}
// Get property count (includes START code)
propCount := binary.BigEndian.Uint16(data[123:125])
if propCount < 1 {
return
}
// Skip START code at data[125]
dmxLen := int(propCount) - 1
if dmxLen > 512 {
dmxLen = 512
}
if len(data) < 126+dmxLen {
return
}
var dmxData [512]byte
copy(dmxData[:], data[126:126+dmxLen])
r.handler(universe, dmxData)
}
// DefaultInterface returns the default interface for capture
func DefaultInterface() string {
return "any"
}