Engage Cmd Basics - rallytac/pub GitHub Wiki

The basics of engage-cmd

Our engage-cmd application is a great little app that we use internally at Rally Tactical Systems to test our APIs, conduct load tests, prove out scnearios, and more. While its not a production-quality application [remember that its an internal testing too], we do make it available to partners and customers to use for their own engineering purposes.

So what is it?

Simply put, engage-cmd is a terminal/command-line application that is statically linked with the Engage Engine library, resulting in a single executable. Its available on Windows, Linux, and Mac right now - i.e. operating systems that offer some kind of terminal interface.

What's needed to run it?

To use engage-cmd you're minimally going to need:

  • The engage-cmd executable for your platform - check out https://hq.rallytac.com/builds/ for binaries.
  • A certificate store - an encrypted file containing X.509 certificates and keys that the underlying Engage Engine within engage-cmd will use.
  • An engine policy file which is a JSON text file containing engine-level policy information such as licensing, global settings, and so on.
  • A mission file; also a JSON text file that defines the set of groups/channels that engage-cmd will be using.

Here's a quick example: Let's say that your certificate store is named all-rts-certs.certstore, your policy file is ep.json, and your mission file is mi.json.

Let's further assume all these files are in the same directory.

dev t % ls -lsa
total 24848
    0 drwxr-xr-x   6 dev  staff       192 May 23 08:31 .
    0 drwxr-xr-x  25 dev  staff       800 May 23 08:27 ..
   16 -rw-r--r--   1 dev  staff      5122 May 23 08:28 all-rts-certs.certstore
24816 -rwxr-xr-x   1 dev  staff  12702072 May 16 16:18 engage-cmd
    8 -rw-r--r--   1 dev  staff       409 May 23 08:29 ep.json
    8 -rw-------   1 dev  staff       374 May 23 08:28 mi.json
dev t %

You can download these example configuration files:

And they look as follows:

all-rts-certs.certstore (a binary file)

Engage CertStore^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@ ....

ep.json

{
        "dataDirectory": "./data/${node_id}",

        "licensing": {
                "entitlement": "",
                "key": "",
                "activationCode": ""
        },

        "networking": {
                "defaultNic": "en0",
                "preventMulticastFailover": true
        },

        "audio": {
                "internalRate": 8000,
                "internalChannels": 1
        }
}

NOTE: Take note of the defaultNic setting in the policy file. In the example the NIC name is en0 - you'll need to change this value to the name of the network interface card on the compuer where engage-cmd is running. This is very important for IP multicast because multicast has no routing and therefore the Engage Engine needs to be told which NIC to use for sending and receiving multicast. If you don't specify a network interface, Engage will pick the first multicast-capable NIC it finds - and that might not be the one you want used.

mi.json

{
  "id": "a unique id for this mission",
  "name": "Sample mission",
  "description": "Just a sample mission",
  "groups": [
    {
      "id": "AS-01",
      "type": 1,
      "name": "AirSupport",
      "txAudio": {
        "encoder": 25,
        "framingMs": 60
      },
      "tx": {
        "address": "237.1.1.1",
        "port": 21000
      },
      "rx": {
        "address": "237.1.1.1",
        "port": 21000
      }
    },
    {
      "id": "C2-MAIN",
      "type": 1,
      "name": "Primary C2",
      "txAudio": {
        "encoder": 25,
        "framingMs": 60
      },
      "tx": {
        "address": "237.1.1.2",
        "port": 21002
      },
      "rx": {
        "address": "237.1.1.2",
        "port": 21002
      }
    }
  ]
}

To run engage-cmd with these files, your command line would be:

./engage-cmd -cs:all-rts-certs.certstore -ep:ep.json -mi:mi.json -acj

You should get something like:

sbotha@Shaun-M4 t % ./engage-cmd -cs:all-rts-certs.certstore -ep:ep.json -mi:mi.json -acj
---------------------------------------------------------------------------------
Engage-Cmd version 1.251.9091 [RELEASE] for darwin_arm64
Copyright (c) 2019 Rally Tactical Systems, Inc.
Build time: May 16 2025 @ 15:28:31
Revision: 7720542ea.3922026.3e38fc5
---------------------------------------------------------------------------------
[252592546][3]:[EngageCmd]:crypto engine is NOT FIPS validated
2025-05-23 08:44:39.058 [93729/0x20afd9f00-          ecMain] I/CertStore: Loading 'all-rts-certs.certstore'
2025-05-23 08:44:39.068 [93729/0x20afd9f00-          ecMain] I/CertStore: loaded 'rtsCA'
2025-05-23 08:44:39.068 [93729/0x20afd9f00-          ecMain] I/CertStore: loaded 'rtsFactoryDefaultRpSrv'
2025-05-23 08:44:39.068 [93729/0x20afd9f00-          ecMain] I/CertStore: loaded 'rtsFactoryDefaultEngage'
2025-05-23 08:44:39.069 [93729/0x20afd9f00-          ecMain] I/====EngageInterface====: Engage Engine version 1.251.9091 [RELEASE] for darwin_arm64 v0.0.0, build time May 16 2025 @ 15:27:08, cpu=16, mdf=256, pid=93729, cwd='/Users/sbotha/Global/shaunwork/rp/t'
2025-05-23 08:44:39.075 [93729/0x20afd9f00-          ecMain] I/====EngageInterface====: nic: name='en0' (Wi-Fi), description=IEEE80211, family=2, addresses='192.168.0.107', available=1, loopBack=0, supportsMulticast=1
2025-05-23 08:44:39.076 [93729/0x16d4c3000-        apiCallQ] I/Engine: using certstore -enginedefault 'rtsFactoryDefaultEngage'
2025-05-23 08:44:39.076 [93729/0x16d4c3000-        apiCallQ] I/Engine: default security certificate:
subject..........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Engage Factory Default Certificate/[email protected]]
issuer...........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
selfSigned.......: [0]
version..........: [1]
notBefore........: [Sep  7 01:53:06 2019 GMT]
notAfter.........: [Sep  4 01:53:06 2029 GMT]
serial...........: [AD:CB:61:C8:99:4E:21:E1]
fingerprint......: [7D:1A:6C:E6:C0:EC:53:1C:9D:99:20:48:81:E3:C3:87:43:31:C3:26]
2025-05-23 08:44:39.076 [93729/0x16d4c3000-        apiCallQ] I/Engine: using certstore -cadefault 'rtsCA'
2025-05-23 08:44:39.076 [93729/0x16d4c3000-        apiCallQ] I/Engine: ca certificate:
subject..........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
issuer...........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
selfSigned.......: [1]
version..........: [1]
notBefore........: [Aug 26 23:39:46 2019 GMT]
notAfter.........: [Aug 23 23:39:46 2029 GMT]
serial...........: [AF:E6:1B:51:BC:F5:04:31]
fingerprint......: [FA:C2:3C:B4:82:10:B9:C8:C4:02:DF:8B:6B:10:B8:2E:95:38:64:AE]
2025-05-23 08:44:39.076 [93729/0x16d4c3000-        apiCallQ] I/Engine: timeline signing certificate:
subject..........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Engage Factory Default Certificate/[email protected]]
issuer...........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
selfSigned.......: [0]
version..........: [1]
notBefore........: [Sep  7 01:53:06 2019 GMT]
notAfter.........: [Sep  4 01:53:06 2029 GMT]
serial...........: [AD:CB:61:C8:99:4E:21:E1]
fingerprint......: [7D:1A:6C:E6:C0:EC:53:1C:9D:99:20:48:81:E3:C3:87:43:31:C3:26]
2025-05-23 08:44:39.076 [93729/0x16d4c3000-        apiCallQ] I/Engine: all audio set to rate of 8Khz mono
2025-05-23 08:44:39.076 [93729/0x16d4c3000-        apiCallQ] I/Engine: using './data/{ef97994c-2bf7-4149-8e41-6f82df4f2707}' as the engine root data directory
2025-05-23 08:44:39.077 [93729/0x16d4c3000-        apiCallQ] I/Engine: using './data/{ef97994c-2bf7-4149-8e41-6f82df4f2707}/timelines' as the timeline persistence root
2025-05-23 08:44:39.077 [93729/0x16d4c3000-        apiCallQ] W/Engine: limiting max tx/rx due to non-license
Groups:
index=0, type=1, id=AS-01, name=AirSupport, encrypted=no, fullDuplex=no
index=1, type=1, id=C2-MAIN, name=Primary C2, encrypted=no, fullDuplex=no
2025-05-23 08:44:39.115 [93729/0x20afd9f00-          ecMain] I/====EngageInterface====: run state transitioning from rsStopped to rsStarting
2025-05-23 08:44:39.116 [93729/0x16d5db000-     engineMainQ] I/TimelineManager: timeline manager configured for 1024000000 memory bytes, 1024000000 disk bytes
2025-05-23 08:44:39.116 [93729/0x16d5db000-     engineMainQ] I/TimelineManager: rebuilding timeline database due to maintenance flag set by engine
2025-05-23 08:44:39.116 [93729/0x16d5db000-     engineMainQ] I/Engine: discovery has been disabled
2025-05-23 08:44:39.116 [93729/0x20afd9f00-          ecMain] I/====EngageInterface====: run state transitioning from rsStarting to rsStarted
2025-05-23 08:44:39.116 [93729/0x16d5db000-     engineMainQ] W/Engine: limiting max tx/rx due to non-license
2025-05-23 08:44:39.116 [93729/0x16d5db000-     engineMainQ] W/Engine: (updateLicense) license invalid - maximum transmit time has been limited
............running >[252592604][4]:[EngageCmd]:on_ENGAGE_ENGINE_STARTED
[252592605][4]:[EngageCmd]:on_ENGAGE_LICENSE_EXPIRED
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_CREATED AS-01 - {"groupCreationDetail":{"id":"AS-01","status":1}}
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_CREATED C2-MAIN - {"groupCreationDetail":{"id":"C2-MAIN","status":1}}
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_JOINED AS-01
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_JOINED C2-MAIN
[252592608][4]:[EngageCmd]:on_ENGAGE_GROUP_CONNECTED C2-MAIN, {"groupConnectionDetail":{"asFailover":false,"connectionType":1,"id":"C2-MAIN","peer":"237.1.1.2:21002/237.1.1.2:21002","reason":""}}
[252592608][4]:[EngageCmd]:on_ENGAGE_GROUP_CONNECTED AS-01, {"groupConnectionDetail":{"asFailover":false,"connectionType":1,"id":"AS-01","peer":"237.1.1.1:21000/237.1.1.1:21000","reason":""}}
2025-05-23 08:44:40.117 [93729/0x16d5db000-     engineMainQ] W/Engine: (housekeeper) license invalid - maximum transmit time has been limited
[252593605][4]:[EngageCmd]:on_ENGAGE_LICENSE_EXPIRED

............running >

NOTE: You might notice that the log complains about not having a license in this example. That's because there isn't a license in the ep.json file. But that's OK because the software will function 100% without a license except for a 3-second limitation on individual audio transmissions.

Now that the app is running, you're presented with a prompt where you can enter commands. There's a whole bunch of these so we won't get into detail but to get a quick idea, type ? end press [Enter].

That said, we will be using a few commands, so let's get to it.

Seeing the list of groups - use the sg command

............running >sg
Groups:
index=0, type=1, id=AS-01, name=AirSupport, encrypted=no, fullDuplex=no
index=1, type=1, id=C2-MAIN, name=Primary C2, encrypted=no, fullDuplex=no
............running >

These are the groups defined in the mi.json file. Index 0 is the first group and is the Air Support group while index 1 is the Primary C2 group. [Remember these index numbers!]

Begin transmitting on a group - use the b command

............running >b0
............running >2025-05-23 08:53:40.173 [94136/0x16de9b000-     engineMainQ] I/OSXMicrophone: using specified hardware id 'BuiltInMicrophoneDevice'
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_MUTED AS-01
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_STARTED AS-01, {"alias":"APL-0000FFDD7EFB","audio":{"ms":0.0,"samples":0},"direction":2,"groupId":"AS-01","groupName":"AirSupport","id":"{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}","inProgress":true,"nodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","started":1748015620171,"thisNodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","type":1,"uri":"file://./data/{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}/timelines/AS-01/{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}.wav"}
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_STARTED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":1,"txId":0}}
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_UNMUTED AS-01
2025-05-23 08:53:43.249 [94136/0x16de9b000-     engineMainQ] W/Group: onAudioCapturePreEncode: maximum allowed transmit time reached
[253136741][2]:[EngageCmd]:on_ENGAGE_GROUP_MAX_TX_TIME_EXCEEDED AS-01, x=
[253136749][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_ENDED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":2,"txId":0}}
[253136749][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_ENDED AS-01, {"alias":"APL-0000FFDD7EFB","audio":{"ms":2850.0,"samples":22800},"direction":2,"ended":1748015623249,"groupId":"AS-01","groupName":"AirSupport","id":"{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}","nodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","started":1748015620171,"thisNodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","type":1,"uri":"file://./data/{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}/timelines/AS-01/{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}.wav"}

See how the index (0) is incorporated into the b as b0? This is how you tell engage-cmd to begin transmitting on group 0. It follows that to transmit on group 1, the command would be b1.

End transmitting on a group - use the e command

............running >b0
............running >2025-05-23 08:56:40.772 [94242/0x16ff93000-     engineMainQ] I/OSXMicrophone: using specified hardware id 'BuiltInMicrophoneDevice'
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_MUTED AS-01
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_STARTED AS-01, {"alias":"APL-0000DDFB3FFE","audio":{"ms":0.0,"samples":0},"direction":2,"groupId":"AS-01","groupName":"AirSupport","id":"{39f08907-dde1-4806-a890-69c5364528db}","inProgress":true,"nodeId":"{25bff073-8153-42be-8511-450fc4879b29}","started":1748015800770,"thisNodeId":"{25bff073-8153-42be-8511-450fc4879b29}","type":1,"uri":"file://./data/{25bff073-8153-42be-8511-450fc4879b29}/timelines/AS-01/{39f08907-dde1-4806-a890-69c5364528db}.wav"}
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_STARTED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":1,"txId":0}}
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_UNMUTED AS-01
............running >
............running >
............running >e0
............running >[253315132][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_ENDED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":2,"txId":0}}
[253315132][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_ENDED AS-01, {"alias":"APL-0000DDFB3FFE","audio":{"ms":600.0,"samples":4800},"direction":2,"ended":1748015801581,"groupId":"AS-01","groupName":"AirSupport","id":"{39f08907-dde1-4806-a890-69c5364528db}","nodeId":"{25bff073-8153-42be-8511-450fc4879b29}","started":1748015800770,"thisNodeId":"{25bff073-8153-42be-8511-450fc4879b29}","type":1,"uri":"file://./data/{25bff073-8153-42be-8511-450fc4879b29}/timelines/AS-01/{39f08907-dde1-4806-a890-69c5364528db}.wav"}

............running >

Now, if you have another instance of engage-cmd on another machine or, frankly, any other VoIP application that can produce and consume RTP audio traffic over IP multicast; you can speak back and forth between engage-cmd and the other app.

Let's use Rallypoints

A significant challenge to what's been described above is that we're using IP multicast in this mission configuration (those tx and rx settings for each group.). And multicast can be particularly challenging! With issues like multicast routing, TTL values on packets, switches pruning traffic, firewalls blocking packets, security issues, and whoatnot; IP multicast is a bit of a bear to deal with.

A much cleaner and simpler solution to these challenges is to use our Rallypoint software. A Rallypoint is technically known as a Selective Forwarding Unit (or SFU) who's primary function to to pass traffic (packets) between entities. The entities in this case are instances of engage-cmd (or any application that uses our Engage Engine.) It accomplishes this by having each entity to connect to it (the Rallypoint) over a mutually authenticated TLS 1.3 tunnel, and then routes traffic based on stream identifiers. The tunnel is a TCP connection and while control-plane traffic always flows over that link, media can do so too or be transported by point-to-point connections over UDP. For now, though, we'll just use TCP for everything to keep things simple.

Setting up a Rallypoint

Just like the engage-cmd executable, the Rallypoint executable (rallypointd) for the various platforms we support is available from https://hq.rallytac.com/builds/. Once you've installed it (it takes just a few seconds), you can connect to it assuming you've got the right X.509 certificates in place. With the configuration we're using, the all-rts-certs.certstore file contains the default RTS certificates recognized and supported by the Rallypoint so we need not concern ourselves with that right now.

The only major configuration item we need to do on the machine hosting the RP is to open TCP port 7443 on the machine's firewall to allow incoming connections to terminate on the RP.

Also, you don't need a cloud server, a seperate machine, or anything fancy like that for the RP. Pretty much any platform will work just fine - including the "client" machine you're already using for engage-cmd. For example: on your Windows, Linux, or Mac where you already have engage-cmd installed, also install an RP. Then - one the same machine - you can run multiple instances of engage-cmd to talk with each other - obviously in seperate terminal windows.

Using the RP from engage-cmd

Remember that mission file (mi.json), it looks like this:

{
  "id": "a unique id for this mission",
  "name": "Sample mission",
  "description": "Just a sample mission",
  "groups": [
    {
      "id": "AS-01",
      "type": 1,
      "name": "AirSupport",
      "txAudio": {
        "encoder": 25,
        "framingMs": 60
      },
      "tx": {
        "address": "237.1.1.1",
        "port": 21000
      },
      "rx": {
        "address": "237.1.1.1",
        "port": 21000
      }
    },
    {
      "id": "C2-MAIN",
      "type": 1,
      "name": "Primary C2",
      "txAudio": {
        "encoder": 25,
        "framingMs": 60
      },
      "tx": {
        "address": "237.1.1.2",
        "port": 21002
      },
      "rx": {
        "address": "237.1.1.2",
        "port": 21002
      }
    }
  ]
}

We can change it to tell engage-cmd (actually the Engage Engine inside engage-cmd) to use an RP instead ... like this:

{
  "id": "a unique id for this mission",
  "name": "Sample mission",
  "description": "Just a sample mission",
  "groups": [
    {
      "id": "AS-01",
      "type": 1,
      "name": "AirSupport",
      "txAudio": {
        "encoder": 25,
        "framingMs": 60
      },
      "rallypoints": [
        {
            "id": "My-RP",
            "host": {
                "address": "192.168.1.42",
                "port": 7443
            }
        }
      ]
    },
    {
      "id": "C2-MAIN",
      "type": 1,
      "name": "Primary C2",
      "txAudio": {
        "encoder": 25,
        "framingMs": 60
      },
      "rallypoints": [
        {
            "id": "My-RP",
            "host": {
                "address": "192.168.1.42",
                "port": 7443
            }
        }
      ]
    }
  ]
}

...OR... Rather than messing about with our mission file, we can simply tell engage-cmd to insert the RP information into the configuration at runtime by specifying an RP file which looks like this:

rp.json

{
  "id": "My-RP",
  "host": {
    "address": "192.168.1.42",
    "port": 7443
  }
}

You can download this example Rallypoint configuration file:

  • rp.json - Example Rallypoint configuration file

... and then passing the -rp command line parameter as below.

./engage-cmd -cs:all-rts-certs.certstore -ep:ep.json -mi:mi.json -acj -rp:rp.json

The logging output on the terminal will look very similar to before but now instead of connecting the groups to the local multicast, the traffic will instead be routed through and by the RP - in this example at 192.168.1.42. (Obviously you'd need to change the address to the host name or IP address where your RP is installed.)

For a more detailed discussion about Rallypoints, check out https://github.com/rallytac/pub/wiki/Engage-Rallypoints.

PS: There's no need to change the mission file as specifying the -rp parameter will cause the software to ignore the tx and rx multicast information in the group configurations.

END