diff --git a/cmd/ipsccli/main.go b/cmd/ipsccli/main.go new file mode 100644 index 0000000..5fc443e --- /dev/null +++ b/cmd/ipsccli/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "io/ioutil" + "os" + "pd0mz/dmr/ipsc" + + "gopkg.in/yaml.v2" +) + +func main() { + configFile := flag.String("config", "dmr.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 := &ipsc.Network{} + if err := yaml.Unmarshal(d, network); err != nil { + panic(err) + } + + repeater, err := ipsc.New(network) + if err != nil { + panic(err) + } + + repeater.Dump = true + panic(repeater.Run()) +} diff --git a/dmr.yaml b/dmr.yaml new file mode 100644 index 0000000..0f1811c --- /dev/null +++ b/dmr.yaml @@ -0,0 +1,4 @@ +radio_id: 2042214 +auth_key: 7061737377307264 +master: brandmeister.pd0zry.ampr.org:55000 +listen: 0.0.0.0:55000 diff --git a/ipsc/ipsc.go b/ipsc/ipsc.go new file mode 100644 index 0000000..243aa10 --- /dev/null +++ b/ipsc/ipsc.go @@ -0,0 +1,332 @@ +package ipsc + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/binary" + "encoding/hex" + "fmt" + "log" + "net" + "time" +) + +type Network struct { + Disabled bool + RadioID uint32 + AliveTimer time.Duration + MaxMissed int + IPSCMode string + PeerOperDisabled bool + TS1LinkDisabled bool + TS2LinkDisabled bool + CSBKCall bool + RepeaterCallMonitoring bool `yaml:"rcm"` + ConsoleApplicationDisabled bool + XNLCall bool + XNLMaster bool + DataCall bool + VoiceCall bool + MasterPeer bool + AuthKey string + Master string + Listen string +} + +type IPSC struct { + Network *Network + Dump bool + + authKey []byte + local struct { + addr *net.UDPAddr + radioID []byte + mode byte + flags []byte + tsFlags []byte + } + master struct { + addr *net.UDPAddr + radioID []byte + mode byte + flags []byte + status struct { + connected bool + peerList bool + keepAliveSent int + keepAliveMissed int + keepAliveOutstanding int + keepAliveReceived int + keepAliveRXTime int + } + } + conn *net.UDPConn +} + +func New(network *Network) (*IPSC, error) { + c := &IPSC{ + Network: network, + } + c.local.radioID = make([]byte, 4) + c.local.flags = make([]byte, 4) + c.master.radioID = make([]byte, 4) + c.master.flags = make([]byte, 4) + + binary.BigEndian.PutUint32(c.local.radioID, c.Network.RadioID) + + var err error + + if c.Network.AuthKey != "" { + if c.authKey, err = hex.DecodeString(c.Network.AuthKey); err != nil { + return nil, err + } + } + if c.Network.AliveTimer == 0 { + c.Network.AliveTimer = time.Second * 5 + } + if !c.Network.PeerOperDisabled { + c.local.mode |= FlagPeerOperational + } + switch c.Network.IPSCMode { + case "analog": + c.local.mode |= FlagPeerModeAnalog + case "", "digital": + c.local.mode |= FlagPeerModeDigital + case "none": + c.local.mode &= 0xff ^ MaskPeerMode + default: + return nil, fmt.Errorf("unknown IPSCMode %q", c.Network.IPSCMode) + } + if c.Network.TS1LinkDisabled { + c.local.mode |= FlagIPSCTS1Off + } else { + c.local.mode |= FlagIPSCTS1On + } + if c.Network.TS2LinkDisabled { + c.local.mode |= FlagIPSCTS2Off + } else { + c.local.mode |= FlagIPSCTS2On + } + + if c.Network.CSBKCall { + c.local.flags[2] |= FlagCSBKMessage + } + if c.Network.RepeaterCallMonitoring { + c.local.flags[2] |= FlagRepeaterCallMonitoring + } + if !c.Network.ConsoleApplicationDisabled { + c.local.flags[2] |= FlagConsoleApplication + } + + if c.Network.XNLCall { + c.local.flags[3] |= FlagXNLStatus + if c.Network.XNLMaster { + c.local.flags[3] |= FlagXNLMaster + } else { + c.local.flags[3] |= FlagXNLSlave + } + } + if len(c.Network.AuthKey) > 0 { + c.local.flags[3] |= FlagPacketAuthenticated + } + if c.Network.DataCall { + c.local.flags[3] |= FlagDataCall + } + if c.Network.VoiceCall { + c.local.flags[3] |= FlagVoiceCall + } + if c.Network.MasterPeer { + c.local.flags[3] |= FlagMasterPeer + } + + if c.Network.Listen == "" { + c.Network.Listen = ":62030" + } + if c.local.addr, err = net.ResolveUDPAddr("udp", c.Network.Listen); err != nil { + return nil, err + } + if c.Network.Master != "" { + if c.master.addr, err = net.ResolveUDPAddr("udp", c.Network.Master); err != nil { + return nil, err + } + } + + c.local.tsFlags = append([]byte{c.local.mode}, c.local.flags...) + + return c, nil +} + +func (c *IPSC) Run() error { + var err error + if c.conn, err = net.ListenUDP("udp", c.local.addr); err != nil { + return err + } + + go c.peerMaintenance() + + for { + var peer *net.UDPAddr + var n int + var b = make([]byte, 512) + if n, peer, err = c.conn.ReadFromUDP(b); err != nil { + log.Printf("error reading from %s: %v\n", peer, err) + continue + } + + if c.Dump { + c.dump(peer, b[:n]) + } + if !c.authenticate(b[:n]) { + log.Printf("authentication failed, dropping packet from %s\n", peer) + continue + } + } + + return nil +} + +func (c *IPSC) authenticate(data []byte) bool { + if c.authKey == nil || len(c.authKey) == 0 { + return true + } + + payload := c.payload(data) + hash := data[len(data)-10:] + mac := hmac.New(sha1.New, c.authKey) + mac.Write(payload) + return hmac.Equal(hash, mac.Sum(nil)) +} + +func (c *IPSC) payload(data []byte) []byte { + if c.authKey == nil || len(c.authKey) == 0 { + return data + } + return data[:len(data)-10] +} + +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) + fmt.Printf(hex.Dump(data)) + return + } + + fmt.Printf("%d bytes of data %s:\n", len(data), addr) + fmt.Printf(hex.Dump(data)) + + packetType := data[0] + peerID := binary.BigEndian.Uint32(data[1:5]) + seq := data[5:6] + + switch packetType { + case CallConfirmation: + fmt.Println("call confirmation:") + case TextMessageAck: + fmt.Println("text message acknowledgement:") + case CallMonStatus: + fmt.Println("call monitor status:") + case CallMonRepeat: + fmt.Println("call monitor repeater:") + case CallMonNACK: + fmt.Println("call monitor nack:") + case XCMPXNLControl: + fmt.Println("XCMP/XNL control message:") + case GroupVoice: + fmt.Println("group voice:") + case PVTVoice: + fmt.Println("PVT voice:") + case GroupData: + fmt.Println("group data:") + case PVTData: + fmt.Println("PVT data:") + case RPTWakeUp: + fmt.Println("RPT wake up:") + case UnknownCollision: + fmt.Println("unknown collision:") + case MasterRegistrationRequest: + fmt.Println("master registration request:") + case MasterRegistrationReply: + fmt.Println("master registration reply:") + case PeerListRequest: + fmt.Println("peer list request:") + case PeerListReply: + fmt.Println("peer list reply:") + case PeerRegistrationRequest: + fmt.Println("peer registration request:") + case PeerRegistrationReply: + fmt.Println("peer registration reply:") + case MasterAliveRequest: + fmt.Println("master alive request:") + case MasterAliveReply: + fmt.Println("master alive reply:") + case PeerAliveRequest: + fmt.Println("peer alive request:") + case PeerAliveReply: + fmt.Println("peer alive reply:") + case DeregistrationRequest: + fmt.Println("de-registration request:") + case DeregistrationReply: + fmt.Println("de-registration reply:") + default: + fmt.Printf("unknown packet type 0x%02x:\n", packetType) + } + fmt.Printf("\tpeer id: %d\n", peerID) + fmt.Printf("\tsequence: %v\n", seq) +} + +func (c *IPSC) hashedPacket(key, data []byte) []byte { + if key == nil || len(key) == 0 { + return data + } + + mac := hmac.New(sha1.New, key) + mac.Write(data) + + hash := make([]byte, 20) + hex.Encode(hash, mac.Sum(nil)) + + return append(data, hash...) +} + +func (c *IPSC) peerMaintenance() { + for { + var p []byte + if c.master.status.connected { + log.Println("sending keep-alive to master") + 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) + + } else { + log.Println("registering with master") + r := []byte{MasterRegistrationRequest} + 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 err := c.sendToMaster(p); err != nil { + log.Fatalf("error sending registration request to master: %v\n", err) + return + } + + time.Sleep(c.Network.AliveTimer) + } +} + +func (c *IPSC) sendToMaster(data []byte) error { + if c.Dump { + c.dump(c.master.addr, data) + } + for len(data) > 0 { + n, err := c.conn.WriteToUDP(data, c.master.addr) + if err != nil { + return err + } + data = data[n:] + } + return nil +} diff --git a/ipsc/mask.go b/ipsc/mask.go new file mode 100644 index 0000000..cea45e5 --- /dev/null +++ b/ipsc/mask.go @@ -0,0 +1,92 @@ +package ipsc + +type Mask uint8 + +// IPSC mask values + +// Linking status +/* + Byte 1 - BIT FLAGS: + + xx.. .... = Peer Operational (01 only known valid value) + ..xx .... = Peer MODE: 00 - No Radio, 01 - Analog, 10 - Digital + .... xx.. = IPSC Slot 1: 10 on, 01 off + .... ..xx = IPSC Slot 2: 10 on, 01 off +*/ +const ( + FlagPeerOperational = 0x40 + MaskPeerMode = 0x30 + FlagPeerModeAnalog = 0x10 + FlagPeerModeDigital = 0x20 + MaskIPSCTS1 = 0x0c + MaskIPSCTS2 = 0x03 + FlagIPSCTS1On = 0x08 + FlagIPSCTS1Off = 0x04 + FlagIPSCTS2On = 0x02 + FlagIPSCTS2Off = 0x01 +) + +// Service flags +/* + Byte 1 - 0x00 = Unknown + Byte 2 - 0x00 = Unknown + Byte 3 - BIT FLAGS: + + x... .... = CSBK Message + .x.. .... = Repeater Call Monitoring + ..x. .... = 3rd Party "Console" Application + ...x xxxx = Unknown - default to 0 +*/ +const ( + FlagCSBKMessage = 0x80 + FlagRepeaterCallMonitoring = 0x40 + FlagConsoleApplication = 0x20 +) + +/* + Byte 4 = BIT FLAGS: + + x... .... = XNL Connected (1=true) + .x.. .... = XNL Master Device + ..x. .... = XNL Slave Device + ...x .... = Set if packets are authenticated + .... x... = Set if data calls are supported + .... .x.. = Set if voice calls are supported + .... ..x. = Unknown - default to 0 + .... ...x = Set if master +*/ +const ( + FlagXNLStatus = 0x80 + FlagXNLMaster = 0x40 + FlagXNLSlave = 0x20 + FlagPacketAuthenticated = 0x10 + FlagDataCall = 0x08 + FlagVoiceCall = 0x04 + FlagMasterPeer = 0x01 +) + +// Timeslot call and status byte +/* + Byte 17 of Group and Private Voice/Data Packets + + ..x.. ....TS Value (0=TS1, 1=TS2) + .x... ....TS In Progress/End (0=In Progress, 1=End) + + Possible values: 0x00=TS1, 0x20=TS2, 0x40=TS1 End, 0x60=TS2 End +*/ + +// RTP mask values +/* + Bytes 1 and 2 of the RTP header are bit-fields, the rest are at least + one byte long, and do not need masked. +*/ +const ( + // Byte 1 + RTPVersionMask Mask = 0xc0 + RTPPadMask Mask = 0x20 + RTPExtMask Mask = 0x10 + RTPCSICMask Mask = 0x0f + // Byte 2 + RTPMRKRMask Mask = 0x80 + RTPPayTypeMask Mask = 0xf7 +) diff --git a/ipsc/message.go b/ipsc/message.go new file mode 100644 index 0000000..8724ebb --- /dev/null +++ b/ipsc/message.go @@ -0,0 +1,74 @@ +package ipsc + +const ( + // IPSC Version Information + Version14 byte = 0x00 + Version15 byte = 0x00 + Version15A byte = 0x00 + Version16 byte = 0x01 + Version17 byte = 0x02 + Version18 byte = 0x02 + Version19 byte = 0x03 + Version22 byte = 0x04 + + // Known IPSC Message Types + CallConfirmation byte = 0x05 // Confirmation FROM the recipient of a confirmed call. + TextMessageAck byte = 0x54 // Doesn't seem to mean success, though. This code is sent success or failure + CallMonStatus byte = 0x61 // | + CallMonRepeat byte = 0x62 // | Exact meaning unknown + CallMonNACK byte = 0x63 // | + XCMPXNLControl byte = 0x70 // XCMP/XNL control message + GroupVoice byte = 0x80 + PVTVoice byte = 0x81 + GroupData byte = 0x83 + PVTData byte = 0x84 + RPTWakeUp byte = 0x85 // Similar to OTA DMR "wake up" + UnknownCollision byte = 0x86 // Seen when two dmrlinks try to transmit at once + MasterRegistrationRequest byte = 0x90 // FROM peer TO master + MasterRegistrationReply byte = 0x91 // FROM master TO peer + PeerListRequest byte = 0x92 // From peer TO master + PeerListReply byte = 0x93 // From master TO peer + PeerRegistrationRequest byte = 0x94 // Peer registration request + PeerRegistrationReply byte = 0x95 // Peer registration reply + MasterAliveRequest byte = 0x96 // FROM peer TO master + MasterAliveReply byte = 0x97 // FROM master TO peer + PeerAliveRequest byte = 0x98 // Peer keep alive request + PeerAliveReply byte = 0x99 // Peer keep alive reply + DeregistrationRequest byte = 0x9a // Request de-registration from system + DeregistrationReply byte = 0x9b // De-registration reply + + // Link Type Values + LinkTypeIPSC byte = 0x04 +) + +var AnyPeerRequired = map[byte]bool{ + GroupVoice: true, + PVTVoice: true, + GroupData: true, + PVTData: true, + CallMonStatus: true, + CallMonRepeat: true, + CallMonNACK: true, + XCMPXNLControl: true, + RPTWakeUp: true, + DeregistrationRequest: true, +} + +var PeerRequired = map[byte]bool{ + PeerAliveRequest: true, + PeerAliveReply: true, + PeerRegistrationRequest: true, + PeerRegistrationReply: true, +} + +var MasterRequired = map[byte]bool{ + PeerListReply: true, + MasterAliveReply: true, +} + +var UserGenerated = map[byte]bool{ + GroupVoice: true, + PVTVoice: true, + GroupData: true, + PVTData: true, +}