diff --git a/cmd/hbcli/main.go b/cmd/hbcli/main.go new file mode 100644 index 0000000..3e153bf --- /dev/null +++ b/cmd/hbcli/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "encoding/hex" + "flag" + "fmt" + "io/ioutil" + "os" + "pd0mz/dmr/homebrew" + + "gopkg.in/yaml.v2" +) + +func rc() *homebrew.RepeaterConfiguration { + return &homebrew.RepeaterConfiguration{ + Callsign: "PI1BOL", + RepeaterID: 2043044, + RXFreq: 433787500, + TXFreq: 438787500, + TXPower: 5, + ColorCode: 1, + Latitude: 52.296786, + Longitude: 4.595454, + Height: 12, + Location: "Hillegom, ZH, NL", + Description: fmt.Sprintf("%s go-dmr", homebrew.Version), + URL: "https://github.com/pd0mz", + } +} + +var ( + callType = map[homebrew.CallType]string{ + homebrew.GroupCall: "group", + homebrew.UnitCall: "unit", + } + frameType = map[homebrew.FrameType]string{ + homebrew.Voice: "voice", + homebrew.VoiceSync: "voice sync", + homebrew.DataSync: "data sync", + homebrew.UnusedFrameType: "unused (should not happen)", + } +) + +func dump(d *homebrew.Data) { + fmt.Println("DMR data:") + fmt.Printf("\tsequence: %d\n", d.Sequence) + fmt.Printf("\ttarget..: %d -> %d\n", d.SrcID, d.DstID) + fmt.Printf("\trepeater: %d\n", d.RepeaterID) + fmt.Printf("\tslot....: %d\n", d.Slot()) + fmt.Printf("\tcall....: %s\n", callType[d.CallType()]) + fmt.Printf("\tframe...: %s\n", frameType[d.FrameType()]) + switch d.FrameType() { + case homebrew.DataSync: + fmt.Printf("\tdatatype: %d\n", d.DataType()) + case homebrew.Voice, homebrew.VoiceSync: + fmt.Printf("\tsequence: %c (voice)\n", 'A'+d.DataType()) + } + fmt.Printf("\tdump....:\n") + fmt.Println(hex.Dump(d.DMR[:])) +} + +func main() { + configFile := flag.String("config", "dmr-homebrew.yaml", "configuration file") + flag.Parse() + + f, err := os.Open(*configFile) + if err != nil { + panic(err) + } + defer f.Close() + + d, err := ioutil.ReadAll(f) + if err != nil { + panic(err) + } + + network := &homebrew.Network{} + if err := yaml.Unmarshal(d, network); err != nil { + panic(err) + } + + repeater, err := homebrew.New(network, rc, dump) + if err != nil { + panic(err) + } + + repeater.Dump = true + panic(repeater.Run()) +} diff --git a/dmr-homebrew.yaml b/dmr-homebrew.yaml new file mode 100644 index 0000000..c5068f1 --- /dev/null +++ b/dmr-homebrew.yaml @@ -0,0 +1,4 @@ +master: brandmeister.pd0zry.ampr.org:62030 +authkey: secret +local: 0.0.0.0:62030 +localid: 123456 diff --git a/homebrew/homebrew.go b/homebrew/homebrew.go new file mode 100644 index 0000000..8dc6f4e --- /dev/null +++ b/homebrew/homebrew.go @@ -0,0 +1,405 @@ +// Package homebrew implements the Home Brew DMR IPSC protocol +package homebrew + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "log" + "net" + "runtime" + "strings" + "sync/atomic" + "time" +) + +var ( + Version = "20151208" + SoftwareID = fmt.Sprintf("%s:go-dmr:%s", runtime.GOOS, Version) + PackageID = fmt.Sprintf("%s:go-dmr:%s-%s", runtime.GOOS, Version, runtime.GOARCH) +) + +type RepeaterConfiguration struct { + Callsign string + RepeaterID uint32 + RXFreq uint32 + TXFreq uint32 + TXPower uint8 + ColorCode uint8 + Latitude float32 + Longitude float32 + Height uint16 + Location string + Description string + URL string +} + +func (r *RepeaterConfiguration) Bytes() []byte { + return []byte(r.String()) +} + +func (r *RepeaterConfiguration) String() string { + if r.ColorCode < 1 { + r.ColorCode = 1 + } + if r.ColorCode > 15 { + r.ColorCode = 15 + } + if r.TXPower > 99 { + r.TXPower = 99 + } + + var lat = fmt.Sprintf("%-08f", r.Latitude) + if len(lat) > 8 { + lat = lat[:8] + } + var lon = fmt.Sprintf("%-09f", r.Longitude) + if len(lon) > 9 { + lon = lon[:9] + } + + var b = "RPTC" + b += fmt.Sprintf("%-8s", r.Callsign) + b += fmt.Sprintf("%08x", r.RepeaterID) + b += fmt.Sprintf("%09d", r.RXFreq) + b += fmt.Sprintf("%09d", r.TXFreq) + b += fmt.Sprintf("%02d", r.TXPower) + b += fmt.Sprintf("%02d", r.ColorCode) + b += lat + b += lon + b += fmt.Sprintf("%03d", r.Height) + b += fmt.Sprintf("%-20s", r.Location) + b += fmt.Sprintf("%-20s", r.Description) + b += fmt.Sprintf("%-124s", r.URL) + b += fmt.Sprintf("%-40s", SoftwareID) + b += fmt.Sprintf("%-40s", PackageID) + return b +} + +type configFunc func() *RepeaterConfiguration + +type CallType byte + +const ( + GroupCall CallType = iota + UnitCall +) + +type FrameType byte + +const ( + Voice FrameType = iota + VoiceSync + DataSync + UnusedFrameType +) + +type Data struct { + Signature [4]byte + Sequence byte + SrcID uint32 + DstID uint32 + RepeaterID uint32 + Flags byte + StreamID uint32 + DMR [33]byte +} + +func (d *Data) CallType() CallType { + return CallType((d.Flags >> 1) & 0x01) +} + +func (d *Data) DataType() byte { + return d.Flags >> 4 +} + +func (d *Data) FrameType() FrameType { + return FrameType((d.Flags >> 2) & 0x03) +} + +func (d *Data) Slot() int { + return int(d.Flags&0x01) + 1 +} + +func ParseData(data []byte) (*Data, error) { + if len(data) != 53 { + return nil, errors.New("invalid packet length") + } + + d := &Data{} + copy(d.Signature[:], data[:4]) + d.Sequence = data[4] + d.SrcID = binary.BigEndian.Uint32(append([]byte{0x00}, data[5:7]...)) + d.DstID = binary.BigEndian.Uint32(append([]byte{0x00}, data[8:10]...)) + d.RepeaterID = binary.BigEndian.Uint32(data[11:15]) + d.Flags = data[15] + d.StreamID = binary.BigEndian.Uint32(data[16:20]) + copy(d.DMR[:], data[20:]) + + return d, nil +} + +type dataFunc func(*Data) + +type authStatus byte + +const ( + authNone authStatus = iota + authBegin + authDone + authFail +) + +type Network struct { + AuthKey string + Local string + LocalID uint32 + Master string + MasterID uint32 +} + +type Link struct { + Dump bool + config configFunc + stream dataFunc + network *Network + conn *net.UDPConn + authKey []byte + local struct { + addr *net.UDPAddr + id []byte + } + master struct { + addr *net.UDPAddr + id []byte + status authStatus + secret []byte + keepalive struct { + outstanding uint32 + sent uint64 + } + } +} + +func New(network *Network, cf configFunc, df dataFunc) (*Link, error) { + if cf == nil { + return nil, errors.New("config func can't be nil") + } + + link := &Link{ + network: network, + config: cf, + stream: df, + } + + var err error + if strings.HasPrefix(network.AuthKey, "0x") { + if link.authKey, err = hex.DecodeString(network.AuthKey[2:]); err != nil { + return nil, err + } + } else { + link.authKey = []byte(network.AuthKey) + } + if network.Local == "" { + network.Local = "0.0.0.0:62030" + } + if network.LocalID == 0 { + return nil, errors.New("missing localid") + } + link.local.id = []byte(fmt.Sprintf("%08x", network.LocalID)) + if link.local.addr, err = net.ResolveUDPAddr("udp", network.Local); err != nil { + return nil, err + } + if network.Master == "" { + return nil, errors.New("no master address configured") + } + if link.master.addr, err = net.ResolveUDPAddr("udp", network.Master); err != nil { + return nil, err + } + + return link, nil +} + +func (l *Link) Run() error { + var err error + + if l.conn, err = net.ListenUDP("udp", l.local.addr); err != nil { + return err + } + + go l.login() + + for { + var ( + n int + peer *net.UDPAddr + data = make([]byte, 512) + ) + if n, peer, err = l.conn.ReadFromUDP(data); err != nil { + log.Printf("dmr/homebrew: error reading from %s: %v\n", peer, err) + continue + } + + go l.parse(peer, data[:n]) + } + + return nil +} + +func (l *Link) Send(addr *net.UDPAddr, data []byte) error { + for len(data) > 0 { + n, err := l.conn.WriteToUDP(data, addr) + if err != nil { + return err + } + data = data[n:] + } + return nil +} + +func (l *Link) login() { + var previous = authDone + for l.master.status != authFail { + var p []byte + + if l.master.status != previous { + switch l.master.status { + case authNone: + log.Printf("dmr/homebrew: logging in as %d\n", l.network.LocalID) + p = append(RepeaterLogin, l.local.id...) + + case authBegin: + log.Printf("dmr/homebrew: authenticating as %d\n", l.network.LocalID) + p = append(RepeaterKey, l.local.id...) + + hash := sha256.New() + hash.Write(l.master.secret) + hash.Write(l.authKey) + + p = append(p, []byte(hex.EncodeToString(hash.Sum(nil)))...) + + case authDone: + config := l.config().Bytes() + fmt.Printf(hex.Dump(config)) + log.Printf("dmr/homebrew: logged in, sending %d bytes of repeater configuration\n", len(config)) + + if err := l.Send(l.master.addr, config); err != nil { + log.Printf("dmr/homebrew: send(%s) failed: %v\n", l.master.addr, err) + return + } + l.keepAlive() + return + + case authFail: + log.Println("dmr/homebrew: login failed") + return + } + if p != nil { + l.Send(l.master.addr, p) + } + previous = l.master.status + } else { + log.Println("dmr/homebrew: waiting for master to respond in login sequence...") + time.Sleep(time.Second) + } + } +} + +func (l *Link) keepAlive() { + for { + atomic.AddUint32(&l.master.keepalive.outstanding, 1) + atomic.AddUint64(&l.master.keepalive.sent, 1) + var p = append(MasterPing, l.local.id...) + if err := l.Send(l.master.addr, p); err != nil { + log.Printf("dmr/homebrew: send(%s) failed: %v\n", l.master.addr, err) + return + } + time.Sleep(time.Minute) + } +} + +func (l *Link) parse(addr *net.UDPAddr, data []byte) { + size := len(data) + + switch l.master.status { + case authNone: + if bytes.Equal(data, DMRData) { + return + } + if size < 14 { + return + } + packet := data[:6] + repeater, err := hex.DecodeString(string(data[6:14])) + if err != nil { + log.Println("dmr/homebrew: unexpected login reply from master") + l.master.status = authFail + break + } + + switch { + case bytes.Equal(packet, MasterNAK): + log.Printf("dmr/homebrew: login refused by master %d\n", repeater) + l.master.status = authFail + break + case bytes.Equal(packet, MasterACK): + log.Printf("dmr/homebrew: login accepted by master %d\n", repeater) + l.master.secret = data[14:] + l.master.status = authBegin + break + default: + log.Printf("dmr/homebrew: unexpected login reply from master %d\n", repeater) + l.master.status = authFail + break + } + + case authBegin: + if bytes.Equal(data, DMRData) { + return + } + if size < 14 { + log.Println("dmr/homebrew: unexpected login reply from master") + l.master.status = authFail + break + } + packet := data[:6] + repeater, err := hex.DecodeString(string(data[6:14])) + if err != nil { + log.Println("dmr/homebrew: unexpected login reply from master") + l.master.status = authFail + break + } + + switch { + case bytes.Equal(packet, MasterNAK): + log.Printf("dmr/homebrew: authentication refused by master %d\n", repeater) + l.master.status = authFail + break + case bytes.Equal(packet, MasterACK): + log.Printf("dmr/homebrew: authentication accepted by master %d\n", repeater) + l.master.status = authDone + break + default: + log.Printf("dmr/homebrew: unexpected authentication reply from master %d\n", repeater) + l.master.status = authFail + break + } + + case authDone: + switch { + case bytes.Equal(data, DMRData): + if l.stream == nil { + return + } + frame, err := ParseData(data) + if err != nil { + log.Printf("error parsing DMR data: %v\n", err) + return + } + l.stream(frame) + } + } +} diff --git a/homebrew/message.go b/homebrew/message.go new file mode 100644 index 0000000..7c153c9 --- /dev/null +++ b/homebrew/message.go @@ -0,0 +1,13 @@ +package homebrew + +var ( + DMRData = []byte("DMRD") + MasterNAK = []byte("MSTNAK") + MasterACK = []byte("MSTACK") + RepeaterLogin = []byte("RPTL") + RepeaterKey = []byte("RPTK") + MasterPing = []byte("MSTPING") + RepeaterPong = []byte("RPTPONG") + MasterClosing = []byte("MSTCL") + RepeaterClosing = []byte("RPTCL") +) diff --git a/ipsc/ipsc.go b/ipsc/ipsc.go index 243aa10..d3c5c4b 100644 --- a/ipsc/ipsc.go +++ b/ipsc/ipsc.go @@ -30,9 +30,27 @@ type Network struct { MasterPeer bool AuthKey string Master string + MasterID uint32 Listen string } +type ipscPeerStatus struct { + connected bool + peerList bool + keepAliveSent int + keepAliveMissed int + keepAliveOutstanding int + keepAliveReceived int + keepAliveRXTime time.Time +} + +type ipscPeer struct { + radioID uint32 + mode byte + flags []byte + status ipscPeerStatus +} + type IPSC struct { Network *Network Dump bool @@ -45,20 +63,13 @@ type IPSC struct { flags []byte tsFlags []byte } + peers map[uint32]ipscPeer master struct { addr *net.UDPAddr - radioID []byte + radioID uint32 mode byte flags []byte - status struct { - connected bool - peerList bool - keepAliveSent int - keepAliveMissed int - keepAliveOutstanding int - keepAliveReceived int - keepAliveRXTime int - } + status ipscPeerStatus } conn *net.UDPConn } @@ -69,7 +80,7 @@ func New(network *Network) (*IPSC, error) { } c.local.radioID = make([]byte, 4) c.local.flags = make([]byte, 4) - c.master.radioID = make([]byte, 4) + c.peers = make(map[uint32]ipscPeer) c.master.flags = make([]byte, 4) binary.BigEndian.PutUint32(c.local.radioID, c.Network.RadioID) @@ -180,6 +191,8 @@ func (c *IPSC) Run() error { log.Printf("authentication failed, dropping packet from %s\n", peer) continue } + + go c.parse(peer, c.payload(b[:n])) } return nil @@ -197,6 +210,37 @@ func (c *IPSC) authenticate(data []byte) bool { return hmac.Equal(hash, mac.Sum(nil)) } +func (c *IPSC) parse(peer *net.UDPAddr, data []byte) { + packetType := data[0] + peerID := binary.BigEndian.Uint32(data[1:5]) + //seq := data[5:6] + + switch { + case MasterRequired[packetType]: + if !c.validMaster(peerID) { + log.Printf("%s: peer ID %d is not a valid master, expected %d\n", + peer, peerID, c.Network.MasterID) + return + } + + switch packetType { + case MasterAliveReply: + c.resetKeepAlive(peerID) + c.master.status.keepAliveReceived++ + c.master.status.keepAliveRXTime = time.Now() + } + + case packetType == MasterRegistrationReply: + // We have successfully registered to a master + c.master.radioID = peerID + c.master.mode = data[5] + c.master.flags = data[6:10] + c.master.status.connected = true + c.master.status.keepAliveOutstanding = 0 + log.Printf("registered to master %d\n", c.master.radioID) + } +} + func (c *IPSC) payload(data []byte) []byte { if c.authKey == nil || len(c.authKey) == 0 { return data @@ -204,6 +248,20 @@ func (c *IPSC) payload(data []byte) []byte { return data[:len(data)-10] } +func (c *IPSC) resetKeepAlive(peerID uint32) { + if c.validMaster(peerID) { + c.master.status.keepAliveOutstanding = 0 + return + } + if peer, ok := c.peers[peerID]; ok { + peer.status.keepAliveOutstanding = 0 + } +} + +func (c *IPSC) validMaster(peerID uint32) bool { + return c.master.radioID == peerID +} + func (c *IPSC) dump(addr *net.UDPAddr, data []byte) { if len(data) < 7 { fmt.Printf("%d bytes of unreadable data from %s:\n", len(data), addr) @@ -292,13 +350,26 @@ func (c *IPSC) peerMaintenance() { for { var p []byte if c.master.status.connected { - log.Println("sending keep-alive to master") + log.Printf("sending keep-alive to master %d\n", c.master.radioID) r := []byte{MasterAliveRequest} r = append(r, c.local.radioID...) r = append(r, c.local.tsFlags...) r = append(r, []byte{LinkTypeIPSC, Version17, LinkTypeIPSC, Version16}...) p = c.hashedPacket(c.authKey, r) + if c.master.status.keepAliveOutstanding > 0 { + c.master.status.keepAliveMissed++ + log.Printf("%d outstanding keep-alives from master\n", c.master.status.keepAliveOutstanding) + } + if c.master.status.keepAliveOutstanding > c.Network.MaxMissed { + c.master.status.connected = false + c.master.status.keepAliveOutstanding = 0 + log.Printf("connection to master %d lost\n", c.master.radioID) + continue + } + + c.master.status.keepAliveSent++ + c.master.status.keepAliveOutstanding++ } else { log.Println("registering with master") r := []byte{MasterRegistrationRequest}