|
|
|
// Package ipsc implements the Motorola IP Site Connect protocol.
|
|
|
|
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
|
|
|
|
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
|
|
|
|
|
|
|
|
authKey []byte
|
|
|
|
local struct {
|
|
|
|
addr *net.UDPAddr
|
|
|
|
radioID []byte
|
|
|
|
mode byte
|
|
|
|
flags []byte
|
|
|
|
tsFlags []byte
|
|
|
|
}
|
|
|
|
peers map[uint32]ipscPeer
|
|
|
|
master struct {
|
|
|
|
addr *net.UDPAddr
|
|
|
|
radioID uint32
|
|
|
|
mode byte
|
|
|
|
flags []byte
|
|
|
|
status ipscPeerStatus
|
|
|
|
}
|
|
|
|
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.peers = make(map[uint32]ipscPeer)
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
go c.parse(peer, c.payload(b[:n]))
|
|
|
|
}
|
|
|
|
|
|
|
|
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) 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
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
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.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}
|
|
|
|
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
|
|
|
|
}
|