RS485 BACnet sniffing

Hello John,

I'm really sorry for not answering your questions earlier; somehow this topic went under my radar...

The very first question (garbage input) you actually solved by yourself (by reversing polarity). Alas, there's a bit of trial and error involved when connecting RS485 lines; usually, A is negative and B is positive, but on some devices, it could be the opposite.

Now, to the next post -- how to break data into packets in the log. For simple visual aids, I'd recommend starting with the Regex Markup feature. It's very simple and could work quite well in your case, actually. Each packet is prefixed with a fixed preamble 0x55 0xff, so you can define a regex as \x55\xff and set "Markup Mode" to "Add delimiters before matches". All packets will be visually separated from one another with red lines.

659467c2-d6cb-4822-a90a-006a81b3dc4b-image.png

Now, if we are not talking about visual aids but rather want to export it into Wireshark as a sequence as separate packets, it's a different story. Here, you would need to write a custom script (using any language of your choise) that would parse the data from .njlog file (generated by IO Ninja), split it into packets, then write those to a .pcap file programmatically.

IO Ninja only knows how to generate a .pcap file when it captures actual network packets via Pcap Sniffer, Ethernet Tap and similar plugins. For Serial Tap and most other plugins, however, the data is a continuous data stream, and it doesn't map to the .pcap format very well -- at least, not well enough to do it automatically. However, with a custom script everything is possible. If you choose this path -- the .njlog file format is very simple, and all the related structure definitions are open source:

<ioninja-dir>/scripts/api/log_RecordCode.jnc
<ioninja-dir>/scripts/api/log_RecordFile.jnc

Finally, your last question is about how to use Jancy to work with BACnet MSTP. It's also possible, of course, but let's clarify your end goal here. Do you want to (a) conveniently prepare packets for sending -- i.e., fill the fields using the property grid, automatically calculate checksums, etc? Or (b) parse the incoming data and show the decoded packets in the log?

For (a) you would want to create a packet template. Here's a simple introduction and tutorial: https://ioninja.com/doc/developer-manual/tutorial-ias-packet.html. Also, for your reference, you can take a look at the Modbus Packet Template library (Packet Template Pane -> Load Stock Script -> Modbus RTU, sources are at /scripts/packets/ModbusRtu.jnc).

If it's (b) that you want, then you need a protocol analyzer plugin. This is a bit more challenging task. Here's a short tutorial: https://ioninja.com/doc/developer-manual/tutorial-plugin-analyzer.html to get you started. Also, I'd recommend skimming through the sources of the Modbus Analyzer plugin for a working real-world protocol analyzer. Sources could be found at /scripts/plugins/Modbus/

Let me know which one (a or b) is your priority, and we'll focus on that. Maybe, we can create a quick skeleton script for you to work on.

Really appreciate the information.
I tried using RegEx Markup with default Session settings and it does not want to break the packets up.
Capture.JPG

Using Session->Settings->Log Engine->Binary Data->Binary Data Merge with a value of 3msec it looks better but I'm really after the timestamp of each packet.
Capture.JPG

We have tried using a 485 to USB to monitor the BACnet traffic, but there is a point where the tokens are shown with the exact same timestamp and I suspect that the USB on the PC can't keep up with data which is surprising given the link speed is only 38400.
Capture.JPG

Thanks again for the help.

@Vladimir I think at this point it would be nice to do a basic packet decode of sender/receiver and and packet type as it is only one byte long so 255 possibilities.

How do you get to the Transmit pane if it is greyed out(un-selectable) ?

@vladimir After adding "Force Latin-1 encoding" to the Log Regex Markup settings I'm getting color highlights but no new timestamps when it see's the pattern.
Capture.JPG

Is there any way to get the log to show a timestamp at the beginning of each 55 FF pattern?

Thanks

Capture.JPG

How do you get to the Transmit pane if it is greyed out(un-selectable) ?

The Transmit pane is greyed out in the Serial Tap plugin because it's a read-only sniffer device. You can transmit using the Serial Terminal plugin and a regular USB-to-RS485 adapter.

I tried using RegEx Markup with default Session settings and it does not want to break the packets up.

After adding "Force Latin-1 encoding" to the Log Regex Markup settings I'm getting color highlights but no new timestamps when it see's the pattern.

Right, I should have mentioned that Latin-1 encoding might be necessary when the pattern contains invalid UTF-8 sequences (such as \x55\xff).

In general, using timestamps to split the data stream into packets is not a reliable enough approach (unless every captured byte is precisely timestamped). Serial Tap doesn't do that; instead, timestamps are assigned to the whole blocks of data as they are received over USB. There are multiple layers of buffering involved (at both the Serial Tap and the PC side), so the timing could be distorted to some extent, and bytes from different packets could end up being read within the same block. It's much better to parse the stream and split it into packets based on the actual data contents.

I've created a draft of the BACnet MSTP protocol analyzer for you: https://tibbo.com/downloads/archive/ioninja/.internal/scripts/BacNetMsTp.7z

It breaks the stream into BACnet frames, gives a human-readable representation of the frame header, and highlights the payload (if any).

ae83d05d-099d-4d65-a9e8-3902fff59533-image.png

Feel free to modify or extend it to your liking. However, please note, that ioninja-5.5.0 has a regression that prevents log.representStruct() used in this script from operating properly (it's already fixed but the service release is not out yet). So, to use this plugin, please either (a) roll back to ioninja-5.4.2 or (b) use the internal build ioninja-5.5.0-a: https://tibbo.com/downloads/archive/ioninja/.internal/prerelease/ioninja-5.5.0-a-windows-amd64.7z

Let me know if it works for you, and feel free to ask me anything about the internal implementation of the analyzer script.

P.S. FYI, just as with any representation generated by log.representStruct(), you can click on a header field, and the corresponding data bytes will be highlighted below -- so that you can see the mapping between header fields and data bytes.

Wow, this is great. Couple questions:

  • The Pre-release does not have an install script(msi file.) Can I just drop the folders into the current C:\Program Files\Tibbo\IO Ninja directory?

Loaded your analyzer(on version 5.5.0) using Settings->Add-on Plugins->Layers->Add and selected BacNetMsTp.njplg(this was a guess).

  • It ran very well except that after a large packet came through it seemed to stop(see timestamp 16:21:35 +00:06:437. Looked through the scripts(everything you sent) to see if there was a max number of bytes for a packet but did not find one.

Capture.JPG

Thanks again.

Version 5.5.0 will not display the contents of BACnet headers properly (after you click [+] to expand). The pre-release .7z file is a portable archive; extract it anywhere and run ioninja-5.5.1\bin\ioninja.exe.

Re "stop" of the analyzer -- could you share the original .njlog with the large packet?

I suspect that the reason is the incorrect handling of padding in the script (it went out of sync immediately after the large frame). Try removing the padding:

size_t frameSize =
	sizeof(BacNetMsTpHdr) +  // header
	hdr.m_length +           // payload
	sizeof(uint16_t);        // CRC
	// (hdr.m_length & 1);      // padding <---

Also, it makes sense to wait and sync on \x55\xff before starting buffering the packet; this way, the parser will re-sync after synchronization is lost (this still can occasionally happen because RS485 does not guarantee lossless delivery).

P.S.
For simplicity, just sync on 0x55; that should be good enough.

In BacNetMsTpParser.parse(...), add this:

	while (p < end) {
		void const* p0 = p;

		// 0. sync on 0x55

		if (!m_buffer.m_size) {
			void const* preamble = memchr(p, 0x55, end - p);
			if (!preamble)
				break;

			p = preamble; // skip everything before the preamble
		}

		...

Re "stop" of the analyzer -- could you share the original .njlog with the large packet?

How do I send you the log that is out of sync. I'll try your fixes and report back. Also here are a couple frame types if you want to add them to the analyzer:

enum BacNetMsTcpFrameType: uint8_t {
	Token                       = 0,
	PollForMaster               = 1,
	PollForMasterReply          = 2,
	TestRequest                 = 3,
	TestResponse                = 4,
	BacNetDataExpectingReply    = 5,
	BacNetDataNotExpectingReply = 6,
	ReplyPostponed              = 7,
	ExtendedDataExpectingReply	= 32,
	ExtendedDataNotExpectingReply = 33,
	CustomFrame					= 254,
	NoFrame						= 255
}

@vladimir Removing the padding gives this trying to load the Analyzer:
Capture.JPG

Inserting the changes to BacNetMsTpParser.jnc(without the modification to the padding) it runs very well with an occasional issues toward the end of a large packet:
Capture.JPG

While using this analyzer should we have been running Settings->Log Engine->Binary Data->Binary Data Merge with a 3msec threshold. Should we still be using this ?

Just want to also add how nice it is to be able to see the packets.

Thanks.

How do I send you the log that is out of sync

Hit "Reply" (not "Post quick reply"); the rightmost button on the toolbar is "Upload File". 7-zip it first so that it has the .7z extension permitted by this forum.

Removing the padding gives this trying to load the Analyzer:

Most likely, you just commented out the padding code, but forgot to add a semicolon ; at the end of the statement. In Jancy, just like in C, declarations and expression statements must be terminated with a semicolon.

While using this analyzer should we have been running Settings->Log Engine->Binary Data->Binary Data Merge with a 3msec threshold.

This won't affect the analyzer, but I think it's completely unnecessary now. You will see packets boundaries anyway -- and not just boundaries, decoded header fields, too.

Most likely, you just commented out the padding code, but forgot to add a semicolon ;

Exactly correct as I had missed the '+' on the line above.

Traces look very good now as I will try to move to your latest code. Tried using the upload images(right most icon) to send a 7z zip file but I don't have privileges for it.

Capture.JPG

Tried using the upload images(right most icon) to send a 7z zip file but I don't have privileges for it.

Not "Upload image", "Upload file". Does your "Reply toolbar" have this button?

8537fcf3-5daa-4d0d-8b6e-dd4ba104fe29-image.png

@vladimir That icon does not show on my Firefox. I'll try a different browser.
Capture.JPG

Apparently, this was a permission issue; it doesn't apply to administrators, so I didn't even know it was there...

Anyway, I've adjusted the file upload permissions for registered users; please try again.

@vladimir Sorry for the delay. Upload symbol is there but I get an error that file is too big. The log is 1Mbyte in size. Is there a way to reduce the size once the log is loaded into IO Ninja?

Thanks.

[0_1709752179535_BACnet_38400_heavery_traffic_error_v4.njlog](Uploading 100%)

No prob, I've increased the upload file size limit to 8MB.

Also, you could have archived it with 7z 😉

@vladimir
Here is a log that is working very well until time stamp 9:55:07 +00:46.470 and then the decode stops.

Thanks again for all the help.

BACnet_38400_heavery_traffic_error_v4.7z

Try the updated analyzer:

BacNetMsTp.7z

It checks CRCs now and discards frames with broken headers (in the previous log, the decoder didn't actually stop, but because a broken header specified a very long payload size, it kept buffering data assuming it still was a payload).

Also, when you upload the log, please detach the Analyzer before saving the log. This way, we can access the original raw & unprocessed data (instead of the already decoded frames).