├── Dockerfile ├── Makefile ├── README.md ├── config.go ├── hep.go ├── main.go ├── main_test.go └── network.go /Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build --no-cache -t negbie/heplify-xrcollector:latest . 2 | # docker push negbie/heplify-xrcollector:latest 3 | 4 | FROM golang:alpine as builder 5 | RUN apk update && apk add --no-cache git 6 | RUN go get -u github.com/negbie/sipparser 7 | COPY . /heplify-xrcollector 8 | WORKDIR /heplify-xrcollector 9 | RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-s -w' -installsuffix cgo -o heplify-xrcollector . 10 | 11 | FROM scratch 12 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 13 | COPY --from=builder /heplify-xrcollector/heplify-xrcollector . -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME?=heplify-xrcollector 2 | 3 | all: 4 | go build -ldflags "-s -w" -o $(NAME) *.go 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # heplify-xrcollector 2 | Is a collector for SIP RTCP-XR voice quality reports and a HEP client. 3 | 4 | ### Installation 5 | Download [heplify-xrcollector](https://github.com/negbie/heplify-xrcollector/releases) and execute 'chmod +x heplify-xrcollector' 6 | 7 | ### Usage 8 | ```bash 9 | -debug 10 | Log with debug level 11 | -hi uint 12 | HEP ID (default 3333) 13 | -hs string 14 | HEP UDP server address (default "127.0.0.1:9060") 15 | -xs string 16 | XR collector UDP listen address (default ":5060") 17 | ``` 18 | 19 | ### Examples 20 | ```bash 21 | # Listen on 0.0.0.0:5060 for vq-rtcpxr and send it as HEP to 127.0.0.1:9060 22 | ./heplify-xrcollector 23 | 24 | # Listen on 0.0.0.0:9066 for vq-rtcpxr and send it as HEP to 192.168.1.10:9060 25 | ./heplify-xrcollector -xs :9066 -hs 192.168.1.10:9060 26 | 27 | # Additionally change HEP ID to 1234 and log with debug level 28 | ./heplify-xrcollector -xs :9066 -hs 192.168.1.10:9060 -hi 1234 -debug 29 | 30 | ``` -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var cfg config 4 | 5 | type config struct { 6 | HepServerAddress string 7 | CollectorAddress string 8 | HepNodeID uint 9 | Debug bool 10 | } 11 | -------------------------------------------------------------------------------- /hep.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "log" 7 | "net" 8 | "time" 9 | ) 10 | 11 | var ( 12 | hepBuf bytes.Buffer 13 | hepVer = []byte{0x48, 0x45, 0x50, 0x33} // "HEP3" 14 | hepLen = []byte{0x00, 0x00} 15 | hepLen7 = []byte{0x00, 0x07} 16 | hepLen8 = []byte{0x00, 0x08} 17 | hepLen10 = []byte{0x00, 0x0a} 18 | chunck16 = []byte{0x00, 0x00} 19 | chunck32 = []byte{0x00, 0x00, 0x00, 0x00} 20 | ) 21 | 22 | func encodeHEP(payload []byte, protoType byte) []byte { 23 | hepMsg := append([]byte{}, makeHEPChuncks(payload, protoType)...) 24 | binary.BigEndian.PutUint16(hepMsg[4:6], uint16(len(hepMsg))) 25 | return hepMsg 26 | } 27 | 28 | // makeHEPChuncks will construct the respective HEP chunck 29 | func makeHEPChuncks(payload []byte, protoType byte) []byte { 30 | hepBuf.Reset() 31 | hepBuf.Write(hepVer) 32 | // hepMsg length placeholder. Will be written later 33 | hepBuf.Write(hepLen) 34 | 35 | // Chunk IP protocol family (0x02=IPv4, 0x0a=IPv6) 36 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x01}) 37 | hepBuf.Write(hepLen7) 38 | hepBuf.WriteByte(0x02) 39 | 40 | // Chunk IP protocol ID (0x06=TCP, 0x11=UDP) 41 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x02}) 42 | hepBuf.Write(hepLen7) 43 | hepBuf.WriteByte(0x11) 44 | 45 | // Chunk IPv4 source address 46 | srcIP := net.IPv4(1, 1, 1, 1).To4() 47 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x03}) 48 | binary.BigEndian.PutUint16(hepLen, 6+uint16(len(srcIP))) 49 | hepBuf.Write(hepLen) 50 | hepBuf.Write(srcIP) 51 | 52 | // Chunk IPv4 destination address 53 | dstIP := net.IPv4(2, 2, 2, 2).To4() 54 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x04}) 55 | binary.BigEndian.PutUint16(hepLen, 6+uint16(len(dstIP))) 56 | hepBuf.Write(hepLen) 57 | hepBuf.Write([]byte(dstIP)) 58 | 59 | // Chunk protocol source port 60 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x07}) 61 | hepBuf.Write(hepLen8) 62 | binary.BigEndian.PutUint16(chunck16, 1111) 63 | hepBuf.Write(chunck16) 64 | 65 | // Chunk protocol destination port 66 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x08}) 67 | hepBuf.Write(hepLen8) 68 | binary.BigEndian.PutUint16(chunck16, 2222) 69 | hepBuf.Write(chunck16) 70 | 71 | // Chunk unix timestamp, seconds 72 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x09}) 73 | hepBuf.Write(hepLen10) 74 | binary.BigEndian.PutUint32(chunck32, uint32(time.Now().Unix())) 75 | hepBuf.Write(chunck32) 76 | 77 | // Chunk unix timestamp, microseconds offset 78 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x0a}) 79 | hepBuf.Write(hepLen10) 80 | binary.BigEndian.PutUint32(chunck32, uint32(time.Now().Nanosecond()/1000)) 81 | hepBuf.Write(chunck32) 82 | 83 | // Chunk protocol type (DNS, LOG, RTCP, SIP) 84 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x0b}) 85 | hepBuf.Write(hepLen7) 86 | hepBuf.WriteByte(protoType) 87 | 88 | // Chunk capture agent ID 89 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x0c}) 90 | hepBuf.Write(hepLen10) 91 | binary.BigEndian.PutUint32(chunck32, uint32(cfg.HepNodeID)) 92 | hepBuf.Write(chunck32) 93 | 94 | // Chunk captured packet payload 95 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x0f}) 96 | binary.BigEndian.PutUint16(hepLen, 6+uint16(len(payload))) 97 | hepBuf.Write(hepLen) 98 | hepBuf.Write(payload) 99 | 100 | var cid []byte 101 | if posCallID := bytes.Index(payload, []byte("CallID:")); posCallID > 0 { 102 | restCallID := payload[posCallID:] 103 | // Minimum length of "CallID:x" = 8 104 | if posRestCallID := bytes.Index(restCallID, []byte("\r\n")); posRestCallID >= 8 { 105 | cid = restCallID[len("CallID:"):posRestCallID] 106 | i := 0 107 | for i < len(cid) && (cid[i] == ' ' || cid[i] == '\t') { 108 | i++ 109 | } 110 | cid = cid[i:] 111 | } else { 112 | log.Printf("no end or fishy CallID in '%s'\n", payload) 113 | } 114 | } 115 | 116 | if cid != nil { 117 | // Chunk internal correlation id 118 | hepBuf.Write([]byte{0x00, 0x00, 0x00, 0x11}) 119 | binary.BigEndian.PutUint16(hepLen, 6+uint16(len(cid))) 120 | hepBuf.Write(hepLen) 121 | hepBuf.Write(cid) 122 | } 123 | 124 | return hepBuf.Bytes() 125 | } 126 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | ) 10 | 11 | // XRPacket holds UDP data and address 12 | type XRPacket struct { 13 | addr *net.UDPAddr 14 | data []byte 15 | } 16 | 17 | func init() { 18 | flag.Usage = func() { 19 | fmt.Fprintf(os.Stderr, "Use %s like: %s [option]\n", "heplify-xrcollector 0.4", os.Args[0]) 20 | flag.PrintDefaults() 21 | } 22 | 23 | flag.StringVar(&cfg.HepServerAddress, "hs", "127.0.0.1:9060", "HEP UDP server address") 24 | flag.StringVar(&cfg.CollectorAddress, "xs", ":5060", "XR collector UDP listen address") 25 | flag.UintVar(&cfg.HepNodeID, "hi", 3333, "HEP ID") 26 | flag.BoolVar(&cfg.Debug, "debug", false, "Log with debug level") 27 | flag.Parse() 28 | } 29 | 30 | func main() { 31 | addrXR, err := net.ResolveUDPAddr("udp", cfg.CollectorAddress) 32 | if err != nil { 33 | log.Fatalln(err) 34 | } 35 | 36 | connXR, err := net.ListenUDP("udp", addrXR) 37 | if err != nil { 38 | log.Fatalln(err) 39 | } 40 | 41 | connHEP, err := net.Dial("udp", cfg.HepServerAddress) 42 | if err != nil { 43 | log.Fatalln(err) 44 | } 45 | 46 | inXRCh := make(chan XRPacket, 100) 47 | outXRCh := make(chan XRPacket, 100) 48 | outHEPCh := make(chan []byte, 100) 49 | 50 | go recvXR(connXR, inXRCh, outHEPCh) 51 | go sendXR(connXR, outXRCh) 52 | go sendHEP(connHEP, outHEPCh) 53 | 54 | for packet := range inXRCh { 55 | outXRCh <- packet 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var ( 11 | listnXR = "127.0.0.1:5060" 12 | addrHEP = "localhost:9060" 13 | invite = "INVITE sip:87.103.120.253:9070 SIP/2.0\r\n" + 14 | "Via: SIP/2.0/UDP 10.0.3.13:3072;branch=z9hG4bK-2atcagwblzv2;rport\r\n" + 15 | "From: ;tag=2ygtpy7bgk\r\n" + 16 | "To: \r\n" + 17 | "Call-ID: 825962570309-8ds5sl3mca99\r\n" + 18 | "CSeq: 1 INVITE\r\n" + 19 | "Max-Forwards: 70\r\n" + 20 | "Contact: ;reg-id=1\r\n" + 21 | "User-Agent: snom821/873_19_20130321\r\n\r\n" 22 | 23 | publish = "PUBLISH sip:87.103.120.253:9070 SIP/2.0\r\n" + 24 | "Via: SIP/2.0/UDP 10.0.3.13:3072;branch=z9hG4bK-2atcagwblzv2;rport\r\n" + 25 | "From: ;tag=2ygtpy7bgk\r\n" + 26 | "To: \r\n" + 27 | "Call-ID: 89596257635d-ip18q8n0lp1b\r\n" + 28 | "CSeq: 2 PUBLISH\r\n" + 29 | "Max-Forwards: 70\r\n" + 30 | "Contact: ;reg-id=1\r\n" + 31 | "User-Agent: snom821/873_19_20130321\r\n" + 32 | "Event: vq-rtcpxr\r\n" + 33 | "Accept: application/sdp, message/sipfrag\r\n" + 34 | "Content-Type: application/vq-rtcpxr\r\n" + 35 | "Content-Length: 804\r\n\r\n" + 36 | "VQSessionReport: CallTerm\r\n" + 37 | "CallID:825962570309-8ds5sl3mca99\r\n" + 38 | "LocalID:\r\n" + 39 | "RemoteID:\r\n" + 40 | "OrigID:\r\n" + 41 | "LocalAddr:IP=10.0.3.13 PORT=57460 SSRC=0x014EA261\r\n" + 42 | "LocalMAC:0004135310DB\r\n" + 43 | "RemoteAddr:IP=10.0.3.252 PORT=10034 SSRC=0x1F634EA2\r\n" + 44 | "DialogID:825962570309-8ds5sl3mca99;to-tag=gqj87t0stF-M8g.kPREKLthaGl030mze;from-tag=2ygtpy7bgk\r\n" + 45 | "x-UserAgent:snom821/873_19_20130321\r\n" + 46 | "LocalMetrics:\r\n" + 47 | "Timestamps:START=2016-06-16T07:47:14Z STOP=2016-06-16T07:47:21Z\r\n" + 48 | "SessionDesc:PT=8 PD=PCMA SR=8000 PPS=50 SSUP=off\r\n" + 49 | "x-SIPmetrics:SVA=RG SRD=392 SFC=0\r\n" + 50 | "x-SIPterm:SDC=OK SDT=7 SDR=OR\r\n" + 51 | "JitterBuffer:JBA=3 JBR=2 JBN=20 JBM=20JBX=240\r\n" + 52 | "PacketLoss:NLR=3.0 JDR=3.0\r\n" + 53 | "BurstGapLoss:BLD=0.0 BD=0 GLD=0.0 GD=5930 GMIN=16\r\n" + 54 | "Delay:RTD=0 ESD=0 IAJ=11\r\n" + 55 | "QualityEst:MOSLQ=4.1 MOSCQ=4.1\r\n" 56 | ) 57 | 58 | func TestMain(t *testing.T) { 59 | cfg.Debug = true 60 | addrXR, err := net.ResolveUDPAddr("udp", listnXR) 61 | if err != nil { 62 | log.Fatalln(err) 63 | } 64 | 65 | connXR, err := net.ListenUDP("udp", addrXR) 66 | if err != nil { 67 | log.Fatalln(err) 68 | } 69 | 70 | connHEP, err := net.Dial("udp", addrHEP) 71 | if err != nil { 72 | log.Fatalln(err) 73 | } 74 | 75 | connXROut, err := net.Dial("udp", listnXR) 76 | if err != nil { 77 | log.Fatalln(err) 78 | } 79 | 80 | inXRCh := make(chan XRPacket, 10) 81 | outXRCh := make(chan XRPacket, 10) 82 | outHEPCh := make(chan []byte, 10) 83 | 84 | go recvXR(connXR, inXRCh, outHEPCh) 85 | go sendXR(connXR, outXRCh) 86 | go sendHEP(connHEP, outHEPCh) 87 | 88 | for i := 0; i < 10; i++ { 89 | connHEP.Write(encodeHEP([]byte(invite), 1)) 90 | } 91 | 92 | for i := 0; i < 10; i++ { 93 | _, err := connXROut.Write([]byte(publish)) 94 | if err != nil { 95 | log.Fatalln(err) 96 | } 97 | } 98 | 99 | go func() { 100 | time.Sleep(10 * time.Millisecond) 101 | close(inXRCh) 102 | }() 103 | 104 | for packet := range inXRCh { 105 | outXRCh <- packet 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | 8 | "github.com/negbie/sipparser" 9 | ) 10 | 11 | const maxPktSize = 4096 12 | 13 | func recvXR(conn *net.UDPConn, inXRCh chan XRPacket, outHEPCh chan []byte) { 14 | for { 15 | b := make([]byte, maxPktSize) 16 | n, addr, err := conn.ReadFromUDP(b) 17 | if err != nil { 18 | log.Println("Error on XR read: ", err) 19 | continue 20 | } 21 | if n >= maxPktSize { 22 | log.Printf("Warning received packet from %s exceeds %d bytes\n", addr, maxPktSize) 23 | } 24 | if cfg.Debug { 25 | log.Printf("Received following RTCP-XR report with %d bytes from %s:\n%s\n", n, addr, string(b[:n])) 26 | } else { 27 | log.Printf("Received packet with %d bytes from %s\n", n, addr) 28 | } 29 | var msg []byte 30 | if msg, err = process(b[:n]); err != nil { 31 | log.Println(err) 32 | continue 33 | } 34 | inXRCh <- XRPacket{addr, msg} 35 | outHEPCh <- b[:n] 36 | } 37 | } 38 | 39 | func sendXR(conn *net.UDPConn, outXRCh chan XRPacket) { 40 | for packet := range outXRCh { 41 | n, err := conn.WriteToUDP(packet.data, packet.addr) 42 | if err != nil { 43 | log.Println("Error on XR write: ", err) 44 | continue 45 | } 46 | if cfg.Debug { 47 | log.Printf("Sent following SIP/2.0 200 OK with %d bytes to %s:\n%s\n", n, packet.addr, string(packet.data)) 48 | } else { 49 | log.Printf("Sent back OK with %d bytes to %s\n", n, packet.addr) 50 | } 51 | } 52 | } 53 | 54 | func sendHEP(conn net.Conn, outHEPCh chan []byte) { 55 | for packet := range outHEPCh { 56 | _, err := conn.Write(encodeHEP(packet, 35)) 57 | if err != nil { 58 | log.Println("Error on HEP write: ", err) 59 | continue 60 | } 61 | } 62 | } 63 | 64 | func process(pkt []byte) ([]byte, error) { 65 | sip := sipparser.ParseMsg(string(pkt)) 66 | if sip.Error != nil { 67 | return nil, sip.Error 68 | } 69 | if sip.ContentType != "application/vq-rtcpxr" || len(sip.Body) < 32 || 70 | sip.From == nil || sip.To == nil || sip.Cseq == nil { 71 | return nil, fmt.Errorf("No or malformed vq-rtcpxr inside SIP Message:\n%s", sip.Msg) 72 | } 73 | 74 | resp := fmt.Sprintf("SIP/2.0 200 OK\r\nVia: %s\r\nFrom: %s\r\nTo: %s;tag=Fg2Uy0r7geBQF\r\nContact: %s\r\n"+ 75 | "Call-ID: %s\r\nCseq: %s\r\nUser-Agent: heplify-xrcollector\r\nContent-Length: 0\r\n\r\n", 76 | sip.ViaOne, 77 | sip.From.Val, 78 | sip.To.Val, 79 | sip.ContactVal, 80 | sip.CallID, 81 | sip.Cseq.Val) 82 | return []byte(resp), nil 83 | } 84 | --------------------------------------------------------------------------------