diff --git a/classify/classifier.go b/classify/classifier.go new file mode 100644 index 0000000..2d7e096 --- /dev/null +++ b/classify/classifier.go @@ -0,0 +1,72 @@ +package classify + +import ( + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +// Classifier classifies packets against a rule table. +type Classifier struct { + rules []Rule +} + +// NewClassifier creates a classifier with the given rules. +func NewClassifier(rules []Rule) *Classifier { + return &Classifier{rules: rules} +} + +// Classify inspects a gopacket.Packet and returns a ClassifiedPacket. +// It checks ICMP first, then extracts TCP/UDP port info and matches against rules. +// Per D-03: returns ClassUnknown if no rule matches. +func (c *Classifier) Classify(pkt gopacket.Packet) ClassifiedPacket { + result := ClassifiedPacket{ + Class: ClassUnknown, + Length: len(pkt.Data()), + } + + // Check ICMP + if pkt.Layer(layers.LayerTypeICMPv4) != nil || pkt.Layer(layers.LayerTypeICMPv6) != nil { + result.Protocol = "icmp" + // Match against rules + for _, rule := range c.rules { + if rule.Protocol == "icmp" { + result.Class = rule.Class + return result + } + } + return result + } + + // Check TCP + if tcpLayer := pkt.Layer(layers.LayerTypeTCP); tcpLayer != nil { + tcp := tcpLayer.(*layers.TCP) + result.Protocol = "tcp" + result.SrcPort = uint16(tcp.SrcPort) + result.DstPort = uint16(tcp.DstPort) + for _, rule := range c.rules { + if rule.Protocol == "tcp" && (rule.DstPort == 0 || rule.DstPort == result.DstPort) { + result.Class = rule.Class + return result + } + } + return result + } + + // Check UDP + if udpLayer := pkt.Layer(layers.LayerTypeUDP); udpLayer != nil { + udp := udpLayer.(*layers.UDP) + result.Protocol = "udp" + result.SrcPort = uint16(udp.SrcPort) + result.DstPort = uint16(udp.DstPort) + for _, rule := range c.rules { + if rule.Protocol == "udp" && (rule.DstPort == 0 || rule.DstPort == result.DstPort) { + result.Class = rule.Class + return result + } + } + return result + } + + // No recognized transport layer -> ClassUnknown per D-03 + return result +} diff --git a/classify/rules.go b/classify/rules.go new file mode 100644 index 0000000..da7b919 --- /dev/null +++ b/classify/rules.go @@ -0,0 +1,28 @@ +package classify + +// Rule defines a classification rule. Protocol is "tcp", "udp", or "icmp". +// DstPort 0 means match any port for this protocol. +// Rules are evaluated in order; first match wins. +type Rule struct { + Protocol string + DstPort uint16 + Class TrafficClass +} + +// DefaultRules is the D-01 protocol map — ordered, first-match-wins. +// Per D-02: config-driven slice, not a switch statement. +var DefaultRules = []Rule{ + {Protocol: "icmp", DstPort: 0, Class: ClassICMP}, + {Protocol: "udp", DstPort: 53, Class: ClassDNS}, + {Protocol: "tcp", DstPort: 53, Class: ClassDNS}, + {Protocol: "tcp", DstPort: 443, Class: ClassHTTPS}, + {Protocol: "tcp", DstPort: 80, Class: ClassHTTP}, + {Protocol: "tcp", DstPort: 22, Class: ClassSSH}, + {Protocol: "tcp", DstPort: 25, Class: ClassSMTP}, + {Protocol: "udp", DstPort: 123, Class: ClassNTP}, + {Protocol: "udp", DstPort: 67, Class: ClassDHCP}, + {Protocol: "udp", DstPort: 68, Class: ClassDHCP}, + // Catch-alls (must be last): + {Protocol: "tcp", DstPort: 0, Class: ClassOtherTCP}, + {Protocol: "udp", DstPort: 0, Class: ClassOtherUDP}, +}