133 lines
2.9 KiB
Go
133 lines
2.9 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"log/slog"
|
||
|
|
"net"
|
||
|
|
"os"
|
||
|
|
"regexp"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
DefaultArtNetPort = 6454 // 0x1936
|
||
|
|
)
|
||
|
|
|
||
|
|
var (
|
||
|
|
verbose bool
|
||
|
|
nameFilter *regexp.Regexp
|
||
|
|
)
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
port := flag.Int("port", DefaultArtNetPort, "UDP port to listen on (default: 6454/0x1936)")
|
||
|
|
bindAddr := flag.String("bind", "0.0.0.0", "IP address to bind to")
|
||
|
|
namePattern := flag.String("name", "", "Filter by node name (regex)")
|
||
|
|
flag.BoolVar(&verbose, "v", false, "Verbose output (show all field details)")
|
||
|
|
flag.Parse()
|
||
|
|
|
||
|
|
if *namePattern != "" {
|
||
|
|
var err error
|
||
|
|
nameFilter, err = regexp.Compile(*namePattern)
|
||
|
|
if err != nil {
|
||
|
|
fmt.Fprintf(os.Stderr, "Invalid name regex: %v\n", err)
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ip := net.ParseIP(*bindAddr)
|
||
|
|
if ip == nil {
|
||
|
|
fmt.Fprintf(os.Stderr, "Invalid bind address: %s\n", *bindAddr)
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
addr := net.UDPAddr{
|
||
|
|
Port: *port,
|
||
|
|
IP: ip,
|
||
|
|
}
|
||
|
|
|
||
|
|
conn, err := net.ListenUDP("udp4", &addr)
|
||
|
|
if err != nil {
|
||
|
|
fmt.Fprintf(os.Stderr, "Failed to listen on UDP port %d: %v\n", *port, err)
|
||
|
|
fmt.Fprintf(os.Stderr, "You may need to run with sudo or use setcap cap_net_bind_service=+ep\n")
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
defer conn.Close()
|
||
|
|
|
||
|
|
slog.Info("listening", "addr", fmt.Sprintf("%s:%d", *bindAddr, *port))
|
||
|
|
if *port != DefaultArtNetPort {
|
||
|
|
slog.Warn("non-standard port", "expected", DefaultArtNetPort)
|
||
|
|
}
|
||
|
|
|
||
|
|
buf := make([]byte, 65535)
|
||
|
|
for {
|
||
|
|
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("read failed", "error", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
packet := make([]byte, n)
|
||
|
|
copy(packet, buf[:n])
|
||
|
|
|
||
|
|
processPacket(packet, remoteAddr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func processPacket(data []byte, src *net.UDPAddr) {
|
||
|
|
result := ValidatePacket(data, src)
|
||
|
|
|
||
|
|
// Apply name filter
|
||
|
|
if nameFilter != nil {
|
||
|
|
name := result.getField("PortName")
|
||
|
|
if !nameFilter.MatchString(name) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build log attributes
|
||
|
|
attrs := []any{
|
||
|
|
"src", src.IP.String(),
|
||
|
|
"size", len(data),
|
||
|
|
"type", result.PacketType,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add summary fields based on packet type
|
||
|
|
for _, kv := range result.SummaryAttrs() {
|
||
|
|
attrs = append(attrs, kv.Key, kv.Value)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add error/warning counts if any
|
||
|
|
if len(result.Errors) > 0 {
|
||
|
|
attrs = append(attrs, "errors", len(result.Errors))
|
||
|
|
}
|
||
|
|
if len(result.Warnings) > 0 {
|
||
|
|
attrs = append(attrs, "warnings", len(result.Warnings))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Log the packet
|
||
|
|
if len(result.Errors) > 0 {
|
||
|
|
slog.Error("packet", attrs...)
|
||
|
|
} else if len(result.Warnings) > 0 {
|
||
|
|
slog.Warn("packet", attrs...)
|
||
|
|
} else {
|
||
|
|
slog.Info("packet", attrs...)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Always log individual warnings
|
||
|
|
for _, w := range result.Warnings {
|
||
|
|
slog.Warn("validation", "src", src.IP.String(), "warning", w)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Always log individual errors
|
||
|
|
for _, e := range result.Errors {
|
||
|
|
slog.Error("validation", "src", src.IP.String(), "error", e)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verbose: log all fields
|
||
|
|
if verbose && len(result.Fields) > 0 {
|
||
|
|
for _, f := range result.Fields {
|
||
|
|
slog.Debug("field", "src", src.IP.String(), "name", f.Name, "value", f.Value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|